Repository navigation
Conversation
MathRadical::new left degree_present unset, so a square root from add_equation, from LaTeX
\sqrt{x} or MathML <msqrt>, wrote an m:rad without m:deg. CT_Rad requires it (ECMA-376
Part 1, 22.1.2.88), and Word writes it empty. A radical read from a file keeps what it had.
Part of tensorbee#432, case 1.
…d a space create_footnote_at and create_endnote_at stored w:footnoteRef / w:endnoteRef as a raw run child at boundary 0, which the run writer puts before w:rPr, so footnotes.xml and endnotes.xml broke CT_R. The mark now sits after w:rPr and a space run follows it, as Word writes. A notes stream with no entry yet starts with the separator and continuation separator notes at ids -1 and 0, as Word 2010 and later write them. Note ids still start at 2, and a notes part that already has entries is left as it is. The text of a note rdocx creates now starts with that space, so tests reading it back, and fixtures that patched the first entry of the notes part, are updated. Part of tensorbee#432, case 2.
…tyle Links written for the caller had no run properties, so they read as plain text in Word and LibreOffice. Document::ensure_hyperlink_style returns the character style with the id Hyperlink, or else named Hyperlink, and adds Word's built-in definition (colour 0563C1, single underline) when neither exists. Internal and heading links, add_hyperlink_to_story, the HTML and Markdown importer and the binding's Paragraph.add_hyperlink set it on the link run. The low-level Paragraph::add_hyperlink is unchanged. Part of tensorbee#432, case 3.
merge_cells concatenated the content of every merged cell, so the lone empty paragraph of an empty cell stayed as a blank line above or below the text. As Word does, an empty cell now adds nothing. When every merged cell is empty, the first cell's paragraph stays. Part of tensorbee#432, case 4.
…rt_column make formatted_empty_cell copied w:tcPr only, so text set into a new cell lost the alignment and the bold of its row or column. The new paragraph now takes the w:pPr of the source cell's last paragraph, without list numbering and tracked changes, and its mark takes the mark's run properties, or else those of the paragraph's last run, as Word's Insert Row and Insert Column do. The cells split_cell adds follow the same rule. Part of tensorbee#432, case 5.
prepare_staged_package renumbered authored definitions and instances by first use. A mutation that publishes its reopened candidate, such as create_footnote or add_picture, then committed the renumbered model, and an id add_numbering_definition had returned named the other definition, so a later bullet list came out numbered. The renumbering now runs in prepare_staged_output only, on the staged copy that is written, so equivalent construction orders still save identical packages and the live ids stay as returned. Part of tensorbee#432, case 6.
With no heading yet, insert_toc wrote the begin, separate and end marks in the title paragraph, and rebuild_toc, which replaces the result with whole paragraphs, refused it once headings existed. An empty paragraph after the title now holds the end mark, the shape insert_toc writes when there are headings. Harness: feature_showcase and report insert their TOC before their headings. Their word/document.xml gains that empty paragraph and their pdf/bytes entry changes with it. The pdf/pages and pdf/resources entries and the page-one PNG are unchanged, as the paragraph draws nothing. The baseline is updated in this commit for these four entries. Part of tensorbee#432, case 7.
replace_text, try_replace_text and rdocx replace also rewrote the strings of each chart's cache, which rdocx text does not show, so their count differed from the text and an --expect taken from it was refused. The cache then disagreed with the embedded workbook, which kept the old label. Literal replacement now searches what regex replacement already searched, and chart data stays with replace_chart_data. Part of tensorbee#432, case 8.
The rdocx and rdocx-oxml archives change with tensorbee#432. The other crates keep their rows.
…roperties set_cell_grid_span, and the row omissions that share its check, took a cell as empty only when its paragraph equalled a bare CT_P, so an empty cell Word leaves in a right-aligned column, <w:p><w:pPr><w:jc w:val="right"/></w:pPr></w:p>, was refused with "horizontal merge cannot consume a nonempty cell". The check now uses the emptiness merge_cells applies since case 4: one paragraph without content, whatever its paragraph properties and its mark's run properties. The first cell's paragraph stays. A cell holding text is still refused. Part of tensorbee#432, case 4.
The rdocx archive changes with the grid span fix. The other crates keep their rows.
The rdocx archive with the grid span follow-up of tensorbee#441 on top of the integration. Tests, python3 scripts/readme_doctests.py --record-measurements Harness, unchanged, measurements only
…n empty cell with properties)
The absorbed cell of set_cell_grid_span or Cell.merge may hold one empty
paragraph that carries properties, as Word leaves in an empty cell of a
right-aligned column or as insert_column now copies them: rdocx refused it
("horizontal merge cannot consume a nonempty cell", also on v0.16.4). Carried
as a test of tensorbee/rdocx#441, failing on e00c8f3c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The Word release includes adapted markup/import repairs from this reference work. It preserves schema-valid radical order, authored note markers and special records, supported hyperlink/numbering identities, new-cell paragraph-mark formatting. TOC insertion order and chart-cache/workbook text-replacement exclusions are also covered under the documented bounded contracts. Thank you @hadim for contributing the reference implementation. |
|
Thank you @hadim for the schema/authoring reference work. All eight #432 cases were integrated through F-X277 contract and evidence and its children, with ordered notes and radicals, stable numbering identities, preserved table paragraph formatting, corrected TOC lifecycle and visible-text replacement boundaries. The installed consumers, native/schema checks and successful integrated CI qualified the integrated result. Closing this PR as superseded by eb980e94 in Word 0.17.0, not merged unchanged. Imported unsupported omissions keep their preservation contracts rather than being normalized during unrelated edits. |
Fixes #432
This PR fixes the eight cases of #432. The markup rdocx writes now matches what the schema requires or what Word writes, and each case script prints only PASS on this branch.
add_equationwrites an emptym:deg.w:rPrcomes beforew:footnoteRef/w:endnoteRef, and a space run follows it. A new notes part starts with the separator and continuation separator notes.Cell.mergedoes not carry the lone empty paragraph of an empty cell into the merged cell.insert_columnandadd_rowcopy the paragraph formatting and the paragraph-mark formatting of their source cell.add_numbering_definitionandadd_numbering_instancereturn still name the same definition or instance after a footnote, a picture or any other edit that republishes the package.rebuild_toc()fills a TOC thatinsert_toc()wrote before the document had headings.replace_text,try_replace_textandrdocx replaceleave chart labels alone, so their counts matchrdocx text.Cause
MathRadical::new(crates/rdocx-oxml/src/math.rs) setdegree_present: false, sowrite_xmlemittedm:degonly for an n-th root.CT_RadisradPr?,deg,e(ECMA-376 Part 1, 22.1.2.88).create_footnote_atandcreate_endnote_at(crates/rdocx/src/document.rs) stored the reference mark as a raw run child at boundary 0.CT_R::to_xmlwrites that boundary before the typedw:rPr. The note text followed the mark directly, and a new notes part held only the real notes.Paragraph::add_hyperlinkandadd_internal_hyperlinkcreate a plain run. None of the callers that write a link for the user gave it a style:append_internal_hyperlink_staged,add_hyperlink_to_story, the HTML and Markdown importer, and the binding'sParagraph.add_hyperlink.Table::merge_cells(crates/rdocx/src/table.rs) concatenated the content of every merged cell, including the empty paragraph of an empty cell. The case 4 script in rdocx: markup the library writes breaks the schema or differs from Word (equations, notes, hyperlinks, cells, numbering, TOC, replace) #432 read<w:p/>and the paragraph after it as one paragraph. That hid the "first cell empty" case, which also kept an empty paragraph (['', 'Sum']). This PR fixes both orders, and the Tests section uses a corrected script.formatted_empty_cellcopiedw:tcPronly, and the new cell held a bareCT_P::new().canonicalize_numbering_idsran inprepare_staged_packageand renumbered authored definitions and instances by first use in the body. Saving works on a clone, so the live ids stayed as returned. An edit that publishes its reopened candidate (create_footnote,add_pictureand others) then committed the renumbered model, and a held definition id named the other definition.insert_toc_in_placeput the begin, separate and end marks in the title paragraph.rebuild_tocreplaces the result with whole paragraphs and refuses a result that stays inside one paragraph. The regression tests pin that refusal (toc_rebuild_accepts_a_single_paragraph_tocand two tests on opaque entry owners), so this PR changes whatinsert_tocwrites and leaves the refusal in place.replace_in_xml_partsranreplace_many_in_chart_xmlon every chart part. That rewrote only the chart cache, so the cache and the embedded workbook disagreed. The count also included labels thatrdocx textdoes not show.Change
One commit per case, each with its test, then the archive measurements. A follow-up commit for the grid span and its own measurements commit come after.
math.rs:MathRadical::newsetsdegree_present: true, so a square root writes an emptym:degafterm:radPr. Radicals read from a file keep what they had.document.rs, notes: the reference mark moves to raw boundary 1, afterw:rPr, and a" "run follows it, as Word writes. When the notes stream has no entry yet,word_note_separatorsadds the separator and continuation separator at ids -1 and 0, the ids Word 2010 and later write. They carry Word'sw:spacing w:after="0" w:line="240" w:lineRule="auto". Note ids still start at 2, so no id changes, and a notes part that already has entries is left as it is.document.rs, links: the new publicDocument::ensure_hyperlink_style()returns the id of the character style with the idHyperlink, or else the one namedHyperlink. When neither exists, it adds Word's built-in definition (colour0563C1, single underline, priority 99,unhideWhenUsed).append_internal_hyperlink_staged(used byadd_internal_hyperlink_atandadd_heading_hyperlink_at),add_hyperlink_to_story, the HTML and Markdown importer, and the binding'sParagraph.add_hyperlink.Paragraph::add_hyperlinkandadd_internal_hyperlinkare unchanged.table.rs, merge: an empty cell adds nothing to the merged content. "Empty" means one paragraph without content, ignoring its properties and its root attributes. When every merged cell is empty, the first cell's paragraph stays.table.rs, grid span (follow-up to case 4):set_cell_grid_spanand the row omissions (w:gridBefore/w:gridAfter) treated a cell as empty only when its paragraph had no properties at all. They refused an empty cell that Word leaves in a right-aligned column,<w:p><w:pPr><w:jc w:val="right"/></w:pPr></w:p>, with "horizontal merge cannot consume a nonempty cell". This failed on main too.cell_is_discardable_for_grid_editnow uses the same emptiness test asmerge_cells: one paragraph without content, whatever its paragraph properties and its mark's run properties. The first cell's paragraph stays, and a cell holding text is still refused.table.rs, new cells:new_cell_paragraph_propertiesgives the new paragraph thew:pProf the source cell's last paragraph, without list numbering, section properties and tracked changes. The paragraph mark takes the source mark's run properties. If the source mark has none, it takes those of the source paragraph's last run, the formatting that text typed at its end would continue. This coversadd_row,insert_columnand the cellssplit_celladds.document.rs, numbering:canonicalize_numbering_idsnow runs inprepare_staged_outputonly, on the staged copy that is saved, serialized or read throughpart_xml. Saved packages are unchanged, so equivalent construction orders still save identical packages (equivalent_construction_orders_allocate_declared_stable_identifierspasses). The live model keeps the ids the facade returned.document.rs, TOC: with no heading,insert_tocadds an empty paragraph after the title to hold the end mark. This is the shape it already writes when there are headings.document.rs, replace: the chart pass is removed fromreplace_in_xml_parts, and thereplace_textandreplace_regexdocs say so. Indocs/hld/10-bindings-spec.md, the two paragraphs on the literal replacement scope are updated.Behaviour changes
Document::footnotes()and the binding'snote.paragraphs[0].textread" note content". Three Rust tests and three Python tests that read note text back are updated for it.footnotes.xml, which is now the separator. They now patch the first normal note, through a smallreplace_in_first_notehelper.Limits
settings.xmldoes not get thew:footnotePr/w:endnotePrlists that Word writes for the separators. The schema makes them optional, and Word and LibreOffice find the separators byw:type. As before, the note paragraph does not take the FootnoteText or EndnoteText style.Paragraph::add_hyperlinkstays unstyled unless the caller sets the id fromensure_hyperlink_style()on the returned run.rebuild_tocstill refuses a single-paragraph TOC field written by another producer, as on main. Word writes one when it found no entries.Tests
crates/rdocx/tests/regression_test.rs, one test per case. Each fails on main:issue432_a_square_root_equation_writes_the_degree_its_schema_requires: LaTeX\sqrt,\sqrt[3]and MathMLmsqrt.issue432_notes_write_the_mark_run_properties_first_with_separators_and_a_space: both parts, and a second note adds no separator.issue432_links_written_by_rdocx_take_the_hyperlink_character_style: a heading link (the style is added), Markdown with a template whose Hyperlink style is kept, HTML, and a story link.issue432_merging_with_an_empty_cell_keeps_no_blank_paragraph: five cases, including both cells empty.issue432_a_cell_holding_only_paragraph_properties_merges_as_an_empty_cell:set_cell_grid_span_checkedabsorbs a cell whose paragraph carries onlyw:jcand a markw:b, and still refuses a cell with text.merge_cellsdrops that cell horizontally and vertically.issue432_cells_added_by_insert_column_and_add_row_keep_paragraph_formatting.issue432_a_numbering_definition_id_names_its_definition_after_a_republishing_edit: the live ids, and the list format of each paragraph in the saved package.issue432_a_toc_inserted_before_any_heading_rebuilds_once_headings_exist.issue432_literal_replacement_leaves_chart_labels_to_their_workbook.crates/rdocx-py/tests/test_core.py:test_issue432_python_links_take_the_hyperlink_character_stylecovers the binding's ownParagraph.add_hyperlinkpath.test_issue432_a_cell_with_only_paragraph_properties_merges_as_emptyruns the follow-up repro throughTable.set_cell_grid_spanand throughCell.merge, horizontally and vertically, and checks that a cell with text is still refused.regression_test(191 passed), therdocx --libtable tests (52 passed), the matching Python tests intest_core.pyandtest_formatting_tables.py, clippy, fmt, prose and the hash harness all pass. In Python, 493 passed. The 3 failures are fx200, which needsRDOCX_CLI, and two fx268 cases that also fail on a debug build of main. The harness gives 49 entries matching, and the last commit re-records only therdocxarchive row.RUST_MIN_STACK=8388608:cargo test -p rdocx-oxml: 760 passed.-p rdocx-layout: 355 passed, 1 ignored.cargo test -p rdocx --test regression_test: 2477 passed, 7 ignored.--test integration_test: 467 passed, 4 failed. All four fail on this machine's LibreOffice 26.2.6.3, against the pinned 26.2.5.2.--lib: 821 passed, 1 failed. The failure isfx195_variable_svg_paint_matches_independent_static_instances, which needshb-subset14.4.0.body_lookup_work_is_linear_at_250_and_1000was skipped, as it runs for more than half an hour here.cargo clippy -p rdocx-oxml -p rdocx -p rdocx-py --all-targets -- -D warnings,cargo fmt --all --checkandscripts/prose_check.py: clean.-n 8, with python-docx 1.2.0, overtest_core.py,test_formatting_tables.py,test_python_docx_parity.py,test_shared.pyandtest_rendering_threads.py: 1154 passed, 2 skipped, 62 failed. None of the failures comes from this branch:F_X195_*oracle environment.test_fx200_python_style_saved_output_paints_exact_expected_colours_with_deterministic_native_clineeds the coordinator-authenticatedRDOCX_CLI.test_fx268_picture_normalization_reuses_actual_checked_roundscases fail on a debug build of main as well (KeyError: 'producer.seed_eligible').scripts/hash_harness.py --check: 49 entries match after every commit. The one intentional delta is in the TOC commit, which also updates the baseline:insert_tocbefore they add their headings, so theirword/document.xmlgains the empty paragraph and theirpdf/bytesentry changes with it.pdf/pagesandpdf/resourcesentries and their page-one PNG are unchanged, since the paragraph draws nothing.scripts/readme_doctests.py --record-measurementsin the last commit: only therdocxandrdocx-oxmlarchive rows change.cell.paragraphsinstead of the regular expression, as explained under Cause.