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
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.

## Diagrams

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
15 changes: 15 additions & 0 deletions src/odr/document_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ enum class ValueType {
float_number,
};

/// What a sheet cell holds past the text it shows β€” the text stays in the
/// cell's children. A formula cell describes the result its producer cached.
struct CellValue final {
ValueType type{ValueType::unknown};
/// 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;
/// In the format's own syntax β€” `of:=SUM([.A1:.B2])` for odf, `SUM(A1:B2)`
/// for ooxml. Set and empty for an ooxml cell whose shared formula only the
/// group's master spells.
std::optional<std::string> formula;
};

/// Collection of list types.
enum class ListType {
unordered,
Expand Down Expand Up @@ -332,6 +345,8 @@ class SheetCell final
[[nodiscard]] bool is_covered() const;
[[nodiscard]] TableDimensions span() const;
[[nodiscard]] ValueType value_type() const;
/// @ref value_type is the narrower and cheaper question the renderer asks.
[[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
8 changes: 8 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,14 @@ 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.
/// 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
7 changes: 5 additions & 2 deletions src/odr/internal/ooxml/spreadsheet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ 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
map as anchor `span` + `is_covered` flags at parse time.
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
`fonts`/`fills`/`borders`/`cellStyleXfs`/`cellXfs`. A cell's `s` attribute
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
17 changes: 17 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,22 @@ class ElementAdapter final : public AdapterBase {
}
return ValueType::string;
}
/// ECMA-376 18.3.1.4 `c`: `v` is the value, `f` the formula, whose
/// expression a shared group spells on its master only.
[[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
19 changes: 19 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,30 @@

#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)};
// every format we decode writes a `.`, whatever the host's locale is
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`
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
7 changes: 7 additions & 0 deletions src/odr/internal/util/number_util.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#pragma once

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

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

/// Reads @p text as a decimal number with a `.` separator, whatever the host's
/// locale β€” a german one would read `1234.5` as `1234`. Only blanks may
/// surround it: a unit or a group separator is refused, not truncated.
[[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
100 changes: 100 additions & 0 deletions test/src/internal/odf/odf_sheet_value_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#include <odr/document.hpp>
#include <odr/document_element.hpp>
#include <odr/file.hpp>
#include <odr/logger.hpp>

#include <odr/internal/abstract/file.hpp>
#include <odr/internal/common/file.hpp>
#include <odr/internal/open_strategy.hpp>

#include <gtest/gtest.h>

#include <memory>
#include <string>

using namespace odr;
using namespace odr::internal;

namespace {

/// A flat sheet holding one cell, written as @p cell.
std::string flat_sheet(const std::string &cell) {
return R"(<?xml version="1.0" encoding="UTF-8"?>)"
R"(<office:document office:mimetype=")"
R"(application/vnd.oasis.opendocument.spreadsheet">)"
R"(<office:body><office:spreadsheet>)"
R"(<table:table table:name="s"><table:table-row>)" +
cell +
R"(</table:table-row></table:table>)"
R"(</office:spreadsheet></office:body></office:document>)";
}

CellValue value_of(const std::string &cell) {
const Document document =
DecodedFile(open_strategy::open_file(
std::make_shared<MemoryFile>(flat_sheet(cell)), {},
Logger::null()))
.as_document_file()
.document();
const Sheet sheet = (*document.root_element().children().begin()).as_sheet();
return sheet.cell(0, 0).value();
}

} // namespace

/// [ODF 1.2] 19.386: the number is `office:value`; the `text:p` beside it is
/// the producer's formatting of it.
TEST(OdfSheetValue, a_float_cell_states_its_number) {
const CellValue value = value_of(
R"(<table:table-cell office:value-type="float" office:value="1234.5">)"
R"(<text:p>1 234,50</text:p></table:table-cell>)");

EXPECT_EQ(value.type, ValueType::float_number);
ASSERT_TRUE(value.number.has_value());
EXPECT_DOUBLE_EQ(*value.number, 1234.5);
EXPECT_FALSE(value.formula.has_value());
}

TEST(OdfSheetValue, a_string_cell_states_no_number) {
const CellValue value =
value_of(R"(<table:table-cell office:value-type="string">)"
R"(<text:p>1234.5</text:p></table:table-cell>)");

EXPECT_EQ(value.type, ValueType::string);
EXPECT_FALSE(value.number.has_value());
}

/// [ODF 1.2] 19.642 `table:formula`, whose namespace prefix is the syntax it
/// is written in and stays part of the string.
TEST(OdfSheetValue, a_formula_cell_states_both_formula_and_result) {
// a `)"` inside the attribute would close a default-delimited raw string
const CellValue value =
value_of(R"xml(<table:table-cell table:formula="of:=SUM([.B1:.C1])")xml"
R"( office:value-type="float" office:value="7">)"
R"(<text:p>7</text:p></table:table-cell>)");

ASSERT_TRUE(value.formula.has_value());
EXPECT_EQ(*value.formula, "of:=SUM([.B1:.C1])");
ASSERT_TRUE(value.number.has_value());
EXPECT_DOUBLE_EQ(*value.number, 7);
}

/// The type is read from `office:value-type` alone; reading the number format
/// is what would settle it.
TEST(OdfSheetValue, a_percentage_states_a_number_the_type_does_not_admit) {
const CellValue value = value_of(
R"(<table:table-cell office:value-type="percentage" office:value="0.25">)"
R"(<text:p>25%</text:p></table:table-cell>)");

EXPECT_EQ(value.type, ValueType::string);
ASSERT_TRUE(value.number.has_value());
EXPECT_DOUBLE_EQ(*value.number, 0.25);
}

TEST(OdfSheetValue, a_number_is_read_in_one_spelling_only) {
const CellValue value = value_of(
R"(<table:table-cell office:value-type="float" office:value="1234,5">)"
R"(<text:p>1234,5</text:p></table:table-cell>)");

EXPECT_FALSE(value.number.has_value());
}
Loading
Loading