* Table formulas: read the Panamanian balboa B/. in numbers
Word's table formula and sort number reader ignores a currency sign or short currency word around a number; the balboa's B/. was not in the list. Add it as a whitespace-delimited sign, as Word reads it: B/. 1,250.50 and 1,250.50 B/. are numbers, while B/.1,250.50 and B/ 1,250.50 are not. Tested in the locale reader and through =SUM(ABOVE); the localization parity doc records the behaviour.
* Table formulas: read the balboa cell in the document's language
The formula test summed a B/. cell through ev, which supplies no document
or language, so a regression in locale selection could pass. Evaluate
=SUM(ABOVE) with a document whose default text is es-PA and a pl-PL
editing language: 1,250.50 is a number under es-PA and not under pl-PL,
so the assertion fails if the formula stops consulting the cell's
language. The converse case reads B/. 1 250,50 from a pl-PL document with
an en-US fallback, which English alone cannot parse.
* docs: close the parenthesis in the localization-parity change note (#3546)
---------
Co-authored-by: Francisco Herrera <14956584+alosslessdev@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* WC6-06: preserve adjustRightInd in DOCX
* WC6-06: honor grid indent defaults and XML order
* Update lockfile for DOCX test dependency
* Format DOCX ordering assertion
* Keep main lockfile for paragraph property PR
* PR 5384: tighten grid indent docs and regression
* PR 5384: match paragraph check formatting
`comment_export_discovery_scales_with_unrelated_markers` timed `notes` on 2,000 and 16,000
markers through /proc/thread-self/schedstat. That count moves only at a scheduler tick or switch,
so a 17 µs baseline read as nothing or a whole tick, and gate failed on linear code (58.68x on
PR #3228's run 41932894607). As #5608 did for the balloon anchors, the test now counts the
characters the walk visits (`wordcraft_doc::work`): 4,010 -> 32,010 steps, 8.0x, every run. A
prefix scan per marker injected into `notes` counts 63.9x and fails it.
`work::steps` becomes public so a crate that walks a paragraph's text itself can count it; outside
tests it is still nothing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: preserve numbered REF results during field updates
Adapt and validate the patch published in WordCraft #4533, retaining main's bounded numbering renderer and adding relative-context/cache guards.
Co-authored-by: Alyosha-dot-shop <218732667+Alyosha-dot-shop@users.noreply.github.com>
* M6: reuse bookmark lookup for numbered REF updates
* REF: use explicit imports for the workspace clippy gate
* #4609: adapt synthetic fixtures to shared document data
* #4609: checkpoint review fixes and isolated validation notes
* #4609: retain rendered REF labels and isolate relative context by list
---------
Co-authored-by: Alyosha-dot-shop <218732667+Alyosha-dot-shop@users.noreply.github.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* docx: keep a paragraph's text alignment
w:textAlignment (ECMA-376 17.3.1.39) was not read, so a save dropped it. Word sizes a line by it when a
list's bullet is larger than the text: without it, those lines grow and every page after them moves.
ParaProps gains text_alignment, read and carried through styles, and written back after w:jc. Kept for
the file only; the layout does not use it yet.
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
* doc, docx: keep the compound border lines
The nine thin and thick compound lines (thinThickSmallGap to thinThickThinLargeGap, ECMA-376 17.18.2)
were read as a single line and written back as one, on paragraphs, tables, cells and pages. BorderStyle
gains them; they are written back by name and drawn as a single line of the border's width, as before.
RTF export and import carry them too (the importer read four of them as double).
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
anchors_walk_dense_unrelated_markers_in_linear_work compared CPU times of
a few microseconds, so cache and timer noise could push the ratio past its
24x limit on a linear walk (43.52x in a CI run).
wordcraft-doc now counts, in tests only, the characters its paragraph
object lookups walk (objects_before, object_offsets and
objects_with_offsets), through a work::count helper behind the existing
test-support feature. The test asserts that 8x the markers take at most
12x the steps, as the layout linear-time tests do: the walk takes 2001 ->
16001 steps (8.0x), and the object_offsets + object_at walk it replaced
takes 2003001 -> 128024001 (63.9x).
- update_generated keeps what precedes a wrapped list's field (r4468529243)
- from_doc keeps objects in hidden text when hidden text is written (r4469018820)
- clipped cell lines in a turned frame are drawn once, clipped, turned (r4469018900)
- kept::while_writing works out reply map and page fingerprint once per export (r4468529227)
- odt_charts fixture declares its namespaces (r4468529259)
- guard test for character styles based on a missing parent (r4469018843, not a bug)
- odf.preset: word (Microsoft Word style, default) or libreoffice; wordcraft still read as word
- Options, CLI (WORDCRAFT_ODF), docs and ROADMAP follow the two styles
- CodeRabbit: kept groups and story fingerprints follow fill_objects; padding serde default;
list field depth; literal doubled quote; ASK/SET results not exported; dc:date offsets;
first_pages order; unique picture names; finite raise clamp; PDF rotated-text guard;
.ott in the password message; flat element bounded by depth; cell tile fallback;
ROADMAP and docs reconciled
- Remove clean-room wording from the ODT tests
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
`<p> </p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment, tracked change or index mark reaching from one paragraph
into another, or a text box continuing in another's, ties paragraphs
into a group: written as read while every paragraph of it is as read,
else all from the model, which names what ties them afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Opening draws charts from their data, which changed their paragraphs'
fingerprints, so their markup never matched. Drawing goes through
Document::fill_objects, which moves the kept markup with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section in a table's cell is kept around the cell's paragraphs and
closed in the cell. One starting or ending at a paragraph with twins
(an empty one) is kept by that paragraph's own identity; the writer
opens each kept section once, so a copy can't open it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Paragraphs and tables holding bookmarks are kept; the writer takes
the names kept markup begins and ends, so either end may be as read.
- Footnotes and endnotes: each note's text fingerprinted when read;
kept while it and the note numbering are as read.
- Formulas, charts and other objects in kept markup, with their files
and preview pictures (only while that markup is written).
- Page-anchored frames LibreOffice puts in the text before a paragraph
are kept before its markup.
- Styles of different families sharing a name are kept apart.
- The text keeps text:use-soft-page-breaks while kept markup holds them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The paragraphs a drawing's text holds count as its story's, so the
paragraph showing it can be kept. Each text box's and shape's story is
fingerprinted when read (with the boxes and notes it holds); one edited
sends its paragraph to the model. Linked boxes and a comment inside a
box's text stay with the model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph whose comments (range, replies, end) or tracked changes
(start and end) are all its own is kept as read. Each top-level comment
is fingerprinted with its replies when read; one edited, resolved,
answered or deleted sends its paragraph to the model, as a reply written
elsewhere in the file does. The change regions kept markup marks are
written as read (those their deleted content marks first), beside
WordCraft's own, which take the writer's prefix; one without a date is
written from the model. An older file's region gets its ODF 1.3 xml:id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Kept definitions are keyed by name space (a list style may share a
paragraph style's name, §16.2), so unused list styles survive a save.
- Section wrappers kept as read go around tables as well as paragraphs,
nested ones too, balanced however the blocks are edited.
- An unchanged chart keeps its replacement picture.
- Note citations use the document's number format and first number.
- draw:z-index is read into the float's stacking order and written back.
- The no-edit gate counts elements, catching a section of three lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The file's office:forms keeps its own head and each form:form with what
WordCraft doesn't model; controls are merged with the file's own. An
index whose generated entries nobody edited is written as read, with
its source settings and entry templates; an edited one is written from
the model. The no-edit gate saves nine samples of our own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More of what WordCraft doesn't change is written back as it was read:
tables; the document properties it doesn't model (template, statistics,
editing time, printing); the declarations opening the text and the
document's scripts; page layouts and master pages while the page setup
is unchanged, each section starting with the master page it started
with; named sections around unedited paragraphs. Package files and
folders kept markup names (a style's fill picture, a header's embedded
object) are written with it, with their manifest entries.
Files are saved as ODF 1.3; markup kept from an older file is brought to
1.3's meaning (a gradient's or hatch's unitless angle was tenths of a
degree). WordCraft's own drawings state style:flow-with-text, which the
standard leaves open.
odt::tests_more::lossless saves seven samples of our own without edits
and fails on anything lost (nothing is allowed), and checks that edits
keep the saved parts well-formed.
Over the 133-file corpus the round-trip difference falls from 191k to
51k pixels; 95 files come back pixel-identical (62 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph nothing changed since it was read (by a fingerprint of how it
was modelled) is written with its own markup, unknown elements and
attributes included, together with the automatic styles, common styles,
font faces, namespaces and package files it names. While no style or list
definition changed, styles.xml's office:styles is the file's own (unused
list styles, note and line numbering configuration, attributes WordCraft
doesn't read); drawing definitions and the default graphic style stay
WordCraft's, as drawings are written from the model.
Paragraphs holding drawings, comments, tracked changes or ranged marks,
or starting a page, are written from the model, as is everything edited.
Kept markup lives in memory only (wordcraft_doc::kept, 64 MB at most) and
is never part of the document's content.
Over the 133-file corpus the round-trip difference in LibreOffice falls
from 528k to 191k pixels; 62 files come back pixel-identical (46 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.
LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
User fields (text:user-field-decl, text:user-field-get: LibreOffice's
report templates, 58 documents of the corpus) were read as custom
document properties and written back as meta:user-defined properties
with text:user-defined fields: the look and values stayed, the kind
didn't. Custom properties now remember being user fields
(CustomProp::odf_user_field) and are written back as
text:user-field-decls with their values and text:user-field-get fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by comparing WordCraft's own documents in LibreOffice and
bisecting settings.xml (observed only, black-box):
- The settings set's name, ooo:configuration-settings, is a qualified
name: without xmlns:ooo declared LibreOffice ignored every setting
WordCraft wrote (its Word layout rules, tab measuring).
- A set LibreOffice reads must be complete: a setting missing from it
gets its old pre-OpenOffice value, which changes the layout. WordCraft
now writes LibreOffice's current layout defaults (as a new LibreOffice
document has them), overlaid with Word's layout rules for documents
WordCraft lays out as Word does (not for ones opened from ODF), and
its own choices.
- How the space between paragraphs adds up follows a file's
AddParaTableSpacing (false: the larger, as Word); unstated, the ODF
profile's new sumParagraphSpacing (WordCraft: the larger, LibreOffice:
the sum). WordCraft writes it as it lays the document out.
WordCraft-origin documents in LibreOffice: 142k -> 80k differing pixels
over 42 documents; the LibreOffice corpus: no regressions, 40k -> 39.6k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice's elegant agenda puts a small gradient picture behind cells
(§17.3 style:background-image in a table-cell style). Cells can now
carry a background picture (CellProps::picture: media, own size,
repeat none/tile/stretch, alignment), which layout draws over the
cell's shading and under its text, clipped to the cell (Placed::Clip of
Placed::Image; tagged as an artifact in PDF). The ODT reader reads it
(style:repeat, style:position, the picture from the package) and the
writer writes it back with the picture in the package (once per
picture).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).
- Table styles: ODF has no table styles of Word's kind, so each cell now
states what the style gives it (fills, its borders, bold header text,
its paragraph spacing), under the paragraph's own styles as layout
applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
table_char_patch). The style's border mapping moved to
styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
single spacing changed their text.
- Columned sections say whether their columns end balanced
(text:dont-balance-text-columns, §20.428, read as its name says; its
value descriptions contradict it). Unstated, the ODF profile decides
(balanceSectionColumns: LibreOffice balances, WordCraft as Word).
SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
an invisible frame keeping the graphic's box), which LibreOffice draws
as WordCraft does.
LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODF 1.3 §20.206–20.209 define page margins by referring to XSL's
(§7.10), without saying whether XSL's initial value 0 comes with the
reference; LibreOffice uses 2 cm. The margin a page layout leaves out
is now the profile's `pageMargin`: WordCraft 0 (XSL's), LibreOffice
2 cm, or custom (File › Options › Advanced › OpenDocument files,
`file.options` `odf.pageMargin`). A stated margin is the file's under
every preset. With the LibreOffice preset, LibreOffice's CV, sans-serif
business letter and one-page résumé templates match LibreOffice again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- File › Options › Advanced › OpenDocument files: the preset is a
drop-down like Measurement units and Region; shape line width is in
points (a 0.25 pt hairline showed as 0.00").
- WordCraft's preset keeps a header's or footer's stated spacing when
its style doesn't say whether it is dynamic: with no height stated,
growing into it always dropped the spacing the file states. Custom
profiles can still grow into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).
Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.
- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
ROADMAP updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Form controls:
- Placement: a control hanging from the baseline (as LibreOffice places
those it makes) was saved with no placement. LibreOffice then puts it
at the line's top, so every control row shrank. Its raise is now kept
(`ContentControl::form_raise`, read as in-line frames are) and
written, standing on the baseline when it had none.
- Multi-line fields: these were saved without LibreOffice's `MultiLine`
property, so they showed one centred line. They now carry it.
- Text fields get LibreOffice's flat border.
- Also: `pt()` no longer writes "-0pt".
Custom shapes:
- A shape drawn from its own geometry was saved as WordCraft's path of
it, and a preset WordCraft draws as a rectangle (a cloud) was saved as
a rectangle. The geometry as read (type, path, equations, handles) is
now kept (`ShapeExtra::odf_geometry`) and written back, mirrored as
the shape now is.
- `shape.change` and the ink-to-shape conversion drop the kept geometry,
since the shape is then another kind. `ShapeExtra` holds it as an
`Arc<String>` to keep `Placed::Shape` small.
- The reader's `end` hands skipped elements to `end_skipped`.
Results:
- The forms document round-trips pixel-identical (2,949 pixels before),
with its text the same.
- The shape presets fall from 2,467 to 1,220. The rest is LibreOffice's
hairline outline, which WordCraft keeps as 0.5 pt.
- Across the corpus the round-trip difference falls from 45k to 41k
pixels. 42 of 125 documents round-trip pixel-identical, 92 with their
text the same, and none gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC