* fix(docx): collect an embedded object's relationship ids in a set on save
* style: rustfmt
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Charts: an automatic series colour follows the series' c:idx (#5963)
A series without its own colour took the accent for its place in the
file. Word picks it by the series' c:idx (c:order doesn't matter), so a
chart whose series aren't numbered 0, 1, 2... in file order, such as a
lone series with idx 2 after a deletion, got different colours.
c:idx is read strictly as the unsignedInt the schema says; a missing or
invalid value falls back to the place in the file.
* Charts: the renumbered test helper fails if ser's layout changes (#5963)
---------
Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
## What changed
Word stores paragraph indents (`w:ind` left/start, right/end, hanging, firstLine) in 16 signed bits and paragraph
spacing (`w:spacing` before/after) in 16 unsigned bits. A file that writes a larger plain integer is read by Word
modulo 2^16: `w:left="66256"` is a 720-twip (0.5") indent in Word. We read the full value, so such a paragraph was
indented by about 46 inches and its text was pushed off the page.
- `crates/docx/src/units.rs`: new `tw_i16` / `tw_u16` readers. A plain integer keeps its low 16 bits (two's
complement for indents, so 66256 -> 720 and 65535 -> -1); values with a unit or a fraction read as before.
- `crates/docx/src/read/props.rs` and `read/styles.rs` use them for `w:ind` and `w:spacing` before/after, in direct
and style formatting.
- In-range values read exactly as before.
## Testing
- `crates/docx/tests/it/twips_wrap.rs`: synthetic documents with out-of-range indents and spacing in paragraph and
style properties check the wrapped values; in-range values and values with units are unchanged.
- Unit tests for the two readers; `cargo xtask ci` passes.
Found by a real-file corpus comparison against Word's own PDFs: a file whose indents exceed 16 bits shows its text on
the page in Word and off the page here.
Co-authored-by: tpawley2001 <268951182+tpawley2001@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Word treats the end-of-cell mark as a paragraph in the default (Normal)
style, so a cell's last paragraph with contextual spacing in that style
(or in an undefined style, formatted as Normal) loses its space after, as
beside any same-style neighbour; one in another style keeps it.
`layout_box` kept the space after of every last cell paragraph except
automatic spacing, so the row grew by it and the text below the table sat
that much lower (docxide-pdf cell-end-text-context, cell-end-empty-context:
8 pt).
A middle or bottom cell of a row split across pages lines up with the
other cells' kept text, which `align_in_part` counted with its full space
after; it now stops at the cell's content end, so a dropped space after
(contextual or automatic) no longer pushes the aligned cell down.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* layout: line numbers take the "Line Number" style, not a fixed 10 pt grey
Line numbers were laid out in 10 pt grey in the default font, whatever
the document said. Word formats them with the "Line Number" character
style over the document's defaults: in a manuscript whose defaults are
Aptos 12 pt, Word's PDF shows them in Aptos 12 pt black (measured in
Word for Mac), WordCraft's in 10 pt grey.
Lay the number out with the document's "Line Number" style when it has
one, else its plain defaults, so size, font and colour follow the
document.
Test: line numbers take the document's default size and automatic
colour, and a "Line Number" style's size and colour when there is one.
* docs: progress log and parity entries for line-number-style
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Word for Mac sets raised and lowered text at 0.65 of the run's size
rounded to the half point (measured from digit advances in its PDF
export at base sizes 8 to 36 pt: 11 pt -> 7, 12 -> 8, 24 -> 15.5).
WordCraft used 0.65 exactly, so lines with superscripts were up to
0.5 pt wider and some broke a word early.
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
## What changed
The DOCX writer's content-control planner (`crates/docx/src/write/sdt.rs`) decides whether a control that
covers a whole paragraph is written block-level (`w:sdt` around `w:p`) or run-level (inside the paragraph).
It only treated the control as covering the whole paragraph when nothing but control-end markers followed
its end. Bookmark, comment and permission range markers after the end now count as "nothing" too, so the
control stays block-level and the markers stay at the end of its paragraph.
## Why
Word writes a body-level `_GoBack` bookmark (`<w:bookmarkStart w:name="_GoBack"/><w:bookmarkEnd/>` directly in
`w:body`) in nearly every file it saves. When the last body-level content control was followed by it, the
reader put the bookmark at the end of the control's paragraph, and the writer then turned the control (and
any controls nested in it) into run-level `w:sdt`s inside that paragraph. For a repeating section
(`w15:repeatingSection` / `w15:repeatingSectionItem`), which must wrap whole paragraphs or rows, Microsoft
365 Word then refuses to open the saved file ("the file appears to be corrupted"). Plain nested block
controls changed what they cover the same way.
Found by the real-file corpus: Word reported our saved copy of a form with a repeating section as corrupted.
## How it was tested
- New tests in `crates/docx/tests/it/nested_block_controls.rs` (synthetic packages): a repeating section around
one paragraph, plain nested block controls, nested block controls in a table cell, a run-level control that
must stay in its paragraph, and repeating sections / block controls followed by a body-level bookmark (with
and without data bindings) all round-trip with the controls at the same level and the text kept.
- `cargo xtask ci` passes.
Co-authored-by: tpawley2001 <268951182+tpawley2001@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #5843: resolve LibreOffice semicolon-separated font fallback lists to first installed family
* #5843: address review comments on font fallback list resolution
* #5843: verify fallback list text on cache hit and accept Times New Roman in tests
* #5843: bound the fallback lists read from a DOCX and reuse their classification
A document could name thousands of fallback families in `w:ascii`/`w:hAnsi`.
Nothing bounded that value, and the name-based rules (roman classification,
Thai family, named substitutes) then scanned every candidate for each shaped
piece, so opening the document took seconds.
`w:ascii` and `w:hAnsi` now keep the leading families within MAX_FONT_NAME
bytes, cut at a `;` so no name is split, as `w:cs`/`w:eastAsia` already bound
their names. The family a real list would pick is kept: a LibreOffice list
names a few families, well under the bound.
The Thai-family and named-substitute answers are worked out once per list and
kept in the fallback-list cache, keyed by the list text, so shaping no longer
rescans candidates for every piece. Resolution still reads every candidate.
A 500-paragraph document whose font names 20,000 candidates laid out in 10.8 s;
bounded on read it takes 17 ms.
Tests:
- a hostile fallback list keeps its leading families, cut on a `;`
- a real LibreOffice list is read whole
- a later Thai or named fallback still classifies, and lists are cached
The web shell queues the page's calls as control requests (the channel the desktop control server uses) and answers them on the next frame. The API is installed in every mode; opening, saving, the file name and the unsaved flag stay with the host messages.
Text nodes were decoded without line-end normalisation, so a file
saved with Windows line ends kept the CR of each CR LF: a line break
inside w:t read as two spaces. Text and CDATA now go through
quick-xml's xml_content, which reads CR LF and a lone CR as one line
feed; a CR written as stays a CR. Attribute values were already
normalised.
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A .doc bracket or brace AutoShape (MSOSPT 85-88, 185, 186) is drawn as
its box, and with no fFilled property it got the default white fill, an
opaque block over the text behind it. Word's shapetypes for these shapes
are unfilled by default (VML filled="f"), so they now get a fill only
when fFilled is set.
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Word drops the space before of an empty paragraph that follows a document's
leading floating table: its PDFs of docxide-pdf's leading-empty-before-float-0
and -12 put the text after the empty anchor (beside the table) at the same
place for 0 pt and 12 pt before.
Flow::gap_before (crates/layout/src/flow.rs) kept it: a floating table
takes no room in the flow and clears `pb.prev`, so the paragraph after it
was laid out like a document's first paragraph, with its full space before.
It now drops it for body block 1 when block 0 was placed floating on this
page (the page's float_tables), the page is fresh and the paragraph holds
nothing but markers (bookmarks, comment and permission ranges). It
doesn't check that the paragraph sits beside the table, so below a
full-width leading float an empty paragraph loses its space before too;
that case is untested. Later pages and columns, and paragraphs with text,
keep their space before.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* picture.change: keep the width and take the new picture's aspect ratio
Change Picture swapped only the media key, so a replacement of another
shape was stretched into the old frame (a revised figure came out ~2%
taller in a real manuscript). Keep the display width, set the height
from the new picture's pixel size, and drop the old crop, which
described the old picture. Regression test included.
* docs: progress log and parity entries for picture-change-aspect
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A floating table with tblOverlap=never that is moved below another floating
table steps in to its distance from the text's left edge only in Word 2013
layout (compatibility mode 15). clear_of_float_tables read the mode from the
legacy shift, the first cell's left margin before mode 15, being zero, so a
mode 12 table whose first cell has no left margin was taken for Word 2013
layout and moved 9 pt right (docxide displaced-anchor-*-compat12-overlap0-*:
Word 72 pt, WordCraft 81 pt). Pass the mode itself.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
UAX #14 allows a break between a full stop (IS) and an opening
parenthesis (OP), so `break_positions` (crates/layout/src/para/paragraph.rs)
let "29.05.2023г.(оформление ЗУ)" and "2023.(Example detail)" wrap after
the full stop. Word keeps such a token whole and wraps it at the space
before, as the docxide-pdf dot-open-token* Word references show. The
break between the two is now dropped (before the kinsoku pass, so
break-anywhere wrapping still applies).
Test: layout::tests::a_full_stop_before_a_parenthesis_is_no_break.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A row is split across pages from its flattened items, so a row of a nested
table marked w:cantSplit was cut between its lines like any other text in
the outer cell. Word leaves such a row whole: the outer row moves to the
next page whole when that row comes first in it (docxide-pdf
nested-first-row-prefix300/330-cant1), and otherwise splits above it
(inferred).
table_flow now moves the cut up to just below the top of the highest nested
Can't Split row it runs through (nested_keep_cut): the outer row's bottom
band below that top, so the text and nested rows ending there stay on the
page. A row runs through the cut when it ends below the room left for text,
as in the split itself; one walk over the rows from the lowest top up moves
the cut until none does. A vertically merged cell doesn't stretch its row's
band, and a cell aligned to the middle or bottom is measured from its top, as
the split runs it. This applies to the first part of a row and to the kept-chain and
header-row checks. On a fresh page, where moving on gains nothing, a nested
row taller than the room still splits at the room rather than running into
the bottom margin.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Proofing: Check spelling as you type is one setting for every open document (#5740)
Each open document's view kept its own copy of the setting. Turned off with one document active, it stayed on in the others, and the settings saved for the next run took the value of whichever document was active then, so it came back on. A document made active now takes the saved preference, as Read Mode's page colour already does.
* Contributors: credit ashokram-si as Ashok Ram
* Proofing: every view made active follows the app-wide settings, a window's view put back too (#5740)
put_view restored a window's view with the Check spelling as you type value it had when it was taken, after switch_to had applied the shared preference, so the settings saved for the next run could take the old value again. One ViewState::follow_prefs now applies the app-wide settings a view keeps a copy of (Read Mode's page colour and Check spelling as you type) wherever a view is made active: switch_to, a new document's view, set_document and put_view.
* test(ui): Check spelling off in a second window stays off in every window (#5740)
The whole sequence through the app: View › New Window, spelling turned off in the second window, the first window's own view put back (put_view), a new document's window, back to the second window, then the saved settings read by a new app. Fails if put_view doesn't follow the setting.
---------
Co-authored-by: ashokram-si <347905169+ashokram-si@users.noreply.github.com>
Word 97-2003 documents were laid out with the model default of mode 15. Word opens them in mode 11, which changes justification and line breaking.
Test: a_doc_opens_in_word_97_2003_compatibility_mode
Rejecting an inserted paragraph (or accepting a deleted one) whose mark carries a section break
left it empty; remove_empty_para refused it for the break, so the next paragraph was joined into
it and took its properties (style, numbering, indents). The empty paragraph now goes whole and
its section joins the following one first, as deleting the break does (remove_section_break).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
In a section of several text columns the horizontal ruler was one white
strip from margin to margin. The space between each two columns is now
shaded like the margins, so the ruler shows where each column ends and
the next starts.
The edges of a space can be dragged, through layout.columns, one undo
step per drag, to change the spacing. Equal columns stay equal, so every
space changes with it, as in Word for Mac. Between unequal columns only
the column at that edge and the space change. As in Word for Mac, the
middle of a space has no grip of its own. Columns stay at least a
quarter inch wide and spaces 0-300 pt, as the Columns dialog allows,
snapped to 1/16 inch. Right-to-left sections are read from the right
edge.
Outside the main text, outside Print Layout and in a table the ruler is
unchanged. A selection across sections shows no spaces, since a drag
would set the columns of each section it touches; nor does a page the
caret isn't on. Columns from a file that the command wouldn't take back
(over 12, a column under a quarter inch, a space over 300 pt, wider than
the text) are shaded but not dragged. A tab stop's marker keeps its own
drag where it sits on an edge. The indent markers still follow the page
rather than the caret's column; that is #2196 (PR #2215).
Fixes#5197.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Layout: a picture in a paragraph whose hidden mark closes a section keeps its room (#5288)
A paragraph with a hidden mark and no plain text was a quiet mark: laid out
with no room, its drawing kept. An in-line picture adds no plain text, so a
hidden-mark paragraph holding only a picture qualified, and the text after it
(the next continuous section in the report) was drawn over the picture. Such
a paragraph, or one holding a note reference mark, now keeps its line; one
with only floating objects stays quiet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: a legacy check box in a hidden closing mark keeps its room too (#5288)
A FORMCHECKBOX field has an empty result, so a hidden-mark paragraph holding
only a check box still counted as quiet and the next continuous section was
drawn over the box. Fields now count as visible content when the paragraph
layout shows them as something other than their result: their code, a legacy
check box, or a drop-down's entry (the same test as the layout's `field`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Count locked SEQ fields and not tracked-deleted ones in output
Exporting, Word counts a locked SEQ, its \r too, though the field keeps its result, and a SEQ in a tracked deletion keeps its result and doesn't count, with markup shown or hidden (measured in Word for Windows). Output counted it the other way round: a locked SEQ was skipped and a deleted one counted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: locked and tracked-deleted SEQ fields in layout parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* count_seq walks the deleted ranges with one cursor
The runs come in order and the objects by offset, so a forward cursor
replaces the per-field scan of every deleted range.
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>
Word wraps "Dataprotection-/Secrecyviolation" as "Dataprotection-" /
"/Secrecyviolation". UAX #14 (LB13) forbids a break before the slash, so
the compound had no break opportunity: lines moved it whole and auto-fit
table columns widened to fit it, taking width from their neighbours.
Add the break after U+002D when a slash and a letter follow, next to the
existing break before a digit (#2712); Word keeps "fitness-/1589" whole,
so a slash before a digit gains no break. Always sort the added breaks.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A file that turns an inline picture but writes no wp:effectExtent (or a zero
one), as other tools do, makes Word keep the picture's line to its frame and
let the turn overhang it, cut at the line's top and bottom: the frame as it
is in Word 2007 layout (compatibilityMode < 15), with width and height
exchanged from 45 to 135 and 225 to 315 degrees in Word 2013+ layout.
WordCraft always reserved the exact rotated bounds (Float::spin_pad), which
is what Word's own files encode through their effect extents, so the text
after such a picture sat up to 34 pt too low (docxide-pdf
standalone-picture-compat12-* / compat15-angle45/135).
The docx reader now marks such objects (Float::bare_turn, cleared when the
object is turned again); the inline room comes from Float::line_pad, which
display uses too to place the frame in it, and the line clips the picture's
overhang. The writer keeps writing no turn overhang for them, so a save
round-trips. Word-saved pictures are unchanged.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* WC6-07: preserve paragraph text direction
* Resolve gaps summary merge
* WC6-07: retain paragraph direction variants
* WC6-07: preserve strict paragraph direction
* Update lockfile for DOCX test dependency
* Close paragraph direction implementation
* Format paragraph direction imports
Compatibility-mode documents store text boxes as VML only. The reader
ignored v:textbox's text direction, so a header text box turned 270
degrees was drawn level. It now reads it as Word for Windows does:
mso-layout-flow-alt:bottom-to-top is vert270 whatever layout-flow says;
otherwise layout-flow:vertical or vertical-ideographic is vert; anything
else stays horizontal.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* fix(ui): the File page scrolls across its full width, with the bar at the right edge (#5282)
Turn off horizontal auto_shrink on the File page ScrollArea so the scroll area expands across the full CentralPanel width. This positions the vertical scrollbar at the right edge of the window and allows mouse wheel scrolling over the entire panel width.
* fix(ui): the File page's scroll area reaches the window edge (#5282)
The side padding moves from the panel into the pages, so the scrollbar sits at
the edge and the wheel also works in the rightmost 40 points.
* test(ui): assert the File page's scrollbar sits at the window edge (#5282)
* Layout: look at the text after a decimal tab once, not once per cluster
Text after a decimal tab is shifted after each of its clusters, and each
shift walked the text again from the tab to find its first '.'. A long run
without a '.' (soft hyphens take no room, so it stays on one line) made a
single paragraph's line breaking quadratic. The scan now carries on from the
cluster it reached.
* address review: layout progress doc for decimal-tab scan
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Layout: a page break before applies at a continuous section's top mid-page
A continuous section moves the column top to the current line, so at_top() was true mid-page and the paragraph's page break before was skipped. Check fresh() instead, as Word does.
* 1485: record continuous-section page-break parity
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* M2.1: find smart-quoted words with a keyboard apostrophe (#5388)
* M2.1: align Find parity status and regression docs (#5388)
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A Strict document opened with its text and no shapes at all. Strict puts
shapes, groups and canvases (wsp, wgp, wpc) and their parts in its one
WordprocessingDrawing namespace, which the reader maps to wp:, while the
drawing reader looks for Word 2010's wps:, wpg: and wpc: names. Word also
writes a Strict text box's text in wne:txbxContent.
The XML reader now names those elements as Transitional documents do,
from the element and its parent (xml::element_prefix), and reads a text
box's content from wne:txbxContent and from the schema's own
txbxContent. Saving writes them as Transitional.
Claude-Session: https://claude.ai/code/session_01LvG6E1XTfHtZDbfYwfREhR
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #5469: Size keeps the side it isn't asked to change
Typing only the width of a straight line (height 0) gave it a 1 pt
height: picture.size fitted both sides into 1-4000 pt, also the one the
call never set. A side the call leaves as it was now keeps its size,
a line's 0 and a side past 4000 pt from a file too.
Sides set in proportion (the original size, or a scale or one side
typed with the proportions locked while both sides have a length) still
fit the limits together, even when one of them comes out as it was;
any other side the call sets fits on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5469: the table test checks exact sizes
Where both sides follow each other the test only checked that they
stay within 1-4000 pt and keep their proportions. The rule gives one
size there (the one factor nearest 1 that fits both), so each case now
checks that size, worked out by hand.
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>
Pin that FILENAME and FILENAME \p in a header show the new name and path after Save As, that a Word-style FILENAME \* MERGEFORMAT field drops its cached name, and that Rename is picked up by Update Field. No production code changes.
Refs #3796
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* #5267: a linked picture shows and follows its link; DOCX keeps it on the drawing
A link put on a picture from its context menu was stored, but nothing showed
it: hovering the picture gave no tip, and Ctrl+click picked the picture for a
copy instead of following the link. Now the hover tip and Ctrl/Cmd+click work
on a linked picture, shape or text box, in line or floating, as on linked text.
DOCX: Word keeps a picture's hyperlink on the drawing (wp:docPr/a:hlinkClick,
ECMA-376 Part 1 20.4.2.5). It is now read (a w:hyperlink around the run wins)
and written beside the w:hyperlink, sharing one relationship. A link to a place
in the document stays the w:hyperlink w:anchor only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: ink and Office 2016 charts take their drawing's link; Ctrl+click over text on a Behind Text picture stays a text click
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: a chart or ink takes the link of the choice it was read from; Ctrl/⌘+click in the docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: the decoy choice in the chart-link fixture needs a namespace no reader knows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: inside a linked text box, hover and Ctrl/⌘+click take the box's link
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: the picture-link test sets the image's fx after merging main
Main gave InlineObject::Image picture effects (fx); the round-trip test this PR adds built
an image without them and no longer compiled.
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>
* Arabic letters join across a change of formatting (#4093)
Each run of a paragraph was shaped on its own, so a letter at a run's edge
took the form of a word's start or end. A coloured letter stood apart from
its word, and a word edited with Track Changes on read with a space in it
(ال ذكاء).
Runs are now shaped with the text shown on either side of them, through the
shaper's pre- and post-context (fonts::ShapeContext). Runs left out of the
layout (hidden text, deletions in a final view) are skipped, so letters join
what is on the page. Only paragraphs with a joining script do the work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
* #4093 review: no letter joins a drop cap formatted on its own
A drop cap letter in a run of its own (coloured, say) was still passed to
the next run as the text before it, so that run's first letter joined the
drop cap, which is drawn apart from the word. The shown text now has a floor
past the drop cap, and no run takes context from before it. The drop cap
test also covers the letter in a run of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
---------
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #5105: font menu entries are drawn from a few shared textures
Each font menu entry was a texture of its own, created and freed as the list
was browsed: on Windows (DirectX 12) the entries showed other textures' memory,
black strips in light mode, white or red ones in dark mode, as the Equation
tab's pictures did before #1002. They now share an atlas written in place, the
one the equation pictures use (MathAtlas, now PreviewAtlas).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5105: font menu entries are drawn again in every window once fonts change
The font atlas remembers the font database generation its entries were drawn
with and starts over when it changes, so another window's entries and a removed
font's entry (Recently Used Fonts) don't keep an old face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5105: the atlas test doesn't compare with the shared font generation
Review: tests running alongside add and remove fonts, so the database's
generation may change between the draw and the assertion.
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>
A Word template (.dotx, .dotm, .dot) the platform hands the app, from a
double-click in the file manager, the command line or the Finder, opened
as itself: the window called it saved, AutoSave was on, and Ctrl+S wrote
over the template. Word's default verb for a template makes a new,
untitled document based on it.
WordApp::open_from_platform sends a Word template to file.newFromTemplate
and opens anything else as File > Open does; main.rs (also for a name that
isn't valid Unicode) and the Apple Events handler use it.
file.newFromTemplate reads the template as file.open reads a file
(password, data, an empty file) and, with replaceBlank, takes an untouched
blank document's place, so a launch shows one window. The Password box
retries the command that asked for it, so a protected template still
starts a new document. File > Open and drops still open a template itself.
Claude-Session: https://claude.ai/code/session_01LvG6E1XTfHtZDbfYwfREhR
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #4985: a numFmt none list level shows its literal text
Word labels a paragraph whose level has numFmt none and lvlText RESPONSE:
with RESPONSE:. Counters::next_label returned no label for any none level;
now only a none level of placeholders shows nothing. Its own %n stays
empty, and it still doesn't count. Numbering::shows_label agrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: a "none" list level counts, and only its own number is empty
Taken from MeHail-02's numbering-literal-none branch (46405e66), checked in Word
for Windows: a none level is an ordinary level whose own number reads as
nothing. It counts and restarts the levels below it, stays empty in legal
numbering, and an empty label still tabs the text to the indent unless it
already ends there (the Chapter 1 list's Heading 2 stays at the margin).
Co-Authored-By: MeHail <mihailsmetanin15@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF in full context keeps a none-level heading's parent
Review: a none level now has a label, so its headings reach the style index,
which leaves the none level out of the levels it keeps; \w and \r took the last
kept level for the heading's own and dropped the parent. Levels now carry their
index, and the levels above are picked by it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF number switches on a none-level heading
Review: in full context an empty label of its own added a trailing period (3.),
and the suppress switch gave a none level's label (Note), which holds no number,
back whole. An empty own part is left out, and a none level's label with no
number is suppressed to nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF \s and \t on none-level labels
Review: \s took a none level's label (Note) for a chapter number; the heading's
count stands in, as for an unnumbered heading. \t matched any ancestor's number
inside a word (the a of Chapter); only the numbers the label's template shows
count now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF takes a label's numbers by their spans
Review: \s took the last word of a label (Response of "3 Response") and fell
back on the heading count for a none level showing its parent's number (%1);
\t kept text attached to a number (Section3). A label's numbers are now the
spans of the values its template shows, with their delimiters (a letter number
only standing apart from other letters); \s takes the last of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: tests edit the numbering through Arc::make_mut after merging main
Main shares the numbering between undo steps (#2694), so `Document::numbering` is an
`Arc<Numbering>`; the tests this PR adds wrote to it directly and no longer built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: MeHail <mihailsmetanin15@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Linux: ignore an AppImage environment inherited from another program (#5653)
An AppImage app that opens a document through xdg-open passes its APPIMAGE,
APPDIR, ARGV0 and OWD on to the WordCraft it starts. The self-update trusts
APPIMAGE, so an installed WordCraft replaced the other app's file with its own
AppImage.
The app now starts over without those variables, first thing in main, unless
its executable is under APPDIR. The update then goes through the package or
plain file this copy really is, and a real AppImage keeps its variables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
* #5653 review: turn the self-update off when starting over fails
If the process can't start again without another program's AppImage
variables, it still carries APPIMAGE, and an accepted update would replace
that program's file. The app now turns its self-update off for that
process: no update is offered, and Help > Check for Updates says why. The
app still opens, so the document the user asked for isn't lost.
Also documents the test helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
---------
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #5398: the title bar's Search box takes the typing in place, its results dropping down under it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5398: Search in Focus opens the dialog, and Search whose box is gone doesn't keep Escape
Review: Focus hides the title bar's Search box, so Alt+Q on the frame Focus comes on opened
Search in a box that wasn't drawn, and that hidden Search kept Escape from leaving Focus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5398: Search takes the keys from its first frame; Enter on a result isn't also the new dialog's OK
Review fixes:
- The Search box is focused before it reads the frame's input, so keys typed on the frame
right after Alt+Q go into it (they were lost). Alt+Q's own text is still swallowed: Alt is
read from the shortcut's key event too.
- Enter that runs a result is used up, so a dialog the result opens (Word Count) doesn't take
the same Enter as its OK and close at once.
- The Focus test opens Search with the command Alt+Q runs, so it also runs on macOS, where
Option+Q types a character (the test failed on the macOS runner).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5398: Tab moves from the Search box to its results, and Enter runs the focused one
Review: the box took the focus back every frame, so Tab couldn't reach a result, and Enter
always ran the first hit. The box now takes the focus back unless it is on one of its results
(read from last frame's rects, so the page taking it is still undone), and runs the first hit on
Enter only while it has the focus; a focused result takes Enter or Space itself. Either way the
Enter that ran a result is used up, so the dialog it opens doesn't take it as its OK.
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>
* ODT gap 19: a file's font declarations are kept
WordCraft declared each font itself, guessing its kind, in place of the file's
declaration. A reader without the font picks its substitute by that, so on a
computer without it a document came back in LibreOffice from a save with no
edits reflowed, 70 pages instead of 83. A font the file declares stays declared
as the file does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: font declarations in single quotes and their name spaces are kept (gap 19)
Single-quoted names weren't recognised, so the declaration wasn't replaced and
could be added twice. A prefix only the declarations' container declared was left
unbound in the saved part. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a font name spelled another way in XML is the same declaration (gap 19)
Declarations were matched on their raw text, so O'Brien and O'Brien counted as two fonts and one was declared twice. They are now compared as they read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a face named other than its family gives that family the file's declaration (gap 19)
A face named F1 of the family Mystery: text written from the model names Mystery, and WordCraft's declaration of it kept a guess at the font's kind. It now says what the file's says. Faces are matched in one walk, and namespace prefixes are tracked in a set, as a hostile file with many on its top-level elements could stall import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: font faces are found by parsing and renamed at their own attribute (gap 19)
The walk matched text, so a kept script reading like a declaration could be rewritten. It now takes the declarations the parser finds. A rename uses the position of style:name itself, not a search that another attribute with that in its name could mislead.
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>
In the mirrored Arabic interface the Home tab's Styles gallery still laid its
tiles out left to right with the arrows on the right, so Normal sat at the
far end. Tiles now start at the right edge and the up/down/more arrows sit at
the left; left-to-right is unchanged. The "more" button gets the Styles Pane
label it opens, so screen readers can name it.
* #4482: test that every Navigation tab fits the narrowest pane in every language
The new kittest draws the Navigation pane body at the 200 pt minimum width with the
longest translation of each tab from craft-i18n (Überschriften, Страницы, Eredmények)
as a fixture language, so it runs in CI without the catalogs. With the catalogs built
in, it also checks every interface language. Each tab must lie inside the 178 pt body.
Main already wraps the tabs onto a second row, so the test passes there. It fails on
a single-row tab strip that doesn't wrap, such as the one in #4480.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4482: draw the tab test in the app's style; catch a stale fixture
The test now applies the app theme with the fonts. With the catalogs built in, it
also fails when any language's tab label is wider than the fixture's, so a
craft-i18n bump that brings a longer label asks for the fixture to be updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4482: the tab test fails when a tab elides its label to fit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4482: the tab test puts English back even when an assertion fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4482: the stale-fixture failure says which label to put in the fixture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/formats/src/markdown/export.rs:377 (write_blocks, Kind::Heading arm)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #4753: Styles pane scroll bar keeps clear of the a / ¶ markers
The Styles pane list used egui's default floating scroll bar, which is drawn
over the right edge of the rows; on hover it widens to 10 pt with a translucent
track and covers the right-aligned "a" and "¶" style-type markers. The list now
uses a solid scroll bar, which takes a column of its own beside the rows (as in
the TOC options dialog and vectorcraft#2232's menus). Its handle keeps the text
colour: the solid preset fills it with the button colour, which is white on the
pane's white track and would leave the handle invisible.
The regression test draws the pane with the app theme, hovers the bar and
checks that no scroll bar rectangle meets a marker and that the handle stands
out from the track.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4753: Styles pane scroll bar test hovers the bar and checks dark too
The test now takes the hover point from the pane's own right edge instead of
the harness size (kittest insets the UI, so the old point sat just outside the
bar), asserts the pointer is on the bar, and runs in the dark appearance as
well as the light one.
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>
* Layout: a picture lowered past its height lowers its line as far, as in Word
A picture lowered further than its own height had its lowering cut at that
height. Word keeps its top on the baseline but lowers the line as far as the
position says: a 6 pt picture lowered 30 pt makes a 30 pt line. Exact spacing
keeps the line's baseline. Closes the gap noted with #3984: all 156 measured
lines now match Word.
* Lowered pictures: an exact-spacing test; gaps and progress log
An exact-spacing line keeps its baseline when a picture is lowered past its
height (the test fails without the cap). Gap 4 and the progress log note the
change.
---------
Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #4685: Shading on text selected inside a paragraph shades the text
Home › Paragraph › Shading always set the paragraph's shading. Word shades the selected
text when the selection is inside one paragraph (w:shd in the run properties), and the
paragraph for a caret or a whole paragraph. So an orange text shading from a .docx (which
Text Highlight Color › No Color leaves, in Word too) couldn't be taken off from the ribbon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4685: review — a paragraph's whole text is text unless it's the last; a selected object isn't text
A selection with a paragraph's mark ends at the next paragraph's start, so 0..len of a
paragraph that isn't the last of its story, cell or box is text only and No Color clears its
shading. A selected text box or picture (its object character) shades the paragraph as before,
instead of every line of the box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4685: review — a selected field is text; Apply to: Paragraph shades the paragraph
Only a selected picture, shape or text box (`object_selection`) keeps Shading on the
paragraph; a field's object character is text, so No Color clears its shading. The Borders
and Shading dialog's Apply to: Paragraph passes `paragraph: true`, so it shades the
paragraphs even for text selected inside one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4685: ui-parity wording (CodeRabbit): text shading applies to part of one paragraph
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4685: ui-parity wording (CodeRabbit): removes text shading imported from a file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI parity: status line after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4685: a test of Shading in a cell's last paragraph
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>
* M14: keep native CLI input names after options
* docs(M14): clarify native CLI path option exceptions
Qualify the roadmap summary and history: text option values need Unicode, while run --file and run --script retain native paths. Addresses review4485129728. Documentation only; no CLI behavior changes.
* docs(M14): qualify Unicode values in format parity notes
Exclude the existing run --file and run --script native path operands from the text option Unicode requirement. Addresses review4485129728; documentation only.
* docs(M14): qualify Unicode values in CLI gap notes
Document the existing native path exceptions for run --file and run --script. Completes the three-file wording correction requested in review4485129728, without changing readiness estimates or CLI behavior.
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* RTF: honor the declared Unicode fallback count
* RTF: record declared Unicode fallback counts
* M12: count unknown RTF symbols in Unicode fallback
Address the unrecognized-control-symbol review finding in parse.rs. Each symbol consumes one Unicode fallback unit; outside fallback it remains ignored. Four additional actual-import cases cover uc0, uc1, uc2 and uc18. The uc2 regression fails before the fix (≤ rest versus ≤b rest). All 664 formats tests, scoped rustfmt and strict all-target formats Clippy pass in the exact-source isolated closure. Binary fallback remains separate in #4806.
* M12: count structural RTF words in Unicode fallback
Treat par, pard, cell and row like other control words while consuming declared Unicode fallback. Add actual import cases for those fallback units and a uc0 paragraph control. Scoped rustfmt passes; full validation is pending hosted CI.
* M12: describe RTF fallback units precisely
Address the review wording finding: the Unicode fallback count measures units, including control words and escaped symbols, rather than fixed-length bytes. Preserve every roadmap history and progress record.
* M12: count all RTF fallback control symbols
Consume non-hex control symbols before dispatch while Unicode fallback remains, covering escaped LF/CR and star without applying their side effects. Keep valid hex escapes on their four-byte path, and include revision-author text in the existing control-word guard. Add six actual body-import cases and two actual revision-author cases, including uc0 controls. Both regressions fail before the patch in the exact current closure; all 665 formats tests, scoped rustfmt and strict all-target formats Clippy pass afterward. Binary and invalid-hex handling remain separate. Full hosted gate will be checked on the draft PR.
* M12: preserve the roadmap final newline
Restore the trailing LF omitted by copying the Raw view. The roadmap now differs only by the requested fallback-unit wording; production code and tests are unchanged.
* M12: skip RTF info effects during Unicode fallback
Move the existing info_depth assignment after the existing fallback-control-word guard. A skipped info word inside a Unicode title fallback must not overwrite metadata depth and drop the following author. Preserve the binary and escaped-hex paths, other destination handling and uc0 effects. The supplied metadata import regresses before the move; all 667 formats tests, scoped rustfmt and strict all-target formats Clippy pass afterward. Regression coverage follows in this draft PR.
* M12: cover RTF fallback metadata depth
Add four actual RTF import cases asserting title, creator and body text: skipped info controls at uc9 and uc1 retain the following author; uc0 title and a real info group after body Unicode still work. The supplied uc9 case fails before the parser reorder with an empty creator instead of Alice. All 667 formats tests, scoped rustfmt and strict all-target formats Clippy pass in the exact c153 source closure plus this two-file follow-up. Full hosted CI will be checked on the draft PR.
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* 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>
The document used egui's floating scroll bars, invisible until the pointer
reached the right edge. The canvas now draws solid bars in the 14 px strip
it already left for them, with a grey thumb that stands out from its track
in light and dark themes and darkens under the pointer. Read mode still has
no bars.
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
File › Options' "Show the community button in the title bar" hid only the
title-bar pill; the backstage menu still showed "Discord community" at
its bottom — one setting, half honored. The menu now draws the link only
when the same setting is on, next to About.
Co-authored-by: Bra1nZ <bra1nz@astraoffice.com>
* Ctrl+K on a selection shows the changed Text to display
With text selected, the Link dialog's Text to display was ignored:
insert.link only linked the selection. A changed text now retypes the
selection (keeping its formatting, tracked with Track Changes on) and
links it. The same text, text with a line break, or a selection over
several paragraphs or holding a picture or field keeps its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Links: keep selections a changed Text to display can't replace
Review of the Ctrl+K retype: a selection holding a manual line break,
page or column break, or sitting in a field's result is only linked, so
neither both lines nor the field's result are lost. `select.text` clears
the caret formatting as `select.word` does, so a second retyped link
keeps its selection's bold instead of the first link's default props.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Selections: select.all, select.range and select.word drop pending formatting
A link leaves plain formatting pending for what is typed after it. These
commands kept it, so retyping the next selection took that instead of the
selection's own formatting, as select.text and select.word's caret moves
already avoid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docbin: a field's cached result keeps its formatting per run, not per character (#5477)
The result was a Vec<(char, CharProps)>: 365 bytes per character, 1.44 GB
at peak for a 4 MB file with one field. It is now a String plus a run
list that gains an entry only when the formatting changes (25 MB peak
for the same file). Output is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: show the objects when the field-result test fails
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: resolve character formatting per run, not per character (#5478)
The text loop looked up the piece four or five times and the CHPX once
(a backward scan over up to 101 FKP entries) for every character, and
cloned the memoised 368-byte CharProps each time. Now the walk keeps the
piece span and the CHPX run of the previous character and reuses them
while the next character is inside, and hands the memoised properties
to the paragraph builder by reference: they are cloned when a run
starts and not compared while the resolved formatting is unchanged.
Output is identical, also on corrupt pages (a run's FC range is
bounded by the smallest later start).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: tests for run ranges across bins, empty pieces and a whole read
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* M14: keep Page Setup margin and grid editors within their width (#5650)
Use the pinned egui DragValue clipping option for four margins and both grid counts. Canonical actual WordApp regression and 12 Page Setup controls pass; strict all-target UI Clippy and scoped rustfmt pass.
* M14: keep Page Setup paper-size editors within their width (#5650)
Apply the same bounded text editing to paper width and height while preserving their existing limits, units and speed. Actual Page Setup regression and existing controls pass.
* M14: regress Page Setup paste, blur and reopen widths (#5650)
Exercise all eight numeric fields through the actual WordApp harness. Baseline Bottom paste reproduced width growth from 354 to 2973 pt; the fixed canonical 12-test Page Setup suite passes. Private rendering hooks are excluded.
* M14: record bounded Page Setup number editors (#5650)
Add the focused UI fix to the progress and revision logs and status line. Readiness estimates remain unchanged.
* M14: note the focused Page Setup paste fix in gaps (#5650)
Record bounded numeric editing in the gap note, status and revision history; no ranked gap or readiness estimate changes.
* M14: document bounded Page Setup numeric input (#5650)
Update the UI parity note, status and revision history for long input scrolling inside the existing fields. Readiness estimates remain unchanged.
* M14: bound unfocused Page Setup margin values (#5650)
Keep the unfocused DragValue inside an allocated, clipped field without changing numeric parsing, units, speed or accepted values. Regress a finite long paste followed by Tab in all four margins. The actual baseline grew from 354 to 458.53125 pt after Tab; the canonical 13 Page Setup tests and scoped formatting checks pass, and the inspected fixed renders stay 354 pt. Hosted validation remains pending in draft PR #5696.
* M14: record unfocused Page Setup bounds (#5650)
Update the gap and UI parity notes for the accepted-value paste followed by Tab regression. Retain the existing progress history, value ranges and readiness estimates.
* M14: log the Page Setup blur regression (#5650)
Record the bounded unfocused margin display and accepted-value regression. Preserve every upstream progress entry and the previous validation history.
* M14: regress RTL margin re-entry (#5650)
Cover re-editing the visible portion of a wide blurred margin in the real Page Setup dialog under an asserted RTL fixture. Raw pointer events restore editing and accept a normal value; the existing thirteen tests remain unchanged. Fourteen Page Setup tests, package formatting, strict all-target UI Clippy and source-integrity checks passed. No production or dependency changes.
Saving a document for the first time suggested Document1.docx whatever
it held. As in Word, a document that was never saved and has no title
of its own is now offered the first words of its first paragraph with
text: up to the first sentence or clause end, without the characters a
file name can't hold, white space collapsed, at most 50 characters cut
at a word's end (Minutes of the board meeting.docx). A path, a file
name or a Title property still name it, and the window title stays
Document1.
Fixes#5793
* #3719: run the file commands through the hostile-parameter test
hostile_params_never_panic skips file.*, insert.picture and
insert.textFromFile, the commands that take a path and parse what is
there. hostile_file_params_never_panic runs them with the same junk
params, and with paths to an empty file, a folder, a missing name and
noise under each extension they open or save. Automation roots hold
them to a temporary folder.
It found no panic, so this adds only the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5688: check the whole working directory, on the sample document
The working-directory check looked only for hostile-relative names, but
an empty path comes out as .pdf or .dotx there. It now compares the
whole folder before and after the runs.
The runs now use the sample document, like hostile_params_never_panic:
on an empty document the export commands had almost nothing to write.
The docs and comments name the three groups of commands, because other
commands also read files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5688: a failing file check names the files, not their bytes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
About > Contributors still showed an old snapshot, because nothing
regenerated contributors/contributors.json before a release. Opt in to
the shared refresh from craft-common-actions: a read-only credits job
regenerates the snapshot for the release commit, and the shared release
workflow installs it before every source build. The snapshot is not
committed back.
* ODT gap 19: a table row that doesn't break across pages stays so
fo:keep-together on a table row wasn't read, so rows kept whole came back from
a save free to break across pages in LibreOffice. Read and write it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a row follows the default style's keep-together (gap 19)
A row whose style doesn't say took "free to break" instead of the default style's
setting. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a row free to break stays so over a default that keeps rows together (gap 19)
Nothing was written for it, and the file's default row style stays, so a save took
that away.
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>
* ODT gap 19: a heading's text of another size keeps it
Every run in a heading, title or code paragraph lost its size to the style, so
a smaller word in a heading came back at the heading's size in LibreOffice
after a save. Only text at the style's size goes with the style.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a heading's text is sized against the heading (gap 19)
The comparison used the size outside the paragraph, so text stating the heading's
own size kept it through a style change. A size in percent was of 12 pt, not the
heading's. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: text in a heading style stating no size keeps its own (gap 19)
The run's size was compared with Standard's, so with Standard at 14 pt a 14 pt run lost its size and took WordCraft's Heading 1 size of 20 pt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a span stating no size has none in a heading stating none (gap 19)
The size used to work out a percent stayed on a span saying nothing of size, so a bold word in such a heading took Standard's size. It came in with the last commit here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a percent in a parentless heading is of the default size (gap 19)
It was of Standard's, which a style with no parent isn't based on: 50% over a 10 pt default and a 14 pt Standard came out 7 pt, not 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a style's percent size is of what is under it (gap 19)
It was of 12 pt, whatever the parent or default: 50% over a 10 pt default made a 6 pt style, and a 5 pt span in it lost its size and showed at 6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ODT gap 19: a frame offering several picture formats shows one it can read
LibreOffice writes its own .svm metafile before a PNG of the same picture.
WordCraft took the first whatever it was, so the picture was missing, and gone
from the file once the paragraph was written from the model. Take the first
it can read, as the frame's order of preference means.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a replacement picture before its object is kept (gap 19)
A frame's picture WordCraft can't show was dropped before the object came, so a
save from the model lost the object's replacement. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a readable picture after an unreadable replacement is shown and kept (gap 19)
With an unreadable replacement first, the readable picture after it was skipped: WordCraft drew a placeholder and a save from the model lost the picture. It is now shown and written after the replacement. A placeholder is still not written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
* #5710: the Print panel lists printers without asking their drivers
On Windows the printer list waited until every printer's driver said
whether it prints on both sides. A networked printer in power save can
hold that query for a long time, so the list stayed empty and Refresh
stayed unavailable. WordCraft never used the answer.
Discovery now uses craft-print's printer_queues, which lists the same
queues and default without asking any driver (storytold/craft-libs#618).
Printing still checks the job's options with the driver. Only the
craft-print pin moves; craft-ui doesn't depend on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: the docs say what is proven, not that the sleeping printer is fixed
Review: the progress log and gap rows stated the reported outcome as
certain, but the sleeping network printer hasn't been retested. They now
say the driver query is removed and the reported printer awaits retest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: craft-print pin moves to the merge of storytold/craft-libs#618
e653048 is craft-libs main's merge of #618; crates/print is identical
to the reviewed and tested head 81c2488.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: a print job waiting on its driver can be cancelled, and quitting doesn't wait for it
On Windows a print job asks the printer's driver for its capabilities
before it is sent, and a networked printer in power save can hold that
query for a long time. The Print page stayed locked until the driver
answered, and quitting waited for the job before saving preferences
(CodeRabbit on #5732).
Each job now carries a craft-print Cancel token. Cancel printing
(ui.printCancel) stops a job that hasn't started reaching the spooler,
so it is never sent, and frees the Print page with the settings kept;
a job already being sent completes, and the page says to cancel it
from the system's queue. Quitting cancels jobs still waiting and waits
only for jobs already being sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: quitting cancels every waiting print job before it waits; Cancel asks the active job
Review: quitting cancelled and waited for jobs one by one, so a job left
waiting while an earlier one was being sent could start sending
meanwhile. Every job is now cancelled first, then only those already
being sent are waited for. Cancel printing asked every unfinished job,
so a job that had just been sent could be reported as cancelled and its
receipt lost; it now asks the job being submitted, which answers that it
was already sent.
The tests cover both orders, junk parameters for ui.printCancel, and the
kept printer list and status message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: craft-print pin moves to storytold/craft-libs#622 for submit_cancellable
85c8ca1 is #622's head (craft-libs main e653048 plus the Cancel token);
it moves to #622's merge on craft-libs main before this merges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: craft-print pin follows storytold/craft-libs#622 (230d522)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5710: pin craft-print to merged cancellation fix
* #5710: allow print cancellation after closing the document
* #5710: correct print-cancellation status date
* #5710: clarify when a print job cannot yet be cancelled in the queue
* #5710: isolate Windows print submissions in cancellable helper processes
* #5710: strengthen helper retry and crash-cleanup regressions
* #5710: erase helper inputs before driver work
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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
The transports, the message types and the origin checks come from the shared crate (storytold/craft-libs#420); WordCraft keeps the host save and dirty hooks, the saveAs rule, the open size limit and the author.
data-host="page" on the canvas opts a page that loads the module into its own document into the host bridge. The ?host=parent messages travel as non-bubbling CustomEvents on the canvas; the bridge holds a Transport (Parent or Page) with one send, and the open, save, dirty, ready and failure handling is shared. ?host=parent is checked first and unchanged, including no bridge for an unframed page. The hosting guide documents both modes.
* docbin: a field's cached result keeps its formatting per run, not per character (#5477)
The result was a Vec<(char, CharProps)>: 365 bytes per character, 1.44 GB
at peak for a 4 MB file with one field. It is now a String plus a run
list that gains an entry only when the formatting changes (25 MB peak
for the same file). Output is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: show the objects when the field-result test fails
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
`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>
* Alt codes type their character in Find and other text boxes (#4403)
Alt+numpad codes worked only on the page. In a text box (Find what,
Replace with, a dialog's field) the digits were typed instead on Linux,
which sends their text with Alt held; on Windows the box got only what
the system composes, a byte for a code above 255.
A pass before the frame is drawn now gives the focused text box the
same codes as the page: the digits' text is dropped, letting go of Alt
types the character the code names in the same code pages, and a
platform's own composed character is not typed twice (up to 255 the
system's replaces ours, as on the page).
* Alt codes in text boxes: letting go of Alt ends the digits' text
The flag that drops a digit's own text stayed set when no text came
for the last digit, so the first text after the release was lost: on
Windows the system's composed character for a code we name no
character for (CodeRabbit), and on Linux whatever followed a frame
whose digits sent no text (CI). A digit's text comes with its key,
before Alt is let go, so the release now clears the flag everywhere.
* docbin: cap paragraph and cell marks, keep the table context only for table marks (#5476)
The walk budget counts characters, not paragraphs, so a file made of
paragraph marks grew memory about 2,000 times faster than its size
(4 MB of marks: 9.4 s, 8.4 GB peak). Marks now draw on one document-wide
cap (2,000,000, all stories together); past it the read returns
DocbinError::Limit. A ParaOut no longer embeds the 296-byte RowInfo of a
table mark: it is an optional box, present only for table marks.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: say what the paragraph cap bounds; name the budget test for what it checks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: refuse an exhausted paragraph budget before assembling a story (review point)
CodeRabbit pointed out that the header, footnote, endnote and text box walks
assembled their partial paragraph lists before the budget check, a transient
memory peak. They now share story_blocks, which checks first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin: test that the paragraph cap is shared by the body and a footnote (review nitpick)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docbin test: review fixes (assert empty pairs, comment wording)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Content controls: a press on the drop-down list button no longer moves the caret (#5389)
The list button hangs over whatever follows the control. The canvas put the
caret under the pointer first, which left the control, and the list closed in
the frame it opened. A press on the button now skips the caret move.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Content controls: only a press that began on the list button skips the caret move (#5389)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Content controls: a press and release in one frame, and a table tool, no longer hit the button guard (review points)
CodeRabbit pointed out that egui clears press_origin when a press and its
release arrive in the same frame, so the caret still moved off the control and
closed the list; the press is now also read from the frame's events. It also
pointed out that the guard ran before the table pen, so a stroke could not
start over the button; the guard is skipped while a table tool is on.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Content controls: feed the same-frame press and release as one frame in the test
Harness::event runs one frame per event, so the earlier version of the test did
not exercise the single-frame case.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 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>
* Mini toolbar: hovering a formatting button previews it on the selection (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Keep mini toolbar stable during alignment preview
* Track live mini toolbar anchor during preview
* Mini toolbar: hovering a font, size, highlight or font colour previews it (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mini toolbar preview: the page holds its scroll position (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mini toolbar: hovering the highlight and font colour buttons previews their colour (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mini toolbar: the font list and colour menus mark the choice in effect (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Skip hover previews in context toolbar
* Pass preview mode in toolbar test
* Font list: a picked family keeps its place until the list next opens (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font and size lists close when an entry is picked (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mini toolbar: a clicked button holds its preview until the pointer leaves (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mini toolbar: a button already on does not preview (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Alignment buttons follow the paragraph a selection covers; a toggle previews its undo on return (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font list: a recent family is marked as chosen in Recently Used Fonts only (#4664)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Fluorine <joseph.anglada92@gmail.com>
* #3614: reuse paragraph-prefix lookups during ink discovery
* Update ink lookup status date (#3614)
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Let every credits column be dragged, and refit changed tables
The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.
A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.
fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refit credits tables on what they measure, not the fitted widths
Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.
It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.
The table now compares the natural widths and the clip rect's width,
which scrolling does not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The page the section before ends on is finished with its own margins and
footnote separators before the starting section's replace them, so the
footnotes pending on it are placed where the page they sit on says.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
* 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>
Spelling as you type now notes that a web or e-mail address after a
no-break, narrow no-break (U+202F), figure (U+2007) or ideographic space
is not marked while Ignore web and file addresses is on, as
address_after_a_wide_space_is_an_internet_token in craft-proof checks.
* 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>
metafile_size() parsed the whole metafile, decoding every bitmap and building every path, to
read two numbers at picture insert and replace. A new wordcraft_metafile::size reads the frame
from the EMF header or the placeable WMF header, and falls back to the full parse for every
other file (plain WMF, an EMF without a usable frame, a file with no records), so it returns
what parse would. For an EMF with one 8000x5000 1 bpp bitmap the size now costs well under a
microsecond instead of about 280 ms.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
decode_clip matched the bit depth, bounds-checked and (for 16 and 32 bpp) divided per channel
for every pixel. The depth, palette and masks are now turned into a row decoder once per
bitmap: a 256-entry colour table for 1/4/8 bpp, plain channel swaps for 24 bpp and default
32 bpp, and precomputed shifts and scale tables for masked 16/32 bpp. Output is byte for byte
the same; the old per-pixel code stays in the tests as the reference.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
decode_clip only capped width x height at 40 MP. A 32 x 1,250,000 pixel 1 bpp bitmap (about
5 MB in the file) was decoded to 160 MB of RGBA and kept, then dropped by Item::take_pixels
because its side is over MAX_BITMAP_SIDE, while counting against the parse's 256 MB budget.
decode_clip now returns None when the clipped width or height is over the cap, so crops of a
large bitmap that fit still decode.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin that a comment id past u32 is refused, not truncated (#3677)
`validate_comment_id` refuses an `id` that does not fit a `u32`, so the `as
u32` casts behind it can no longer reach another comment -- but nothing pinned
it, and the casts are still there to be reached if the guard moves.
The ids the test sends are the ones that would alias onto real comments if the
truncation came back: 2^32 truncates to 0 (and the first comment is 0), 2^32 +
id to that comment, 4294967301 to 5 as the report describes, and u64::MAX to
u32::MAX. Each of the four commands that take an id is tried with each, then
the comments are checked to be unchanged and unresolved, and a valid id is
accepted afterwards so the guard is shown to be a range check rather than a
refusal.
* Comment ids: the values past u32 go in the existing invalid-id test, refused by the range check
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 now keeps what comes before a wrapped list's field, which can be
another list field; rebuild from the converted field's code instead of the
paragraph's first list field, and let index::rebuild_from take that code.
- 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)
* Arabic interface: mirrored right-to-left window and Arabic UI fonts
Adds the Arabic (`ar`) interface language's code: its catalogs are craft-i18n's
`locales/ar`, so this changes no catalog.
- `ar` is registered in `i18n::LANGUAGES` and `Lang::rtl()` says it is written
right to left; its translations are used as written (egui's text layout
orders each line's bidi text itself). A ribbon location reads from the right
with a leftwards mark.
- `rtl.rs`: the window is mirrored. Containers open in `rtl::root`; columns,
grids, checkboxes, radio buttons, menus, rows, panels, the dialog tab strip
and the ribbon's scrolling row follow the language's direction. Title bar,
status bar, ribbon tabs and groups, File pages (including Print, with the
settings on the right and a solid scrollbar gutter), dialogs, menus and the
task panes (their docking edge swaps) are laid out from the right. egui's
`Grid` panics in a right-aligned layout, so grids go through `rtl::grid`.
- Arabic UI font fallback: the craft-fonts Arabic face, else an installed
Arabic font, loaded when the interface is Arabic.
- The "Auto (200%)" size label keeps its percent sign inside the isolated
number.
- Tests: Arabic locale, saved preference and lookup order on fixture catalogs;
an interface audit (skipped without the craft-i18n Arabic catalog) that lays
the app out right to left at several widths, checks tab, group, pane, Print
and Go To positions and that nothing panics.
The Arabic work builds on Abdelhaq El Amraoui's mirrored-interface PR (#737).
Co-Authored-By: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: pin craft-i18n at a commit with the Arabic catalog; review fixes
- craft-i18n.rev and the Flatpak manifest pin 19358491 (craft-i18n main), the
first commit line that has locales/ar/wordcraft.tsv. The old pin had no
Arabic catalog, so `ar` was registered but not embedded and two i18n tests
(the_language_setting_is_a_ui_command_and_persists,
the_languages_offered_are_the_embedded_ones_of_the_list) failed in CI.
- The Arabic font fallback is requested for any right-to-left interface
(`Lang::rtl()`), not only after the language picker sets the flag, so an
automatic or saved Arabic gets the installed Arabic face.
- The file browser, the update window and Reading Mode's toolbar run their
bodies in `rtl::root`, so their far-side buttons and rows are mirrored.
- A group in a right-to-left ribbon row is measured from the right edge: the
cursor's left edge is negative infinity there, so its width was NaN and the
ribbon planner dropped every group, never compacting a narrow Arabic window.
A narrow window now folds groups into drop-downs.
- The mirrored checkbox and radio marks are labelled by their text, so screen
readers name them.
- Tests: group width in a right-to-left row; the Arabic interface audit checks
Reading Mode's toolbar.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: ask for the installed Arabic font separately from the CJK one
A previous commit made every right-to-left interface request the installed CJK
fallback font (`system_cjk`) so that the installed Arabic font would load. That
also made every Arabic launch read a large system CJK font file when no
craft-fonts input is embedded (the Flatpak build has none).
`install_ui_fonts` now passes a separate `system_arabic` flag (`Lang::rtl()`):
the installed Arabic font is read for it, and `system_cjk` is again set only
by a CJK interface or by CJK text on screen. `WordApp::fonts_system_arabic`
remembers what was installed, so a language change reinstalls the fonts.
Tests: the_installed_arabic_font_is_asked_for_by_arabic_alone (the Arabic flag
adds the installed font only when none is embedded, and the CJK request alone
adds no Arabic font). Checked a headless screenshot of the Arabic interface
without craft-fonts: the installed Arabic font draws the labels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix the AutoSave switch knob: on is at the right, mirrored in Arabic
The title bar's AutoSave knob was placed with `lead_x(rtl == on, ..)`, which
reverses main's mapping in both directions: left to right it drew the knob at
the left when the switch was on, and right to left at the right. main draws it
at the right when on and at the left when off, so the mirrored interface must
draw it at the left when on: `rtl != on`.
The position is now `autosave_knob_x(rtl, on, track)`, pinned by a test for all
four cases. A headless screenshot of the off switch shows the knob at the left
in English and at the right in Arabic.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: file names and paths in right-to-left lines; kerning row order
Tester feedback on the Arabic interface:
- File > Open's Recent list, the built-in file browser and the title bar drew
file names and paths raw, from the left edge. In an Arabic line the marks of
a name ("(نهائي) 2.docx") and the folders of a path with Arabic parts were
scrambled. Rows now start at the side reading starts from (`rtl::lead_x`),
names go through `i18n::isolate` (a first-strong isolate, with a left-to-right
mark before the extension so `2.docx` stays `2.docx`) and paths through
`i18n::isolate_path` (a left-to-right isolate with a mark after every
separator, so neighbouring Arabic folders do not swap). Other languages get
the text untouched; saved and spoken text never carries the marks.
- Font dialog, Kerning row: the dialog's grid is drawn left to right inside a
right-to-left window, so "Points and above" came after its number; in an
Arabic interface it now comes first, so the line reads "14 نقطة فأكثر" from the
right. (The grid's columns themselves still keep their left-to-right order.)
Tests: names_and_paths_are_isolated_in_a_right_to_left_interface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: mirror through craft_ui::direction and Style::right_to_left
The suite's craft-ui now has the direction helpers (`craft_ui::direction`) and
its egui fork a `Style::right_to_left` that mirrors egui's own chrome. Use them
instead of our parallel wrappers:
- `rtl::sync(ctx)` applies the interface direction every frame the language's
changes (`direction::apply`): it sets the shared direction state and
`Style::right_to_left` on every theme, which draws check boxes and radio
buttons with the mark at the reading start, mirrors submenus, scroll bars,
title bars and the combo boxes' arrows (the Print page's choice lists, the
font list) and opens combo menus from the right.
- `rtl::checkbox`, `radio_value`, `radio` and `indeterminate_checkbox` are
gone: about 200 call sites use plain `ui.checkbox`, `ui.radio_value`, ... and
egui does the mirroring (and names the control by its text, as a plain widget).
- `lead_x`, `lead_center`, `lead_top`, `start_panel` and `end_panel` delegate to
`craft_ui::direction`. `root`, `flows`, `island`, `grid`, `menu`, `vertical`,
`far_side` and the other layout helpers stay: the shared crate has no
equivalent that fits them.
Visual changes: the combo boxes' drop-down arrows and lists are mirrored in
Arabic; nothing changes in left-to-right languages (checked headless
screenshots of the English main window, Font dialog and Print page).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Recent files and the file browser: full path on hover, long names keep their start
Review feedback:
- Each row of File > Open's Recent list shows the document's full path as its
tooltip (the path line under the name can be longer than the row).
- A long name in the built-in file browser is laid out in one row, with an
ellipsis at its end, from the side reading starts from, instead of being
clipped at the row's edge: in a right-to-left interface the old text anchored
at the right edge cut the end of an Arabic name and the start of a Latin one.
The whole name is the row's tooltip when it was shortened.
Checked headless screenshots of the file browser with a long English and a long
Arabic name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font combos in a right-to-left row: the arrow joins its field on the left
The ribbon's font name and size boxes are drawn from a field and a separate
arrow box that share one outline: the field is square and the arrow's box is
rounded on the side where the arrow goes, painted a border width under the
field. That assumed the arrow is right of the field. In a right-to-left row
(the Arabic ribbon) the arrow comes after the field on its left, so the field
was square on the wrong side and the arrow box rounded on the side facing the
field: two boxes with a gap-like seam, the arrow looking outside the frame.
`arrow_box_shape(rtl, ..)` mirrors the radii and the underlap, and the field's
square corners are chosen by the row's direction (the nw/sw pair in right to
left). Left-to-right rows are drawn exactly as before (English before/after
crops are identical).
Test: the_arrow_box_is_rounded_away_from_its_field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n 4612850 (merged Arabic WordCraft rows, craft-i18n#76)
The pin and the Flatpak manifest commit move from 1935849 to 4612850, which is on
craft-i18n main and adds the 70 Arabic rows this branch relies on.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n cac2cf0 (Arabic number agreement and status-bar rows)
Moves the pin and the Flatpak manifest commit from 4612850 to the current craft-i18n
main, which includes craft-i18n#95 (48bed03) and #81 (ef58ff3).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: isolate paths shown beside labels; mirror the Print page zoom
File > Info shows Location as its own left-to-right path label (cut short with the full path on hover, not wrapped), and the open-failure, Exported and dictionary-file messages isolate their paths. The Print preview zoom puts minus at the slider's left end and plus at its right in Arabic, as the status bar does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: mirror the label/field rows of the Font, Info, Paragraph, Page Setup, Insert Table and Options forms
rtl::Table lays a form out as an egui Grid in a left-to-right interface (same id, columns and spacing, so the output is unchanged) and, where the interface is mirrored, with each row's cells running from the right, every column as wide as its widest cell. File > Info puts Properties at the right and Statistics after it, and the Font dialog's kerning row reads 14 points and above.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs: record the mirrored Arabic interface in the roadmap, gaps and localization parity
Progress log, the Arabic language row, gap 11 and the status lines and revision histories of the three docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Insert Hyperlink, Word Count, Phonetic Guide, New Style, Modify Style, Encrypt, merge rule and Match Fields dialogs read right to left
Their label-and-field rows use rtl::Table, so in a mirrored interface the label is at the right with its field to its left. rtl::Table gains striped() for Match Fields. English renders are pixel-identical (only a caret-blink strip differs in an untouched dialog).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Line Numbers, Hyphenation, Drop Cap, Caption, Cross-reference, Index, Mark Index Entry and Source dialogs read right to left
Their label-and-field rows use rtl::Table (label at the right, field to its left). English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Picture Size, Split Cells, Convert Text to Table, Footnote and Endnote, Table Properties, Cell Options and Custom Properties dialogs read right to left
Their label-and-field rows use rtl::Table. Picture Size's Height and Width pairs read Height first at the right. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Define New Multilevel List, Track Changes Options, content control properties, custom colours and table style dialogs read right to left
Label, field and swatch rows use rtl::Table. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns widths, Special Characters, Compare, AutoCorrect and About Models tables read right to left
Table columns now start at the right edge (rtl::Table gains top(), min_row_height() and an unset column count). About > Models stays a left-to-right scroll box with its columns listed from the right. English renders are pixel-identical (only the caret blink differs).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: table-of-contents styles, recipient filter and sort, and the ribbon Paragraph group read right to left
rtl::Table now stacks its rows top to bottom whatever way the area around it runs (a ribbon group lays out in a row). English renders are pixel-identical (caret blink and a status toast differ in untouched dialogs). The TOC options and recipient filter/sort rows are converted by pattern and not seen in an image.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns preset tiles and Preview label, Macros, Add/Remove arrows, Zoom slider and the Recipient List grid
A preset tile is as wide as its caption and centres it (the long Arabic captions ran into the next tile); the Preview column keeps its width so its label no longer wraps letter by letter; Macros puts the name box and list at the reading start with the buttons beside them; the Add >> and << Remove arrows swap in a mirrored interface so they point the way the items move; the Zoom slider is drawn left to right as in English; the Recipient List data grid scrolls left to right instead of running past the left edge. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: the chart type and SmartArt layout galleries list their tiles from the right
The chart type dialog, the Insert SmartArt dialog and the ribbon's SmartArt Layouts gallery use rtl::Table rows, so the first tile sits at the reading start. English renders are pixel-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Take main's panes/host.rs after the merge
The merge kept a use of mirrored() that main's pane-dock fix removed; this branch does not touch the panes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: Columns preset captions are translated in the mirrored tile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: one Arabic and one French row in the localization parity table; About > Models columns read from the right
The merge left the Arabic and French rows twice in docs/localization-parity.md; each
is now a single row (main's current French text, the Arabic row with the dialogs
note). About > Models keeps main's fitted table but lists its columns, their
alignments and grow weights reversed when the interface is mirrored, so Company
starts at the reading edge. The credits.rs change comes from tareq's helper merge.
Co-authored-by: tareq <tareq.halaby@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* About > Models: the mirrored table keeps its own widths and scroll position
The columns are listed in reverse when the interface is mirrored, and egui keeps the
resizable column widths and the scroll offset by id. A language switched at run time
would apply one direction's widths to the other's columns, so the mirrored table uses
its own id.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: About > Models alignment mirrors as its comment says
The mirrored Models table reversed the per-column right-align flags but
did not negate them, so in Arabic the text columns stayed left-aligned and
the counts right-aligned, the opposite of the comment ("counts line up on
the right (left in RTL)"). Negate after reversing so the text sits at the
reading edge and the counts on the left. Keep this line when touching
models_ui.
* About > Models: a mirrored table that overflows opens at its reading edge
The reversed Arabic table puts Company in the rightmost column, but the
horizontal scroll area started at the left, so when the table was wider
than the dialog (narrow window, widened columns) Company and Model were
offscreen. Stick the scroll area to the right in a mirrored interface.
---------
Co-authored-by: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: email <email@email.com>
Co-authored-by: tareq <tareq.halaby@gmail.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
- 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
* Gap 4: flag document fonts that aren't installed
The font box (ribbon, mini toolbar, both Font dialog boxes) shows a
warning mark when the current font isn't installed; its hover note
names the font drawn instead. The status bar says "N fonts not
installed" and lists each with the faces drawn instead, plus Add Font….
Agents get `font.missing` and `format.state.fontShownAs`.
Layout records, where it picks a face per text segment, the fonts the
shown text asks for that aren't installed and the faces drawn instead
(ParaLayout::substituted, DocLayout::substituted_fonts), so hidden
text, unused styles and orphaned stories never count; list labels,
field results and East Asian and complex-script text do, and so does a
missing roman font drawn in Cambria. Metric-compatible substitutes and
symbol fonts drawn as Unicode aren't flagged. Bounded for hostile
files: 64 pairs a paragraph, 1024 a document, names kept to 256
characters, 20 rows in the popup.
Ported from alehel/wordcraft#1 onto current main; English strings only
(translations belong in storytold/craft-i18n).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: missing-font review fixes
- Judge a padded font name by the face layout drew for it, and skip blank names.
- Name the face CJK text is drawn in for a missing East Asian font, not Cambria.
- Keep the missing-fonts list per window, so several windows don't rebuild it every frame.
- Expose the font box's warning to screen readers as its description.
- Count missing fonts with craft-i18n plurals (`i18n::tn`).
- One table of metric twins (`fonts::word::METRIC_TWINS`): a missing font is drawn in an
installed twin first, and the warning reads the same table.
- Find the status-bar item by part of its label in the click test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: warn for a font picked at the caret; drop fonts of hidden list items
- At a collapsed caret the font box warns for a missing font typing will use, also at the end
of text in another font (it waited for the first letter).
- A list item that shows nothing (#3982) reports no missing font: its number was shaped, and
its font recorded, before layout dropped it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: say where Add Font… is, and that it needs the font file
The font box's note said "Add Font… installs it": Add Font… opens a file picker at the end of
the list, so it only helps with a font file at hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: docs say the popup lists 20, and when the font box warns ahead
CodeRabbit review: the status-bar popup lists the first 20 missing fonts, then a count; the font
box also warns at a collapsed caret for a font no shown text uses yet; `fontShownAs` is the
font's substitute, fallback faces aside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Font dialog warns for a missing font over a selection
CodeRabbit review: the dialog's boxes show the fonts OK applies, which no shown text may use
yet (an Asian text font over Latin text, a font typed into the dialog), so they name the
substitute whatever is selected. The ribbon's box over a selection still reads the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Asian font box names the face CJK text gets
CodeRabbit review: the status bar's list is by font name, so for a font Latin text also uses
it named the Latin text's substitute. The Asian text box now always looks up the face layout
draws Chinese, Japanese and Korean text in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md status on the current line, missing-font scope and twin wording
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md says which missing fonts are flagged and what font.missing lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: font.missing lists what the current view lays out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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
Bump the workspace version for the point release carrying the CP4.3
proofing work: one language list, the missing-pack bar on a fresh
install, and the Options › Language pane.
Every element of the ODF 1.3 schema checked against both corpora and a
document of our own for each one no file held (docs/odt-element-audit.md).
Found and fixed: index and reference marks over text, plugins and
floating frames weren't kept as read, and a form's data source was
taken for a control. Left: password and grid form controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
row_names uses the shared display_name helper, so a built "Language (Region)" name is translated in
parts. The dictionaries-folder comments in prefs.rs and tests.rs say File > Options > Language.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Swedish (Finland) is built at run time, so no catalog row can match it. One helper
(ribbon::display_name) now translates the language, the region and the {language} ({region})
template separately and joins them; the Set Proofing Language list, the Language menu, the status
bar and the dialog all use it. English output is unchanged and matching never uses a name.
gap 8 no longer describes the missing-pack bar, which is in its own branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
* #3867: review — header repeats, picture outlines and linked-box widths bounded the same way every layout
- Header repeats: a Clip (floating Drawing Canvas) counts its members, depth-bounded, so a 1000-member canvas no longer counts as one item (CodeRabbit thread 4373103686); the budget is per layout (Ctx), not per table.
- Tight-wrap outlines: each cache entry stores its picture's pixel cost; a cached hit is charged to the layout's decode budget like a decode, so cold and warm layouts (and the CLI) wrap alike.
- Linked boxes: widths are budgeted by laid-out work (MAX_FLOW_WORK), not 16 widths, so ordinary stories keep exact widths for every box.
- header_rows.contains -> index compare; layout-parity doc revision.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: review — a picture that fails to decode still costs its pixels when cached; header repeats restored on retry
- contour: Alphas records the pixels each picture charged (costs map), also when the body then fails to decode, so the cache entry's cost is right and warm layouts charge it like cold ones.
- Snap saves and restores ctx.header_repeats so balance_pass retries don't keep their charge.
- Tests: truncated large PNG cold vs warm; canvas header row end to end through place_table; linked-chain bound <= 8; vertical box's layout differs from every other box's.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: CodeRabbit — header and linked-box budgets count every clip descendant
placed_cost counts iteratively (explicit stack, stops past the limit) and is shared by flow_work.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: docs — progress log, gap 20 note for layout resource caps and linear-time passes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: review — roadmap gives each cap its unit: header items, wrap-cache pixels, linked text box lines and items
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3867: CodeRabbit — the vertical-box check runs past the work cap too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — a linked chain's work counts the story it lays out, not only the lines it shows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — header-row cost import follows main's explicit layout imports (#4501)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — a test pins the header repeats that column balancing gives back; docs keep main's caps row and both gap status lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Found checking 220 ODT files from other applications (local only):
- What a section hidden with text:display="none" holds is read as
hidden text, so it stays hidden written without the section.
- A top-level list whose items are all as read is written as read
whole (label spacing, picture bullets, list headers); a list going on
with it names it; a section may begin or end with a list.
- A damaged package's media type isn't printed in its error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The Dictionaries folder moves here from Proofing with the Dictionary files list (#1063) kept. Set as
default sets only the saved default proofing language (the new pure review.language.preferred command), and
proof.packs returns the languages rows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
With no list of packs yet the bar offers a language with no dictionary and Install fetches the
list, then the pack. The caret's typing language counts, the spelling context menu heading is in
the interface language, and closing the bar is remembered per document, pack locale and tag.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Every published pack is named in English, Valencian is ca-ES-valencia, and rows are matched by
tag (language, script, region, variant), never by the name shown. One region helper
(packs::is_region, packs::without_region) next to pack_for_tag serves the engine and the UI.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
The Contributors table and the Models table were egui Grids inside a
two-way scroll area: they opened wider than the dialog, so they scrolled
sideways, and Company and Model sat almost touching. Both are now
egui_extras tables. Each column opens at the width of its header and widest
cell, the spare width goes to the text columns (Name; Company and Model),
and every column edge can be dragged. Widening a column grows the table
like a spreadsheet, and only then does it scroll sideways; the header stays
put while the rows scroll. Counts and dates are right-aligned and every
cell is vertically centred.
The sorted column stays highlighted and its direction shows on the
toolbar's sort-order button, so headers no longer reserve room for an
arrow. The opening widths come from fit_widths, a pure function with unit
tests that a fitted table fills its room exactly and that only the text
columns give way when room is short.
The About dialog opens 820 points wide, so every column fits at its natural width.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A kept part without a media type (LibreOffice's empty accelerator
configuration, a file the manifest didn't list) was written without an
entry, and LibreOffice refused the saved file. ODF 1.3 Part 2 §4.3
lists every file; its media type stays empty as read.
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
* #3382: a long comment's card and balloon stop at ten lines
A comment or reply field, in the Comments pane and in a selected balloon,
is at most ten lines tall and scrolls beyond that, keeping its caret in
view. A balloon that isn't selected shows the first ten lines of the
comment and of each reply and cuts off the rest; clicking it opens the
scrolling field. The buttons and the next comment stay in view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3382: record the ten-line comment cap in the UI parity doc; test the drawn rows
The Comments entry of the UI parity doc notes that a long comment's card,
balloon and fields stop at ten lines. A new test checks what an unselected
long balloon paints: its first ten rows inside the card's clip and none of
the rows after them, in Latin and Arabic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3382: check the shown balloon rows start inside the clip
The drawn-rows test now also checks that each of the first ten rows starts
at or below the top of the balloon body's clip, so a clip that hid the
first rows would fail it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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
A paragraph or table holding pictures and shapes without text is kept
as read while unchanged, with the pictures it shows (only those linked
from markup actually written, so a deleted picture stays deleted) and
its frames' styles. Text boxes, shapes with text, objects and controls
stay with the model. The file's Graphics and Frame styles are written as
read; WordCraft's own pictures don't take the file's picture names. The
flat writer inlines a picture whatever form its element has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section at a text box's top level keeps its wrapper, like the body's;
the box's story closes whatever it opened, so an edit inside the box
can't leave a section open past it.
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
* fix(docbin): keep one override per table cell instead of a copy per repeated sprm
* DOC import: vertical alignment reads only the cells a row can hold, like the other cell-range sprms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two wgpu devices drawing at once crash Windows' software rasterizer (WARP, d3d10warp.dll: 0xc0000005, sometimes heap corruption 0xc0000374), which is the adapter a machine without a GPU or a remote session gets. The ui-egui tests that render (a dragged shape, a rotation drag, and the ignored spelling review preview) now hold one lock while their harness lives. On Windows 11 over SSH the two rendering tests crashed 8 of 20 runs together (0 of 20 alone), and the whole ui-egui lib run 2 of 6; with the lock 0 of 20 and 0 of 10.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic interface: mirrored right-to-left window and Arabic UI fonts
Adds the Arabic (`ar`) interface language's code: its catalogs are craft-i18n's
`locales/ar`, so this changes no catalog.
- `ar` is registered in `i18n::LANGUAGES` and `Lang::rtl()` says it is written
right to left; its translations are used as written (egui's text layout
orders each line's bidi text itself). A ribbon location reads from the right
with a leftwards mark.
- `rtl.rs`: the window is mirrored. Containers open in `rtl::root`; columns,
grids, checkboxes, radio buttons, menus, rows, panels, the dialog tab strip
and the ribbon's scrolling row follow the language's direction. Title bar,
status bar, ribbon tabs and groups, File pages (including Print, with the
settings on the right and a solid scrollbar gutter), dialogs, menus and the
task panes (their docking edge swaps) are laid out from the right. egui's
`Grid` panics in a right-aligned layout, so grids go through `rtl::grid`.
- Arabic UI font fallback: the craft-fonts Arabic face, else an installed
Arabic font, loaded when the interface is Arabic.
- The "Auto (200%)" size label keeps its percent sign inside the isolated
number.
- Tests: Arabic locale, saved preference and lookup order on fixture catalogs;
an interface audit (skipped without the craft-i18n Arabic catalog) that lays
the app out right to left at several widths, checks tab, group, pane, Print
and Go To positions and that nothing panics.
The Arabic work builds on Abdelhaq El Amraoui's mirrored-interface PR (#737).
Co-Authored-By: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: pin craft-i18n at a commit with the Arabic catalog; review fixes
- craft-i18n.rev and the Flatpak manifest pin 19358491 (craft-i18n main), the
first commit line that has locales/ar/wordcraft.tsv. The old pin had no
Arabic catalog, so `ar` was registered but not embedded and two i18n tests
(the_language_setting_is_a_ui_command_and_persists,
the_languages_offered_are_the_embedded_ones_of_the_list) failed in CI.
- The Arabic font fallback is requested for any right-to-left interface
(`Lang::rtl()`), not only after the language picker sets the flag, so an
automatic or saved Arabic gets the installed Arabic face.
- The file browser, the update window and Reading Mode's toolbar run their
bodies in `rtl::root`, so their far-side buttons and rows are mirrored.
- A group in a right-to-left ribbon row is measured from the right edge: the
cursor's left edge is negative infinity there, so its width was NaN and the
ribbon planner dropped every group, never compacting a narrow Arabic window.
A narrow window now folds groups into drop-downs.
- The mirrored checkbox and radio marks are labelled by their text, so screen
readers name them.
- Tests: group width in a right-to-left row; the Arabic interface audit checks
Reading Mode's toolbar.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: ask for the installed Arabic font separately from the CJK one
A previous commit made every right-to-left interface request the installed CJK
fallback font (`system_cjk`) so that the installed Arabic font would load. That
also made every Arabic launch read a large system CJK font file when no
craft-fonts input is embedded (the Flatpak build has none).
`install_ui_fonts` now passes a separate `system_arabic` flag (`Lang::rtl()`):
the installed Arabic font is read for it, and `system_cjk` is again set only
by a CJK interface or by CJK text on screen. `WordApp::fonts_system_arabic`
remembers what was installed, so a language change reinstalls the fonts.
Tests: the_installed_arabic_font_is_asked_for_by_arabic_alone (the Arabic flag
adds the installed font only when none is embedded, and the CJK request alone
adds no Arabic font). Checked a headless screenshot of the Arabic interface
without craft-fonts: the installed Arabic font draws the labels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix the AutoSave switch knob: on is at the right, mirrored in Arabic
The title bar's AutoSave knob was placed with `lead_x(rtl == on, ..)`, which
reverses main's mapping in both directions: left to right it drew the knob at
the left when the switch was on, and right to left at the right. main draws it
at the right when on and at the left when off, so the mirrored interface must
draw it at the left when on: `rtl != on`.
The position is now `autosave_knob_x(rtl, on, track)`, pinned by a test for all
four cases. A headless screenshot of the off switch shows the knob at the left
in English and at the right in Arabic.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: file names and paths in right-to-left lines; kerning row order
Tester feedback on the Arabic interface:
- File > Open's Recent list, the built-in file browser and the title bar drew
file names and paths raw, from the left edge. In an Arabic line the marks of
a name ("(نهائي) 2.docx") and the folders of a path with Arabic parts were
scrambled. Rows now start at the side reading starts from (`rtl::lead_x`),
names go through `i18n::isolate` (a first-strong isolate, with a left-to-right
mark before the extension so `2.docx` stays `2.docx`) and paths through
`i18n::isolate_path` (a left-to-right isolate with a mark after every
separator, so neighbouring Arabic folders do not swap). Other languages get
the text untouched; saved and spoken text never carries the marks.
- Font dialog, Kerning row: the dialog's grid is drawn left to right inside a
right-to-left window, so "Points and above" came after its number; in an
Arabic interface it now comes first, so the line reads "14 نقطة فأكثر" from the
right. (The grid's columns themselves still keep their left-to-right order.)
Tests: names_and_paths_are_isolated_in_a_right_to_left_interface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: mirror through craft_ui::direction and Style::right_to_left
The suite's craft-ui now has the direction helpers (`craft_ui::direction`) and
its egui fork a `Style::right_to_left` that mirrors egui's own chrome. Use them
instead of our parallel wrappers:
- `rtl::sync(ctx)` applies the interface direction every frame the language's
changes (`direction::apply`): it sets the shared direction state and
`Style::right_to_left` on every theme, which draws check boxes and radio
buttons with the mark at the reading start, mirrors submenus, scroll bars,
title bars and the combo boxes' arrows (the Print page's choice lists, the
font list) and opens combo menus from the right.
- `rtl::checkbox`, `radio_value`, `radio` and `indeterminate_checkbox` are
gone: about 200 call sites use plain `ui.checkbox`, `ui.radio_value`, ... and
egui does the mirroring (and names the control by its text, as a plain widget).
- `lead_x`, `lead_center`, `lead_top`, `start_panel` and `end_panel` delegate to
`craft_ui::direction`. `root`, `flows`, `island`, `grid`, `menu`, `vertical`,
`far_side` and the other layout helpers stay: the shared crate has no
equivalent that fits them.
Visual changes: the combo boxes' drop-down arrows and lists are mirrored in
Arabic; nothing changes in left-to-right languages (checked headless
screenshots of the English main window, Font dialog and Print page).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Recent files and the file browser: full path on hover, long names keep their start
Review feedback:
- Each row of File > Open's Recent list shows the document's full path as its
tooltip (the path line under the name can be longer than the row).
- A long name in the built-in file browser is laid out in one row, with an
ellipsis at its end, from the side reading starts from, instead of being
clipped at the row's edge: in a right-to-left interface the old text anchored
at the right edge cut the end of an Arabic name and the start of a Latin one.
The whole name is the row's tooltip when it was shortened.
Checked headless screenshots of the file browser with a long English and a long
Arabic name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font combos in a right-to-left row: the arrow joins its field on the left
The ribbon's font name and size boxes are drawn from a field and a separate
arrow box that share one outline: the field is square and the arrow's box is
rounded on the side where the arrow goes, painted a border width under the
field. That assumed the arrow is right of the field. In a right-to-left row
(the Arabic ribbon) the arrow comes after the field on its left, so the field
was square on the wrong side and the arrow box rounded on the side facing the
field: two boxes with a gap-like seam, the arrow looking outside the frame.
`arrow_box_shape(rtl, ..)` mirrors the radii and the underlap, and the field's
square corners are chosen by the row's direction (the nw/sw pair in right to
left). Left-to-right rows are drawn exactly as before (English before/after
crops are identical).
Test: the_arrow_box_is_rounded_away_from_its_field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n 4612850 (merged Arabic WordCraft rows, craft-i18n#76)
The pin and the Flatpak manifest commit move from 1935849 to 4612850, which is on
craft-i18n main and adds the 70 Arabic rows this branch relies on.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n cac2cf0 (Arabic number agreement and status-bar rows)
Moves the pin and the Flatpak manifest commit from 4612850 to the current craft-i18n
main, which includes craft-i18n#95 (48bed03) and #81 (ef58ff3).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: isolate paths shown beside labels; mirror the Print page zoom
File > Info shows Location as its own left-to-right path label (cut short with the full path on hover, not wrapped), and the open-failure, Exported and dictionary-file messages isolate their paths. The Print preview zoom puts minus at the slider's left end and plus at its right in Arabic, as the status bar does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: mirror the label/field rows of the Font, Info, Paragraph, Page Setup, Insert Table and Options forms
rtl::Table lays a form out as an egui Grid in a left-to-right interface (same id, columns and spacing, so the output is unchanged) and, where the interface is mirrored, with each row's cells running from the right, every column as wide as its widest cell. File > Info puts Properties at the right and Statistics after it, and the Font dialog's kerning row reads 14 points and above.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs: record Arabic mirrored interface landing (#1195)
Update the language counts, Arabic parity row, gap #11, and UI/ROADMAP
status so they match the craft-i18n catalog and mirrored shell already
on this branch.
* Fix Organizer Copy arrow in RTL and refresh roadmap status.
Point the Copy glyph at the Document column when the shell is mirrored,
and bump docs/roadmap.md status/revision history for Arabic UI (#1195).
* fix(ui): keep pane dock left/right physical in RTL
ui.paneDock and view.stylesPaneDock now store and report physical window sides; only unset defaults still open on the reading-start edge. Also wrap the language-packs grid for RTL, document the contract, and load a system Arabic face for Arabic names in LTR UIs.
* docs: clarify mirrored default pane edges in RTL
Start-default panes open on the physical right in Arabic; end-default
panes open on the physical left — not every never-docked pane on the right.
* refactor(ui): one PaneState for a saved pane place
The four saved-dock arms built the same PaneState; only the place and
whether it was set explicitly differ.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style(ui): rustfmt the panes re-exports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: The Legend Of MTS Dev <tlo.mts.tsh@gmail.com>
Co-authored-by: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: email <email@email.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
The standard (§18.3.1, since ODF 1.2) reads an angle without a unit in
degrees. WordCraft read unitless gradient angles in tenths of a degree in
every file; in ODF 1.3 files it now follows the standard.
LibreOffice and OpenOffice wrote gradient and hatch angles in tenths of a
degree in their files before ODF 1.3 and still read them so; read by the
standard, every older LibreOffice document's gradients would turn the
wrong way. So in files older than 1.3 they are still read in tenths: a
deviation from the standard, out of necessity, now listed in
docs/odf-interpretation.md (a new rule and section for such deviations).
WordCraft always writes these angles with a unit.
The ODF 1.2 -> 1.3 changes (ODF 1.3 Part 3 Appendix G and their OASIS
issues) were checked: none other gives a value written for 1.2 another
meaning; the new defaults match how LibreOffice already read them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
paragraph_issues dropped issues touching a run left unchecked by testing
every run of the paragraph against every issue. A paragraph can have many
runs and many issues within the proofing size limit, so the filter was
quadratic in the paragraph's size. The unchecked ranges are now merged and
sorted once, each issue is looked up by binary search, and paragraphs
without unchecked runs skip the filter.
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 placeholder test used the label <Title>, which LibreOffice's letter
templates also use; it now uses an invented one. The kept-markup tests
say that every sample (and any sample file the no-edit loss gate adds)
is written by us, never taken from someone else's files.
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
LibreOffice reads an author field without text:fixed as fixed (the
standard says not fixed) and writes false itself; an empty author field
stayed empty after a save instead of showing the author. Sender fields
are written as LibreOffice writes them, without the attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The default paragraph style's text colour (fo:color) is read into the
document's default and written back: Google Docs' grey résumé came back
black. What a covered table cell holds (not shown, §9.1.5) is skipped;
it went before the table as empty paragraphs, pushing ONLYOFFICE's table
down the page.
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
Kerning turned on or off span by span (style:letter-kerning) is read and
written where it differs from the document's default; without it
LibreOffice set ONLYOFFICE's text wider and every page broke elsewhere
after a save. A note reference keeps its own formatting (a span around
text:note), and a note starting with a tab keeps it: only the space
WordCraft puts after the mark is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A fixed date or time (text:fixed) opens as a locked field and is written
back fixed, with the value its text shows under its picture
(text:date-value / text:time-value), which LibreOffice needs to keep it.
One whose text can't be read back is written as text. Sender fields open
as SENDER fields and are written back as text:sender-*.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Another ui-egui test's frame runs language_packs::tick, which puts installs on the engine's
thread while this test's backend is set; start_install then returned Ok and the test failed
in the full suite (it passed alone). Red on main since the language packs merge (#5224).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Word's TOC over entries in columns keeps its TOC Heading title in a one-column
section above a blank paragraph that ends it; Update Table moves the field into
that blank paragraph. Replace (references.toc with settings) put the new title
under the break, inside the two-column section, and Remove kept the blank
paragraph and its break.
Observed black-box in Word for Mac 16.114.3 on a synthetic document: Insert
Table of Contents keeps the title, the blank paragraph and the break and only
changes the field; Remove Table of Contents takes the title, the blank
paragraph and its break with the entries.
Replace now keeps the title above the break (renamed when a title is given)
and rebuilds the field and entries in place; Remove takes the title and the
blank section-ending paragraph with the table. A paragraph of another style
above the break stays.
Co-authored-by: Harrison Smith <harrison@hsmith.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #5290: Page Up/Down move by a page in Outline, Draft and Web Layout
These views lay the document out as one long page, so caret.pageUp/pageDown looked for a previous or next page, found none and left the caret where it was. Move by the text area height of the caret's section instead, the same distance Print Layout moves.
* #5290: Page Up/Down pick the line past the caret, and page from a hidden outline selection end
Choose the destination among the lines past the caret by height first (DocLayout::line_toward), so narrow right-indented paragraphs can't keep the caret on its own line when the step is short. The above/below test is shared with vertical().
In Outline view, a selection that ends in text a collapsed heading hides pages from where the outline shows it (outline::shown_pos, pulled out of after_command), keeping the anchor. Avoid f32::clamp, which panics on a NaN page height.
When a region is chosen in File › Options, start-up replaces the first blank document with one
on that region's paper through `session.set_document`. That changes the session's active
document, but the window still showed the old one, so on the first frame `sync_document` took
the change for a switch of document and cleared the dialog. A password-protected file named on
the command line (a double-click in the file manager) had just asked for its password with
`file.open`, so the Password box was dropped before it was drawn and a blank Document1 stayed.
`WordApp::replace_startup_document` swaps the document and lets the window follow at once; the
start-up code uses it. Before the file is opened, so nothing is open to lose.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Found by a loss check comparing what a file uses with what a WordCraft
save writes back (over the 133-document local corpus):
- Every font a file uses is declared (office:font-face-decls, §16.21:
family, generic family from its name, pitch) and named where it is
used (style:font-name and its Asian and complex forms beside the
fo:/style: font families), so a reader without the font picks a like
one. Lost in 132 documents.
- meta.xml keeps who changed the document last (dc:creator), when
(dc:date) and how often it was saved (meta:editing-cycles), read into
and written from CoreProps. Lost in 120.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The WOFF2 decoder rebuilds every collection font's table directory (16 bytes
per listed table) before checking its output size, while the file spends as
little as one byte per listed table. A small collection listing the same
tables over and over made it allocate hundreds of megabytes twice.
Preflight the collection directory before decoding: the rebuilt directories
must fit 16 MiB and no font may list a table twice.
A name record can hold 64K characters, and every face copies its family into
each localized name and every variable-font named instance (with their
localized names again), so a small font could claim hundreds of megabytes of
name strings before any later check.
name() now keeps at most MAX_NAME_CHARS characters, the limit localized names
already used, so every copy stays small.
The TIFF guard read the first Compression tag, but the tiff crate keeps the
last copy of a tag. An allowed first value (none, LZW...) could hide a JPEG
compression after it, and the JPEG strips are then decoded by a nested
decoder with its own limits, outside the caller's budget.
Building the queue checked every character of the document against every
citation field, so a document with many citations made Read Aloud do
characters x citations comparisons before playback started.
The citation spans are now sorted and merged once, and each position is
looked up by binary search.
Found with Google Docs' and ONLYOFFICE's ODT files (now in the local
corpus):
- A heading's look comes from its style, its outline level from its
element: a text:h without text:outline-level (level 1, §19.850.4) in a
style based on Heading 2 looks as Heading 2, as LibreOffice draws it,
and keeps outline level 1 (it became Heading 1). The level is carried
apart from the style (ParaProps::outline_level through the flow) and
written back as text:outline-level.
- Widow and orphan control is stated in the default paragraph style
(fo:widows/fo:orphans 2) when Normal has it: LibreOffice reads none
unsaid as off, so pages broke differently.
Google's 2011 Fundamentals export now round-trips with its 88 pages and
its text (1.27M -> 139k differing pixels).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- PACK_INDEX: language-packs-2 index.json, sha256 b35c7aef…, built from craft-i18n 20467b63
- craft-proof pinned at storytold/craft-libs 47c9f625 (flag-based compounding, #497; review follow-ups and en-AU, #496)
- Options › Language: ProofLang::EnAu has no Hunspell dictionary name of its own (English uses SCOWL)
- docs: Danish, German, Dutch, Swedish, Norwegian, Hungarian, Estonian, Croatian, Persian and Esperanto are packs; Mongolian and Thai stay hidden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
The padding inside an ODT page border (§20.210) was capped at Word's
31 pt border spacing when opening, so the text of WordCraft's own page
borders (24 pt from the edge, 1 inch margins: 45 pt of padding) moved
towards the border on reopening. In ODF the padding is the distance to
the text, any length on the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
WordCraft's watermark was left out of ODT files. It is now written as
LibreOffice and Word write theirs: a fontwork custom shape
(draw:text-path, draw:type fontwork-plain-text) named
PowerPlusWaterMarkObject, behind the text and centred on the page, in
every header (a header made for it alone fits inside the top margin, so
the text doesn't move). It doesn't grow to its text (draw:auto-grow-*
false), which LibreOffice needs to stretch the text to it. Such a shape
in a header, from LibreOffice or WordCraft, opens as the document's
watermark (text, colour, see-through, turn, size), not as a shape.
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
Found with LibreOffice's style templates.
- An in-line frame placed against the text (`style:vertical-rel` "text"
or "char", with `middle`, `top` or `bottom`) stood on the baseline. It
is now raised or lowered against the text's height, from the size of
the text it sits in. Measured in LibreOffice, `middle` centres the
frame on the text: its middle half the ascent less the descent above
the baseline. Placement against the line still stands on the
baseline.
- An in-line picture or chart was saved with no placement style. In
LibreOffice that puts it at the line's top (measured: the title beside
the Finances logo moved up 19 pt). It is now saved standing on the
baseline ("top" to "baseline"), as WordCraft and Word show it.
Two templates' round-trip differences fall from 1,874 and 1,433 pixels
to 521 and 586. Across the corpus the difference falls from 47k to 45k,
and no document gets worse.
Left in these templates:
- template instantiation: LibreOffice refreshes a template's fixed dates
and fills its sender fields from the user's data when opening it;
- a first-page footer's frame placed against the page content;
- a page background picture centred in the margins rather than on the
page.
gaps.md row 19 lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Charts: on save, WordCraft wrote each chart anew from its chart model
(type, data, title, legend, labels), losing the look it doesn't model:
fonts and sizes, axis and grid lines.
- The chart object as read is now kept with the chart (`Chart::source`
in the flow model, `Graphic::source` in the document).
- `chart::redraw` keeps it when the chart is drawn again on open or
resize.
- The ODT writer writes it back whole while it still reads as the same
chart. An edited chart is written from the model, as before; DOCX
still writes charts from their model.
- The chart documents now round-trip pixel-identical (chartcol: 5,917
pixels to 0).
LibreOffice's serif business letter (4,223 pixels to 116):
- A text frame outlined by a drawn stroke (`draw:stroke`, a 0 width a
hairline) and no `fo:border` lost its outline. It is now the frame's
line; an explicit "none" border still wins.
- A header or footer whose style states no height, or that has no style,
was 0 tall. LibreOffice makes it 0.5 cm (measured by varying the
height), so the text had moved up 1.5 pt.
Across the corpus the round-trip difference falls from 60k to 47k
pixels, and 41 of 125 documents round-trip pixel-identical. No document
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
Found with LibreOffice's agenda templates. Saving wrote a table's first
row as `table:table-header-rows`, which LibreOffice repeats at the top
of each page, so a table crossing a page showed its first row twice
("<bring>" twice). The cause: the flow model marks a first row as a
header when the table style's look has a header row, which Word's look
has by default. That only affects the style's formatting, and HTML and
Markdown still rely on it for heading cells.
- The flow table now carries how many rows repeat
(`TableExtra::header_rows`, Word's Repeat as header row).
- The ODT writer writes only those rows as header rows.
- That extra block is now always kept for ODT flows, where it had been
dropped from tables without an alignment, width or row height.
All 16 agenda templates now round-trip with their text the same; the
modern and simple ones fall from about 5,200 differing pixels to 145.
Across the corpus, documents with the same text after a round trip rise
from 75 to 91 of 125, and the round-trip difference falls from 82k to
60k pixels.
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
* DOCX: VML horizontal rules take their line
A web page's <hr> pasted into Word is a VML rule: `v:rect` with
`o:hr="t"`, often `width:0`. WordCraft took the width as written and
drew a 1 pt dot where Word draws a line across the page.
Measured on Word with a synthetic document: a rule with no width and
no `o:hrpct` spans the line inside the paragraph's indents; `o:hrpct`
is a share of it in tenths of a percent (500 = half); `o:hrpct="0"`
keeps the shape's own width; `o:hralign` places a shorter one (left,
centre, right); `fillcolor` and `stroked` apply as to any VML shape.
WordCraft now matches each case within 0.1 pt.
- wordcraft-doc: ShapeExtra::hr (HorizontalRule: share, alignment,
Word's standard look).
- wordcraft-docx: `o:hr`, `o:hrpct`, `o:hralign`, `o:hrstd` read; a rule
is written back as the same VML rule (DrawingML has none), and Word
opens the re-saved file with the same lines.
- wordcraft-layout: a rule takes its whole line and is drawn at its
share; measured for a table column it asks for no width, so it
follows its column. (Word draws a rule in a cell at the page's text
width, centred on the cell and past its edges; it stays in the cell
here.)
docs/file-format-parity.md is updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: only an inline, shown rule is one
Review: the rule arm of the writer ignored the shape's placement, so a
rule imported with position:absolute or visibility:hidden came back
inline and visible. Only a rule in the text and shown is read as a rule
now (a pasted <hr> is one); a positioned or hidden one stays a plain
shape, which keeps both when saved. The writer checks the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: a see-through fill is kept; column measuring asks for the right width
Review: `write_hr` wrote only the fill's colour, so a rule with `v:fill
opacity` came back opaque after a save; the opacity now goes back as `v:fill`.
Review: measured for a table column, every rule asked for no width, so one of
its own width (`o:hrpct="0"`) could end up clipped by a narrow column; it now
asks for its width. AutoFit to contents measures on a line of its own width
that the rule took for a real line, so a rule sharing its line made the
column as wide as it could go; any measuring width now counts.
`Placed::Shape` boxes its extras: with the rule, `Placed` went past clippy's
large_enum_variant limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: placed in what the line has left; rotation and flips kept on save
Review: a whole-line rule ignores a first-line indent or a line narrowed by a
float. Measured in Word, it does too: the width stays the share of the
paragraph's width and the rule runs past the margin, or under a float on its
right. What did differ was the placement: Word centres and right-aligns a rule
in what the line has left, with a first-line indent moving the end of that room
along, and a right-aligned rule beside a float was drawn off the page here. The
display now places it that way (`ParaLayout::rule_room`).
Review: a save dropped a rule's rotation and flips; they now go back in its
style, as Word writes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: VML horizontal-rule revision row at the top of the history
---------
Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Shapes: a solid fill keeps its transparency
A shape's fill colour can carry a:alpha, its opacity. WordCraft dropped
it, so a translucent white wash laid over a picture was drawn opaque and
hid the picture. The transparency is now read into the shape, drawn on
screen and in the PDF, and written back.
* Fix#1600 freeform and soft-edge fill opacity
* #1600: wrap canonical caret sampler signature
* #1600: format canonical integration
* #1600: assert canonical DOCX alpha round-trip
* #1600: document canonical shape opacity API
* Adapt alpha fixture to rectangle render bounds
* docs: one file-format-parity row for soft-edge fill opacity, status line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found with LibreOffice's report templates and one-page résumé.
Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
without a border; measured, 0.5 in of padding and no border moves the
text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
`Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
setting, now renamed `Settings::odf_layout` to cover all of
LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
Word documents are unchanged.
Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
and sections) were made equal. They now keep their widths and are
written back as shares. The writer leaves out `fo:column-gap` beside
them, because LibreOffice ignores a page's column widths when a gap is
stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
with Word's column break, written back on the paragraph.
The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document 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
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.
- A new document setting, `Settings::object_lines_fit_objects`, makes
the layout size such lines by the pictures alone. The ODT reader turns
it on, so other documents keep Word's layout. An equation or any text
on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
Letter page with 1 inch margins. It now uses the region's paper with
LibreOffice's 2 cm margins.
WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's elegant agenda:
- Paragraph size: a paragraph holding only pictures lost its own font
size, since the flow model's picture runs carry no formatting, so its
line grew. Such a paragraph (`Para::textless`) now keeps its formatting
as the paragraph mark's, which its pictures' runs take too. Read and
written as for empty paragraphs.
- Raise: an in-line frame's raise was dropped when a parent style gave
it a relation no in-line frame has (the Graphics style's
`style:vertical-rel="paragraph"`). It is now measured from the baseline,
as LibreOffice reads it.
The agenda's round-trip difference falls from 6,542 to 1,331 pixels, and
two business fax templates' from about 5,300 to 270 and 350. Across the
corpus it falls from 185k to 155k. No document changes page count.
WordCraft's own rendering of a picture-only line is still taller than
LibreOffice's, a layout difference left for later.
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
Found with LibreOffice's report templates.
Tables:
- A table filling the margins (`table:align="margins"`) but wider than
them ran off the page. It is now fitted to the text width, its columns
in proportion. Checked in LibreOffice, which fits such a table with or
without relative column widths; the old test's claim otherwise is
corrected.
- A table's own space above and below (`fo:margin-top` and
`fo:margin-bottom` on the table) was dropped. It is now read into new
`TableProps::space_before`/`space_after`, laid out (added to the
spacing of the paragraphs around it, as LibreOffice does) and written
back.
Headers and footers:
- A header taller than its band lost its spacing from the body, moving
the text up. LibreOffice keeps the spacing below the content unless it
is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
`SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
past a tall header or footer, and the writer writes it back. Word keeps
no gap, so DOCX documents are unchanged.
Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame that doesn't state its anchor or position now takes them from
its graphic style chain (ODF lets `style:graphic-properties` carry
`text:anchor-type`, `svg:x` and `svg:y`).
LibreOffice's business card template relies on this:
- its Labels style holds the card in line;
- its Graphics style puts the credit at the top of the page.
WordCraft had floated the card, so a save scrambled the layout and added
a page.
Across the 125-document corpus, no document now changes page count
through a save (3 did), and the round-trip pixel difference falls from
333k to 241k. 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
Saving LibreOffice's letter and fax templates added a page and moved
their logos. Fixes:
- Default font size: the writer always wrote the default style's size as
12 pt. It now writes Normal's size (11 pt in these letters).
- Page-anchored frames: a frame placed "from the paragraph" was moved by
the page margin. A page has no paragraph, so it is now placed from the
page, and "from the paragraph's content" from the page's text area, as
LibreOffice places it.
- Text box styles: a custom style used only by paragraphs in text boxes
(the fax's centred "Frame contents") was dropped. The style pass now
walks text boxes and groups.
- Empty paragraphs: an empty paragraph's own font size (its height) was
dropped. It is now kept as the paragraph mark's formatting, a new
field in the shared flow model that the ODT reader and writer use.
The marine, fax and bottle letters keep one page after a save. Over the
125-document corpus the round-trip pixel difference falls from 408k to
333k, and the business letters' differences roughly halve.
Also moved a doc comment that 48c60a5 left on the wrong function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT:
- turned characters;
- conditional text;
- control looks;
- embedded spreadsheets' contents;
- dates in non-US locales.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- The document's default font size (`fo:font-size` in
`style:default-style`, or the Standard style's: the document's
defaults) was ignored, leaving 12 pt; LibreOffice's letters are
10 pt. It is now the document's default. A named style with no parent
keeps the default style's size, as ODF inherits it.
- A date style ordered by the locale (`number:automatic-order`) showed
its parts as listed: "2026November2". It now shows in US English
order: "November 2, 2026", or "11/2/2026" with a numeric month, as
LibreOffice shows it here.
WordCraft's own rendering of LibreOffice's letter templates comes much
closer: one letter goes from 11,377 to 6,237 differing pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A page layout's background picture (`style:background-image`, §17.3:
LibreOffice's report templates' world map) was dropped. It is now
WordCraft's page picture (Design › Page Color › Fill Effects) and is
written back with its picture:
- repeated: tiled;
- stretched: stretched;
- not repeated: centred at its own size, a new fill that the screen
renderer and PDF export draw in the middle of the page. DOCX writes
it stretched, as VML has no centred fill.
LibreOffice's report template with the map goes from 11,719 to 3,602
differing pixels, and shows the map after a save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- A table starting a page (`fo:break-before="page"` on the table,
LibreOffice's agenda minutes) didn't. Its first paragraph now has a
page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
now moves the whole table to the next page, as Word does. It was
ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
LibreOffice's columns never fit their content.
LibreOffice's 6orange agenda now round-trips with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
* #3660: refuse a package that declares too many zip entries before zip parses them
zip::ZipArchive::new parses and keeps every central directory header
the end record declares before its caller can look at len(). A ZIP64
end record can declare one per 46 bytes of the file: a 47 MB package of
one stored file listed 1,000,000 times takes zip 3.0.0 about half a
second and a large transient allocation, and len() then says 1, since
the copies share a name, so the docx reader's MAX_ENTRIES cap never
fires. ODT and spreadsheet packages had no cap at all.
wordcraft_doc::zip_guard::check finds the end records the way zip finds
them (backwards; the ZIP64 record from the locator's offset on; a plain
record when it has no entries or a central header before it) and refuses
a package declaring more than the reader's limit. The docx package
reader, the ODT reader, the spreadsheet reader (mail-merge data) and the
template-title probe call it first, with 20,000 entries as the limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* formats: keep the extensions doc line on EXTENSIONS
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Count the runs proofing text walks, instead of timing it (#5185)
`proof_text_walks_the_runs_once` was the one linear-time test here that timed
its walk, and the ratio crept over its 24x limit when the crate's tests ran in
parallel -- 28.2, 26.4, 25.6 -- from cache and memory contention rather than
from any regression.
The file's own module doc says why the others don't have this problem: "The
cost is the steps the lookups' walks count (crate::work), not a timing, so a
busy machine can't make a linear lookup look quadratic or the other way round."
It then says this one is timed because "proofing text, whose walk is the
document model's" counts no steps -- but `proof_text` does the walk itself,
advancing a runs iterator alongside the text, so it can count the runs it looks
at like every other walk here. Nothing in `wordcraft-doc` had to change.
before 8x the runs took 28.2x the time (limit 24), under load
after 50000 steps for n, 400000 for 8n, ratio 8.0 (limit 12)
The count is the same on any machine under any load, so the flakiness is gone
rather than made less likely: raising the limit to 32x would have left the same
failure available on a busier machine. It is also about 100x faster -- 0.03 s
against three rounds of eight repetitions -- and `work::steps` is `#[cfg(test)]`
inside, so release builds are untouched.
The test still catches what it was written for. Putting the quadratic walk back
(the run for each character found from the first run, every time) gives:
proof_text: 1250025001 steps for n, 80000200001 for 8n, ratio 64.0
panicked: 8x the input took 64.0x the steps: not linear
Verified under the reported condition: three runs of the whole crate suite in
parallel, 559 tests, all green.
* linear tests: proofing text is counted now, not timed
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
`w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
disc the dot, a dot below the under-dot.
Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
already draws nothing for SET, so its result stays as it was, as
`ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- ODT variables (`text:variable-set`, `text:variable-get`, §7.4) were
read as their text. A set is now Word's SET field, followed by a REF
when it is displayed; a get is a REF. They are written back declared
(`text:variable-decls`), the sets hidden with a get after them.
- The engine now evaluates SET (ECMA-376 §17.16.5.58). It shows
nothing and gives its name a value. REF to a name no bookmark has
shows that value; a REF before the SET shows the last value set, as
Word's bookmark does after an update. This works for Word documents
too.
- Custom character styles now keep what they turn off under a parent
that turns it on (`fo:font-weight="normal"`…), as paragraph styles
already do.
A LibreOffice document with variables round-trips with the same text,
and the saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT after the template work:
- emphasis marks and turned characters, which have no WordCraft model;
- variable fields;
- control looks;
- embedded spreadsheets' contents;
- tables wider than the margins;
- what a character style turns off.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More from LibreOffice's bundled templates:
- WMF and EMF pictures (LibreOffice's fax and letter logos) were
dropped. They now open as pictures, at their own size (a placeable
WMF's or an EMF's frame) when the frame gives none, and are saved
with their media types.
- A floating picture inside a text box, saved by WordCraft, didn't
show in LibreOffice. Picture frames' styles now derive from the
Graphics style (declared, like Frame), which LibreOffice needs to
show it.
- A named paragraph style turning off its parent's bold (agenda
dates under bold titles) showed bold, both in WordCraft and in
LibreOffice after a save. What a style turns off (bold, italic,
underline, strike-through) is now kept and written.
- Columns' shares (`style:rel-column-width`) are now written only for
tables filling the margins. LibreOffice lays out an aligned table
oddly with them: WordCraft's sample table lost its proportions.
- Drawings waiting for a paragraph now join only a paragraph of the
text itself, never one in a text box or note.
With the harness's paper setting fixed (it had made LibreOffice draw
unsized pages as Letter), most of the 78 templates compare much closer
page by page. The CV template and the contour sample are now
pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More fixes from LibreOffice's bundled templates:
- A page margin the file leaves unsaid was 1 in, and a page layout with
no properties at all was ignored. Both now take LibreOffice's
defaults: 2 cm margins (observed black-box). LibreOffice's CV template
now lays out as LibreOffice draws it.
- Drawings anchored outside any paragraph (LibreOffice's
page-anchored frames before the first paragraph) made a paragraph of
their own. That paragraph started the document on the default page
style rather than the one the first paragraph asks for, which put the
running header on a letter's first page and pushed the letter onto
two pages. They now join the next paragraph; before a table or at the
end, they still get a paragraph of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice ships 78 text templates (letters, faxes, agendas, reports,
CVs). Round-tripping them through WordCraft exposed these problems:
- Placeholders (`text:placeholder`, "<Recipient's name>") were
printed text. They now open as plain-text content controls showing
them, so a click selects one and typing replaces it. Their
description becomes the control's title. They are written back as
placeholders of their type while they still show; what was typed is
written as text.
- A fixed date with neither a value nor text was lost. It is now a date
field.
- `text:author-name` opens as USERNAME, which the engine left empty.
USERNAME and USERINITIALS now show the user's name and initials
from File › Options; a quoted text after the name is shown instead.
- A "First Page" master followed by another (`style:next-style-name`)
put the running header on the first page. It is now a section with a
different first page: its own (empty) header on page 1, the next
master's after that.
- A page layout without a size was Letter. It is now the default
page's, the user's region's, as LibreOffice uses its default paper.
ODT import takes the regional page like the other importers.
- Style-only templates (no `content.xml`) failed to open. They now
open empty with their styles.
- Table columns are now written with their share of the table
(`style:rel-column-width`) too. A table filling the margins of a
narrower page keeps its proportions in LibreOffice.
All 78 templates now open and round-trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
An alternative format chunk holding a Word document with w:matchSrc keeps its
own styles: a style the document already has comes along under a new id, the
chunk's document defaults (and Word's, where they say nothing) go into its
base styles, and its unstyled paragraphs use its own Normal. Without
w:matchSrc the document's styles win, as before. Adopting another document
now also renumbers table styles and the revisions of table, row and cell
property changes.
A document whose chunk shares style names with it (Normal with an exact line
height, an italic Subtitle) ran a page longer than in Word: the chunk's tables
took the document's styles. It now keeps its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word brings a document's alternative format chunks (w:altChunk) in where they
stand when it opens the file; WordCraft left them out, so a document made of
one (as generators and mail-merge tools write them) opened empty.
- docx: read_with/ReadOptions with a ChunkImporter for formats other than
WordprocessingML; nested Word documents are read here (at most 4 deep,
256 chunks and 512 MB of chunk bytes a file, XML depth counted on).
- doc: Document::adopt (moved from paste) brings another document's media,
lists, styles, notes, comments and revisions along under new ids; pictures
in groups and charts are relinked. Insert > Text from File uses it and now
keeps the file's pictures and lists.
- engine: chunks in MHT, HTML (in their charset), RTF and plain text go
through the format importers.
- formats: MHT web archives open (MIME, quoted-printable/base64, charsets,
pictures by Content-Location or cid). HTML reads style element rules
(simple selectors, cascade, budgets), paragraph margins, indents, line
height, cell widths and borders; an opened page takes Word's web-page
defaults (Times New Roman 12 pt, single lines, black headings, automatic
spacing). html::decode knows the single-byte Windows code pages.
docxide-pdf suite: croatian_regulations_altchunk goes from one empty page to
Word's 8 pages, SSIM 2.7% -> 78.7%; no other document changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hidden form controls (`form:hidden`: a name and value the form sends,
not shown) were dropped. They are now kept verbatim, with their
properties, and written back into the form with ids of their own.
- OpenDocument text templates (`.ott`) weren't opened. They now open
as documents, and save as templates: the template's media type goes
in `mimetype` and in the manifest's root entry (ODF 1.3 Part 2 §3.3).
This works with a password too. `.ott` is listed wherever `.odt` is:
open and save formats, file dialogs, export, the web app and its
media type.
A LibreOffice template round-trips with the same text. 300 fuzzed
documents exercising custom shapes, cell formulas, kept parts, turned
text and forms caused no crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- craft-proof now names ja, zh and th in missing_dictionary: packs::missing_dictionary hides it
for a pack language when no pack for it is listed (status bar and Options).
- de-CH and pt-PT are pack languages in craft-proof (de-ch, pt-pt): names, hyphenation and
grammar stay German and Portuguese (tests); pack names fall back to the region-aware name.
- About › Language data shows craft_proof::scowl::notice() when SCOWL is embedded (test).
- Flatpak manifest's craft-i18n commit follows craft-i18n.rev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- proof.packs reports index_override; the CLI prints the test source; the bar's Install tooltip
says it turns on downloading of fonts and Language packs, and names a test source.
- Docs: Install works once language-packs-1 is published (until then only a test index); with a
test index the sha256 only catches a broken download.
- About › Language data keeps its size: a fixed-height scrolling list, licence texts read again
only when the dictionaries change.
- The engine's Cancel is the font store's own flag (no snapshot), so a cancel stops a fetch or a
stalled download too.
- Index text shown without control, bidi or format characters; long names cut; the bar works out
what to offer again when the list changes (index generation, not its length).
- ui_shot's store is a temporary folder (or storeDir); tests reset the backend even on panic; a
fixture pack proves per-run proofing and hyphenation from a pack.
- The default-build check follows craft-proof's carry-over (de, pl, pt, it, ru embedded until packs
ship; CRAFT_PROOF_EMBED=en is English only); the Polish hyphenation check runs again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- Install, Show available languages and Refresh turn on the font store's downloading only on the
person's own click (input_from_control, as in #959); a faked click says why instead.
- proof.installPack and proof.packs {refresh} run on the engine's thread in the app ("started"),
with progress, results and errors in proof.packs; the CLI and headless MCP stay synchronous.
- A session's layout is keyed on the dictionaries' generation, so parked documents proof again.
- proof.removePack refuses while that pack installs; pack commands that need a locale are left out
of Search; options text names Refresh; licence tooltip says Source; errors go through tl!;
the bar rescans at most once a second while typing; English names for every pack language and
sr-latn; About has one Close button; xtask doc comment back on cmd_i18n_report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- `review.issues` and `file.options` report a run's language with `ProofLang::tag()`, so a
Pack language reports its locale, not "".
- The test-source warning reads craft-font-store's `language_pack_index_overridden()`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- craft-proof (benandez/craft-libs 6ae4f03, storytold/craft-libs#485) and craft-font-store /
craft-font-catalog (630e42f, #486) by git rev instead of local paths.
- ProofLang::Pack: the bar offers every language with a pack (zxx/und never).
- Options › Language warns when CRAFT_LANGUAGE_PACK_INDEX supplies a test index.
- Pack `source` (upstream link) when the index gives it, else the archive.
- The bar's close button is the code-drawn close icon: "✕" had no glyph in the interface fonts
and egui panicked drawing it.
- `cargo xtask language` allows our own ru-extra, which stays embedded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- engine `packs`: the packs Backend (the font store on the desktop and CLI, a fixed list in tests
and ui_shot), the pinned index, installs and removals through craft-font-store's
`language-pack` kind, craft-proof reading the store's `language-packs/` folder at once.
Commands open nothing and never turn downloading on (only an Install click in the app does);
confined automation can't write the packs folder outside its write root.
- ui-egui `language_packs`: File › Options › Language › Language packs (name, size, licence,
source, Install/Remove, progress, errors), the missing-pack bar (dismissible per document,
never for a language without a pack), About › Language data (full licence texts).
- CP4.4: `cargo xtask language` (in `ci`): the default build embeds only en-* Language data.
- CP4.5: docs (help, MCP, control protocol, gaps #8, localization parity, roadmap).
- Path deps to Lane 4's worktrees while developing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
CodeRabbit on #5205:
- NOTICE: the SCOWL English dictionaries (en_US, en_GB-ise, en_CA) with
Kevin Atkinson's SCOWLv2 notice, Benjamin Titze's notice and Geoff
Kuenning's Ispell BSD licence for the affix files, as their licences ask of
distributions.
- docs/gaps.md: gap 8's evidence says each English checks its own SCOWL
2026 dictionary, with the #40 and #520 regression tests; #40 (closed) is no
longer listed as open. Status line and revision history.
Also: docs/localization-tr.md's craft-proof path change (3110dcf02, #5202)
and docs/architecture.md's no-data wording (b7dad64d9) get their status lines
and revision rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
craft-proof moves to 0675d58: an English no longer takes another English's
SCOWL words (en-US flagged colourblind, en-GB colorblind), British English
keeps -ize, and English suggestions drop junk splits and names.
craft-i18n.rev and the Flatpak source move to storytold/craft-i18n#114's merge
commit (059cbf9: judgment/judgement right in every English).
No wordcraft test depended on the dialects crossing
(engine 1120 + 43, layout 578 pass). localization-parity's English row says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
craft-proof moves to storytold/craft-libs#483's head (e6e948f, on #482's
9e5862a): each English checks against its own SCOWL rel-2026.02.25 dictionary
through the affix engine (ordinals via COMPOUNDRULE), with en-variants.txt as
the dialect policy. Lookup order: dictionaries folder, [English pack], SCOWL,
system Hunspell, the built-in Moby list; Moby stays for hyphenation.
craft-i18n.rev and the Flatpak source move to storytold/craft-i18n#109's merge
commit (efb1127), which adds the SCOWL files.
Typed-text tests pin British spellings in en-GB (#40), modern words (#520),
Canadian/Australian/NZ/Indian English (#1281, #1045) and ordinals; they skip
without the Language data, as the other data tests do. Docs: typing-parity,
localization-parity, ROADMAP progress log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
CodeRabbit on #5202:
- AGENTS.md, ATTRIBUTION.md, README.md, docs/architecture.md: without
CRAFT_I18N_DIR English spelling falls back to an installed Hunspell English
or craft-proof's built-in Moby list, and only pattern hyphenation is off
(Bulgarian, Turkish and Russian hyphenate by rule); they said English
wasn't spell-checked. (CLAUDE.md carries the same sentence; left for the
maintainer, as agents don't edit it.)
- NOTICE: craft-i18n's patterns are language-data/<lang>/hyphenation.pat,
not en-us.pat, pl.pat, pt.pat, de.pat, it.pat.
- docs/spelling-dictionaries.md: the Flatpak's dictionaries folder is
~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries; the host's
folders are outside its sandbox.
- printing.yml: the repository checkout doesn't persist credentials.
- tests.rs no_language_data: also probes the American/British variant table,
so a craft-i18n checkout missing some files skips the dialect tests
instead of failing them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
After custom shapes, cell formulas, kept package parts, turned text,
and control sizes and buttons: ODT is ~90% in file-format-parity (20–35
h left). gaps.md row 19 lists what's left in ODT:
- emphasis marks;
- turned characters;
- conditional and variable fields;
- hidden controls and control looks;
- embedded spreadsheets' contents.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fix. A button kept verbatim was still written as the button
after its content control was made another kind (a check box, a list).
The kept form XML is now used only while the control is still the
plain text control it was read as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On save, form controls were sized to their text. Buttons and other
controls WordCraft has no kind for were dropped.
- Sizes: each control keeps the size of its `draw:control`
(`svg:width`, `svg:height`), stored on the content control.
- Buttons, image buttons, labels, file and value-range controls, frames
and generic controls now show their label, locked, as a content
control. Their form XML is kept verbatim and written back as it was,
so LibreOffice's button round-trips as a button.
- A kept control that names a namespace WordCraft doesn't declare is
left out. So is one longer than 64 KiB.
- The `ooo`, `oooc`, `script` and `dom` prefixes that LibreOffice's
controls use are declared, so the content validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Text turned down or up opened across. This covered table cells and text
boxes (`style:writing-mode` §20.404, and LibreOffice's own
`loext:writing-mode="bt-lr"` for upward text).
- Table cells now open and save with WordCraft's cell text direction.
- Text boxes now open and save with their text direction.
LibreOffice's turned cells render as LibreOffice draws them. LibreOffice
reads WordCraft's turned text boxes back identically.
Turned characters (`style:text-rotation-angle`) still open upright:
WordCraft has no turned characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Package parts WordCraft doesn't read were dropped on save. These
include LibreOffice Basic macros and dialogs (`Basic/`, `Dialogs/`),
`manifest.rdf` and other applications' files. They are now kept as
they are and written back with their manifest entries. LibreOffice
finds a round-tripped document's macro intact; WordCraft never runs
macros.
Dropped on save: the old thumbnail, and digital signatures
(`META-INF/*signatures*.xml`), because they sign the package as it
was. Also dropped: paths that climb out of the package, and paths
WordCraft writes itself (pictures, embedded objects).
Kept parts are bounded: 1000 files, 64 MiB in all. They survive
password protection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT cell formulas (`table:formula`) were lost, and only their value was
kept as text.
- Reading: LibreOffice's formulas, in its own syntax
(`ooow:sum <A1:A3>`, `<B1>*<B2>+<B3>`, `mean`, `min`, `max`,
`product`, `abs`, `sign`, `count`, `add`/`sub`/`mul`/`div`/`pow`, lists
joined by `|`), open as Word formula fields (`=SUM(A1:A3)`) showing
the cell's value. Update Field recalculates them.
- Writing: formula fields in cells are written back as cell formulas
with their value. ABOVE, LEFT, BELOW and RIGHT become the cells they
name.
- Cells holding a plain number are written as numbers, so LibreOffice
calculates with them. The `ooow` prefix is declared; without it,
LibreOffice reports "Expression is faulty".
A formula one side can't say keeps its value as text. A LibreOffice
table of sums round-trips with the same text, and the saved file
validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fixes to the custom shape geometry:
- An equation that couldn't be worked out wasn't remembered. Equations
each referring twice to the next, ending in one that refers to
itself, took about 2^64 steps. Each equation is now worked out once,
failures and those being worked out included.
- Points far outside the view box (1e300) became infinite as f32. They
are now clamped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A custom shape whose type WordCraft has no kind for (hexagon, octagon,
smiley, can, moon, lightning, block arc, cross, pentagon, flowchart
shapes…) opened as a rectangle. It is now drawn from its own geometry
(ODF 1.3 §10.6.2) as a freeform:
- the enhanced path (§19.145): moves, lines, curves, ellipses (T, U),
arcs (A, B, W, V) and quarter ellipses (X, Y), arcs followed in
short straight steps;
- its values worked out from the shape's equations (§10.6.3) and
adjustment values: numbers, other equations, modifiers, the frame's
keywords, + - * /, abs, sqrt, sin, cos, tan, atan, atan2, min, max
and if.
Path length, equation count and nesting, and points are all bounded.
LibreOffice fills no part of a `draw:path` that has an open part. So a
freeform with both closed and open paths is now saved as a custom shape
whose path leaves the open ones unfilled (`F`).
LibreOffice's preset shapes render as LibreOffice draws them. The
saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT citations (`text:bibliography-mark`) were read as their text, and
the sources they carry were lost. So an ODT bibliography had nothing
to update from.
- Reading: each mark is now a `CITATION` field of its source, which
becomes one of the document's bibliography sources (the first mark
of an identifier gives it). The identifier is the tag, the type the
kind, plus author, title, year, publisher, address, journal, volume,
pages, URL, editor, edition, number and month.
- Writing: a `CITATION` of a known source is written back as a mark
carrying the source.
References › Update rebuilds an ODT bibliography from them. A
LibreOffice citation and bibliography round-trip pixel-identical and
validate against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Linked text boxes (`draw:chain-next-name` on a `draw:text-box`, naming
the frame its text continues in) lost their link on open. The
following box's text was gone, since only the first box holds the
story.
- Reading: each box keeps its frame name and the name of the next box.
Once all boxes are made, names are resolved to WordCraft's link
(`TextBody::next`). A link to a missing box, to the box itself or
round in a loop is dropped.
- Writing: each box is named after its story, and a linked box names
the next one.
Flow model: `BoxLook::chain` (name, next).
A LibreOffice chain round-trips pixel-identical, and WordCraft flows
the text from the first box into the second. A linked box keeps its
own height where LibreOffice grows it to the rest of the text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Two picture properties were dropped.
- Crop: `fo:clip` (top, right, bottom, left, in lengths of the
picture's own size, its pixels at 96 per inch) is now WordCraft's
crop fractions. A picture's crop is written back as `fo:clip`, with a
graphic style even for an in-line picture.
- Contour wrapping: `style:wrap-contour` with
`style:wrap-contour-mode` `outside` is Word's Tight wrapping, and
`full` (the default) is Through. The frame's `draw:contour-polygon`
becomes the wrap polygon, scaled from its view box. A polygon whose
points lie well outside its view box is in another coordinate system
(LibreOffice writes one for some pictures), so the outline is
derived from the picture instead. All of this is written back, the
polygon last in the frame.
A cropped LibreOffice picture round-trips pixel-identical, and a
contour-wrapped one round-trips with WordCraft wrapping text as
LibreOffice does. Both validate against the schema. `Body::start`'s
frame parts moved to a helper (clippy's complexity limit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This round added flow breaks, table placement, ruby, index marks, text
borders, connectors and custom properties. A 20,000-paragraph document
round-trips in about a second with nothing lost, and 400 fuzzed
documents opened without a crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the custom property commit: an ODF date property may be a
bare date, while Word's `filetime` (which a DOCX save writes) wants a
UTC date and time. A bare date is now midnight UTC, a date and time
without a zone is taken as UTC, and a value that isn't a date is kept
as text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT custom document properties (`meta:user-defined` in `meta.xml`) were
dropped, and the fields showing them or a user field were read as
plain text.
- Properties: `meta:user-defined` values (string, number, true/false,
date) are now WordCraft's custom document properties, and so are
user field declarations (`text:user-field-decl`).
- Fields: `text:user-defined` and `text:user-field-get` are Word's
`DOCPROPERTY` fields, which WordCraft updates from those properties.
- Writing: custom properties go back into `meta.xml`, and a
`DOCPROPERTY` naming one is written as `text:user-defined`.
A LibreOffice document with a custom property and a user field
round-trips pixel-identical, valid against the schema. Its user field
comes back as a document property.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the connector commit: a connector inside a drawing group
wasn't converted. Its group's shapes weren't recorded, so its ends
lost their glue. Group members are now recorded for their kind (never
for placement, since they sit in the group's coordinates), and
connectors in groups have their glue points made their shapes' sites
too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT connectors (`draw:connector`) were left out. They now open as
WordCraft connectors: straight (`line`), curved (`curve`), or with
bends (`standard`, `lines`).
- Gluing: each end is glued to the shape it names (`draw:start-shape`,
`draw:end-shape`). The shape's four default glue points (§9.2.19:
top, right, bottom, left) become its connection sites. An end glued
to a glue point of the shape's own, or to a shape not read, is left
unglued.
- Placement: a connector's own coordinates are in LibreOffice's drawing
layer, not its anchor's. So a connector glued at both ends to shapes
anchored in its paragraph is placed between their sites.
- Writing: connectors are written back as `draw:connector` with their
type and glue, and the shapes they glue to get ids
(`xml:id`/`draw:id`). LibreOffice re-routes them from the glue.
LibreOffice's connector document round-trips with its connectors
drawn between the shapes, and WordCraft draws them between the right
sites. Other formats still get connectors as straight lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Polish/Portuguese/Czech review commands, the custom dictionary file and the two typing squiggle
tests pass without CRAFT_I18N_DIR at 48ed6f8. English dialects, regions' English and Hyphenation
Options still need the variant table and patterns, so they keep the skip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
storytold/craft-libs#482's new head: without Language data English spells from the built-in
Moby list plus en-extra with edit-1 suggestions, and a system English Hunspell loads leniently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- wordcraft-proof is now `package = "craft-proof"` from craft-libs (local
path for now; TODO in Cargo.toml for the git+rev pin after the craft-libs
PR merges). `wordcraft_proof::` paths are unchanged.
- crates/proof and assets/{spelling,hyphenation} deleted with their
ATTRIBUTION.md rows; one row now points at craft-i18n's Language data;
NOTICE paths updated.
- craft-i18n.rev (and the flatpak source) moved to the craft-i18n commit that
adds language-data/ (CI sets CRAFT_I18N_REQUIRED=1, so the pin must have it).
- xtask layers: proof removed. Engine tests carry their own copy of the
synthetic Czech fixture (they reached into crates/proof/src).
- Docs: architecture.md, CLAUDE.md/AGENTS.md layer table, README build note
(CRAFT_I18N_DIR), localization-tr path, ROADMAP progress log.
Before/after wordcraft-cli on a 17-language proofing document: issues,
suggestions, manual hyphenation, PDF text and PDF bytes identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
* Markdown export: a list inside a block quote stays in the quote
`> - one / > - two` was imported as quoted list paragraphs (the quote kind on each item) but
the list branch of the exporter ignored that kind and wrote `- one / - two`, so every save as
Markdown moved the list out of its quote. A quoted list item now takes `> ` on each of its
lines, nested levels and continuation lines included. The same paragraphs come back from a
.docx (Quote style with numbering), so the quote now survives that round trip too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Markdown export: a list after a quoted list stays outside the quote
* fmt
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word draws an arrowhead 2, 3 or 5 times the line's width long and wide
(small, medium, large; `a:headEnd`/`a:tailEnd` `len`/`w`), a line under
2 points as a 2-point one. Every arrowhead was drawn one size and saved
as a medium triangle: the kind, width and length are now read (DrawingML
and VML), kept and written back, and the size is drawn as Word does.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #5178: the line between columns is drawn for a section starting mid-page
Columns for selected text are a continuous section in the middle of a page, and
the line between columns was only added when a page started, down to the page
bottom. The lines are now drawn per section on each page, from the top of its
text there to the bottom of its tallest column, when the page or the section ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the line between columns spans the text's lines, not its paragraph spacing
Review: the line started above the section's first line (its space before) and
ended below its last (its space after). It now runs from the highest text line or
table cell of the section on the page to the lowest, falling back on the flow's
extent when there are none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the column lines find the section's text by place, and draw nothing without it
Review: the section's items were the page's items after an index, which objects
behind the text (inserted at the front) shift; the flow's extent drew a line
where the section had only spacing; overflow lines of exact-height cells counted.
The text is now the page's top-level body lines and table cells below the
section's top; lines in cells count by their cell's box; no text, no line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the column lines take the section's text by its blocks
Review: a floating table of an earlier section placed lower on the page would
pass a filter by place. The section's text is now the page's items of top-level
blocks from the section's first block on the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The numeral and automatic-space tests compare two layouts of one text. Tests
running alongside share the font database, and one loading a font between the
two layouts can change the face a character falls back to: U+06DD drew in
Segoe UI in one layout and Noto Sans Arabic in the other on Windows, failing
arabic_mode_keeps_arabic_indic_digits_as_typed and
context_mode_follows_run_direction now and then, and
spaces_punctuation_hangul_and_latin_alone_get_nothing failed the same way. The
pairs are now laid out again until both use the same faces.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #5066: .doc default font is the stylesheet's standard font
The .doc reader took the default character font from the font table's
first entry. A Word 365 .doc keeps Times New Roman there and names Aptos as
the stylesheet's standard font (Stshif.rgftcStandardChpStsh), so its text
came out in Times New Roman. The defaults are now the standard ASCII, East
Asian and complex-script (ftcBi) fonts, else entry 0 as before; runs'
sprmCRgFtc1 and sprmCFtcBi are read too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5066: the complex-script standard font is read only when the header has it
ftcBi is optional in the Stshi, and the latent style data that follows it starts
with cbLSD (4): a header that leaves ftcBi out no longer makes cbLSD the
complex-script font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5066: a header ending with ftcBi = 4 keeps it
Review: a 4 at ftcBi's offset was taken for cbLSD even when the header ends
there, so a complex-script standard font of index 4 was lost. It is read as
left out only when more data follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
When a row's first part on a page that already holds something would leave
a cell empty because that cell's inline picture (one line, never split) or
first line runs past the room, the row moves on whole instead of leaving the
other cells' text beside the empty cell. An at-least row grown taller than its
minimum by its picture used to pass the #1742 check (room >= declared minimum)
and then split, the picture going on alone. At the top of a page such a row
still splits.
Co-authored-by: Harrison Smith <harrison@hsmith.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Measured in Word for Windows: when a table row breaks across pages, a cell
aligned to the middle or bottom whose text all fits in the part of the row
left on the page is aligned within that part, as tall as the other cells'
text there (with its space after), and a cell whose text doesn't fit runs
from the top of the cell with the rest on the next page. WordCraft kept the
offset the whole row's height gave the cell, so a short centred label
beside a long cell went to the top of the next page and left its cell empty
on the first, and a long centred cell started far down the page.
The vertical offset each cell's content got is kept with the row, so a split
moves the content back to the top before it decides what fits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layout: tab leader dots spaced and placed as in Word
Measured in Word for Windows with six fonts at four sizes: Word spaces leader
dots by the dot's advance at the font size in whole 600-dpi pixels (the size
itself in whole pixels), so a TH SarabunPSK dot at 16 pt (2.59 pt) is drawn
every 2.64 pt and a Tahoma one at 22 pt every 6.6 pt. The dots sit on the
page's grid of that pitch, from the first place after the text before the
tab to the last one that ends before the text after it. WordCraft drew them
at the unrounded advance, half a dot after the text and 1.2 dots short of
the number, so a table of contents line had a dot or two more than Word's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: each leader dot's place comes from its grid index
The leader's dots were placed by adding the pitch to the last place, so on
a long leader the rounding of each addition added up. From 72 to 528 pt at
2.64 pt it came to more than the 0.001 pt allowed at the end, and the last
dot, which ends exactly on 528 pt, was left out. `leader_xs` now works out
each place as its grid index times the pitch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: name the leader slack and glyph limit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: leader dots use the font size rounded to whole pixels
Word rounds the font size to the nearest 600-dpi pixel before it rounds the
dot's advance (Arial 14 pt: 116.7 px, so 117, dots every 3.96 pt); taking the
floor drew them 3.84 pt apart, as measured at 11, 14, 17, 20 and 26 pt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* leader_xs: saturate the glyph range end
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Lists: a level above counts its first number at the first deeper item
Word counts a list level that hasn't been used yet as started at its first
number when a deeper item comes first: after 10.1-10.4 with no level-1 item
before them, the next level-1 item is 11. WordCraft showed 10 in the deeper
labels but didn't count it, so the next level-1 item repeated it ("10.
Qualifications", then "10. Penalties" in a tender document whose sub-items
come first).
Counters::next_label now starts each unstarted level above the one it counts,
at that num's start override if it has one, else the level's start. Measured
in Word with ListFormat.ListString: 1.1, 1.2, 2., 3.; (1), (2), 2. when the
deeper text doesn't show level 1; 1.1.1., 1.2., 2. from level 3.
a_list_s_own_level_definitions_win expected the old count; its level 0 item
after level 1 items is now 2., as in Word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lists: record the counting fix in the progress log and gaps
AGENTS.md asks for the progress log and the gap list when work lands. The
ROADMAP progress log has the entry; gaps.md records that no ranked gap
changed. Each with its status line and revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lists: test a num's start override for the level above
The deeper-item-first test now also gives the num a start override for
level 0 (10) under level-1 items: 10.1., 10.2., then 11. It fails if the
override is ignored for the level above (1.1., 1.2., 2.).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test a start override of 5 and a restart after a level-1 item
Review: 5.1., 6., 6.1. with the num's start override, and level 2 restarting below the next level-1 item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
LibreOffice writes a border around text as `loext:border` (or its
sides) with `loext:padding`; ODF 1.3 has no attribute for it. It was
dropped. It is now read as Word's text border (one line all round, the
first side with a line, the padding its distance) and written back
the same way, which LibreOffice draws. A document with a text border
is then extended ODF rather than strict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
* #5036: paragraph side borders lie beyond their distance from the text
push_para centred the left and right rules on the distance, so a 3 pt border
at no distance reached 1.5 pt into the text and, drawn after the lines, covered
the first letter. In a heading whose border matches its shading the letter
looked cropped, on the page and in the ribbon's style tiles. All four lines now
lie outside the distance, as the top and bottom ones did. Top and bottom reach
the corners and the shading fills out to the outer edges, so no seam shows
where rules and shading meet.
* #5036: pin the right border on its own in the side borders test
The test only compared the right border with the end of the top rule, and both come from the same value, so a wrong right edge still passed. Check it against the line's own bound.
* docs: progress lines for #5088
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/formats/src/latex/export.rs:411-444 (escape_text, `c => o.push(c)` for ' ')
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* T280: anchor header drawings below tall headers
* #2485: share rendered header boundary with pins and obstacles
* docs: layout-parity status line for #2485
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Gap 19's row now says what ODT reads and writes and what is left, and
its estimate drops from 18–27 h to 10–16 h. The roadmap's progress log
covers the ODT work since text boxes: shapes through password
protection, forms, ruby and flow breaks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT index marks (`text:alphabetical-index-mark`) were dropped. So
References › Update Index on an opened ODT index found no entries and
left the old list in place. Now:
- a mark at a point is Word's `XE` field, its `text:key1`/`text:key2`
the levels above its entry;
- a mark over text (`-mark-start` … `-mark-end`, in one paragraph) is
an `XE` field whose entry is the marked text, which stays text;
- `XE` fields are written back as point marks.
Engine: Update rebuilt only lists in WordCraft's own form, a paragraph
holding the field. A list held as a field around its entries, as ODT
and Word files hold an index, bibliography or table of figures, is now
first made the field's own paragraph, then rebuilt. That happens only
when the field starts its paragraph, so no document text is replaced.
A LibreOffice document with an index mark round-trips pixel-identical,
the mark kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Ruby (`text:ruby`, ODF 1.3 §6.4: a base text with ruby text over it,
furigana or pinyin) was read as its base and ruby text run together,
and the writer wrote any ruby as plain text (its comment wrongly said
ODF has none).
Ruby is now read as WordCraft's Phonetic Guide. It is written back as
ODF ruby, above its base and centred, with an automatic ruby style.
A ruby inside a tracked change is still written as its base text,
which the change marks, so the tracking isn't lost. LibreOffice shows
the round-tripped ruby over its base, and the content validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Six features of LibreOffice documents were lost on open and save. A
two-page document came back as one page, because the page break after
a paragraph was dropped. Now read and written:
- a page break after a paragraph (`fo:break-after="page"`), as a page
break before the next paragraph, as Word has it;
- lines kept together (`fo:keep-together`);
- widow and orphan control (`fo:widows`/`fo:orphans`): on with any line
count, off with none;
- hyphenation (`fo:hyphenate`): a paragraph's in ODF, the document's in
WordCraft. A document with a hyphenated paragraph hyphenates, and its
other paragraphs opt out;
- a table's alignment and width (`table:align`, `style:rel-width`);
- its rows' heights (`style:row-height` exact, `style:min-row-height` at
least).
Flow model: `FTable::extra` (alignment, width, row heights), filled by
formats that keep them.
A LibreOffice document with all of these round-trips at 573 px
difference instead of 4,407, keeps its two pages, and validates
against the ODF 1.3 schema. No other test document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.
PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.
Clicks and the caret inside a turned box still go by its upright text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Three causes of the largest remaining LibreOffice round-trip
differences:
- In-line frames and shapes centred on or hanging from the baseline
(`style:vertical-pos` `middle`, `bottom`, or `from-top` with
`svg:y`, relative to the baseline) were read as standing on it. They
are now read as their run's lowering, which the layout honours, and
written back as `from-top`.
- Leading: LibreOffice also leaves the leading after a paragraph's last
line. It is now added to the paragraph's space after.
- Text box frames didn't derive from LibreOffice's Frame style, without
which LibreOffice ignores a frame's `fo:border` and outlines it in
its default shape colour. They now do, and an empty Frame style is
declared.
LibreOffice round trips (pixel difference): shapes 13,864 → 1,273,
spacing 2,973 → 706, text box 736 → 0. No other test document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.
A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tests and notes for rules that #4129, #4113, #4109 and #4101 added and
left uncovered:
- A border line at the top of a story with formatting left out of the
Track Changes markup is not a formatting change, and Reject All keeps
it.
- The author's own earlier formatting change on that paragraph stays the
one change.
- Reject All keeps the table of a "+---+" line.
- A border line that a content control's lock stops Backspace from
deleting stays as it is, and Enter is refused, untracked too.
- With tab indents on, Tab in a table cell moves to the next cell; with
them off, Backspace before an indented paragraph joins it.
- AutoCorrect and the spelling check's web and e-mail addresses after a
narrow no-break space and a figure space.
docs/typing-parity.md describes each, and lists a no-break space typed
after a word, which does not finish the word for AutoCorrect, as a
known gap.
ODT form controls (ODF 1.3 §13: controls in `office:forms`, each
placed in the text by a `draw:control`) were dropped on open, values
and all. They now open as WordCraft content controls, each with its
value, entries and selection; its name becomes the tag and its label
the title:
- text fields and text areas: plain text controls;
- check boxes: check boxes;
- radio buttons: check boxes with round symbols;
- list boxes: drop-down lists;
- combo boxes: combo boxes;
- date fields: date controls.
Saved as DOCX they become Word content controls.
Content controls of those kinds in the body are written back as form
controls with a `draw:control` anchor; in headers and footers they stay
their text. LibreOffice shows the round-tripped controls with their
values, and the content validates against the ODF 1.3 schema.
Not read: buttons, images, hidden and other controls. Controls' sizes
and looks aren't kept.
Flow model: new `Inline::Control`, kept only by flows that ask for
boxes (ODT); other formats get its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A password-protected ODT (ODF 1.3 Part 2 §3.4) failed to open. The
package is now decrypted with the password into a plain one, which the
reader opens as before.
- Supported: AES-256-CBC with a SHA-256 or SHA-1 start key, PBKDF2
with HMAC-SHA-1, and SHA-256 or SHA-1 1K checksums (LibreOffice's
encryption).
- Blowfish (the old default) is refused with a message naming it.
- A missing or wrong password gives the same errors as Word files, so
the app asks for the password.
- Hostile parameters are bounded: iteration count, IV size, ragged
data, inflated size.
Saving with a password (and saving a document opened with one) writes
the ODT encrypted as LibreOffice does. Every file but the `mimetype`
and manifest is deflated and encrypted, each with its own salt and IV,
and the manifest validates against the ODF manifest schema.
LibreOffice opens WordCraft's encrypted files with the password and
refuses a wrong one; WordCraft opens LibreOffice's.
No new dependencies: the AES, SHA, HMAC, miniz and getrandom crates
the workspace already uses for Word encryption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the objects commit found two gaps.
- The header and footer reader wasn't given the package's entry list,
so embedded objects there were still lost on open. It now shares the
body reader's list and manifest, borrowed rather than copied.
- Headers and footers were flattened for writing without drawings, so
shapes, text boxes, charts and kept objects in them were dropped on
save. They now go out as in the body.
The full LibreOffice round-trip harness shows no change on the other
test documents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.
Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
placeholder of the object's size drawn in code (ruled for a
spreadsheet).
- ODT save: written back as the same object, with its manifest entries
and original replacement. LibreOffice renders a round-tripped
spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
linked from the picture's `ole` by a marker. The DOCX writer skips
the marker and writes the picture alone.
Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the chart commit: the engine drew opened charts only at the
paragraph level, so a chart that was a member of a `draw:g` group
opened blank. The pass now goes into groups (bounded depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A chart object (`draw:object` whose sub-document is an `office:chart`,
ODF 1.3 §10.4.3, Part 3 §11) used to open as its replacement picture.
It is now read into WordCraft's chart model: column, bar (stacked or
not), line, area, pie, doughnut and scatter charts, with categories,
series names and values (gaps included) taken from the chart's own
table through the series' cell range addresses, plus title, legend
position and data labels. The engine draws opened charts in the
document's theme colours, and Chart Design's commands edit them.
Charts WordCraft can't edit as they are (3D, percent-stacked, mixed
plot types, a second value axis, radar and the like) keep their
picture.
WordCraft charts are written as ODF chart objects shaped like
LibreOffice's: a data table, each series and slice in its accent
colour, pies clockwise from the top. They validate against the ODF 1.3
schema, and LibreOffice draws them like WordCraft does. In flat
documents they are written inline as an `office:document`.
Bug fixed: flat ODT reading ended the body at the first nested
`</office:body>`, so an inline sub-document dropped the text after it.
Element lookup now follows nesting, with a bounded depth.
Flow model: new `Inline::Chart`, kept only by flows that ask for
boxes (ODT); other formats still get the alt text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Remove the feature-only preview workflow from this feature PR. Retain all Rust implementation and regressions unchanged; document direct manual rendering and the exact-source historical previews.
Credit: @totoz64
LibreOffice's leading (`style:line-spacing`, extra space between lines)
has no WordCraft line spacing mode. It was read as a multiple of a
12 pt line, which spreads the extra space differently from LibreOffice,
so the paragraph started too high. It is now read as at least a 12 pt
line and the leading: the extra space goes above each line, as
LibreOffice draws leading (it also leaves some after the last line,
which isn't followed).
A LibreOffice spacing document went from 6,190 px apart after a round
trip to 2,973, and WordCraft's drawing of it from 8,570 to 5,521.
Regression test: `leading_is_at_least_a_line_and_the_leading`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tracked formatting changes (`text:format-change` regions, ODF 1.3
§5.5.7, between `text:change-start`/`-end` marks) were dropped. The
text between the marks now gets a formatting revision with the change's
author and date. ODF records no old formatting, so the old formatting
is the text's own: the revision shows and can be accepted, and
rejecting it changes nothing (LibreOffice can't reject one after
reopening either). WordCraft's formatting revisions are written as
format changes around their text.
Fixed: changed regions were written with `text:id` only, but ODF 1.3
requires `xml:id` (which the change marks refer to), so any document
with tracked changes failed schema validation. Both are written now,
and a region named by `xml:id` alone is read.
A LibreOffice document with a tracked formatting change round-trips
pixel for pixel; documents with tracked insertions, deletions and
format changes validate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drop caps (`style:drop-cap`, ODF 1.3 §17.9) are read into WordCraft's
drop cap (lines and distance; one line is none) and written back as
the paragraph style's `style:drop-cap`. WordCraft drops one letter, so
a drop cap of several letters or a word opens as its first letter.
Fixed: the paragraph style key kept its child elements after a
separator, and the background, borders, contextual spacing and
right-to-left direction were added after the tab stops, so a paragraph
with its own tab stops wrote them into the tab stops' place instead of
onto the paragraph properties, and lost them. Attributes and child
elements are now gathered apart
(`tab_stops_and_other_paragraph_properties_together`).
A LibreOffice document with a drop cap round-trips pixel for pixel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`Kmm` with `km/K` suggested `Km`, which the checker flags; `Km` itself got no
suggestion. A suggestion whose dictionary form is only correct through a
KEEPCASE stem now keeps that case (`km`). Other stems are re-cased as before
(`MIXCASX` → `MIXCASE`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
A flat ODT holds the whole document in one `office:document` element
(ODF 1.3 Part 3 §2.1.2), its pictures inline as base64
`office:binary-data` and its formulas inline in their `draw:object`.
It is opened by taking it apart into a package's files (content,
styles, meta, settings; pictures and formulas as entries, each part's
root keeping the document's namespaces) and reading that package, and
saved by putting a package's files together: one set of automatic
styles, every part's namespaces declared once on the root, pictures and
formulas inline without the link attributes.
`.fodt` opens and saves from Open, Save As, the CLI and the web app.
LibreOffice's own flat documents round-trip as their packages do, and
the written files validate against the ODF 1.3 schema.
The parity doc's ODT fidelity is re-estimated at ~80%, and its status
line, which hadn't been updated since the Asian fonts change (a text
replacement that matched nothing), now lists every change since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Page number format: the page layout's `style:num-format`, and a page
number field's own (`PAGE \* Arabic`, `roman`, `ROMAN`,
`alphabetic`, `ALPHABETIC`), both ways.
- Page colour (the first page style's `fo:background-color`).
- Page borders: ODF's sit at the margin with the text inside their
padding; WordCraft measures a page border from the page edge and the
text margin from the edge, so the border's space is ODF's margin and
the margin takes in the border and padding, and back on save.
- Footnote and endnote numbering (`text:notes-configuration`: format,
first number, counted from 0 in ODF, restart, page foot or document
end); only what the document sets is written.
- Line numbering (`text:linenumbering-configuration`, on unless it says
otherwise, ODF 1.3 §19.845: interval, distance, restart on each page).
A LibreOffice document with all of these round-trips pixel for pixel;
WordCraft draws it 7,213 px from LibreOffice (24,025 with the border at
the page edge). The written styles validate against the ODF schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Headers and footers were put into the document under the ids their
sections named (1, 2…), assuming the body had made no stories of its
own; but notes, comments and text boxes now are stories made first, so
in a document with a footnote and a footer the footer took the note's
id and the note showed the footer's text (a page number field).
Headers and footers now take free ids and the sections are pointed at
them.
Regression test: `a_footnote_keeps_its_text_beside_a_footer`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Fields that were read as plain text now open as Word's, so Update Field
recomputes them, and are written back as ODF's:
- dates and times in their date or time style (`number:date-style`,
`number:time-style`) ↔ `DATE`/`TIME \@ "picture"`: day, month (by
number or name), year, weekday, hours (12-hour with AM/PM), minutes,
seconds and the text between, both ways; data styles are written as
automatic styles and the `number` namespace declared;
- sequences numbering figures and tables (`text:sequence`) ↔ `SEQ` with
its number format; on save the sequences are declared
(`text:sequence-decls`); on open a bookmark named by the sequence's
reference name covers the caption's label and number ("Figure 1"),
what references to it show;
- chapter fields (`text:chapter`) ↔ `STYLEREF "Heading n"`, by name or
number (`\n`);
- reference marks as bookmarks, and references to them and to sequences
(`text:reference-ref`, `text:sequence-ref`) as `REF`/`PAGEREF`.
Captions renumber, references to them and chapter fields refresh after
Update Field (engine test `odt_fields.rs`). A LibreOffice document with
all of them round-trips 13 px apart with the same text; the written
content and styles validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's date-time pictures (`DATE \@ "…"`) put literal text in single
quotes, two quotes in a row standing for one: `H' o''clock'` shows
"8 o'clock". The formatter read the quotes as text and the letters
inside as instructions (an `a` became AM or PM). Quoted text is now
shown as it is; an unclosed quote runs to the end.
Regression test: `date_pictures_keep_quoted_text`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A bookmark over text (`text:bookmark-start`…`text:bookmark-end`, in one
paragraph or across several) was read as a point at its start, and a
Word bookmark over text was written as a point: a reference to it then
updated to nothing. The flow model gets the end of a bookmark
(`Inline::AnchorEnd`, in flows asked for fields, as ODT's is), the
document gets the bookmark's start and end where they are, and the
writer writes a start and an end (a point, `text:bookmark`, when it
ends where it starts). A bookmark started twice, ended twice or ended
before it starts is kept once, as a point when it has no end after it.
References to bookmarks, LibreOffice's references to headings included,
now update to the bookmarked text (engine test `odt_fields.rs`, through
a save and reopen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tables of contents, tables of figures (`text:illustration-index`,
`text:table-index`), alphabetical indexes and bibliographies open as
WordCraft's: Word's `TOC` (levels from the source's
`text:outline-level`; `\c` and the caption name for figures), `INDEX`
and `BIBLIOGRAPHY` fields over their entries as last generated. With a
title the field goes in the title paragraph, as References › Table of
Contents inserts one; without, it spans the entries (new flow inlines
`FieldStart`/`FieldEnd`, mirroring the document's range fields). Update
Table then regenerates them from the headings (engine test
`odt_toc.rs`, titled and untitled, through a save and reopen).
LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles
are WordCraft's TOC 1–9, TOC Heading and Table of Figures, both ways,
also when a file names them without defining them. WordCraft's tables
are written as ODF indexes with their source and entry templates, so
LibreOffice updates them, the entries' links in the Index Link style.
User and object indexes are read as their paragraphs; index marks are
not read yet.
Found on the way, both shown by the table of contents' round trip:
- a style based on LibreOffice's generic Heading style (as its headings
and index headings are) lost that parent on save, and with it its
font and spacing;
- a paragraph's tab stops were measured from its own indent only, not
from an indent its style gives (LibreOffice's `TabsRelativeToIndent`),
and a named style's stops not from its indent at all.
A LibreOffice document with a table of contents, a table of figures and
an alphabetical index round-trips pixel for pixel (it was 29,717 px
apart and a page short). The parity doc's ODT fidelity is re-estimated
at ~72%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A pure move, no behaviour changed: `odt/body.rs` (1,720 lines) and
`odt/writer.rs` (1,716) are split into child modules by topic, the
methods of `Body` and `Writer` moved in `impl` blocks of their own.
- body.rs (930): paragraphs, text, lists, element dispatch;
body/drawing.rs frames, shapes, groups, formula objects;
body/notes.rs notes, comments, fields; body/tracked.rs tracked
changes; body/tables.rs tables.
- writer.rs (618): the writer, paragraphs, inlines, blocks, styles;
writer/props.rs text and paragraph style properties;
writer/drawing.rs pictures, text boxes, shapes, groups, formulas;
writer/tracked.rs tracked changes; writer/notes.rs notes and
comments; writer/tables.rs tables.
Items the rest of `odt` uses are re-exported where they were. Every
harness document (21) exports content.xml, styles.xml and settings.xml
byte for byte as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Polygons and polylines (`draw:points`), paths (`svg:d`) and regular
polygons and stars (`draw:corners`, `draw:concave`, `draw:sharpness`)
open as WordCraft freeforms in their view box (none: the box around
their points) instead of being skipped; circles (`draw:circle`, by size
or by centre and radius) open as ellipses.
The path data reader follows SVG path syntax: moves, lines, horizontal
and vertical lines, cubic and quadratic curves and their smooth forms,
and elliptical arcs (converted from endpoint to centre form), absolute
and relative, with repeated commands; curves are followed in 16 straight
steps, a subpath closed by `Z` is a closed path, and input is bounded
(numbers, points, paths, coordinates).
WordCraft's freeforms (ink included) are written as `draw:path` in a
view box of hundredths of a point (whole numbers, as ODF's view box is).
A LibreOffice document with a polygon, a polyline and a closed Bézier
curve (relative `c` and smooth `s` commands) round-trips 333 px apart,
and WordCraft draws it 529 px from LibreOffice; the written content
validates against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Graphic styles' arrowheads (`draw:marker-start`/`-end`), line dashes
(`draw:stroke-dash`) and gradient fills (`draw:gradient`), defined by
name in the styles, open as WordCraft's arrowheads, dash patterns and
gradients:
- dashes: up to two kinds of dash and one space, in line widths or
absolute lengths, matched to WordCraft's named dashes when equal;
- gradients: linear and axial along their angle (ODF's 0° runs top to
bottom, anticlockwise; WordCraft's left to right, clockwise; an angle
without a unit is tenths of a degree, as ODF 1.2 wrote it), radial,
ellipsoid, square and rectangular out from their centre (end colour in
the middle), with border and intensity.
They are written back as definitions in `styles.xml` (WordCraft's own
triangle arrowhead marker; equal definitions share a name; the header
writer's names are prefixed apart). Frames keep gradient fills too.
A line now keeps the direction it is drawn in (`svg:x1`/`y1` is its
start): one drawn leftwards or upwards is flipped that way, so its
arrowheads stay at their ends, and it is written from its start.
A LibreOffice document with arrowheads, dashes and gradients round-trips
215 px apart; WordCraft draws it 518 px from LibreOffice. Exported
styles.xml and content.xml validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The renderers draw a line from its frame's top left to its bottom right
(DrawingML's `line`; flips turn the frame), but the arrowhead code and
`end_direction` assumed a line from the bottom left to the top right, so
a sloped line's arrowheads were drawn at the other diagonal's corners,
apart from the line. Horizontal and vertical lines hid it. The
arrowheads now start from the line's own ends, pointing along it.
Regression test: `tests::a_lines_arrowheads_sit_at_its_ends`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Groups (`draw:g`) of shapes, text boxes and pictures open as one
WordCraft group instead of being skipped: anchored and wrapped as the
group is, over the box around its members, each member at its place in
it (from its `svg:x`/`svg:y`, its line ends or its `draw:transform`,
turned members turned); groups inside a group are flattened into it.
WordCraft's groups are saved as `draw:g`, members at their offsets with
no anchor of their own, nested groups nested (a canvas as a plain group).
A frame, shape or group whose styles state no `style:wrap` now has text
on both sides, as LibreOffice wraps it (ODF 1.3 §20.400 states no
default), not above and below only; `biggest` is read as the larger
side. A shape whose only paragraph is LibreOffice's empty `<text:p/>`
gets no empty text story.
A LibreOffice document with turned shapes and a group was 7,464 px apart
after a round trip; now 586, with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Shapes and pictures turned by `draw:transform` (rotate, translate and
scale, applied in turn to the object's own coordinates) open turned: the
file's anticlockwise turn about the object's top left corner, followed by
a move, becomes WordCraft's clockwise turn about the centre of the
unrotated box at its offset, as Word places them. They are written back
the same way, in line as well as floating. LibreOffice writes no
`svg:x`/`svg:y` for a turned shape, so before, a turned shape lost both
its turn and its position.
Custom shapes keep their mirroring (`draw:mirror-horizontal`/`-vertical`
on their geometry), and a turned line is written by its turned ends. The
flow keeps the placement of an in-line object that is turned or mirrored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drawing shapes (`draw:rect`, `draw:ellipse`, `draw:line`, and
`draw:custom-shape` of a predefined geometry named by its
`draw:enhanced-geometry` `draw:type`) open as WordCraft shapes instead of
being skipped: size, placement (in line or floating, as frames are),
fill and line from the graphic style chain and the default graphic style
(LibreOffice's solid fill and hairline where nothing is stated), their
text, and a line's direction from its two ends. LibreOffice's type names
and its `ooxml-` preset names map onto WordCraft's kinds; a geometry
WordCraft has no shape for opens as a rectangle.
WordCraft's shapes are saved the same way: a custom shape by its type
alone (LibreOffice rebuilds the geometry from it), a line by its ends,
connectors as lines. A shape is saved with `draw:auto-grow-*` off unless
it resizes to fit its text, so LibreOffice keeps its size.
The flow model's text box becomes `Inline::Shape` with a kind; text
boxes are the `TextBox` kind and are written as frames, as before.
Not yet: shapes in groups, polygons, paths, rotation, arrowheads,
dashes, gradients.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A text box frame's line (colour and width), fill and padding are read
from its graphic style and that style's parents (LibreOffice's Frame
style included) instead of a fixed thin black line on white, and written
back. ODF's defaults (no line, fill or padding) apply where the styles
say nothing. A frame without a height, or with a `fo:min-height` on its
text box, opens as a box that resizes to fit its text and is saved with
a least height again. A LibreOffice text-box document was 1,117 px apart
after a round trip; now 736.
Layout: a floating text box that resizes to fit its text now takes its
text's height (drawn and wrapped around), as inline ones already did;
before, it kept its stored height (an ODT frame without one showed as a
1 pt line with the text spilling over the paragraph).
A formula sub-document's content.xml stays a bare `math:math` root, as
ODF 1.3 Part 3 §2.2.9 B requires (pinned by a test), and the reader now
skips a wrapping `office:math` some writers add instead of taking it for
the MathML root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The Asian and complex-script fonts (`style:font-name-asian`/`-complex`,
through their font-face declarations, or `style:font-family-*`), the
complex script's size, weight and posture, and the Asian and complex
languages (`style:language-*`, `style:country-*`) are read on runs and
from the default style, into the run's and the document's
character properties, and written back. Hebrew, Arabic and CJK text
lost its fonts on a save (an RTL test document: 2,043 px apart in
LibreOffice after a round trip, now 529).
A run's own complex-script weight or posture is written once, not also
copied from its Latin bold or italic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:writing-mode` `rl-tb` (or `rl`; ODF 1.3 §20.404) on paragraph
properties opens as a bidirectional paragraph, from automatic and custom
paragraph styles alike, and is written back on the paragraph's or the
custom style's properties. Right-to-left paragraphs opened left to right
and left-aligned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `draw:frame` holding a `draw:text-box` (ODF 1.3 §10.4.3) opens as a
WordCraft text box: a text box shape of the frame's size, in line or
floating as the frame is anchored and placed, whose story holds the
box's paragraphs (read as a note's are, with the paragraph and frame
around them put aside meanwhile, so a picture inside the box is a frame
of its own). The text was dropped on open.
Export writes each text box as a frame with a thin border and white fill
(`draw:fill` as well as `fo:background-color`: LibreOffice fills a frame
without `draw:fill` with its default blue). The module documentation now
lists what the reader and writer handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice gives a heading style the outline's numbering unless the
style names no list style (`style:list-style-name=""`, as LibreOffice
writes its own). Our heading styles named none, so a heading whose level
the outline doesn't number still took that level's default indent: a
Word-made document's headings sat indented after a save. Heading styles
of unnumbered levels now name no list style; numbered ones stay
attached. Round-trip of that document: 3,302 px apart, now 1,121.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Contextual spacing (`style:contextual-spacing`, ODF 1.3 §20.255: no
space between paragraphs of one style) is read from paragraph, custom
and kind styles and written back, and the List Paragraph style is
written like any other, so bulleted items keep their spacing; they were
written in Standard and spread apart.
LibreOffice switches many layout behaviours by `settings.xml`, and a file
without them is laid out its own way. A file's own `settings.xml` is now
kept through a save; a document from elsewhere gets the switches
LibreOffice uses for Word files, as WordCraft lays text out as Word
does. Tab stops are written from the margin either way. A Word-made
document saved as ODT drew 38,652 px apart from LibreOffice's own ODT of
it; now 3,302, with the same line breaks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common text style other than LibreOffice's link, note-mark and
list-label styles (Emphasis, a user's own) opens as a document character
style with its display name, the custom character style it is based on
and its own formatting; the text in it names it, and only what the
spans' automatic styles add is direct formatting. Before, the style was
lost and its formatting copied onto the text. An id a paragraph style
already has gets "Char" added.
Export writes each custom character style the text uses, and those it is
based on, as common text styles; a span's automatic style is based on
its style and holds only the run's direct formatting. LibreOffice keeps
the style when it saves the file. Automatic styles of styles.xml are
never taken for custom styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each list level's indent and hanging (`fo:margin-left` and
`fo:text-indent` of `style:list-level-label-alignment`) and its label's
alignment (`fo:text-align` of `style:list-level-properties`, ODF 1.3
§17.19-17.20) are read into the document's list levels, for lists and
heading numbering, and written back. Every list was written 36 pt a
level with left-aligned numbers, so LibreOffice's right-aligned numbers
and closer bullets moved on a save (lists test document: 658 px apart
in LibreOffice after a round trip, now 8).
The flow label keeps the position in twentieths of a point, its prefix
and suffix now up to 8 bytes ("Chapter " still fits), so list
paragraphs stay as small as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each paragraph's style chain was searched for its custom style by
building every custom style in it (cloning its tab stops and the rest)
just to see whether there was one. A cheap check now decides; the style
is built once, by the import that adds it to the document. A test opens
20,000 paragraphs on a 15-deep chain of styles with 60 tab stops each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common paragraph style other than Standard, the paragraph kinds' and
the note texts' (LibreOffice's Text body, a user's own style) opens as a
document style with its display name, the style it is based on (a
custom parent, a kind's style or Normal) and its own formatting:
spacing, indents, line spacing, alignment, keep with next, borders,
background, tab stops and text formatting. Its paragraphs stay in it;
only what the automatic styles over it add is direct formatting. Before,
the style was lost and its formatting copied onto every paragraph.
Headers, footers, notes and comments are covered too.
Export writes each custom style the paragraphs use, and the custom
styles they are based on, as common styles named by an XML-safe form of
the style id and shown by the style's name; LibreOffice keeps them when
it saves the file. A custom style may not take a name the writer uses
for its own styles (Standard, Footnote, Endnote, the kinds'), so no
style is written twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each equation is written as an embedded formula object (ODF 1.3
§10.4.3): a frame in line with the text whose `draw:object` points at a
sub-document `Object N/` (declared in the manifest as a formula) whose
content.xml is the equation's MathML, with its linear text as an
annotation. The reader already opens LibreOffice's formula objects, so
equations round-trip. They were written as their linear text.
LibreOffice scales a formula to its frame, so the frame is sized from
the equation's structure (a box per node: fractions, scripts, radicals,
n-ary operators, delimiters, matrices) and centred on the line.
Checked in LibreOffice 24.2 with LibreOffice Math.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export wrapped a heading numbered through its style in a `text:list`
with WordCraft's default list style, so LibreOffice laid its number out
with that list's indents: level 2's "1.1." sat 54 pt in from the margin
(measured against LibreOffice's own file). A heading numbered only
through its style is now a plain `text:h`, numbered by the outline
style. The outline levels also get label alignment followed by a tab,
and the heading styles no longer state zero indents, which in
LibreOffice override the numbering's placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame anchored to a paragraph, character or page (`text:anchor-type`,
ODF 1.3 §19.11) opens as a floating picture: its graphic style's
`style:wrap` and `style:run-through` give the wrap (square on both
sides, left, right or the larger side; top and bottom; behind or in
front of text), `style:horizontal-pos`/`-rel` and `style:vertical-pos`/
`-rel` its alignment or, with `svg:x`/`svg:y`, its offset and what it is
relative to, and `fo:margin-*` its distance from the text. Every
picture used to open in line with the text.
Export writes floating pictures anchored the same way, placed by a
graphic style of their own; pictures in line stay `as-char`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Paragraph borders (`fo:border*`, ODF 1.3 §20.183; each side's
`fo:padding*` as its distance from the text) and background
(`fo:background-color`, `transparent` as none) are read from paragraph
styles, the nearest in the chain but the kind's style and Standard, and
written back on the paragraph's style. Only an empty paragraph with a
bottom border was kept before, as a horizontal rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:outline-style` (ODF 1.3 §16.34) numbers the headings of its
levels. On open its levels (format, prefix, suffix, levels shown, start)
become one list, each level linked to its heading style, and Heading
1-6 number their paragraphs through it, as Word's numbered headings do:
LibreOffice's "1. / 1.1." headings keep their numbers (they were lost).
Levels without a number stay unnumbered.
Export writes a `text:outline-style` from the numbering of the heading
styles; levels without one show no number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `style:font-name` names a `style:font-face` in office:font-face-decls
(ODF 1.3 §16.23, §20.194); the font is that face's `svg:font-family`.
The reader took the face's name for the font, so LibreOffice's
`FreeSans1` opened as an unknown font "FreeSans1" instead of FreeSans.
Faces declared in content.xml and styles.xml both count; a name with no
face is still taken as the font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Cell borders (`fo:border*`: width, style, colour; ODF 1.3 §20.183) and
padding (`fo:padding*`) are read from table-cell styles. LibreOffice
gives an edge two cells share to one of them only (the left cell's
right side `none`, the right cell's left side drawing it); on open the
line goes to both cells, as WordCraft's cells each draw their own sides.
A table whose cells all say "no line" opens without a grid.
Export writes each cell's padding (its own, its table's or Word's
default) and its borders with each shared edge once, as LibreOffice
does (left and bottom, the first row's top, the last column's right), so
lines no longer double; cells used a fixed 4 pt padding and a grid even
in tables without lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On open, the file's own Heading 1-6, Title, Quotations and Preformatted
Text styles (named or displayed so, their whole chain but Standard)
give WordCraft's matching styles their font, size, bold, italic,
colour, spacing, indents, alignment and keep with next: a LibreOffice
heading is bold Liberation Sans, no longer WordCraft's blue heading.
`fo:font-size` percentages are read as a share of the inherited size.
On save, those styles are written from the document's own definitions
in full, instead of fixed ones, so a WordCraft heading looks the same in
LibreOffice. The styles test document now round-trips pixel-identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tab stops (`style:tab-stops`, ODF 1.3 §17.7-17.8) open with their
alignment (left, centre, right, decimal) and leader (dot, hyphen,
underline, middle dot), and are written back on the paragraph's style;
the default tab interval (`style:tab-stop-distance`) round-trips.
LibreOffice measures tabs from the paragraph's indent when its
`TabsRelativeToIndent` setting is on (its default); opening follows the
setting, and saved files carry a settings.xml that turns it off, so
positions run from the margin as in Word and WordCraft.
The flow model carries the tabs in `Para::extra` only for flows that
ask (FlowOpts::paras).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:text-properties` beyond bold, italic and colour now open and
save: underline styles (double, thick, dotted, dashes, wave, words only)
and colour, double strike, caps, small caps, hidden text (kept, hidden),
letter spacing, scale, text raised or lowered at full size (a position,
not superscript), outline, shadow, emboss, engrave and language.
A monospaced font keeps its own name; it was read as "code" and written
as Courier New. The default style's pair kerning (`style:letter-kerning`)
is read into the document defaults and written back, so lines break
where LibreOffice breaks them (round-trip renders of the test documents
are now pixel-identical in LibreOffice).
The flow model carries this in `Fmt::extra` only for flows that ask
(FlowOpts::chars); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each level of a list style (ODF 1.3 §16.30-16.33) is read with its
number format (1, i, I, a, A, none), prefix and suffix, levels shown,
start value and bullet character, and written back. Each `text:list` is
a list of its own unless it continues the one before it
(`text:continue-numbering`) or the one it names (`text:continue-list`);
an item's `text:start-value` restarts from there. A list inside a note
or table cell leaves the list around it alone.
Before, consecutive lists merged into one: three LibreOffice lists
numbered 1., i. and A. opened as one decimal list 1-7.
The flow model's ListInfo carries a list number and a compact label
(prefix and suffix up to 12 bytes, so "Chapter %1" fits) only for flows
that ask for them (FlowOpts::lists); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export wrote only the direct replies of a comment, so a reply to a reply
was lost on save. Every reply in a comment's thread is written now, each
answering the comment that starts the thread, as LibreOffice's threads
are flat. A reply chain that loops (hostile) is written once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`office:annotation` (ODF 1.3 §14.1) opens as a document comment with its
author, date, initials and text; `office:name` and
`office:annotation-end` (§14.2) give its range, across paragraphs too.
LibreOffice's resolved state (`loext:resolved`) and reply threads
(`loext:parent-name`) are kept: a reply is a comment of its parent with
no range of its own. An end with no comment is dropped, and a comment
whose end never comes covers nothing. LibreOffice's all-zero date is no
date. Before, comments were dropped on open.
Export writes each comment named, with its end where its range ends,
and its replies after it naming it; LibreOffice keeps the threads when
it saves the file again. Truncated XML no longer loses the paragraph a
note or comment sits in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:note` (ODF 1.3 §6.3) opens as a note story built as Word's: its
paragraphs in Footnote Text or Endnote Text, the first starting with the
note's own mark, the reference in the text in the reference style, and a
`text:label` custom mark kept. The paragraph a note sits in (with its
spans, list and table cell) is put aside while the note's content is
read, and goes on after it. Notes inside notes, in deleted text or in
frames are left out. Before, notes were dropped on open.
Export writes each note with a citation numbered as LibreOffice shows
it (footnotes 1, 2, 3; endnotes i, ii, iii) or its custom mark, its
paragraphs in Footnote or Endnote styles defined in styles.xml. An
insertion open around a note ends before it and goes on after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A LibreOffice header 2 cm high with 1 cm spacing puts the text 2 cm below
the page margin: `fo:min-height` takes in the `fo:margin-bottom` spacing.
Import added the spacing on top, so the text started a spacing too low
(measured: 113.9 pt in LibreOffice, 142.3 pt after a WordCraft save). The
spacing now counts only past the height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export writes each section's headers and footers into its master page
(style:header, -left for even pages, -first for a different first page;
an empty element where Word shows nothing), with their automatic styles
in styles.xml under names of their own. Word's page-edge header and
footer distances become ODF's page margin plus header/footer band
height, and import converts back, so headers sit where LibreOffice
draws them.
Text fields (ODF 1.3 §7: page number and count, date, time, document
information, bookmark references) open as WordCraft fields with their
shown text as the result, and are written back as ODF fields. Fixed
dates and fields without a Word equivalent stay text; Word fields ODF
lacks are written as their result. The flow model carries fields only
when asked (FlowOpts), so the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Read `fo:margin-*`, `fo:text-indent`, `fo:line-height` (percent, length,
normal), `style:line-height-at-least` and `style:line-spacing` (leading,
as a proportional approximation) from automatic and common paragraph
styles (ODF 1.3 §20.204-209, §20.225, §20.317-318), and write them back
from each paragraph's direct formatting.
The default paragraph style and Standard give the document defaults, on
ODF's initial values: an ODT paragraph without spacing has none, not
Word's 8 pt after and 1.15 lines. Export writes the document's Normal
spacing as the default paragraph style, so the copy reopens the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A pStyle id the style sheet doesn't define gave an empty style chain, so the
paragraph got the document defaults alone. Word formats it as Normal: a Pandoc
footer naming an undefined Footer style was taller than Word's and pushed the
last paragraph of the page over.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A hyperlinked TOC wrote each entry's page number as plain text inside the
link. Word writes it as a PAGEREF _Toc… \h field, so Update Field in Word
moves the number with its heading; with plain text the numbers stay what
they were when the file was saved.
Entries that link to a bookmark now end with that field (result: the page
number). Entries without a link and \n levels are unchanged. The DOCX span
test looks for the TOC's own end, the last one, since PAGEREF fields now end
inside it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/engine/src/cmd/mailings/recipients.rs:6-18 (csv_separator)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Treat <!--> and <!---> as empty comments in HTML import
Found while testing crates/formats/src/html/lexer.rs:70-73 (Lexer::next, `<!--` branch)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs: file-format-parity HTML row for empty comments (#5040)
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Keep with next asked for two lines of the paragraph after a kept heading under widow/orphan
control. A three-line paragraph cannot split at all there (two lines and a widow, or an orphan
and two), so it moved over whole and left the heading alone at the foot of the page. Word moves
the heading with it: ask for all three lines in that case.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Keytips: Alt, F shows the Backstage pages' letters; Alt, F, P and Alt, F, V print (#4960)
Keytips ended as soon as File opened the Backstage, so Alt, F, P and
Word's older Alt, F, V (Print Preview) did nothing. Alt, F now badges the
side menu with Word's letters (Home H, New N, Open O, Info I, Save S,
Save As A, Print P, Export E, Close C, Options T), and V opens the Print
page too, as it does in Word. A page picked this way acts as a click on
it does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keytips: closing the Backstage ends its page letters (#4960)
The Backstage closed by its back chevron or a mouse Save while Alt, F's
badges showed left keytips waiting for a page letter. The next key was
eaten instead of typed, and picked a page with the Backstage gone: C
closed the document and S saved it. Keytips now end with the Backstage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keytips: keep the doc comment on works_without_document; ui-parity and ROADMAP rows (#4960)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4734: the print job's PDF leaves out link annotations
craft-print refuses to fit a page with annotations to the paper ("flatten page annotations
before using shrink-to-fit"), and the desktop app always prints with ShrinkToFit, so any
document with a hyperlink or a TOC (its entries are links) failed to print. Links print
nothing; `PdfOptions::links` (default on) lets the print job leave them out. Exported PDFs
keep them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: review — the print note is in every desktop column; gaps and progress log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: doc wording: the print PDF omits link annotations; hardware still to validate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: only the printer's PDF drops links; Save these pages as PDF keeps them
Session::print_job_pdf is what ui.printSubmit sends to the spooler, without
link annotations; Session::print_pdf (file.printPdf) keeps them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
count_words split only on whitespace, so unspaced Chinese or Japanese
text counted as one word per run. Word counts each Han ideograph and
kana as a word; Hangul (spaced) still counts by tokens. Fixes the status
bar, the Word Count dialog and NUMWORDS, which all use count_words.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* View › Macros opens a dialog to run, delete and record macros (#4327)
* Docs: date the Macros dialog row and bump the ui-parity status line (#4327)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): saturate page parameters on 32-bit and wasm (#3676)
The file.exportPng and edit.goto paths cast a u64 page to usize and subtracted one. On 32-bit and wasm, page 2^32 truncated to 0 and the subtraction wrapped to page 1 (a debug panic).
Add p::page_index, which saturates instead of wrapping, and use it in both engine paths. The MCP render path already has the same guard on main; the new tests also pin that behavior.
Regression tests cover the helper, Go To, PNG export and the MCP render path.
* fix(engine): guard caret.set page cast too (#3676)
caret.set takes a 0-based page; pg as usize truncated on 32-bit and wasm and could select page 0. Use the same checked conversion and add a regression test.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The shared dictionary slot reports no file while a dictionary given
with install_dictionary is in use, so this test had accepted null
alongside the folder's file, for installs from tests running at the
same time. That also let it pass if file.options stopped reporting the
folder's file at all. The tests' install helpers and this test now take
turns on one lock, and each language must report the folder's file
again. An install from before the test doesn't matter: changing the
folder reloads every slot.
main split Czech's suggest_in into one helper per round for the
cognitive_complexity limit (#4501). On this branch that code is the
shared affix_suggest::suggest_in (36 of 30), so cs/suggest.rs stays the
thin wrapper and the shared function gets the same split. The candidates
are offered in the same order; a dump of 13,081 calls over every
language's test dictionary matches the old engine except one: the typo
round counts the lower-cased letters, so "İ" (two letters lower case)
now gets "I" in Polish, where the old count stopped one letter short.
Set persist-credentials: false for preview tests and join the two newly added documentation blockquotes as requested by review. No Rust source change.
Validation: independently reviewed exact workflow and Markdown diffs and tree hashes. Cargo fmt passed on the unchanged Rust source; cargo xtask ci was attempted but blocked by missing local dlltool.exe. Fresh hosted checks are required for this revision.
Merge main 436d444ef0. Port proofing selection/navigation and app/control/pane hooks to extracted modules, retain per-window state and stale-target guards, preserve upstream menus, and extend preview path filters. Keep the original pane implementation and targeted tests, with explicit imports after test relocation.
Workspace formatting passes. Local cargo xtask ci is blocked during Clippy by missing dlltool.exe in the Windows GNU toolchain; no local test success is claimed. Hosted navigation, UI, rendering and full checks on this tree remain required.
Checked against the Hunspell 1.7 command line:
- Without a tab or a morphological field the whole line is the entry, so
a phrase with single-byte flags (tök jó/396 in hu_HU) no longer fails
the load or loses its alias; leading blanks belong to the entry.
- With an AF table a flag field is the alias its leading digits number
(fal/2x is alias 2, lab/b has none); an undefined alias, in a .dic
entry or a continuation class, reads as no flags instead of failing
the dictionary; a table without its count line is ignored.
- A REP count line may carry a comment, like AF and ICONV.
normalize copies the input only from its first change on, and SET and
FLAG are found in one pass over the .aff.
Checked against the Hunspell 1.7 command line:
- The first AF and ICONV line is the count even with a trailing comment
(AF 2 # aliases), so alias numbers no longer shift and ICONV 1 # no
longer turns 1 into #.
- A .dic entry ends where Hunspell ends it (dic_entry_end), so ad hoc/B
is one entry and ad alone is no word.
- An undefined alias in a .dic entry reads as no flags: the stem loads
without affixes instead of failing the whole dictionary.
- IGNORE characters are removed from the appended text only; a stripped
text that holds one never matches a stem, as in Hunspell.
- FLAG UTF-8 and SET are found anywhere in the .aff, not only in the
first 8 KB.
Also: normalize borrows the input when no conversion or ignored character
applies, the UTF-8 .aff reader counts fields as it goes and builds its
error context only on error, and the single-byte flag reader is
decode_latin1.
Without FLAG UTF-8, Hunspell reads a field that names one flag (PFX/SFX,
KEEPCASE, FORBIDDENWORD and the like) through decode_flag, which takes its
first byte: SFX é is the flag 0xC3. Such a field no longer fails the load.
SYLLABLENUM lists flags and is read byte by byte like AF. SET and FLAG
read only their first argument, so FLAG UTF-8 with a trailing comment
keeps Unicode flags.
The Hungarian Hunspell dictionary declares SET UTF-8 but writes its flags as single bytes, uses AF aliases and 158 distinct flags. Read flag fields byte by byte unless FLAG UTF-8 is set, expand AF aliases, skip AM, apply ICONV and IGNORE, honour KEEPCASE, and widen flag masks to 256 bits. Compounding and NEEDAFFIX stay unsupported for now.
Remove the extra blank line before the new navigation regression module's closing brace. No behavior change.
Local cargo xtask ci was attempted but cargo is absent; git diff --check passes. Hosted checks must validate this new SHA.
Give the exact selected issue priority over overlapping grammar when querying the focus, including reversed selections. Keep point lookup for explicit other positions. Add command and pane regressions for overlapping repeated misspellings, correction and undo.
Include command/window/document/canvas integration paths in preview workflow triggers. Preserve historical three-platform evidence while recording f126's successful gate/previews/Windows/Linux and outstanding macOS validation after pre-run cancellations.
These address the three CodeRabbit findings on official #4471. Local cargo xtask ci was attempted but cargo is absent; git diff --check passes. New hosted checks must validate this SHA before any claim of success.
Upstream #4401 merged the baseline Page/Rgb import fix. Keep main's exact tests_linear.rs and merge its current head, removing the equivalent temporary import patch from the final official diff. No proofing/UI behavior changes.
The last fully validated pane revision is 056b18b; the subsequent Read Aloud alignment and this merge need hosted checks and new PNG inspection. Local cargo xtask ci was attempted but cargo is absent; git diff --check passes.
The 056b18b preview confirms full-width context and visible actions, but the text child still shrinks to its contents and pulls the Read Aloud control inward. Keep that child at its allocated width so the control aligns at the right edge as observed in Word.
All five workflows passed at parent 056b18b; both light/dark artifacts were downloaded and inspected. This alignment revision needs new hosted checks and preview inspection. Local cargo xtask ci was attempted but cargo is absent; git diff --check passes.
Inspect the successful 20205a5 light/dark artifacts against the observed Word arrangement. Fill the sentence frame across the pane and bound the suggestion list to roughly three cards, retaining all results through scrolling and keeping ignore/dictionary actions below the list visible.
The direct-correction/undo regression now clicks the first visible engine suggestion rather than relying on an offscreen fourth card. Update comparison/progress records. Normalize the baseline test file's line endings so its final diff contains only the two missing imports.
Parent 20205a5 passed full CI, three-platform Printing/UI checks, targeted proofing tests and preview generation. This layout revision needs new hosted validation and inspected PNGs. Local cargo xtask ci was attempted but cargo is absent; git diff --check passes.
The full gate found missing Page/Rgb imports in main's tests_linear.rs (also tracked by upstream #4401). Add the imports already validated with the French contribution. No layout behavior changes.
The preview run rendered both images and passed all six correction tests, but upload searched the workspace root. Cargo tests run in the crate directory; collect the PNGs from crates/ui-egui instead.
At parent 574cef, Printing passed on Linux, macOS and Windows, including UI tests and browser compatibility. Local cargo xtask ci was attempted but cargo is absent; git diff --check passes. This revision needs new hosted validation.
Replace two deprecated WidgetType aliases with Role::Button, as required by the hosted Windows and macOS Clippy checks.
Local cargo xtask ci was attempted but cargo is not installed on this machine. git diff --check passes. Hosted CI and the preview workflow must validate this revision.
Apply the six formatting hunks reported by the exact-head Printing/CI runs. No behavior change. cargo xtask ci was attempted locally but cargo is absent; hosted checks must validate this revision.
Observe F7 in the installed Word with synthetic text, then replace the central correction form with a right-side task pane. Show sentence context, directly clickable suggestion cards and ignore/dictionary links. Use the existing pane host, palette and original icons; copy no Microsoft assets.
Keep per-window state and clear targets on document switch. Adapt UI tests, pseudo-localization exemptions and light/dark previews. Record observed measurements and remaining feature differences. Local cargo xtask ci was attempted but cargo is absent; exact-head hosted checks and visual comparison remain required.
Keep compact context menus, but show a correction form for user spelling/editor commands. Advance without wrapping after change, ignore or dictionary actions; reject stale targets and retain programmatic command results.
Add navigation/UI regressions and light/dark preview CI. cargo xtask ci was attempted on the preparation PC but cargo is absent; hosted validation is required before integration.
A dictionary is read outside the lock. If the dictionaries folder changed
while it was being read (File › Options › Proofing), the reload emptied
the slot first and the late load then filled it with the old folder's
dictionary. It stayed until the next reload. Every language's copy of
the loader had this window; the shared slot now closes it in one place.
The slot counts reloads. A load that started before a reload still
answers the check that asked for it, but it isn't kept: the next check
reads the new folder.
Found by CodeRabbit on #4191. Tests: a_load_from_before_a_reload_is_not_kept;
not counting reloads fails it. Proof: 288 tests.
The suggestion rounds don't run in rising cost: a MAP diacritic drop (11)
comes before a REP confusion (10, 9 for Russian). The engine checked each
candidate once, at the first cost it was offered, so the same word found
again more cheaply kept the dearer cost and could rank one place too low.
Czech's engine always did this, and the merged engine took it over.
The candidate set is now a map to the cheapest cost offered so far, and
an offer that is no cheaper is skipped as before.
Found by CodeRabbit on #4191. Tests: a_cheaper_later_offer_wins; skipping
every repeat offer again fails it. Proof: 288 tests.
Czech, Croatian, Indonesian, Italian, Polish, Portuguese and Spanish began
their spelling checks with the same thirteen lines, byte for byte: no
dictionary means no marks, single letters and words without letters pass,
words with digits and short words in capitals pass when the File › Options
› Proofing choices say so, and words in another script pass. Each also had
its own copy of the Latin-script test. These were the lines #3966 had to fix
in six copies one by one, and where the Russian and Turkish copies stayed
wrong until the first commit here.
spelling.rs has them once (`word_to_check`), and each language goes on
from there with its own rules (Polish apostrophe endings, Italian elisions,
Indonesian clitics, Uruguayan `vos` forms).
Tests: words_needing_no_lookup. Ignoring the uppercase option there fails
it and all seven languages' proofing-options tests. Proof: 286 tests.
lib.rs named every dictionary language in nine places: reloading for a new
folder, the installed list, load errors, the missing-dictionary hint, the
loaded check, the spelling message, spelling, suggestions and grammar. The
lists had drifted. dictionary_load_error had no Russian or Czech entry, so a
Russian or Czech dictionary that failed to load showed no reason in File ›
Options › Proofing, though the status bar, which reads another list, said
why.
LANGUAGES holds one row per language: its dictionary slot, its spelling
message, and its spelling, suggestion and grammar functions. The nine
functions read the row, so a language now joins with its module, its
ProofLang and one row. The per-language reload_dictionary and
installed_path wrappers had no callers left, inside the crate or out, and
are gone.
Tests: every_dictionary_language_reports_its_load_error, which fails for
Russian on the old code, and the_language_table_covers_the_proofed_languages
(each ProofLang::PROOFED language but English has exactly one row). Proof
285, engine 927 and layout 501 (lib).
Czech's dictionary-driven ranking (case, MAP, REP, missing spaces, KEY,
TRY edits, a stem scan) had five older copies. Polish, Portuguese,
Croatian, Italian and Russian each kept one: 300 to 400 lines of the same
rounds with a few lines of language knowledge mixed in. Fixes reached some
copies only. NOSUGGEST (#1048) and the first-difference tie-break (#797)
never got to all of them, and none had the KEY round or the bound on TRY
letters.
affix_suggest.rs is that ranking once. A language's `Tuning` holds what is
its own: the short words that run into their neighbours (Polish,
Portuguese, Croatian, Italian), Russian's split rule, the letter groups and
confusions its dictionaries lack (Croatian, Russian), letters to try for a
dictionary without a TRY line, and cost hooks for Polish final devoicing
and Russian е/ё. Czech, Indonesian and Spanish use the dictionary's tables
alone. Turkish keeps its own suggester, which reads another dictionary type
with rounds of its own.
The language tables add to the dictionary's instead of replacing them, and
TRY letters are tried in the dictionary's order, commonest first, as the
copies did (the Czech engine sorted them).
Old and new compared on the test dictionaries, with every one-letter typo
of each word plus the tests' misspellings, top eight suggestions: Czech,
Croatian, Italian and Portuguese give the same for all 7,903 inputs.
Polish differs for 44 of 3,244 and Russian for 33 of 2,346, all orderings
among candidates of equal cost, from the tie-break they now share (`ie` →
`nie` before `i`).
New tests pin the moved knowledge: the tuning mechanism, Croatian's groups
without MAP lines, and Russian's costs and split rule. Turning each knob
off fails one. Proof: 283 tests.
user_dictionary_persists_through_a_file saves the whole user dictionary to
a file and loads it back. Run beside
user_dictionary_and_ignore_all_match_both_normalization_forms, it could
catch a word that test had added and bring it back after the test removed
it, so the other test's last check failed now and then (once in about two
dozen full runs here). It now holds DICTIONARY_TESTS, as the other test
does.
Nine languages each kept their own copy of the same code: the installed
dictionary behind a lock, read on first use and logged, replaced by
install_dictionary, forgotten by reload_dictionary, and its file reported
by installed_path. The copies had drifted already. Every one documented that
a dictionary given with install_dictionary has no file and isn't reported,
but only Russian and Czech did that (#3252); the other seven reported the
disk file anyway.
`DictSlot` (proof/src/slot.rs) is that code once. It is generic over the
dictionary type, so Turkish's `TrDict` fits, and keeps the dictionary and
its in-memory origin under one lock as #3252 made it. Each language keeps
its functions as one-line wrappers over its slot, so no caller changes, and
the log lines are the same.
installed_path now follows the documented rule in every language. Nothing
outside tests installs a dictionary in memory, but tests do, concurrently,
so the dictionaries-folder tests in proof and engine could race on the
reported path of a language another test installs. They now accept the
folder's file or none there, and check that the folder is searched first
with hunspell::find, which no install affects.
Tests: proof 279, engine 927, layout 501 (lib); slot.rs has a test of its
own.
#1048 (6e25d482c) taught affix.rs about NOSUGGEST, the dictionary flag for
words that are correct but never offered (vulgarisms, rare forms), and
patched the Polish and Portuguese copies of the suggestion engine; Czech's
started with it. The Croatian, Italian and Russian copies never got it: they
test candidates with `contains`, so a suggestion could be a word the
dictionary says not to offer. They now use `contains_suggestible` and drop a
suggestion with any part `can_suggest` refuses, as the others do.
Tests: Croatian and Italian run the shared assert_nosuggest_is_honoured.
Russian gets its own, since its MAP and REP tables are built in and the
shared Latin-script check doesn't reach them. Each fails on the old code.
#3966 made Ignore words in UPPERCASE and Ignore words that contain numbers
reach six languages' own checks, one copy at a time, and Russian and Turkish
were left out. lib.rs still called `ru::is_correct(word)` and
`tr::is_correct(word)`, which accept words with digits (both) and short words
in capitals (Russian) whatever the options say, so turning either option off
changed nothing in those languages.
Both now have `is_correct_with_options`, like the others. Russian's rule for
a part in capitals (`ИИ-консультант`) follows the option too, as Indonesian's
does, and its digits rule keeps counting superscripts (`м²`). Turkish had no
test dictionary, so its two dictionary tests returned early wherever none was
installed, CI included; it has a synthetic one now, also in the
dictionaries-folder test.
Tests: proofing_options_reach_the_russian_check and
proofing_options_reach_the_turkish_check. Calling the old checks from lib.rs
fails each, and so does ungating the Russian part rule or the Turkish hyphen
recursion.
2026-10-24 19:19:25 -01:00
695 changed files with 76307 additions and 67227 deletions
# Only this job publishes the GitHub release, so only it gets write access.
permissions:
contents:write
packages:write
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy.
1.**A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments.
2.**Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish.
3.**Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location.
4.**Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix.
5.**Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true.
6.**Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why.
7.**Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready.
8.**`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception.
9.**Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop.
10.**Keep secrets and personal details out** of the repository, logs and requests.
11.**CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there.
12.**Guards say why.** A check that looks removable says what it prevents. Read that before changing it.
13.**Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it.
14.**Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit.
15.**No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that.
These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib).
1.**Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix.
2.**Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it.
3.**Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository.
4.**Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build.
5.**Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md).
6.**Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`.
## Licence allowlist
Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules.
-`ISC`
-`MIT`
-`BSD-2-Clause`
-`BSD-3-Clause`
-`Apache-2.0`
-`OFL-1.1`
-`Ubuntu-font-1.0`
-`Bitstream-Vera`
-`CC0-1.0`
-`CC-BY-4.0`
-`CC-BY-SA-4.0`
-`LicenseRef-Public-Domain`
`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so.
<!-- craft-house-rules:end -->
# WordCraft — instructions for agents
WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../pdfcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign). Shared rules and learnings: [`storytold/craftrules`](https://github.com/storytold/craftrules) (`../../craftrules`) — read its `AGENTS.md`.
WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../pdfcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign).
## Start every session here
1.Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. `plan/` is gitignored (local only); if it's missing, start from `ROADMAP.md`, `docs/gaps.md` and `docs/roadmap.md` (Current focus).
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §5). Don't stop to ask unless a decision is genuinely the owner's (licensing, publishing, pushing to new remotes, secrets).
Read [`ROADMAP.md`](ROADMAP.md), [`docs/gaps.md`](docs/gaps.md) and [`docs/roadmap.md`](docs/roadmap.md). `plan/` is an optional gitignored notebook. Do not ask for it and do not stop when it is absent.
## Never crash
People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it. Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it.
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature returns an error or is disabled; it never panics.
- **Input-derived numbers are hostile** (files, commands, MCP/control params): `get()` instead of `[i]`, slice strings only at char boundaries, checked/saturating arithmetic, clamp sizes, cap allocations, bound recursion.
@@ -20,7 +72,7 @@ People trust WordCraft with their writing; a crash loses their work. **This outr
- Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily".
- Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it.
- **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file.
- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts.
- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds, but English spelling falls back to an installed Hunspell English or the built-in Moby word list (with no American/British variant table) and pattern hyphenation is off (Bulgarian, Turkish and Russian still hyphenate by rule), so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts.
- Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded.
## Clean room
@@ -28,22 +80,22 @@ People trust WordCraft with their writing; a crash loses their work. **This outr
- Never read, disassemble or copy anything inside the Word bundle (file names/listings only). Never commit files produced by Word. Never copy Microsoft wording beyond feature names.
- File formats come from public specs: ECMA-376 / ISO 29500 (OOXML), the published RTF spec, OASIS ODF 1.3. Never copy GPL/LGPL/AGPL code (LibreOffice, AbiWord, pandoc…).
| external | `craft-proof` (craft-libs, used as `wordcraft-proof`, sits at L1) | proofing: spelling, grammar, hyphenation; its word lists and patterns are craft-i18n's `language-data/` (`CRAFT_I18N_DIR`) |
- **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs.
- **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity-checklist.md`; a test enforces a floor that only rises. Implement missing ids to raise it.
- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin and reads `Session` state; colours come from `theme::Tokens`.
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math). A crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
@@ -63,27 +115,26 @@ Typing, Enter, Backspace, Tab, lists, AutoCorrect and AutoFormat follow Word's o
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done (disk).
## Roadmap
Progress docs follow [`craftrules/standards/progress-docs.md`](https://github.com/storytold/craftrules/blob/main/standards/progress-docs.md): `ROADMAP.md` (stage, headline numbers, progress log), `docs/target-app-parity.md` (the assessment), `docs/gaps.md` (ranked work list), `docs/roadmap.md` (milestones, Current focus), `docs/architecture.md`, and the parity checklists `docs/file-format-parity.md`, `docs/layout-parity.md`, `docs/typing-parity.md`, `docs/ui-parity.md`, `docs/hardware-parity.md`, `docs/localization-parity.md`. When work lands, update the gap, the parity doc and the progress log, and bump each touched doc's status line and revision history.
Progress docs: `ROADMAP.md` (stage, headline numbers, progress log), `docs/target-app-parity.md` (the assessment), `docs/gaps.md` (ranked work list), `docs/roadmap.md` (milestones, Current focus), `docs/architecture.md`, and the parity checklists `docs/file-format-parity.md`, `docs/layout-parity.md`, `docs/typing-parity.md`, `docs/ui-parity.md`, `docs/hardware-parity.md`, `docs/localization-parity.md`. When work lands, update the gap, the parity doc and the progress log, and bump each touched doc's status line and revision history.
## Contributor credits (About window)
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
(commit stats; never hand-edit) and `contributors/people.toml` (names people chose for
themselves). See `docs/contributors.md`.
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
whether they want to be credited by more than their username: a real name, a display name, and/or
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
and include it in their PR, committed as them. If no, change nothing: they are credited as
`@username` anyway.
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
GitHub profiles). Never hand-edit `contributors.json`.
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
re-verifies who wrote each `people.toml` entry; `--check` only verifies).
## CI workflows
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
New workspace crates include `workspace = true` under `[lints]` in `Cargo.toml`. Production crate roots declare the crate-level lint attributes before modules; crate-level `//!` documentation may precede them.
@@ -10,15 +10,6 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
|---|---|---|---|---|
| `egui` dependency: `epaint_default_fonts/fonts/egui-icons.ttf` | John Slegers (emoji-icon-font) | https://github.com/storytold/egui/blob/173cc7375d35dc5fbc5b5d0994fdf787f547c5d1/crates/epaint_default_fonts/fonts/egui-icons.txt; original https://github.com/jslegers/emoji-icon-font | MIT (full copyright and permission text in NOTICE) | Small egui special-icon subset; embedded by default_fonts, retained in FontDefinitions::default() |
| `egui` dependency: `epaint_default_fonts/fonts/emoji-icon-font.ttf` | John Slegers | https://github.com/storytold/egui/blob/173cc7375d35dc5fbc5b5d0994fdf787f547c5d1/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt; original https://github.com/jslegers/emoji-icon-font | MIT (full copyright and permission text in NOTICE) | Parent monochrome icon/emoji face embedded by monochrome_emoji_fonts; separate from the egui-icons subset |
| `assets/hyphenation/en-us.pat` | DesignCraft contributors (generated) | trained from the Moby list by DesignCraft's patgen | Public Domain (CC0-1.0) | Our own Liang hyphenation patterns (no TeX patterns) |
| `assets/hyphenation/en-us.dic` | Grady Ward (Moby Hyphenator II word list); converted by DesignCraft contributors | https://www.gutenberg.org/ebooks/3204 (file `mhyph.txt`, SHA-256 `eeb30474c86b8af3469035ec1a0913e35905325ca885290db9dfed9881e230ac`), via DesignCraft | Public Domain | ~165k words with break points; drives spelling and hyphenation |
| `assets/spelling/en-extra.txt` | WordCraft contributors | original word list written for this repository (common modern words the Moby list lacks); no other dictionary copied | MIT OR Apache-2.0 | ~240 lowercase English base forms supplementing spelling (#520), including British spellings (#40) |
| `assets/spelling/en-variants.txt` | WordCraft contributors | original list written for this repository from general knowledge of English spelling; no other dictionary or variant list copied | MIT OR Apache-2.0 | ~280 American/British spelling pairs (-or/-our, -er/-re, -ize/-ise, -yze/-yse, -ense/-ence, doubled l, ae/oe…) with a policy per English: en-US, en-CA and the British-spelling Englishes (#1281, #1045) |
| `assets/spelling/ru-extra.txt` | WordCraft contributors | original word list written for this repository (common modern Russian words older Hunspell dictionaries lack, with their declension classes); no other dictionary copied | MIT OR Apache-2.0 | ~200 Russian base forms supplementing spelling; their forms are generated by `crates/proof/src/ru/extra.rs` |
| `assets/hyphenation/pl.pat` | Hanna Kołodziejska, Bogusław Jackowski, Marek Ryćko (Polish TeX hyphenation patterns, 1987–1995); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pl.tex`, SHA-256 `83ddbbd21bc06a44bc4f4a4c49c38782e809cff5f026b764c044daa29e818843`), bundled unmodified | MIT (the file is offered under MIT or Knuth's public-domain terms; WordCraft uses MIT, whose notice is in the file's header) | 4,053 Liang patterns and 20 exception words; Polish hyphenation |
| `assets/hyphenation/pt.pat` | Pedro J. de Rezende, J. Joao Dias Almeida, Leonardo Araujo, Aline Benevides (Portuguese TeX hyphenation patterns, 1987–2024); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pt.tex`, SHA-256 `c110a39e501db372d1d755e1c64464321a2efa7d8e9617f66001000389b941f2`), bundled unmodified | BSD 3-clause (the file's licence header names it and carries the notice) | 426 Liang patterns and 2 exception words; Portuguese hyphenation |
| `assets/hyphenation/it.pat` | Claudio Beccari (Italian TeX hyphenation patterns, 2008–2014); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-it.tex`, SHA-256 `6ce56ed6ed2ca7c0688838e366e6595e0429f984f4fc6e96ec045cebb1e76a47`), bundled unmodified | MIT (the file is offered under LPPL 1.3+ or MIT; WordCraft uses MIT, whose notice is in the file's header) | 355 Liang patterns, no exception words; Italian hyphenation |
| `assets/hyphenation/de.pat` | Deutschsprachige Trennmustermannschaft: Stephan Hennig, Werner Lemberg, Günter Milde, Sander van Geloven, Georg Pfeiffer, Gisbert W. Selke, Tobias Wendorf, Keno Wehr (German TeX hyphenation patterns, reformed spelling, 2013–2024); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-de-1996.tex`, SHA-256 `374ad1ce3263f8a2791ec070ef9b516c532dae64456f3fb656e617e8ae53a9d3`), bundled unmodified | MIT (the notice is in the file's header) | 36,709 Liang patterns, no exception words; German hyphenation (de-DE, de-AT, de-CH) |
| `assets/app-icon/wordcraft-1024.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands |
| `packaging/macos/dmg/background.svg` | @XusBadia | original work for this repository, derived from the WordCraft app icon (`assets/app-icon/wordcraft.svg`, referenced via `<image>`, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0, like the code ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE)) | WordCraft macOS DMG window background (2026-10-08); used for the Finder window of the macOS DMG, rendered to `background.tiff` by `packaging/macos/dmg/generate.py` |
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0, like the code ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE)) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") |
@@ -59,14 +50,15 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `docs/images/dark.png` | WordCraft contributors | screenshot of WordCraft itself (sample document written for WordCraft, rendered by `ui_shot`) | MIT OR Apache-2.0 | Original; no Microsoft UI or assets |
| `docs/images/backstage.png` | WordCraft contributors | screenshot of WordCraft itself (sample document written for WordCraft, rendered by `ui_shot`) | MIT OR Apache-2.0 | Original; no Microsoft UI or assets |
| `docs/images/layout.png` | WordCraft contributors | screenshot of WordCraft itself (sample document written for WordCraft, rendered by `ui_shot`) | MIT OR Apache-2.0 | Original; no Microsoft UI or assets |
| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only |
| craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese and Chinese UI, Japanese document fallback |
| craft-i18n (optional build input, not files in this repo): the interface translations | see https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md, which credits every translator of WordCraft's former in-repo catalogs (`crates/ui-egui/src/i18n/*.tsv`, migrated at `wordcraft@806a945`) | embedded only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | MIT OR Apache-2.0 | Interface languages other than English; clean-room translations from the English interface labels |
| craft-i18n Language data (optional build input, not files in this repo): the spelling word lists and hyphenation patterns (`language-data/`: the Moby Hyphenator II list and our patterns for English, `en-extra.txt`, `en-variants.txt`, `ru-extra.txt`, and the hyph-utf8 German, Polish, Portuguese and Italian patterns) | see the Language data section of https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md (authors, upstream sources with SHA-256, licences); moved there from `assets/hyphenation/` and `assets/spelling/` byte for byte | embedded by craft-proof (storytold/craft-libs) only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | Public Domain / CC0-1.0, MIT, BSD-3-Clause, MIT OR Apache-2.0 (per file; see NOTICE) | Spelling and hyphenation; without it English spelling falls back to an installed Hunspell English or craft-proof's built-in Moby list, and pattern hyphenation is off |
WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../pdfcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign). Shared rules and learnings: [`storytold/craftrules`](https://github.com/storytold/craftrules) (`../../craftrules`) — read its `AGENTS.md`.
## Start every session here
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. `plan/` is gitignored (local only); if it's missing, start from `ROADMAP.md`, `docs/gaps.md` and `docs/roadmap.md` (Current focus).
2. Follow the autonomous operation protocol (`plan/execution-plan.md` §5). Don't stop to ask unless a decision is genuinely the owner's (licensing, publishing, pushing to new remotes, secrets).
## Never crash
People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it. Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature returns an error or is disabled; it never panics.
- **Input-derived numbers are hostile** (files, commands, MCP/control params): `get()` instead of `[i]`, slice strings only at char boundaries, checked/saturating arithmetic, clamp sizes, cap allocations, bound recursion.
- **Last-resort guard:** `Session::run` turns an escaped panic into an error and restores the document. It's a safety net, not a licence.
- **Prove it:** every crash fix lands with a regression test. `crates/engine/src/tests.rs::hostile_params_never_panic` runs every command with junk params — keep it green.
- Production crate roots carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it and add `[lints]` with `workspace = true` to their `Cargo.toml`. Tests may `unwrap` (`clippy.toml`).
## Assets: absolutely no Microsoft (or Adobe/Avid/Autodesk) iconography or images
**This rule is non-negotiable.** Breaking it is the worst mistake you can make in this repo.
- Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily".
- Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it.
- **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file.
- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts.
- Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded.
## Clean room
- Microsoft Word is installed on the dev machine and may be **observed** black-box: run it, use its UI with synthetic documents, take screenshots **of its window only** (by window id — the machine runs other work; never capture the whole desktop) stored only under `plan/word/screenshots/` (gitignored, never committed).
- Never read, disassemble or copy anything inside the Word bundle (file names/listings only). Never commit files produced by Word. Never copy Microsoft wording beyond feature names.
- File formats come from public specs: ECMA-376 / ISO 29500 (OOXML), the published RTF spec, OASIS ODF 1.3. Never copy GPL/LGPL/AGPL code (LibreOffice, AbiWord, pandoc…).
- **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs.
- **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity-checklist.md`; a test enforces a floor that only rises. Implement missing ids to raise it.
- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin and reads `Session` state; colours come from `theme::Tokens`.
Before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). Commit after every arc of work that builds, with a task id in the message (`M2.1: line spacing dialog`).
## Typing behaves like Word
Typing, Enter, Backspace, Tab, lists, AutoCorrect and AutoFormat follow Word's observed behaviour in [`docs/typing-parity.md`](docs/typing-parity.md), pinned by `crates/engine/src/tests/typing.rs` (keys go through the same commands as the keyboard). Change behaviour, doc and tests together; a typing fix lands with a test there. Compare headlessly (`ui_shot`); don't drive GUIs with keystrokes on the shared machine.
## Running and looking at the app
-`cargo run --release -p wordcraft -- --sample --control 7981` (sample document + control channel).
- Drive it with JSON lines on `127.0.0.1:7981`, each with the window's key from `~/.config/wordcraft/control-key.local-7981` (macOS: `~/Library/Application Support/WordCraft/`), e.g. `{"id":1,"key":"…","method":"engine.execute","params":{"command":"text.insert","params":{"text":"Hello"}}}` then `{"id":2,"key":"…","method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`; docs: `docs/control-protocol.md` (Keys).
- **Headless window screenshots** (no focus stealing, works with a locked screen): `cargo run --release -p wordcraft-ui-egui --example ui_shot -- script.jsonl` (see the example's header).
- **For UI work, look at the result** (screenshot, read the PNG) and compare with Word side by side.
- MCP: `wordcraft-cli mcp` (headless) or `wordcraft-cli mcp --connect 127.0.0.1:7981` (drives the running app). See `docs/mcp.md`.
- Shell gotcha: `mv`/`cp` may be aliased interactive — use `/bin/mv -f` / `/bin/cp -f`.
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done (disk).
## Roadmap
Progress docs follow [`craftrules/standards/progress-docs.md`](https://github.com/storytold/craftrules/blob/main/standards/progress-docs.md): `ROADMAP.md` (stage, headline numbers, progress log), `docs/target-app-parity.md` (the assessment), `docs/gaps.md` (ranked work list), `docs/roadmap.md` (milestones, Current focus), `docs/architecture.md`, and the parity checklists `docs/file-format-parity.md`, `docs/layout-parity.md`, `docs/typing-parity.md`, `docs/ui-parity.md`, `docs/hardware-parity.md`, `docs/localization-parity.md`. When work lands, update the gap, the parity doc and the progress log, and bump each touched doc's status line and revision history.
## CI workflows
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
@@ -124,7 +124,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes
| **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities |
| **Review** | Spelling and grammar with suggestions (English and Polish, by each run's language), thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector |
| **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski or Slovenščina (follows the system language by default) |
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski, Nederlands, Dansk, Română, العربية (mirrored) or Slovenščina (follows the system language by default) |
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
assert!(left<super::super::MAX_GRAPHIC_WORK,"the nested chart's work is the file's");
// With the file's budget spent, the nested document doesn't get one of its own.
assert_eq!(read(0),(false,0));
}
}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.