You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Editing is off for spreadsheets everywhere: odf::Document::is_editable returns true only for text/presentation/drawing (odf_document.cpp, // TODO fix spreadsheet editability), and xlsx returns false outright — and cannot save at all (ooxml_spreadsheet_document.cpp, UnsupportedOperation). So the practical target is ods, where save already rebuilds the package from content.xml.
The reason it is off is real, and it is the sparse sheet model. parse_sheet (odf_parser.cpp) only creates a sheet_cell Element for a cell that is non-empty; empty cells are recorded as a range in the sheet's position-keyed maps with null_element_id. And a cell with table:number-columns-repeated / number-rows-repeated gets one Element per column repeat, all pointing at the samepugi::xml_node, flagged is_repeated — which element_is_editable already refuses. Both are exactly the cases where writing through the current editor would be wrong: text_set_content splices the DOM of one text run, so writing a repeated cell writes all of its repeats, and an empty cell has no text run to splice at all.
First step: the cells that already work
A non-empty, non-repeated cell already has a paragraph → text subtree, element_is_editable already walks the parent chain and clears it, and DocumentPath already addresses a cell by position (DocumentPath::Cell, "cell" prefix) rather than by sibling index — so the path survives a sheet that is mostly holes. The missing piece is mostly the document-level gate.
Let is_editable be true for DocumentType::spreadsheet, so translate emits contenteditable + data-odr-path for those cells and html::edit can navigate back to them.
office:value has to move with the text. A cell carries office:value-type plus a typed attribute; the <text:p> is only the display form. Typing 42 into a float cell and rewriting only the paragraph leaves the old office:value behind, and LibreOffice reads the attribute — the cell silently reverts. So a cell edit is not a text edit: it needs to re-infer the type (or drop to string and clear the typed attributes), which is the first thing that pushes past modifiedText.
Formula cells (table:formula) have the same problem, worse: the text is a cached result. Simplest honest behaviour is to refuse, or to clear the formula and keep the literal.
A round-trip test through soffice --convert-to is the check that the saved package is actually valid, not just that the bytes changed.
The broader solution: repeated and empty cells
Both are the same shape — the element tree does not have a place to write to — and both are fixed at write time, not at parse time. Neither should inflate the model: expanding repeats or materialising every empty cell up front is what #762 is already dying from.
Repeated cells: split on write. Editing one cell of a number-columns-repeated="10" run means rewriting that run as up to three nodes (before / the edited one / after) and fixing the repeat counts, then repointing the affected registry entries. Same on the row axis with number-rows-repeated, which is the harder one because a row split rewrites whole <table:table-row> nodes.
Empty cells: materialise on write (edit empty spreadsheet cells #236). An empty cell has a position and a style but no Element. The write path needs to be able to take a DocumentPath::Cell position, create the <table:table-cell> (splitting the surrounding repeat run to make room), give it a <text:p>, and register the new Element — i.e. the same splitting primitive as above, plus insertion.
That primitive — give me a writable, unshared xml node for position (c, r) — is the piece worth designing first; both cases and any later style edit go through it. // TODO covered cells / // TODO mark as repeated in odf_parser.cpp sit next to it and are probably worth cleaning up in the same pass, since covered cells must never become writable targets.
What the diff protocol needs
html::edit accepts one thing: {"modifiedText": {path: string}}, and it hard-requires element.as_text(). An empty cell has no text element, so it can never appear in that map — the browser has nothing to attach data-odr-path to either, since translate_sheet writes empty cells as bare <td>. That means:
the renderer must mark editable cells (not only text runs) with a path, and
the diff needs a cell-shaped entry — value plus type — that edit can apply to a position that has no Element yet.
Worth deciding whether that is a new key alongside modifiedText or a general "modifiedCells" that supersedes the text case for sheets.
Scope
Step one is small and shippable on its own (gate + value/type handling + a round-trip test). The splitting/materialising work is the real body and only makes sense for ods until xlsx can save at all. Related: #236 (empty cells, subsumed by this), #238 (cell overflow), #762 (why nothing here may expand the model).
Editing is off for spreadsheets everywhere:
odf::Document::is_editablereturns true only for text/presentation/drawing (odf_document.cpp,// TODO fix spreadsheet editability), and xlsx returnsfalseoutright — and cannotsaveat all (ooxml_spreadsheet_document.cpp,UnsupportedOperation). So the practical target is ods, wheresavealready rebuilds the package fromcontent.xml.The reason it is off is real, and it is the sparse sheet model.
parse_sheet(odf_parser.cpp) only creates asheet_cellElement for a cell that is non-empty; empty cells are recorded as a range in the sheet's position-keyed maps withnull_element_id. And a cell withtable:number-columns-repeated/number-rows-repeatedgets one Element per column repeat, all pointing at the samepugi::xml_node, flaggedis_repeated— whichelement_is_editablealready refuses. Both are exactly the cases where writing through the current editor would be wrong:text_set_contentsplices the DOM of one text run, so writing a repeated cell writes all of its repeats, and an empty cell has no text run to splice at all.First step: the cells that already work
A non-empty, non-repeated cell already has a
paragraph→textsubtree,element_is_editablealready walks the parent chain and clears it, andDocumentPathalready addresses a cell by position (DocumentPath::Cell,"cell"prefix) rather than by sibling index — so the path survives a sheet that is mostly holes. The missing piece is mostly the document-level gate.is_editablebe true forDocumentType::spreadsheet, sotranslateemitscontenteditable+data-odr-pathfor those cells andhtml::editcan navigate back to them.office:valuehas to move with the text. A cell carriesoffice:value-typeplus a typed attribute; the<text:p>is only the display form. Typing42into a float cell and rewriting only the paragraph leaves the oldoffice:valuebehind, and LibreOffice reads the attribute — the cell silently reverts. So a cell edit is not a text edit: it needs to re-infer the type (or drop tostringand clear the typed attributes), which is the first thing that pushes pastmodifiedText.table:formula) have the same problem, worse: the text is a cached result. Simplest honest behaviour is to refuse, or to clear the formula and keep the literal.soffice --convert-tois the check that the saved package is actually valid, not just that the bytes changed.The broader solution: repeated and empty cells
Both are the same shape — the element tree does not have a place to write to — and both are fixed at write time, not at parse time. Neither should inflate the model: expanding repeats or materialising every empty cell up front is what #762 is already dying from.
number-columns-repeated="10"run means rewriting that run as up to three nodes (before / the edited one / after) and fixing the repeat counts, then repointing the affected registry entries. Same on the row axis withnumber-rows-repeated, which is the harder one because a row split rewrites whole<table:table-row>nodes.DocumentPath::Cellposition, create the<table:table-cell>(splitting the surrounding repeat run to make room), give it a<text:p>, and register the new Element — i.e. the same splitting primitive as above, plus insertion.// TODO covered cells/// TODO mark as repeatedinodf_parser.cppsit next to it and are probably worth cleaning up in the same pass, since covered cells must never become writable targets.What the diff protocol needs
html::editaccepts one thing:{"modifiedText": {path: string}}, and it hard-requireselement.as_text(). An empty cell has no text element, so it can never appear in that map — the browser has nothing to attachdata-odr-pathto either, sincetranslate_sheetwrites empty cells as bare<td>. That means:editcan apply to a position that has no Element yet.Worth deciding whether that is a new key alongside
modifiedTextor a general "modifiedCells" that supersedes the text case for sheets.Scope
Step one is small and shippable on its own (gate + value/type handling + a round-trip test). The splitting/materialising work is the real body and only makes sense for ods until xlsx can save at all. Related: #236 (empty cells, subsumed by this), #238 (cell overflow), #762 (why nothing here may expand the model).