Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ producer's layout recorded β€” odf's `text:soft-page-break` β€” are not parsed.
|------|------|
| `src/odr/*.hpp` | **Public API**: `file`, `document`, `document_element`, `html`, `style`, `quantity` (`Measure`), `odr`. |
| `src/odr/internal/abstract/` | Core interfaces: `File`/`DecodedFile`, `Document` + `ElementAdapter`, `Filesystem`, `Archive`, `HtmlService`. |
| `src/odr/internal/common/` | Reusable impls: `Path`/`AbsPath`, base `Document`, the shared `ElementRegistry` + `ElementAdapter`, filesystem, `style`, table cursor/range, temp files. |
| `src/odr/internal/common/` | Reusable impls: `Path`/`AbsPath`, base `Document`, the shared `ElementRegistry` + `ElementAdapter`, filesystem, `style`, table cursor/range, `TextCursor`, temp files. |
| `src/odr/internal/util/` | Helpers: `byte_stream_util`, `string_util`, `stream_util`, `document_util`. |
| `src/odr/internal/magic.*`, `open_strategy.*` | File-type detection + open/dispatch. |
| `src/odr/internal/file_type_table.*` | **The** per-`FileType` table: extensions, MIME types, category, document type, `FileTypeCapabilities`. Every public lookup in `odr.hpp` is a thin forward into it β€” extend the table, not the lookups. |
| `src/odr/internal/formula/` | Spreadsheet formulas: one AST, parsed from both OpenFormula and OOXML. Format-agnostic, and shared by the odf and ooxml engines. |
| `src/odr/internal/html/` | Generic HTML renderer. |
| `src/odr/internal/html/frontend/` | The stylesheets and scripts the renderer writes into the page, as the files a browser reads. `cmake/frontend_assets.cmake` embeds them into the library; `frontend.cpp` names them and decides which view writes which. |
| `src/odr/internal/cfb/`, `zip/` | Container formats (CFB, ZIP). |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- `TablePosition::try_to_column_num` / `try_to_row_num` read a column or row
spelling as an optional instead of throwing, and without case. **Fix**:
`to_column_num` wrapped silently past the index range instead of refusing.

- **Breaking**: the codes the page reports through `odr.onError` and
`odr.onEditRefused` moved from 1 to 9 onto 1001 to 1009. `readOnly` is 1005,
not 5; the `reason` string is unchanged.
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ set(ODR_SOURCE_FILES
"src/odr/internal/csv/csv_file.cpp"
"src/odr/internal/csv/csv_util.cpp"

"src/odr/internal/formula/formula_parser.cpp"

"src/odr/internal/html/common.cpp"
"src/odr/internal/html/document.cpp"
"src/odr/internal/html/document_style.cpp"
Expand Down
12 changes: 7 additions & 5 deletions docs/design/spreadsheet-editing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Spreadsheet editing design

Status: **steps 0, 1 and 2 landed; step 3 is next.** This
Status: **steps 0, 1 and 2 landed; step 3 is under way.** This
records why spreadsheet editing is staged the way it is, what the code already
gives us, and the order the steps go in. It is a plan, not a record β€” update it
as steps land.
Expand Down Expand Up @@ -461,10 +461,12 @@ Each step ships on its own. "Both" means `.ods` and `.xlsx`.

### Step 3 β€” Formulas, read side

1. Parse both syntaxes into one AST: OpenFormula (`of:=SUM([.A1:.B2])`,
`table:formula`) and OOXML (`SUM(A1:B2)`, `<f>`, shared and array
formulas). References, ranges, sheet-qualified references, named ranges
left as opaque.
1. **Landed.** `internal/formula` parses both syntaxes into one AST:
OpenFormula (`of:=SUM([.A1:.B2])`, `table:formula`) and OOXML
(`SUM(A1:B2)`, `<f>`). One recursive descent takes a `Syntax` and branches
where the two part. A named expression, a reference over several sheets and
a spelling past the grid (`A0`) stay opaque names; a formula that does not
parse answers nothing.
2. Reference extraction β†’ dependency graph per document; `Document` answers
"which cells depend on this position".
3. View: a commit marks dependents stale (a class, the host is told); the
Expand Down
87 changes: 87 additions & 0 deletions src/odr/internal/common/text_cursor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#pragma once

#include <odr/internal/util/string_util.hpp>

#include <cstddef>
#include <string_view>

namespace odr::internal {

/// A cursor over a small language written into one string β€” an odf attribute,
/// a formula. Reads are bounded by what remains, which carries no terminator.
class TextCursor {
public:
explicit TextCursor(const std::string_view input) : m_rest{input} {}

[[nodiscard]] bool empty() const { return m_rest.empty(); }

/// What the cursor has not read, which @ref seek takes back.
[[nodiscard]] std::string_view rest() const { return m_rest; }

/// The character @p ahead of the cursor, or `\0` past the end.
[[nodiscard]] char peek(const std::size_t ahead = 0) const {
return ahead < m_rest.size() ? m_rest[ahead] : '\0';
}

/// The next character, consumed.
char take() {
const char c = peek();
if (!m_rest.empty()) {
advance(1);
}
return c;
}

void advance(const std::size_t count) { m_rest.remove_prefix(count); }

/// Back to a view @ref rest answered earlier, undoing what was read since.
void seek(const std::string_view at) { m_rest = at; }

void skip_whitespace() {
while (util::string::is_ascii_whitespace(peek())) {
advance(1);
}
}

/// Whitespace ahead of it is filler; the text itself is the token.
[[nodiscard]] bool consume(const char c) {
skip_whitespace();
if (peek() != c) {
return false;
}
advance(1);
return true;
}

[[nodiscard]] bool consume(const std::string_view text) {
skip_whitespace();
if (!m_rest.starts_with(text)) {
return false;
}
advance(text.size());
return true;
}

/// The leading run of characters @p accept admits, left in place.
[[nodiscard]] std::string_view
peek_while(const util::string::CharPredicate accept) const {
std::size_t length = 0;
while (length < m_rest.size() && accept(m_rest[length])) {
++length;
}
return m_rest.substr(0, length);
}

/// The same run, consumed.
[[nodiscard]] std::string_view
take_while(const util::string::CharPredicate accept) {
const std::string_view taken = peek_while(accept);
advance(taken.size());
return taken;
}

private:
std::string_view m_rest;
};

} // namespace odr::internal
171 changes: 171 additions & 0 deletions src/odr/internal/formula/formula_ast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#pragma once

#include <cstdint>
#include <optional>
#include <string>
#include <variant>
#include <vector>

namespace odr::internal::formula {

/// The error values a formula states and computes. [OpenFormula] 4.3,
/// [ECMA-376] 18.17.2.4.
enum class ErrorType {
null, ///< `#NULL!`
division, ///< `#DIV/0!`
value, ///< `#VALUE!`
reference, ///< `#REF!`
name, ///< `#NAME?`
number, ///< `#NUM!`
not_available, ///< `#N/A`
};

enum class UnaryOperator {
plus,
minus,
percent, ///< postfix, and the only one
};

enum class BinaryOperator {
add,
subtract,
multiply,
divide,
power,
concat,
equal,
not_equal,
less,
less_equal,
greater,
greater_equal,
range, ///< `A1:B2` where the two sides are not one token
intersect, ///< `!` in OpenFormula, a space in ooxml, which is not parsed
unite, ///< `~` in OpenFormula, `,` in ooxml
};

/// One axis of a reference: the index the file states, and its `$`.
struct Coordinate final {
std::uint32_t index{0};
bool absolute{false};

friend bool operator==(const Coordinate &, const Coordinate &) = default;
};

struct NumberLiteral final {
double value{0};

friend bool operator==(const NumberLiteral &,
const NumberLiteral &) = default;
};

struct StringLiteral final {
std::string value;

friend bool operator==(const StringLiteral &,
const StringLiteral &) = default;
};

struct BooleanLiteral final {
bool value{false};

friend bool operator==(const BooleanLiteral &,
const BooleanLiteral &) = default;
};

struct ErrorLiteral final {
ErrorType type{ErrorType::null};

friend bool operator==(const ErrorLiteral &, const ErrorLiteral &) = default;
};

/// One cell. An unstated sheet is the one the formula sits on, and an unstated
/// axis a whole column or row, which only a range spells (`A:A`, `1:1`).
struct CellReference final {
std::optional<std::string> document{};
std::optional<std::string> sheet{};
bool sheet_absolute{false};
std::optional<Coordinate> column{};
std::optional<Coordinate> row{};

friend bool operator==(const CellReference &,
const CellReference &) = default;
};

/// `A1:B2`, as the two corners the file spells. The second may name its own
/// sheet.
struct RangeReference final {
CellReference from;
CellReference to;

friend bool operator==(const RangeReference &,
const RangeReference &) = default;
};

/// A named expression, left opaque: nothing here resolves what it stands for.
struct NameReference final {
std::optional<std::string> document{};
std::optional<std::string> sheet{};
std::string name{};

friend bool operator==(const NameReference &,
const NameReference &) = default;
};

/// The arguments are the node's children.
struct FunctionCall final {
std::string name;

friend bool operator==(const FunctionCall &, const FunctionCall &) = default;
};

/// The operand is the node's only child.
struct UnaryOperation final {
UnaryOperator op{UnaryOperator::plus};

friend bool operator==(const UnaryOperation &,
const UnaryOperation &) = default;
};

/// The two operands are the node's children.
struct BinaryOperation final {
BinaryOperator op{BinaryOperator::add};

friend bool operator==(const BinaryOperation &,
const BinaryOperation &) = default;
};

/// An argument the formula leaves out: the second of `IF(A1,,B1)`.
struct Missing final {
friend bool operator==(const Missing &, const Missing &) = default;
};

/// `{1;2|3;4}` in OpenFormula, `{1,2;3,4}` in ooxml. The elements are the
/// node's children, row by row.
struct ArrayLiteral final {
std::uint32_t columns{0};
std::uint32_t rows{0};

friend bool operator==(const ArrayLiteral &, const ArrayLiteral &) = default;
};

/// One node of a parsed formula. What it holds says what its children are.
struct Node final {
using Content =
std::variant<NumberLiteral, StringLiteral, BooleanLiteral, ErrorLiteral,
CellReference, RangeReference, NameReference, FunctionCall,
UnaryOperation, BinaryOperation, ArrayLiteral, Missing>;

Content content;
std::vector<Node> children;

template <typename T> [[nodiscard]] bool holds() const noexcept {
return std::holds_alternative<T>(content);
}
/// @throws std::bad_variant_access where the node holds something else.
template <typename T> [[nodiscard]] const T &get() const {
return std::get<T>(content);
}
};

} // namespace odr::internal::formula
Loading
Loading