chore: sync upstream (83 Commits) #21

Merged
alf merged 83 commits from chore/sync-upstream-20261108 into main 2026-11-08 12:21:21 +00:00
Owner

Upstream-Sync von upstream/main (83 Commits). Fast-Forward.

  • 661a7905 Render: skip drawing paths far past every canvas before flattening (#5453)
  • 04217419 DOCX save: collect an embedded object's relationship ids in a set (#5414)
  • 4afff61f fix(geom): measure PNGs from their chunk headers, not the decoder's header read (#5459)
  • e76293a5 fix(doc): refuse export links holding characters the writers drop (#5426)
  • e0d8312f fix(formats): count spreadsheet cell characters as they are added (#5421)
  • 523e3194 fix(formats): parse at most MAX_COLS cells of a LaTeX tabular row (#5415)
  • 735c2a53 layout: keep post-break anchors with text across a wrapped row (#5937)
  • 7592d144 fix(docx): refuse to carry parts named like WordCraft's bookkeeping (#5411)
  • fde39ab0 Desktop: constrain newly opened window dimensions to available screen width (#5871) (#5876)
  • d34e341d Charts: an automatic series colour follows the series' c:idx (#5963) (#5975)
  • 535b675e DOCX: paragraph indents and spacing beyond 16 bits wrap as in Word (#5655)
  • c8618d3e fix(docx): look relationships up by id through an index (#5410)
  • a891d861 Layout: contextual spacing drops the space after a cell's last Normal paragraph (#5450)
  • 85434d2c layout: line numbers take the "Line Number" style, not a fixed 10 pt grey (#5788)
  • 76345539 Layout: superscript and subscript text at 0.65 of its size rounded to the half point (#5802)
Upstream-Sync von `upstream/main` (83 Commits). Fast-Forward. - 661a7905 Render: skip drawing paths far past every canvas before flattening (#5453) - 04217419 DOCX save: collect an embedded object's relationship ids in a set (#5414) - 4afff61f fix(geom): measure PNGs from their chunk headers, not the decoder's header read (#5459) - e76293a5 fix(doc): refuse export links holding characters the writers drop (#5426) - e0d8312f fix(formats): count spreadsheet cell characters as they are added (#5421) - 523e3194 fix(formats): parse at most MAX_COLS cells of a LaTeX tabular row (#5415) - 735c2a53 layout: keep post-break anchors with text across a wrapped row (#5937) - 7592d144 fix(docx): refuse to carry parts named like WordCraft's bookkeeping (#5411) - fde39ab0 Desktop: constrain newly opened window dimensions to available screen width (#5871) (#5876) - d34e341d Charts: an automatic series colour follows the series' c:idx (#5963) (#5975) - 535b675e DOCX: paragraph indents and spacing beyond 16 bits wrap as in Word (#5655) - c8618d3e fix(docx): look relationships up by id through an index (#5410) - a891d861 Layout: contextual spacing drops the space after a cell's last Normal paragraph (#5450) - 85434d2c layout: line numbers take the "Line Number" style, not a fixed 10 pt grey (#5788) - 76345539 Layout: superscript and subscript text at 0.65 of its size rounded to the half point (#5802)
alf added 83 commits 2026-11-08 12:21:19 +00:00
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>
* 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>
* 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 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>
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 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>
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).
* #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>
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
* 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.
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* #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>
* 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>
* #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>
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>
* #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/html/entity.rs:250-251 (unescape)

Co-authored-by: Claude Haiku 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.
* 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&#39;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>
* #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>
* 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>
* #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>
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>
* #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>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
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>
* #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>
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>
* #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>
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>
* 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>
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>
* 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>
* 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)
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(formats): index XLSX worksheet relationships by id

* style: rustfmt

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* 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
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>
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>
* 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>
* 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>
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>
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>
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
Co-authored-by: Bardan <bardan.ghimire.10@gmail.com>
Co-authored-by: Bardan <bardan.ghimire.10@gmail.com>
* 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>
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>
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 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>
* 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>
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>
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>
* doc: preserve following formatting when deleting an empty paragraph (Leaf #152)

* doc: regress empty paragraph merge formatting (Leaf #152)

* engine: cover empty heading deletion, history and DOCX (Leaf #152)

* typing: pin empty heading deletion semantics (Leaf #152)

* docs: record empty paragraph formatting coverage (Leaf #152)

* docs: record the empty paragraph deletion fix (Leaf #152)

* test: retain structured controls across an empty paragraph join

* docs: explain empty paragraph engine regressions

* docs: explain the empty heading keyboard regression

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
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 &#13; stays a CR. Attribute values were already
normalised.

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* docx: normalize TOCHeading gallery content controls with trailing FieldEnd paragraph

* test(docx): assert TOC entries survive round-trip in section-break TOC control test
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.
* #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
## 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>
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>
* 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 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>
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>
* 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>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* layout: keep wrapped-row anchors after page breaks

* test: pin wrapped picture drops, history and DOCX

* docs: record wrapped-row anchor coverage

* docs: log wrapped-row picture anchoring fix

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* 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>
Render: skip drawing paths far past every canvas before flattening (#5453)
Some checks failed
Printing / Print (macos-latest) (push) Has been cancelled
Printing / Print (ubuntu-24.04) (push) Has been cancelled
Printing / Print (windows-latest) (push) Has been cancelled
Printing / Print (macos-latest) (pull_request) Has been cancelled
Printing / Print (ubuntu-24.04) (pull_request) Has been cancelled
Printing / Print (windows-latest) (pull_request) Has been cancelled
ci.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (pull_request) Failing after 0s
nix.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (pull_request) Failing after 0s
packaging-lint.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (pull_request) Failing after 0s
sonarcloud.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (pull_request) Failing after 0s
ci.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (push) Failing after 0s
nix.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (push) Failing after 0s
packaging-lint.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (push) Failing after 0s
sonarcloud.yml / Render: skip drawing paths far past every canvas before flattening (#5453) (push) Failing after 0s
661a790540
* fix(render): skip drawing paths far past every canvas before flattening

* fix(render): refuse paths with non-finite points too

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
alf merged commit 661a790540 into main 2026-11-08 12:21:21 +00:00
alf deleted branch chore/sync-upstream-20261108 2026-11-08 12:21:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
iris/wordcraft!21
No description provided.