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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- An editable sheet view states each formula cell's expression
(`data-odr-formula`, `odr.sheet.formulaAt`) and the cells it reads. A commit
marks the formula cells reading what it wrote with `odr-sheet-stale`, and
raises the new `odr.onCellsStale` callback, `{sheet, cells}`; an undo takes
the marks back. Nothing recomputes a formula yet.

- `Document::dependents(position)` answers which cells' formulas read a
position, directly or through another, and `unresolved_formulas()` those
whose references could not all be read. A position is the new
Expand Down
23 changes: 20 additions & 3 deletions docs/design/spreadsheet-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,13 @@ odr.onEditRefused = function (event) {};
odr.onEditChange = function (event) {};
// {editing, editable, reason, code, message}
odr.onEditModeChange = function (event) {};
// {sheet, cells} - the formula cells an edit left computing an old input
odr.onCellsStale = function (event) {};
```

`onCellsStale` carries no code, because it reports no refusal, and the page
marks the cells itself, so a host that wires nothing still shows something.

**The message is for the console; the code is for the host.** A mobile
snackbar is written in the app's own string catalogue, and nothing in this
library is localised — so the host maps `code` to its wording, and `reason`
Expand Down Expand Up @@ -489,9 +494,21 @@ Each step ships on its own. "Both" means `.ods` and `.xlsx`.
the cells the file *spells* rather than the positions they cover: a
repeated ODS row of 1024 columns over 1048576 rows is a handful of nodes
and a billion positions, and only the first is walked.
3. View: a commit marks dependents stale (a class, the host is told); the
locked formula cell exposes its text (`data-odr-formula`, formula cells
only) so a formula bar or a tooltip can show it.
3. **Landed.** View: a formula cell states its expression
(`data-odr-formula`, which `odr.sheet.formulaAt` hands a formula bar) and
the rectangles it reads (`data-odr-reads`: sheet, columns, rows, `*` for an
axis a reference leaves open). Both are editing scaffolding, so a read-only
render carries neither.

A commit then marks the cells reading what it wrote — and the cells reading
those — with `odr-sheet-stale`, and raises `odr.onCellsStale`. The marks
follow the **log**, not the last write: `repaintStale` recomputes them off
the coalesced log, so an undo takes back what it made stale and a save
clears them with the log. Nothing recomputes a value; step 4 is what does.

The page carries the graph rather than asking the host for it, because a
mark has to keep up with typing. The engine's own graph (item 2) is what
the file side uses.
4. File: the `.ods` answer from the spike — drop the cached value of dirty
dependents, or whatever LibreOffice needs to recompute.

Expand Down
20 changes: 2 additions & 18 deletions src/odr/internal/common/sheet_dependencies.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,15 @@

namespace odr::internal {

namespace {

/// The syntax the engine behind @p file_type writes a formula in. Nothing
/// where it states none at all.
std::optional<formula::Syntax> syntax_of(const FileType file_type) {
switch (file_type) {
case FileType::opendocument_spreadsheet:
return formula::Syntax::opendocument;
case FileType::office_open_xml_workbook:
return formula::Syntax::ooxml;
default:
return {};
}
}

} // namespace

bool SheetDependencies::Read::contains(const SheetPosition &position) const {
return position.sheet == sheet && range.contains(position.cell);
}

SheetDependencies SheetDependencies::of(const abstract::Document &document) {
SheetDependencies result;

const std::optional<formula::Syntax> syntax = syntax_of(document.file_type());
const std::optional<formula::Syntax> syntax =
formula::syntax_of(document.file_type());
const abstract::ElementAdapter *adapter = document.element_adapter();
if (!syntax.has_value() || adapter == nullptr) {
return result;
Expand Down
11 changes: 11 additions & 0 deletions src/odr/internal/formula/formula_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,17 @@ std::string_view strip_prefix(std::string_view formula) {

namespace odr::internal {

std::optional<formula::Syntax> formula::syntax_of(const FileType file_type) {
switch (file_type) {
case FileType::opendocument_spreadsheet:
return formula::Syntax::opendocument;
case FileType::office_open_xml_workbook:
return formula::Syntax::ooxml;
default:
return {};
}
}

std::optional<formula::Node> formula::parse(const std::string_view formula,
const formula::Syntax syntax) {
return formula::Parser(formula::strip_prefix(formula), syntax).parse();
Expand Down
6 changes: 6 additions & 0 deletions src/odr/internal/formula/formula_parser.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <odr/file.hpp>

#include <odr/internal/formula/formula_ast.hpp>

#include <optional>
Expand All @@ -14,6 +16,10 @@ enum class Syntax {
ooxml, ///< the expression an `<f>` holds
};

/// The syntax the engine behind @p file_type writes a formula in. Nothing
/// where it states none, or drops the expression at parse time (`.xls`).
[[nodiscard]] std::optional<Syntax> syntax_of(FileType file_type);

/// Parses @p formula, with or without the `of:=` prefix. Nothing where it does
/// not parse, so a caller reads no reference out of a formula it cannot read.
[[nodiscard]] std::optional<Node> parse(std::string_view formula,
Expand Down
11 changes: 11 additions & 0 deletions src/odr/internal/html/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <odr/html.hpp>
#include <odr/internal/abstract/html_service.hpp>
#include <odr/internal/formula/formula_parser.hpp>
#include <odr/quantity.hpp>
#include <odr/style.hpp>

Expand Down Expand Up @@ -58,6 +59,15 @@ struct WritingState {
m_document_editable = editable;
}

/// The syntax a formula of this document is written in, or nothing where
/// the engine states none.
[[nodiscard]] std::optional<formula::Syntax> formula_syntax() const {
return m_formula_syntax;
}
void set_formula_syntax(const std::optional<formula::Syntax> syntax) {
m_formula_syntax = syntax;
}

private:
HtmlWriter *m_out;
const HtmlConfig *m_config;
Expand All @@ -67,6 +77,7 @@ struct WritingState {
TextDirection m_direction{TextDirection::left_to_right};
bool m_editable_markup{true};
bool m_document_editable{false};
std::optional<formula::Syntax> m_formula_syntax;
};

/// Writes the viewport meta tag. Precedence: `config.viewport_content` (raw,
Expand Down
4 changes: 4 additions & 0 deletions src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <odr/internal/abstract/html_service.hpp>
#include <odr/internal/common/null_stream.hpp>
#include <odr/internal/formula/formula_parser.hpp>
#include <odr/internal/html/common.hpp>
#include <odr/internal/html/document_element.hpp>
#include <odr/internal/html/document_style.hpp>
Expand Down Expand Up @@ -282,6 +283,7 @@ render(const Document &document, const HtmlConfig &config, const Logger &logger,
WritingState state(out, config, resources, logger);
state.set_direction(document_direction(document));
state.set_document_editable(document.is_editable());
state.set_formula_syntax(formula::syntax_of(document.file_type()));
write_head(document, state, name, content_pixels);
body(state);
out.write_end();
Expand All @@ -294,6 +296,7 @@ render(const Document &document, const HtmlConfig &config, const Logger &logger,
WritingState head_state(out, config, resources, logger, &styles);
head_state.set_direction(document_direction(document));
head_state.set_document_editable(document.is_editable());
head_state.set_formula_syntax(formula::syntax_of(document.file_type()));

util::stream::DeferredBuffer buffer(
out.out(), static_cast<std::size_t>(config.spreadsheet_style_buffer),
Expand All @@ -307,6 +310,7 @@ render(const Document &document, const HtmlConfig &config, const Logger &logger,
WritingState state(body_out, config, resources, logger, &styles);
state.set_direction(head_state.direction());
state.set_document_editable(head_state.document_editable());
state.set_formula_syntax(head_state.formula_syntax());
body(state);
}
buffer.release();
Expand Down
139 changes: 119 additions & 20 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@

#include <odr/internal/common/path.hpp>
#include <odr/internal/common/table_cursor.hpp>
#include <odr/internal/formula/formula_dependencies.hpp>
#include <odr/internal/formula/formula_parser.hpp>
#include <odr/internal/html/common.hpp>
#include <odr/internal/html/document_style.hpp>
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/html/image_file.hpp>
#include <odr/internal/html/style_registry.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/xml/xml_util.hpp>

#include <algorithm>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

namespace odr::internal {
Expand Down Expand Up @@ -346,10 +353,78 @@ std::uint32_t sheet_ordinal(const Sheet &sheet) {
return ordinal;
}

/// Every sheet by its name without case, as a formula names one, so a
/// reference into another sheet resolves to the ordinal an op uses.
std::unordered_map<std::string, std::uint32_t>
sheet_ordinals_by_name(const Sheet &sheet) {
Element first = sheet;
for (Element previous = sheet.previous_sibling(); previous;
previous = previous.previous_sibling()) {
first = previous;
}

std::unordered_map<std::string, std::uint32_t> result;
std::uint32_t ordinal = 0;
for (Element current = first; current; current = current.next_sibling()) {
if (current.type() == ElementType::sheet) {
result.emplace(util::string::to_lower(current.as_sheet().name()),
ordinal);
}
++ordinal;
}
return result;
}

/// The open end of an axis a reference leaves out.
std::string spell_bound(const std::uint32_t index) {
return index == std::numeric_limits<std::uint32_t>::max()
? "*"
: std::to_string(index);
}

/// What @p formula reads, as the rectangles the page tests a written position
/// against: `sheet,first column,last column,first row,last row`, space
/// separated, with `*` for an axis the reference leaves open. Empty where the
/// formula does not parse, or reads nothing this document holds.
std::string
cell_reads(const std::string &formula, const formula::Syntax syntax,
const std::uint32_t own_sheet,
const std::unordered_map<std::string, std::uint32_t> &by_name) {
const std::optional<formula::Node> node = formula::parse(formula, syntax);
if (!node.has_value()) {
return {};
}

std::string result;
for (const formula::Extent &extent : formula::references(*node).extents) {
if (extent.document.has_value()) {
continue; // another file, which no edit here reaches
}
std::uint32_t sheet = own_sheet;
if (extent.sheet.has_value()) {
const auto named = by_name.find(util::string::to_lower(*extent.sheet));
if (named == by_name.end()) {
continue;
}
sheet = named->second;
}
if (!result.empty()) {
result += " ";
}
result += std::to_string(sheet) + "," +
spell_bound(extent.range.from().column) + "," +
spell_bound(extent.range.to().column) + "," +
spell_bound(extent.range.from().row) + "," +
spell_bound(extent.range.to().row);
}
return result;
}

/// Why a cell cannot be edited, or null where it can be. The names the page
/// reports to its host; `spreadsheet-editing.md` decision 3 lists them.
const char *cell_lock(const SheetCell &cell, const bool anchors_shapes) {
if (cell.value().has_formula()) {
const char *cell_lock(const CellValue &value, const SheetCell &cell,
const bool anchors_shapes) {
if (value.has_formula()) {
return "formula";
}
// its drawings are what the cell is, and an overlay would cover them
Expand Down Expand Up @@ -501,22 +576,30 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) {

const std::optional<double> print_fit = sheet_print_fit(sheet, end_column);

// scaffolding: a read-only render states neither a lock nor a dependency
const std::optional<formula::Syntax> syntax =
state.config().editable ? state.formula_syntax() : std::nullopt;
const std::uint32_t ordinal = sheet_ordinal(sheet);
const std::unordered_map<std::string, std::uint32_t> ordinals_by_name =
syntax.has_value() ? sheet_ordinals_by_name(sheet)
: std::unordered_map<std::string, std::uint32_t>();

state.out().write_element_begin(
"table",
HtmlElementOptions()
.set_class("odr-sheet")
.set_attributes([&](const HtmlAttributeWriterCallback &clb) {
// every op names its sheet, and a view holds only one
clb("data-odr-sheet", std::to_string(sheet_ordinal(sheet)));
})
.set_style([&]() -> std::optional<HtmlWritable> {
if (!print_fit.has_value()) {
return std::nullopt;
}
// `Measure` renders no exponent form
return "--odr-print-fit:" +
Measure(*print_fit, DynamicUnit()).to_string() + ";";
}()));
"table", HtmlElementOptions()
.set_class("odr-sheet")
.set_attributes([&](const HtmlAttributeWriterCallback &clb) {
// every op names its sheet, and a view holds only one
clb("data-odr-sheet", std::to_string(ordinal));
})
.set_style([&]() -> std::optional<HtmlWritable> {
if (!print_fit.has_value()) {
return std::nullopt;
}
// `Measure` renders no exponent form
return "--odr-print-fit:" +
Measure(*print_fit, DynamicUnit()).to_string() +
";";
}()));

state.out().write_element_begin("col",
HtmlElementOptions()
Expand Down Expand Up @@ -685,9 +768,18 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) {
const std::optional<FoldedCell> folded = fold_cell(
cell, sheet_state, wraps, anchors_shapes, table_row_style.height);

// scaffolding: a read-only render states no lock
const char *lock =
state.config().editable ? cell_lock(cell, anchors_shapes) : nullptr;
const CellValue cell_value =
state.config().editable ? cell.value() : CellValue();
const char *lock = state.config().editable
? cell_lock(cell_value, cell, anchors_shapes)
: nullptr;
// what a formula bar shows, and the rectangles the page marks against
const bool computes = cell_value.has_formula() &&
!cell_value.formula().empty() && syntax.has_value();
const std::string reads = computes
? cell_reads(cell_value.formula(), *syntax,
ordinal, ordinals_by_name)
: std::string();

state.out().write_element_begin(
"td",
Expand All @@ -703,6 +795,13 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) {
if (lock != nullptr) {
clb("data-odr-lock", lock);
}
if (computes) {
clb("data-odr-formula",
xml::escape_attribute(cell_value.formula()));
}
if (!reads.empty()) {
clb("data-odr-reads", reads);
}
})
.set_style(
translate_table_cell_style(cell_style) +
Expand Down
7 changes: 7 additions & 0 deletions src/odr/internal/html/frontend/editing.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
console.log("editing " + (event.editing ? "on" : "off"));
};
odr.onEditChange = function () {};
odr.onCellsStale = function () {};

function fire(name, event) {
if (typeof odr[name] === "function") {
Expand Down Expand Up @@ -162,6 +163,12 @@
fire("onEditRefused", event);
},

/// The cells the edits so far left computing an old input. Raised
/// whenever the set changes, which an undo does too.
stale: function (detail) {
fire("onCellsStale", detail);
},

/// The log a host's save button reads; an editor calls it when its log
/// moved.
changed: function () {
Expand Down
Loading
Loading