Skip to content
Open
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
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

- `SheetCell::value` reads what a cell holds past the text it shows: the number
the file states, and the formula behind a cached result. Filled by odf, ooxml
and csv; `value_type` is unchanged and stays the question the renderer asks.

- `PdfFile::annotate` writes highlight, underline, strike-out, squiggly and ink
annotations into a pdf as an incremental update β€” source bytes untouched,
any viewer reading them β€” in every binding, with an `annotate` capability.
Expand Down
4 changes: 4 additions & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- [Editing design](editing.md) β€” architecture for in-browser editing of ODF/OOXML:
fat-browser op log replayed on save, stable element ids, and a preliminary
implementation plan.
- [Spreadsheet editing design](spreadsheet-editing.md) β€” cells edited by
position through the same op log, a browser-side editing mode with refusal
feedback, and formulas recomputed once, in C++; staged from number/string
cells to a formula engine.
- [The v7 public API](api-v7.md) β€” what the next major removes from
`src/odr/*.hpp`, why, and in which pull requests: one road per operation, and
the removals this document's *Open tasks* deferred until a major.
Expand Down
448 changes: 448 additions & 0 deletions docs/design/spreadsheet-editing.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/odr/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ ValueType SheetCell::value_type() const {
: ValueType::unknown;
}

CellValue SheetCell::value() const {
return exists_() ? m_adapter2->sheet_cell_value(m_identifier) : CellValue();
}

std::string Page::name() const {
return exists_() ? m_adapter2->page_name(m_identifier) : "";
}
Expand Down
22 changes: 22 additions & 0 deletions src/odr/document_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,25 @@ enum class ValueType {
float_number,
};

/// @brief What a sheet cell holds, past the text it shows.
///
/// The text is not repeated here: it stays where it is read from, the cell's
/// children. A formula cell carries the result its producer cached, and that
/// result is what @ref type and @ref number describe.
struct CellValue final {
/// The type of the value, a cached formula result included.
ValueType type{ValueType::unknown};
/// The number the file states, where it states one. Wider than
/// `type == ValueType::float_number`: a percentage or a currency states a
/// number and is typed a string until its format is read.
std::optional<double> number;
/// The formula, in the syntax its own format writes it in β€”
/// `of:=SUM([.A1:.B2])` for odf, `SUM(A1:B2)` for ooxml. Unset for a cell
/// holding none; set and empty for an ooxml cell sharing a formula whose
/// expression the group's master carries.
std::optional<std::string> formula;
};

/// Collection of list types.
enum class ListType {
unordered,
Expand Down Expand Up @@ -332,6 +351,9 @@ class SheetCell final
[[nodiscard]] bool is_covered() const;
[[nodiscard]] TableDimensions span() const;
[[nodiscard]] ValueType value_type() const;
/// What the cell holds. @ref value_type answers the narrower question the
/// renderer asks of every cell, and costs less.
[[nodiscard]] CellValue value() const;
};

/// Represents a page element in a document.
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/abstract/document.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ class SheetCellAdapter {
sheet_cell_span(ElementIdentifier element_id) const = 0;
[[nodiscard]] virtual ValueType
sheet_cell_value_type(ElementIdentifier element_id) const = 0;
[[nodiscard]] virtual CellValue
sheet_cell_value(ElementIdentifier element_id) const = 0;
};

class MasterPageAdapter {
Expand Down
11 changes: 11 additions & 0 deletions src/odr/internal/csv/csv_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <odr/internal/abstract/file.hpp>
#include <odr/internal/common/element_adapter.hpp>
#include <odr/internal/encoding/transcode.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/util/stream_util.hpp>

#include <algorithm>
Expand Down Expand Up @@ -193,6 +194,16 @@ class ElementAdapter final : public AdapterBase {
sheet_cell_value_type(const ElementIdentifier element_id) const override {
return m_document->value_type(column_of(element_id), row_of(element_id));
}
[[nodiscard]] CellValue
sheet_cell_value(const ElementIdentifier element_id) const override {
CellValue result;
result.type = sheet_cell_value_type(element_id);
if (result.type == ValueType::float_number) {
result.number = util::number::parse(
m_document->cell(column_of(element_id), row_of(element_id)));
}
return result;
}

// TextAdapter

Expand Down
9 changes: 9 additions & 0 deletions src/odr/internal/iwork/iwork_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ class ElementAdapter final : public AdapterBase {
sheet_cell_value_type(const ElementIdentifier element_id) const override {
return m_registry->cell_element_at(element_id).value_type;
}
/// A number stays the decimal the file states, which is the cell's text; it
/// is not narrowed to a double here. Formulas live in `CalculationEngine`,
/// which is not read.
[[nodiscard]] CellValue
sheet_cell_value(const ElementIdentifier element_id) const override {
CellValue result;
result.type = m_registry->cell_element_at(element_id).value_type;
return result;
}

[[nodiscard]] TableDimensions
table_dimensions(const ElementIdentifier element_id) const override {
Expand Down
5 changes: 3 additions & 2 deletions src/odr/internal/odf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ Roughly ordered by importance.
- [x] sheets
- [x] dimensions, content range detection
- [x] cell value types (float, string)
- [x] cell values (`office:value`, and `table:formula` as its own string)
- [x] shapes anchored to a sheet
- [ ] computed values (stored values are used as-is; formulas are not
evaluated)
- [ ] computed values (stored values are used as-is; formulas are read but
not evaluated)
- [ ] edit (currently disabled, see `Document::is_editable`)

### Presentation documents (`.odp`)
Expand Down
17 changes: 17 additions & 0 deletions src/odr/internal/odf/odf_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <odr/internal/odf/odf_list.hpp>
#include <odr/internal/odf/odf_parser.hpp>
#include <odr/internal/odf/odf_table.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/xml/xml_util.hpp>
#include <odr/internal/zip/zip_archive.hpp>
Expand Down Expand Up @@ -438,6 +439,22 @@ class ElementAdapter final : public AdapterBase {
}
return ValueType::string;
}
/// [ODF 1.2] 19.386 `office:value`, 19.642 `table:formula`. A date, a time
/// and a boolean state their value elsewhere and are read as their text.
[[nodiscard]] CellValue
sheet_cell_value(const ElementIdentifier element_id) const override {
const pugi::xml_node node = get_node(element_id);

CellValue result;
result.type = sheet_cell_value_type(element_id);
if (const pugi::xml_attribute value = node.attribute("office:value")) {
result.number = util::number::parse(value.value());
}
if (const pugi::xml_attribute formula = node.attribute("table:formula")) {
result.formula = formula.value();
}
return result;
}

[[nodiscard]] PageLayout
master_page_page_layout(const ElementIdentifier element_id) const override {
Expand Down
9 changes: 9 additions & 0 deletions src/odr/internal/oldms/spreadsheet/xls_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ class ElementAdapter final : public AdapterBase {
(void)element_id;
return ValueType::string;
}
/// Every cell is read into its display string at parse time, so neither the
/// number behind one nor a formula expression survives.
[[nodiscard]] CellValue sheet_cell_value(
[[maybe_unused]] const ElementIdentifier element_id) const override {
(void)element_id;
CellValue result;
result.type = ValueType::string;
return result;
}

[[nodiscard]] ParagraphStyle
paragraph_style(const ElementIdentifier element_id) const override {
Expand Down
5 changes: 4 additions & 1 deletion src/odr/internal/ooxml/spreadsheet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ otherwise its own `<v>`/`<is>` children. `get_text` concatenates `t` and `v`
nodes verbatim, so a **formula's cached `<v>` result is shown and `<f>` is
never evaluated**. `sheet_cell_value_type` derives number-vs-string from
`c/@t` (default "n" β†’ `float_number` when a `<v>` exists; dates/booleans/errors
report `string`). Merged ranges from `mergeCells` land in the `SheetCell` side
report `string`). `sheet_cell_value` adds what that leaves out β€” `<v>` parsed
as a number where the type is one, and `<f>` as its own string. A shared
formula writes its expression on the group's master, so a member's formula is
**set and empty** rather than absent. Merged ranges from `mergeCells` land in the `SheetCell` side
map as anchor `span` + `is_covered` flags at parse time.

**Style resolution: styles.xml index vectors.** `StyleRegistry` loads positional
Expand Down
5 changes: 3 additions & 2 deletions src/odr/internal/ooxml/spreadsheet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ Roughly ordered by importance.
- [x] shapes / images anchored to a sheet (`xdr:twoCellAnchor`)
- [x] cell value types (number vs. string; dates/booleans/errors reported as
string)
- [ ] computed values (formulas are not evaluated; the cached `<v>` result is
shown)
- [x] cell values (`<v>` as a number, `<f>` as its own string)
- [ ] computed values (formulas are read but not evaluated; the cached `<v>`
result is shown)
- [ ] edit
- [ ] save

Expand Down
18 changes: 18 additions & 0 deletions src/odr/internal/ooxml/spreadsheet/ooxml_spreadsheet_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <odr/internal/abstract/filesystem.hpp>
#include <odr/internal/common/element_adapter.hpp>
#include <odr/internal/ooxml/spreadsheet/ooxml_spreadsheet_parser.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/xml/xml_util.hpp>

#include <utility>
Expand Down Expand Up @@ -199,6 +200,23 @@ class ElementAdapter final : public AdapterBase {
}
return ValueType::string;
}
/// ECMA-376 18.3.1.4 `c`: `v` is the value, `f` the formula. A shared
/// formula (`f/@t="shared"`) writes its expression on the group's master
/// only, so a member carries the formula and no text.
[[nodiscard]] CellValue
sheet_cell_value(const ElementIdentifier element_id) const override {
const pugi::xml_node node = get_node(element_id);

CellValue result;
result.type = sheet_cell_value_type(element_id);
if (result.type == ValueType::float_number) {
result.number = util::number::parse(node.child("v").text().get());
}
if (const pugi::xml_node formula = node.child("f")) {
result.formula = formula.text().get();
}
return result;
}

[[nodiscard]] TextStyle
line_break_style(const ElementIdentifier element_id) const override {
Expand Down
20 changes: 20 additions & 0 deletions src/odr/internal/util/number_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,31 @@

#include <algorithm>
#include <cmath>
#include <ios>
#include <istream>
#include <locale>
#include <sstream>

#include <fmt/format.h>

namespace odr::internal::util {

std::optional<double> number::parse(const std::string_view text) {
std::istringstream stream{std::string(text)};
// the host's locale decides what a decimal separator is; the classic one is
// the `.` every format we decode writes
stream.imbue(std::locale::classic());

double value = 0;
stream >> value;
if (stream.fail()) {
return {};
}
// `>>` stops at the first character it cannot use rather than failing, which
// would take `1,5` for `1` and `12pt` for `12`
return (stream >> std::ws).eof() ? std::optional(value) : std::nullopt;
}

std::string number::to_string_significant(const double value,
const int significant_digits) {
if (!std::isfinite(value)) {
Expand Down
13 changes: 13 additions & 0 deletions src/odr/internal/util/number_util.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
#pragma once

#include <optional>
#include <string>
#include <string_view>

namespace odr::internal::util::number {

/// Reads @p text as a decimal number, in the spelling every format we decode
/// states one in: a `.` decimal separator whatever the host's locale is.
/// Nothing but surrounding blanks may follow it, so a value carrying a unit or
/// a thousands separator is refused rather than truncated.
///
/// Not `std::strtod`, which reads `LC_NUMERIC` and so truncates `1234.5` to
/// `1234` under a german host locale, and not `std::from_chars`, whose
/// floating point overloads libc++ 18 - the oldest standard library in the
/// profile matrix - does not carry.
[[nodiscard]] std::optional<double> parse(std::string_view text);

/// Renders @p value with @p significant_digits significant digits, without
/// trailing zeros, never in scientific notation, which CSS and SVG lengths do
/// not accept, and never in the host's locale, where a german one would write
Expand Down
2 changes: 2 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ add_executable(odr_test
"src/internal/odf/odf_frame_test.cpp"
"src/internal/odf/odf_geometry_test.cpp"
"src/internal/odf/odf_sheet_repeat_test.cpp"
"src/internal/odf/odf_sheet_value_test.cpp"
"src/internal/odf/odf_table_test.cpp"

"src/internal/oldms/doc_test.cpp"
Expand All @@ -91,6 +92,7 @@ add_executable(odr_test
"src/internal/ooxml/ooxml_crypto_test.cpp"
"src/internal/ooxml/ooxml_text_style_test.cpp"
"src/internal/ooxml/ooxml_spreadsheet_merge_test.cpp"
"src/internal/ooxml/ooxml_spreadsheet_value_test.cpp"
"src/internal/ooxml/ooxml_util_test.cpp"
"src/internal/ooxml/ooxml_presentation_style_test.cpp"

Expand Down
Loading
Loading