* fix(fields): quoted-argument backslash escapes and body PAGE in a cardinal-text section
A backslash inside a quoted field argument makes the next character literal, so TITLE "x \* Upper" shows x * Upper as Word does. A body PAGE field now uses the section's page number format (cardinal text shows words), as headers and footers already did.
* Fields: PAGE in a body table follows the section number format; parity docs
* fmt
* docs: clarify quoted field escape rule
* Fix page fields in fitted text boxes
* Move fitted cache test after layout helpers
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A list level, page number or note mark in Word's thaiNumbers format was
read and saved as such but drawn as decimal (1, 2, 3). It now shows the
decimal number in Thai digits (U+0E50..U+0E59): ๑, ๒ … ๑๐, with the
level's text around it (๑., (๒)). Thai letters and Thai counting still
show decimal until they are drawn too.
Part of #5757 (Thai numerals in automatic numbering).
* ODT gap 19: a watermark change to a file is saved
A file's own page definitions were written back whatever happened to its
watermark, so one added, changed or removed in WordCraft was gone from the
saved file. The watermark now counts as part of the page setup, so a change
writes the page definitions from the model. A watermark with no text isn't
written, so it isn't a change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT gap 19: Different Odd & Even Pages changed in a file is saved
A file's own page definitions were written back whatever happened to this
setting, so the left pages' header stayed after it was turned off, and turning
it on did nothing. The setting now counts as part of the page setup, as the
watermark does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT tests: say what the watermark and odd/even tests check (gap 19)
Doc comments for CodeRabbit's docstring coverage check on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
The #3642 guard refused a file whose DIFAT lists more FAT sectors than
its sectors need: one FAT sector per sector_len / 4 sectors plus one
spare. Real writers reserve FAT for growth, so a real encrypted
package — a compound file of 78 sectors — lists 4 FAT sectors and was
refused: encrypted .docx files stopped opening with "the DIFAT lists
4 FAT sectors in a file of 78 sectors" (#5109, broken since the guard
landed).
Every FAT sector listed is a sector of the file, and none of them
twice (checked next), so a real file lists no more FAT sectors than
the file has. The refusal bound is now that: more than num_sectors is
either duplicates or names cfb refuses anyway, and the FAT cfb builds
(one sector of entries per listed name) stays within about the file's
own size — the #3642 blowup listed 2048 × 127 names in a ~1 MB file
and is still refused (260,096 > 2,048).
Co-authored-by: Bra1nZ <bra1nz@astraoffice.com>
* 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