chore: sync upstream (533 Commits) #6

Merged
alf merged 533 commits from chore/sync-upstream-20261022 into main 2026-10-22 09:39:16 +00:00
Owner

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

  • 8e986366 Header and footer: a click in the other band edits it there (#3557)
  • 653a2311 IME: the caret follows the composition text (#3556)
  • 169581d7 Merge 178 open PRs (backlog round 1) (#3555)
  • b07e045d Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings
  • 6e811389 Integration: #2722 English text for four labels the Dutch and Danish catalogs lack
  • 54003ebc Integration: #2041 English text in the Polish catalog for the keys Italian gained
  • f2b06167 Integration: #2244 VML on/off values read case-insensitively by the shape fill reader
  • 079a743e Integration: #1832 drop a useless .into() in the end-of-document citation test
  • eb12fc6e Integration: cargo fmt
  • c04d6701 Integration: cargo xtask parity
  • 3f46d9c6 Integration: status lines list each change once
  • 2060f86e Merge PR #3139: Fix #3137: Cannot insert a tab inside table cells (missing Ctrl+Tab shortcut)
  • 64f98d82 Merge PR #2280: fix(formats): support xs:time, string-value, and case-insensitive cell types in spreadsheets
  • ef39b78a Merge PR #2886: Recovery: keep explicit New/Open visible after startup discovery
  • 55c31bdb Integration: #1076 English text for the keys the Slovenian catalog lacks
Upstream-Sync von `upstream/main` (533 Commits). Fast-Forward. - 8e986366 Header and footer: a click in the other band edits it there (#3557) - 653a2311 IME: the caret follows the composition text (#3556) - 169581d7 Merge 178 open PRs (backlog round 1) (#3555) - b07e045d Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings - 6e811389 Integration: #2722 English text for four labels the Dutch and Danish catalogs lack - 54003ebc Integration: #2041 English text in the Polish catalog for the keys Italian gained - f2b06167 Integration: #2244 VML on/off values read case-insensitively by the shape fill reader - 079a743e Integration: #1832 drop a useless .into() in the end-of-document citation test - eb12fc6e Integration: cargo fmt - c04d6701 Integration: cargo xtask parity - 3f46d9c6 Integration: status lines list each change once - 2060f86e Merge PR #3139: Fix #3137: Cannot insert a tab inside table cells (missing Ctrl+Tab shortcut) - 64f98d82 Merge PR #2280: fix(formats): support xs:time, string-value, and case-insensitive cell types in spreadsheets - ef39b78a Merge PR #2886: Recovery: keep explicit New/Open visible after startup discovery - 55c31bdb Integration: #1076 English text for the keys the Slovenian catalog lacks
alf added 533 commits 2026-10-22 09:39:14 +00:00
Pasting a table into its own cell doubled the nesting each time, and about a
dozen pastes built a document that overflowed the stack when it was saved as
JSON, copied, closed or replaced by a new one.

Tables now nest at most 24 deep (wordcraft_doc::table::MAX_TABLE_DEPTH), the
depth the .docx reader and writer already keep. Document::insert_fragment
turns a table that would go deeper into its paragraphs, in document order;
Document::insert_block refuses it, so Insert > Table in a cell 24 tables deep
fails with a message and leaves the document as it was. The depth is measured
without recursion. Word has no limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Centimetres, millimetres and picas took their suffix from Unit::suffix()
and never went through the catalog, so a Russian interface showed
"2,54 cm". The suffix now comes from the catalog like "pt" already did,
and a length field accepts the translated unit when typed ("3 см").
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes #1764

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headers and footers of an .odt live on its master pages in styles.xml
(style:header, -left, -first and the footer equivalents); import
skipped them, so a letterhead or logo at the top of every page was
lost.

They are now read with the body reader into header/footer parts and
named on their master page's section, so each section shows its own:
every page, left pages (turns on different odd and even pages) and
the first page (turns on a different first page). Hidden ones
(style:display="false") are left out. A master page without a header
or footer gets an empty one rather than continuing the previous
section's, as each ODF master page stands alone.
Brings the Swedish catalog up to date with upstream main (0aab569).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alt+Shift+- is Word's Collapse shortcut, which only runs in Outline view, but
dispatch claimed the key and swallowed the typed character everywhere. An Alt
shortcut whose command is disabled now leaves the key to typing.
Found while testing crates/engine/src/cmd/find_pattern.rs:97

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Breaks menu tests look for English labels, but an integration test
links the library without cfg(test), so WordApp followed the machine's
interface language and both tests failed on a Polish Windows. Set the
app's language to English in layout_tab(), as native_print already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share and the » that holds the tabs that don't fit are painted, so a screen
reader had no name for them (» read as a glyph). They now say "Share" and
"More tabs", with "More tabs" translated in every catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paper and envelope sizes in the page-size list (decimal comma), the Go To and bookmark prompts, the print-job notice and the Letter template name the Croatian, Swedish and Vietnamese catalogs added. Italian again has every key the other catalogs have.
A row that skips grid columns before its first cell (w:gridBefore) was laid
out from column 0, so its cells sat one or more columns to the left and
wrapped at the wrong width (a text cell landing on a 5 pt column sets one
letter per line). Row::first_grid() gives the column the row's first cell
starts at; Table::grid_col, cell_at_grid and cols(), the layout passes,
vertical-merge matching, cell selection and the insert-column spot count
from it, and cols() also counts gridAfter.

The skipped columns stay empty and borderless, as in Word: a row's own
first and last cell carry the table's outer side borders, and AutoFit gives
the skipped columns the row's wBefore/wAfter width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fallback put Segoe UI Symbol before Segoe UI Emoji, and Symbol has
monochrome glyphs for some emoji (✅), so they drew in black and white
and tests_color_glyphs failed on Windows. Characters with default emoji
presentation now rank the colour emoji fonts first, both when loading a
system fallback and when choosing among loaded faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app read a file whose name isn't valid Unicode (#1440) with a plain
std::fs::read, and file.open on a path (also wordcraft-cli) checked only
the length before std::fs::read. Neither checked for a regular file: a
named pipe blocked the open (app start-up hung), and /dev/zero read until
memory ran out.

The new io::read_document reads a document path inside the automation
read root, only as a regular file of at most 2 GB, through the existing
read_regular_file. open_path_in and the app's open_non_unicode both use
it, so the two read a path the same way.

Regression tests: file.open on a FIFO and read_document on /dev/zero are
refused without blocking (engine), and the app's open of a FIFO or a
/dev/zero symlink under a non-Unicode name is a quick warning that leaves
the open document alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native JSON wrote Document::media and Document::passthrough with serde's
default for bytes, an array of numbers. With a synthetic 27 MiB picture a
recovery copy (compact) was 96.4 MiB and a .json file (pretty-printed,
one number per line) 285.5 MiB.

Each part is now one standard base64 string (doc::byte_store, using the
workspace base64 crate), encoded straight into the writer. Reading accepts
base64 or the old array of numbers, so recovery copies and .json files
written before still open; anything else is an error, never a panic.

Measured (release, same synthetic picture): compact 96.4 -> 36.0 MiB,
pretty 285.5 -> 36.1 MiB, write 252/614 -> 52/57 ms, read 404/533 ->
46/37 ms. A recovery snapshot of a 6,000-paragraph document with the
picture: 99 -> 39 MB, worker time 2.3 -> 0.26 s, start-up scan 1.03 ->
0.14 s.

Tests: byte_store unit tests (round trip, old arrays, hostile values);
recovery_format.rs native_json_stores_bytes_as_base64,
native_json_with_byte_arrays_still_opens, native_json_with_bad_bytes_is_an_error,
ignored measurement native_json_size_with_a_large_picture; desktop.rs
copies_with_byte_arrays_written_before_still_restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/table_formula.rs:721

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Inside a Flatpak, /usr/share/fonts and /usr/local/share/fonts are the runtime's and the home font
folders aren't exposed, so WordCraft listed only the runtime's fonts. Flatpak mounts the host's
system, local and user fonts at /run/host/fonts, /run/host/local-fonts and /run/host/user-fonts
for every app, and its main branch (after 1.19.2) also mounts ~/.fonts at
/run/host/user-fonts-legacy when the XDG user font folder exists too; scan those as well.
Folders that don't exist are skipped, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 2,463-entry Slovenian catalog: every key of every other catalog in the
repository (built-in style names, equation gallery, paper sizes, citation
styles included). Written from the meaning of the English labels in ordinary
Slovenian computing vocabulary; no other product's localisation was used.
Counts use count-neutral wording ("Besede: 21"), since the catalog has no
dual/plural forms.

- sl, sl-SI, sl_SI.UTF-8 select it automatically; "Slovenščina" in Options
  and ui.language with {"value":"sl"}.
- The interface audit checks Slovenian coverage the way it checks Russian.
- Tests: locales and saved preference, catalog coverage and key terms,
  count wording, and bundled fonts covering č, š, ž.
- docs/localization-sl.md with conventions and terminology; parity table,
  README, ROADMAP and ATTRIBUTION updated.
Rebased on main. sl.tsv now has 2,614 entries, every key of every other
catalog, including the native print settings (Tiskalnik, Odtenki sive,
Obojestransko tiskanje), mail merge filter and sort, Go To, bookmarks,
privacy options, interface size and dictionary folder strings. Three keys
main renamed were replaced.

- Slovenian added alongside Vietnamese, Swedish and Croatian in the
  language list, the audit list and the Polish coverage exclusions.
- New test: sl has no keys outside the other catalogs.
- localization-parity.md, localization-sl.md and ROADMAP.md counts,
  language lists and status lines updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/numbering.rs:319

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Some fonts (the static Noto Naskh Arabic builds) draw لا as a lam and a
zero-advance alef with their own clusters at one x. visual_order kept the
tie in logical order, so the right-to-left run's ranges didn't decrease,
krilla dropped /ReversedChars and السلام extracted as السالم. Ties between
clusters now go in reverse text order when right to left (text order
otherwise).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Wayland, AltGr (Right Alt on Polish, German, Czech… layouts) arrives as
the Right Alt key followed by a modifiers event without Alt, since it turns
on Level 3 Shift (Mod5), not Alt. The KeyTips state machine read that event
as Alt let go with nothing pressed in between and opened KeyTips while AltGr
was still held, so the next letter went to the badges instead of typing ś,
ł, ż.

A modifiers event without Alt no longer ends the press while an Alt key is
still down by key events (its key-up does), and a release opens KeyTips only
if the Alt modifier was on during the press (Left Alt always counts), so a
bare AltGr tap doesn't open them either. Right Alt that is Alt (US layout)
still does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apple Pages writes a sizeless w:framePr (xAlign left, y 0, hRule
exact) into w:pPrDefault. WordCraft read it as the default paragraph
frame, so every paragraph inherited it. The header's title of
traditional_skills_job_form was drawn from the page's left edge over
the first table, the footer's page number left the bottom of the page,
and table rows came out shorter than in Word.

Word ignores that frame: its PDF of the same file shows the title at
the left margin and "1/4" in the footer. Taking just that element out
of styles.xml gives WordCraft the same page. The reader now drops the
frame from the paragraph defaults and keeps the rest of them. Frames
from styles and direct formatting are unchanged, and the default frame
isn't written back on save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening note.txt, typing, saving with Ctrl+S and closing still asked
"Do you want to save changes to note.txt?". Save in that prompt wrote
the file but left the window open, and only Don't Save closed it.

file.save marked the document saved only for formats that keep
everything (.docx, .odt, .rtf...). A save in any other format counted
as a copy, even over the file the document was opened from, so a .txt
or .md document could never become clean. The Save Changes prompt then
treated its own Save as "not saved" and cancelled the close.

A save over the document's own file now marks it saved in any format.
Save As to plain text, Markdown or a page image elsewhere is still a
copy that leaves the document unsaved, as before. AutoSave still
covers only the formats that keep everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word for the web writes each table of contents entry as a HYPERLINK
complex field around the heading's text, a tab and a PAGEREF field.
The DOCX reader buffered the whole field into one field object with a
flat text result, so the tab was drawn as a box instead of reaching the
dotted right tab stop, the page number was no longer a field of its own,
and the link could not be followed. Rewriting those fields as
w:hyperlink elements makes WordCraft draw the TOC as Word does.

A HYPERLINK field, complex or simple, now reads the way a w:hyperlink
does: its result stays ordinary content carrying the link (url,
url#bookmark or #bookmark from \l), with nested fields kept. It is
written back as w:hyperlink; the tooltip (\o) and target frame (\t) are
dropped. A HYPERLINK field with no target stays a field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging an In Line with Text picture, shape or text box now moves it within
the text, as in Word: a dashed insertion cursor follows the pointer through
the text, and the drop puts the object there, still inline and selected, in
one undo step (Ctrl+drag drops a copy there). Before, the drop floated it with
Square wrapping where it was let go, and nothing showed where it would land.

arrange.bounds and arrange.copyTo take {"to": Pos} for inline objects; x/y
keep their old meaning for scripts. Floating objects drag as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking the page offered only Cut, Copy and Paste, even with
formatted text on the clipboard. Word's context menu lets you choose how
to paste there.

The context menu now has the same items as Home › Paste's menu, in the
same order and with the same shortcuts: Paste (the clipboard's
formatting kept, Word's Keep Source Formatting), Keep Text Only, Merge
Formatting and Paste Special…. They run the same commands (edit.paste,
edit.pasteText, edit.pasteMerge, edit.pasteSpecial), and
system_clipboard::fill already reads the system clipboard's text, HTML
and RTF for all of them. Every label was already translated in the 22
interface catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1613 draws a missing font in Cambria when the font table classes it as
roman, as Word does. Times New Roman is roman in every font table, so
where it isn't installed (Linux, the web build) every Times document came
out in Caladea: lines 2.1 pt taller at 12 pt and different line breaks.
Word always has Times New Roman, so it never applies that rule to it.

The rule now leaves alone a font whose substitutes are chosen by its name
(a metric-compatible clone or the same design under another name: Times,
Arial, Courier New, Palatino, the symbol, Hebrew and Persian fonts).
Fonts with no clone of their own (Cambria, Georgia, brand fonts) still
follow it.

Checked on 251 real documents against PDFs from Word: exact page counts
201 -> 229, page breaks in the same place 45.4% -> 51.9%.
Word sizes a line by the fonts in it, and a bullet in Symbol (Word's
default, U+F0B7) has a taller ascent than Calibri or Times New Roman.
Without Symbol (Linux, macOS without Office, the web build) WordCraft
draws the bullet from another face and used that face's height, so every
bulleted line was shorter than Word's: 14.5 pt against 15.1 pt in
Calibri 11 pt at 1.08 spacing, 14.9 against 15.7 in Times New Roman 12 pt.

symbol_font_metrics gives Symbol's and Wingdings' ascent and descent as
Word's PDFs state them, and a run in one of them that isn't installed
takes them. The label's ascent now comes from its style, so the existing
rule (the label's extra ascent added unscaled) sees them.

Measured in Word 2016: Calibri 11 / Times 12 bullets 15.08 / 15.72 pt,
WordCraft now 15.07 / 15.75 pt. On 251 real documents against Word PDFs,
page breaks at the same word 45.4% -> 50.1%.
A failure caused by the snapshot itself (size limit, folder or disk budget,
a value the format cannot keep) now keeps the recorded revision, so tick
does not rewrite up to 256 MiB every interval while nothing changes.
Transient failures (locks, I/O) still retry at the next interval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A row of an m:eqArr with no `&` was right-aligned at the first column edge and counted into the
first column width, shifting the wider rows. Per Word, such a row is centred across the whole
array and takes no part in the column widths. Regression test eqarr_rows_without_an_align_marker_are_centred
fails without this. Closes #2617.
Each underline style in the drop-down is now a sample of its own line,
drawn in code in the theme's text colour (Single, Double, Thick, Dotted,
Dashed, Dot-dash, the missing Dot-dot-dash, Wave), named for tooltips and
assistive technology. Words only uses format.wordUnderline (its own
Ctrl+Shift+W); None removes the underline and shows no shortcut; More
Underlines… opens the Font dialog; Underline Color › offers Automatic and
the theme/standard colours with More Colors….

- engine: format.underlineColor {"color": "RRGGBB" | "auto"} sets or
  clears CharProps::underline_color and leaves the style alone;
  format.state reports underlineColor; catalog row Home|Font|Underline
  Color (parity checklist regenerated: 490/505).
- i18n: Dot-dot-dash, More Underlines… and Underline Color in all 22
  catalogs (Underline Color reuses each catalog's "Underline color").
- Tests: underline_color_sets_and_clears_the_underlines_own_colour (engine,
  DOCX round trip, undo, junk params), underline_menu_entries_apply_their_style
  (UI: every sample is a named entry that applies its style; Words only,
  None, Underline Color › Automatic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two new interface languages, each a complete catalog of 2,585 entries:
every key the parity-class catalogs hold (the Polish-complete set — the
keys Italian, Hungarian, Bulgarian, Vietnamese, Swedish and Croatian
still carry beyond it are deprecated interface strings, so they are not
carried here). Written from the meaning of the English labels with each
language's standard software vocabulary: Dutch informal-neutral je,
Danish informal du with accented imperatives.

Registered in the language list (23 -> 25) with locale matching
(nl-NL/nl-BE, da-DK). The preferred-language test now uses the
private-use tag where it used nl as its unsupported example, and checks
that nl wins over da now that both are languages.

Supersedes #1070, whose Korean and Swedish were landed by other pull
requests; its Dutch and Danish catalogs are the ones carried here,
updated to today's key set.
# Conflicts:
#	ROADMAP.md
#	docs/ui-parity.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs in German (de, de-DE, de-AT, de-CH) hyphenated with the English
patterns, giving breaks such as "unterlieg-ende" and "rechtli-cher".
They now use hyph-utf8's German patterns for the reformed spelling
(MIT, bundled unmodified as assets/hyphenation/de.pat). German gets a
ProofLang of its own that, like Bulgarian, hyphenates only: no spelling
or grammar marks yet (#1564). Portuguese, Italian and German share one
pattern lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups: the German patterns' MIT notice goes into NOTICE with
the other pattern sets; the pattern reader skips other commands'
arguments, so hyph-de-1996.tex's \message{} line no longer adds junk
patterns and exception words; a load test for the German set; the
status bar names a bare "de" tag; the docs list the languages with
their own hyphenation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also: the underline-colour test picks a Standard Colors swatch through the
harness instead of running format.underlineColor directly (review nitpick).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/typing-parity.md
#	docs/ui-parity.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/typing-parity.md
#	docs/ui-parity.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1026

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/ui-parity.md
# Conflicts:
#	docs/ui-parity.md
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
comment_list scanned the body only, so a comment anchored anywhere else was
listed with no anchor: no balloon beside its text, Go to did nothing, and
Next/Previous Comment never reached it. It now scans every story except
comment text, body first and then each part in Pos order, which is the
order nav_comment relies on. Reuses tools::all_stories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the first commit found, and ui_shot runs confirmed, three bugs in it:
- Next Comment moved the caret into a removed header or a deleted
  footnote's text: a story nothing shows, so typing went nowhere visible.
- Next visited headers and footers by part id (creation order), so a
  footer made first came before the header.
- Comments in footnotes were claimed but untested.

Document::shown_stories lists the stories a reader sees, in reading order:
the body with the notes and text boxes it shows, then the headers and
footers the sections use with theirs. It reuses the reachable walk that
counted_stories uses. comment_list scans those stories (and returns at
once when the document has no comments: the balloons ask every frame), and
nav_comment orders by a story's place in that list instead of by Pos.
tools::all_stories is private again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/typing-parity.md
#	docs/ui-parity.md
Insert Table of Figures now opens a Table of Figures dialog (caption label,
include label and number, page numbers, right margin, tab leader) instead of
always building a list of Figures. The label list offers the built-in labels,
the user's own and those the document's captions use, so a document can have
a list of figures beside a list of tables.

references.tableOfFigures takes includeLabel, pageNumbers, rightAlign and
tabLeader and writes the TOC switches for them (ECMA-376 17.16.5.68: \c or \a,
\n, \p). Captions are matched on their whole label (SEQ identifier), a Word
table of figures with its switches in any order (TOC \h \z \c "Figure") is
recognised, and Update Table keeps each list's leader. The table of contents
code treats any \c or \a TOC as a table of figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
- The leader asked for is carried through the rebuild by a placeholder
  entry, so it lands on the new list even when a stale list above it
  gains entries and moves it down.
- \c picks captions by their SEQ identifier, as Word does; the label text
  is used only for captions typed by hand.
- The dialog test drives the dialog through egui_kittest: page numbers
  off disables the right margin choice, OK closes it and the entries get
  the chosen leader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP
Every page, area and icon render built its own multi-threaded vello_cpu
RenderContext, which starts and tears down a rayon thread pool: a few
threads per keystroke while typing. Renders now take one kept context,
reset to their size, and give it back; a render that overlaps another
builds its own, and one that panics drops its context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test scene set even-odd itself before its only path that depends on
it and used solid paints only, so a fill rule or paint transform left
over from the last render could not change its pixels. It now fills
nested squares and a gradient on the defaults first, and checks that a
stale rule or paint transform does change the scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the body is edited, headers and footers are drawn dimmed. Each draw
item got the dimming alpha on its own, so wherever two of them overlap the
alpha was applied twice: a header table's borders came out darker where
they meet and along shared cell edges, nearly black at crossings. The
PDF export and the undimmed view were fine.

page_layers gives a page's draws as two layers (headers/footers, body) at
full strength with the opacity to draw each at. The canvas draws a dimmed
layer inside one opacity layer, as Word dims it, and background_at blends
it the same way. page_display is unchanged for the PDF export and tests.
An integration test isn't built with cfg(test), so WordApp follows the system language and the
ribbon's "Breaks" button wasn't found on a French Mac. Set the app's language to English like
native_print.rs does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The top and bottom borders of a paragraph now take their distance plus width
in the text flow, as in Word, and the shading fills that room. Neighbouring
paragraphs with the same borders and indents make one box: the top border goes
above the first only, the bottom one below the last, and the shading and side
borders run on through the space between them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep lines together and keep with next count the paragraph's top and bottom
  border room, and the keep-with-next lookahead the next paragraph's top border
  room (unless the two join in one box): a kept paragraph that fits only
  without its borders moves on whole instead of splitting.
- A mark that stays with its closing page break leaves its bottom border's
  room on that page, as its space after: the next page no longer starts lower.
- Outline view makes room for paragraph borders above and below the lines.

Tests: a_kept_together_paragraph_counts_its_border_room,
a_bottom_border_stays_with_a_closing_page_break,
outline_view_makes_room_for_paragraph_borders (each fails before the change).
# Conflicts:
#	ROADMAP.md
#	docs/ui-parity.md
Word stacks floating objects by wp:anchor/@relativeHeight, not by the
order of their anchors in the paragraph. The reader ignored the
attribute, so a panel anchored after the text boxes it frames, but
lower in the stack, was drawn over them and hid their text.

- wordcraft-doc: Float gets a `z` (0 = none, drawn in document order).
- wordcraft-docx: it is read from relativeHeight and written back as
  read; an object without one gets the next order above, as before.
- wordcraft-layout: place_objects collects each object's items with
  its order and emits them lowest first (stable, so objects without an
  order keep the paragraph's).

Four round-trip tests compare whole floats after a save; saving now
gives objects without an order one, so those tests compare without it
(`unstacked`). The order itself is covered by its own round-trip test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As reported on #2034 with a minimal document: Word keeps the object with
the higher relativeHeight on top even when it comes first in the XML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Word table of figures whose saved list sits in the field's result, in
the field's own paragraph, was listed twice after Update Table: the
cached result stayed and the rebuilt entries followed it. The rebuild now
clears that result and keeps the paragraph's tab leader for the entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hand-typed caption is matched to its label in any case, but `\a` (no
label and number) stripped the label case-sensitively, so "figure 4: Lower"
was listed whole. Found by a local CodeRabbit review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Nnsfu3QGqjD1aFZbBgUWP
The side bar used the light theme's bright blue in both themes. It now
reads a token, `nav`: the same blue in light mode and a muted indigo in
dark mode. White text on it stays above 4.5:1 (tested).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A picture marked decorative (Alt Text > Mark as decorative, `adec:decorative`
in DOCX) was still exported as content:

- tagged PDF: a Figure with its alt text, or with "picture" when it had none,
  inline and floating alike. PDF/UA expects decorative content to be an
  artifact, so screen readers skip it. Draw::Image now carries the mark, and
  the exporter tags a decorative picture (inline, rotated or floating) as an
  artifact instead of a Figure.
- HTML and Markdown (and ODT, which reads the same model): the description,
  which the document keeps when a picture is marked decorative, was written
  as its alt text. A decorative picture now exports with an empty alt text
  (alt="" in HTML), the WCAG way to say "skip this".

Pictures not marked decorative are unchanged. Charts and diagrams are left as
figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shape with a description (Alt Text) was exported as an artifact like any
other shape, so screen readers skipped it and the description was lost: an
arrow or an icon drawn with shapes that means something was invisible to them.

A shape that has a description, is not marked decorative and shows no text is
now a Figure with its description, inline (rotated too) and floating. Shapes
without a description and decorative shapes stay artifacts. A text box keeps
its frame as an artifact and its text read as paragraphs: its description would
replace the text, so it isn't used. The rule lives in `shape_figure_alt`.

Draw::Figure's `kind` becomes an Option (None for a shape), and Placed::Shape
carries the description for floating shapes.

Not covered: a group's own description (its members are placed one by one),
and shapes inside a Drawing Canvas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rotated inline group is drawn inside one rotation. Its tag was chosen for
the whole group: an artifact as soon as one member was decorative, so a
picture next to it lost its Figure and alt text; otherwise one Figure with
the first picture's alt text. The group is still turned as a whole, and each
picture in it is now tagged on its own: decorative ones as artifacts, the
others as Figures with their own alt text.

Found by the CodeRabbit review of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test checked that the description is there, so a Figure written twice
would still pass. It now also counts the alt text entries.

Suggested by the CodeRabbit review of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
File › Print put "Print" between the printer settings and the page
settings, styled like every other button there, so the page's one action
was hard to find and didn't read as an immediate action.

The button now comes first in the settings column, above the printer and
the settings it prints with, as Word has it, and wears the accent the app
gives a dialog's primary button (the file browser's Open and Save, OK and
Save in dialogs): the first thing seen, and plainly the action. It still
prints through `ui.print`, and it is still disabled while a native print
job is busy.
`file.rename` appended the document's extension unconditionally, so
`{"name": "renamed.docx"}` produced `renamed.docx.docx`. The Rename field in
the ribbon edits the stem, which is why it was written that way, but a script
or an agent passes a whole file name.

A name that already ends in the document's extension is now taken as the whole
file name, whatever its case. Anything else still gets the extension: a name
that merely holds a dot (`Report.2026` -> `Report.2026.docm`), and the
extension on its own (`.docm` -> `.docm.docm`), which is a stem rather than a
file name.

Fix: #2983
The extension check counted bytes back from the end of the name and
sliced there, which panics when that offset falls inside a character:

    (file.rename {"name": "abecde"} with a combining accent, .docm)
    byte index 3 is not a char boundary; it is inside 'e' (bytes 2..4)

Splitting at the name's own last dot asks the same question without an
arithmetic offset, and reads the same for every name. Covered for a name
with no extension and one with the document's, both multi-byte.
# Conflicts:
#	crates/fonts/src/word.rs
#	crates/layout/src/para.rs
#	docs/file-format-parity.md
# Conflicts:
#	crates/layout/src/tests.rs
#	docs/layout-parity.md
A floating object's frame was clamped to at least 1 pt on each side.
Word keeps the size: a 0.25 pt high rectangle (a cut mark, a rule drawn
as a filled shape) is 0.25 pt high, and a `line` shape 0 pt high is a
horizontal line. WordCraft drew the rectangle 1 pt high from its top
edge (its middle 0.375 pt too low) and the flat line from its start
down to 1 pt below its end, a slanted stroke.

- layout: `float_rect` keeps sizes from 0 pt (not finite: 0); the
  PDF and raster shape outlines already draw a flat line and skip an
  empty rectangle.

Checked against Word for Microsoft 365 (page-anchored, no outline
unless said; ink top..bottom in pt, 2400 dpi):

  0.25 pt rectangle   Word 99.99..100.26  before 99.99..101.01  after 99.99..100.26
  0.5 pt rectangle    Word 120.00..120.51 before 120.00..120.99 after 120.00..120.51
  0.75 pt rectangle   Word 140.01..140.76 before 140.01..141.00 after 140.01..140.76
  1 and 2 pt          unchanged, as Word
  flat line, 0.25 pt  Word 219.87..220.14 before 219.87..221.13 after 219.87..220.11
  flat line, 1 pt     Word 239.49..240.51 before 239.49..241.50 after 239.49..240.51
  upright line, 0 wide, 0.25 pt: Word 299.88..300.12 across, before
  299.88..301.14, after 299.88..300.12

Test: rectangles of 0.25, 0.5, 1 and 2 pt, a flat line and an upright
line keep their sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Character scale (`w:w`, Format > Font > Advanced > Scale) widens or
narrows the letters themselves. WordCraft multiplied the advances but
drew each glyph at its natural width, so text at 200 % looked letter-
spaced (pdftotext even split it into words) and text at 50 % overlapped.

- layout: `Draw::Glyphs` carries `x_scale` (1 = 100 %), the run's scale
  for its text and its line-end hyphen (whose advance is scaled too);
  list labels, tab leaders, ruby, equations and shape text keep 1.
- pdf: a scaled run is drawn under a horizontal scale from its first pen
  position, the positions brought back so each glyph lands where the
  layout put it; the text and its extraction are unchanged.
- render: each glyph's outline is stretched across from its pen
  position.

Checked against Word for Microsoft 365, "HIHIH" in Arial 20 pt
(Liberation Sans here), right end of the word (pdftotext) and ink:

  200 %  Word 179.78, ink 73.92..176.52; before: split in three words,
         ink to 163.68; after 179.74, ink 74.04..176.52
  150 %  Word 152.19; after 152.52
   50 %  Word 97.76; after 98.07
  100 %  unchanged

Test: in the PDF, letters at 200 % are drawn twice as wide and as high,
at pen positions twice as far apart; at 100 % unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TOC entry whose text just fits showed its page number at the left of a line of its own,
without the leader. Two cases ended the line after the tab and started the next one with its
text at the indent:
- the text after a right, centre or decimal tab didn't fit;
- the text ended past the last stop, and the next default stop was past the right indent
  (the tab was left at the end of the line with no width).

Like Word and LibreOffice, the line now breaks before such a tab: the tab starts the next line
and goes to the first stop there, so a TOC page number ends at its stop after a leader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A left tab whose stop is past the right indent wraps to start the next line.
When that line can't reach the stop either, the tab went past the line's end,
its text didn't fit after it, and the tab was left on a line of its own. As
before the wrap, it now takes no room there and its text starts at the indent.
Test: left_tab_past_the_right_indent_does_not_add_a_line.
A left tab past the right indent, or a right, centre or decimal tab whose text
doesn't fit, wrapped whenever any cluster was before it on the line, so a
bookmark before the tab was left on a line of its own. It now wraps only when
something that takes room is before it.
Test: a_bookmark_before_a_left_tab_past_the_right_indent_is_not_a_line.
Paragraph, cell and table borders were dashed, but not with Word's
patterns: dots were one width on and two off (Word: one and one),
dashes four on and two off (Word: three and one), and dash-dot was
drawn as plain dashes.

- layout: borders use their own strokes (`Stroke::BorderDotted`,
  `BorderDashed`, `BorderDotDash`); `Stroke::dashes` gives every
  pattern in line widths. Underlines and the other dotted lines keep
  theirs.
- pdf, render: lines are stroked with `Stroke::dashes`.

Checked against Word for Microsoft 365 (ink along each border, on
synthetic documents): borders 0.5, 1 and 2.25 pt wide, dotted / dashed
/ dotDash, are 1-1, 3-1 and 3-1-1-1 widths in Word; before 1-2, 4-2 and
4-2; after as Word (2.25 pt dashes: 6.72 on / 2.28 off in both).

Test: in the PDF, a dotted 1 pt border is dashed 1 / 1, a dashed 2 pt
border 6 / 2, a dash-dot 1 pt border 3 / 1 / 1 / 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fills every entry the Polish reference catalog has that Dutch was missing
(main moved on since the PR was written). Wording fixes, chosen by a native
Dutch speaker while using the interface:
- Review tab: Controleren; the Proofing group/options page: Taalcontrole
  (its button already reads "Spelling en grammatica")
- AutoSave: Automatisch opslaan
- Quick Access Toolbar: Sneltoegangslijst everywhere (was three different names)
- Hide Ink (two-line label): Inkt verbergen (word order)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word's text watermark is a VML WordArt shape (`PowerPlusWaterMarkObject`)
whose `v:textpath fitshape` stretches the text over the shape's box (its
`style` width and height), centred on the margins and, when diagonal,
turned by `rotation:315`. WordCraft ignored the box: it drew the text
70 % of the page diagonal wide (half of it on screen), centred on the
page and along the page diagonal (55° on A4, not 45°). A "BROUILLON"
watermark in a 515 x 104 pt box came out about 1.4 times as large as in
Word.

- doc: `Watermark::size`, the shape's box (`None`: Word's default, half
  the page diagonal across, as high as the text's length makes it);
  `Watermark::frame` gives the box for a page, shared by the writer and
  the renderers.
- docx: the box is read from the shape's style and written back; a box
  equal to the default for the document's page reads as `None`, so a
  watermark made by Design > Watermark keeps following the page.
- pdf, render: the text's advance spans the box's width, its em the
  box's height, the em's foot (the font's win descent below the
  baseline) on the box's bottom; the box centred on the margins,
  turned 45° counterclockwise when diagonal.

Checked against Word for Microsoft 365 (ink bounds, pt, left top right
bottom; fonts here: Liberation Sans for Arial and Calibri, Liberation
Serif for Times New Roman):

  BROUILLON, Calibri, 515.4 x 104.1, 315°:
    Word 93.6 222.7 489.1 619.2; before 60.5 110.4 535.7 732.5;
    after 90.7 214.1 495.8 618.7
  SPECIMEN, Arial, 400 x 100, 315°:
    Word 136.3 261.6 455.0 580.3; before 66.7 108.5 539.0 727.7;
    after 137.8 255.4 454.6 573.1
  HXH, Arial, 300 x 100, flat: Word 158.9 378.2 436.3 449.8;
    before 105.1 353.8 490.1 489.6; after 158.9 380.2 435.8 449.3
  HXH, Times New Roman, 300 x 100, flat: Word 149.8 383.0 445.0 449.3;
    after 151.2 383.5 443.5 449.3
  COPIE, Calibri, 300 x 80, flat: Word 153.1 387.8 441.1 440.2;
    before 96.0 373.9 500.2 469.0; after 152.2 387.4 443.5 444.5

Tests: a watermark with a box of its own is written and read back with
it, Word's default box reads back as none; in the PDF, the text of a
300 x 80 pt watermark is 80 pt high, stretched across, starting at the
box's left edge on the margins' centre line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare `run` built its document with Document::new() (Letter) and never went
through file.new, so it skipped the system region's paper. It now runs
file.new, as `run --template` already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
To find the offset from its anchor paragraph, a dropped object was
first laid out at the paragraph's top. Near the bottom of a page that
pushed the paragraph onto the next page, so the object ended up about
a page below the drop point, cut off at the page edge.

Now it is first placed where it was dropped, and the paragraph's
position is read from that layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Dutch reviewer's merge brought three new tab/command labels the
Danish catalog had not covered yet.
After Tab moves the keyboard to the dialog's next control, each box takes
it back with one input-method reset, then none, and the next key reaches it
(review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit review on PR #3115: an xlink:href like "./Object 1/" (some
ODF producers write the object folder with a trailing slash) doubled
the slash before content.xml, so the zip lookup missed and the fix
silently fell back to the undecodable replacement image again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A caret in an empty story also "selects the whole story", the condition that keeps
the view still after Select All and drawing; so after Go to Footer into a footer just
made, the view stayed at the header. A collapsed caret is not a selection of the
story: the view follows it again.
Test: go_to_footer_into_an_empty_footer_scrolls_to_it (fails on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk
lang_script mapped every `pa` tag to Gurmukhi. Measured in Word for Mac:
with w:themeFontLang w:bidi="pa-PK" a majorBidi run takes the theme's
Arab font, with "pa-IN" its Guru font. The test matrix gains both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word records formatting changes to table, row, cell and section properties while tracking; the model, the OOXML reading, the writing and Accept/Reject all existed — only the recorder compared characters and paragraphs. `Recorder.blocks` in `crates/engine/src/cmd/fmt_revisions.rs` now compares those properties too (structure stays out: a table's grid, a cell's merge span, a section's headers and footers), so Reject All puts them back and Accept All keeps them.

- regression test `tracked_table_changes_are_kept_as_formatting_changes` (fails without the recorder)
- docs/gaps.md gap 5 narrowed
Word's Text Pane demotes the focused item on Tab and promotes it on
Shift+Tab; our pane had only the Demote/Promote buttons, so Tab moved
the caret to the next row instead. WordApp::logic consumes Tab while a
Text Pane row is focused and runs smartArt.demote / smartArt.promote on
that item, keeping the row focused so the next Tab acts on it again. An
item that cannot go down a level is refused by the command, as the
button path already is.

A new flag records that a pane row has focus, so Tab from the canvas is
untouched. Regression test drives the harness: Tab on an item that
cannot demote changes nothing and keeps focus; Tab on one after a deeper
item demotes it and its sub-items, the same row takes the next Tab,
Shift+Tab promotes them back.
A row/cell's height was computed from the floating *tables* that grow it
(float_tables -> floats_bottom in crates/layout/src/lib.rs), but a
floating (anchored, non-inline wrap) picture or shape in a cell's
paragraph wasn't tracked the same way. The row sized to the cell's text
alone, so a picture taller than the text hung past the row's own bottom
edge and overlapped whatever came after the table — reproduced with the
reporter's own document and confirmed against their real Microsoft
Office 365 screenshot, which lays the same file out with the table
growing to fully contain the picture.

- crates/layout/src/lib.rs: layout_box now tracks the deepest point any
  floating (Wrap != Inline) picture or shape reaches in a box, the same
  way it already tracks float_tables, and folds it into floats_bottom.
- crates/layout/src/tests.rs:
  a_floating_picture_in_a_cell_grows_the_row_to_hold_it, mirroring the
  existing a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it.

cargo xtask ci: fmt, clippy, assets, layers and wasm all pass. Full
workspace tests pass except the same 3 pre-existing, environment-only
Thai-font failures already noted on #3119 and #3184 (no
CRAFT_FONTS_DIR/TH Sarabun New in this build environment).

Closes #3108.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rect.bottom() measures the unrotated frame; a rotated picture's actual
drawn bottom can extend further down (or less far), so use
Spin::bounds(rect).bottom() like wrap_area does for text wrapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The View tab's Full Screen button is a mode toggle, but it was drawn with
the one-shot big() helper: the fullscreen state was passed as the menu
flag, so while full screen was on the button swallowed its own clicks and
only Escape could leave. Draw it with big_toggle (checked state as
intended) and label it for assistive technology, pinned by a kittest
regression test that clicks the button in and out.
CodeRabbit review: WidgetInfo::labeled passed the fullscreen toggle state
as the enabled flag, so the button announced itself disabled while full
screen was off. A toggle reports its state through WidgetInfo::selected
(enabled, selected), like the other toggle-style button in the ribbon.
poll_printing turned a terminal print message into a status-bar line
but never closed the Print backstage page, so it stayed open after a
successful submission (reported on macOS, where printing never shows
a separate OS dialog — WordCraft's own Print page is the whole flow)
until the user clicked Back, even though the job had already gone to
the spooler. Word returns to the document once the job is away, the
same as after Save.

A failed submission still leaves the Print page open, so the error
and the settings to fix it stay visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only checked that the Print page stayed open on a failed submission,
which could pass even if some other backstage page ended up showing
or the failure feedback itself wasn't retained. Also assert the Print
page specifically is still the one up, and that Feedback::Failed with
the actual error is still there to see.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other place that closes the backstage sets canvas.want_focus so
the document actually takes keyboard input back (open_recent and
others already do); this one didn't, so typing was ignored until the
user clicked the document even though the Print page was gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A save makes the document clean, which queues discarding this
session's now-redundant recovery copy — but that's only queued, on a
background worker thread, the same as a print job. Closing the app
right after Save (the very next frame, with no regular recovery_tick
in between to notice and queue the discard, or with the discard
queued but not yet finished by the worker) could race the process
exiting before the copy was actually removed, so the next launch
wrongly offered to recover a document that was in fact saved cleanly.

Added Recovery::finish and Session::finish_recovery, mirroring the
print spooler's own finish_pending_jobs, and call it from the same
places: on_exit and the macOS hard-quit path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "editing a non-body story" status line only checked "not the
body, not a text box" and always said "Editing header/footer" —
including while editing a footnote or endnote, which isn't one.
Found while checking #3057 against current main (already fixed
elsewhere): a headless screenshot of editing a footnote showed this
label, which turned out to be an unrelated, still-live bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real issues CodeRabbit caught in the first commit, both in
Recovery::finish:

- It didn't repeat tick's own guard against a stale self.id: if the
  session switched documents with no regular tick in between to
  notice and reset that identity, finish would discard the *previous*
  document's still-live recovery copy under the *new*, clean
  document's state. Added the same self.document == document_id()
  check tick already uses, and a test reproducing it (confirmed it
  fails without the guard).

- An in-flight job failing (e.g. a stray autosave) returned
  immediately via `?`, skipping the clean-document discard below it
  entirely — so a save that really did land cleanly right before exit
  could still leave a stale copy, for an unrelated reason. Keeps that
  error to return at the end instead, after still attempting the
  discard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The ribbon now shrinks its groups to fit the window (#662), and on the
View tab the Window group folds into a drop-down at 1440 pt: the Full
Screen button then lives in that menu, so the a11y query no longer finds
it and the test failed on the folded ribbon. Open the group when it is
folded, and click the button where it is.
`home_page` already passes "Good morning", "Good afternoon" and "Good evening"
through `tl!`, but nine catalogs had no entry, so File › Home opened in English
in German, Estonian, Japanese, Brazilian Portuguese, Serbian (Cyrillic and
Latin), Ukrainian and Chinese (Simplified and Traditional). Adds the three
greetings to each, and a test that every language has them.

Co-authored-by: CRBBeesy <CRBBeesy@users.noreply.github.com>
Insert › Header & Footer › Page Number ▸ Format Page Numbers… ran a
plain `layout.pageNumberFormat` call that changed nothing and cleared
the section's Start at. The item now opens Word's dialog: the number
format (1, 2, 3 / I, II, III / i, ii, iii / A, B, C / a, b, c) and
Page numbering — Continue from previous section or Start at: n.

`layout.pageNumberFormat` keeps the section's start when `start` is
left out and numbers on from the section before for `"start": null`;
the dialog reads the section at the caret and writes through the same
command, so scripts and MCP get the same result without it. The
dialog's strings are in every interface catalogue and it joins the
untranslated-text audit. ROADMAP, docs/ui-parity.md and docs/gaps.md
get their rows.

Tests: engine (format and start, keep and continue); ui-egui (the
dialog reads the section, OK sets the format and start, continue
clears it, and a plain command call keeps the start); the i18n audit
draws the dialog's text.
Review fixes: a section starting at 40,000 was lowered to 32,767 when the
dialog was opened and accepted (the engine takes starts to 100,000), and the
non-modal dialog kept writing the numbers read from the section it opened on
into whichever section the caret moved to (the form now follows the caret, as
the Size dialog follows its object). Tests cover both.
The fallback Symbol metrics applied whenever the raw family name wasn't
installed, so Symbol (PCL6) resolved to an installed Symbol still took
them. Use the resolution's substituted state instead.
The button told assistive technology it was enabled whatever the command
could do, so the browser build -- which keeps full screen to itself and
says why -- announced a control that cannot be activated. It now reports
the command's own state, as every other ribbon control does.

Pinned by a test that makes the command unavailable the way the browser
does and checks the button is announced as disabled.
`\ansicpg932`/`936`/`949`/`950`, a font's `\fcharset128`/`129`/`134`/`136`,
`\cpg932` and the OEM charset 255 were read one byte at a time, so Japanese,
Korean and Chinese text came out as mojibake. The reader now holds the 8-bit
text of a multibyte code page and decodes the run with encoding_rs (SHIFT_JIS,
GBK, EUC_KR/UHC, BIG5): a lead byte and its trail byte decode together, escaped
(`\'hh`) or raw. A code page switch (`\f`, `\fcharset`, `\cpg`, `\ansicpg`), a
group end and the end of the document flush what is pending; invalid sequences
read as U+FFFD, as Word writes them, and never lose the rest of the text. The
pending buffer is capped at 4096 bytes.

Single-byte code pages, `\uN?` escapes and their `\ucN` skips, the font table
and every existing test are unchanged. Tests cover Shift-JIS (escaped, raw and
through a `\fcharset128` font), CP949/CP936/CP950, a mixed Japanese/ASCII/
Cyrillic document whose code-page switch must flush, `\uN?` alongside
multibyte text, an invalid sequence and a Windows-1252 regression.
`InlineObject::Ruby` is written as `<ruby>base<rt>reading</rt></ruby>`, in the
base text's run formatting, and `<ruby>`/`<rb>`/`<rt>` are read back into one
ruby object, so docx/doc → HTML → doc keeps the annotation. `<rp>` fallback
parentheses and the tags are not text; unclosed or half-marked ruby (no `<rt>`,
empty `<rt>`, no base) is tolerated and keeps the text it held, with a block
boundary closing the element as a browser does. HTML states no alignment, size or
raise, so import uses the Phonetic Guide's defaults (centred, half the base size,
raised clear of the base text; the run's size, or 10.5 pt when none is stated).

The flow model gains `Inline::Ruby`; formats without ruby (RTF, ODT, Markdown,
LaTeX) write its base text as before. Tests: `html::tests::ruby_*` and
`tests::html_ruby_round_trip`. Docs: file-format-parity (HTML row) and
localization-parity (Japanese row).
The sweep pinned that a page never ends with header rows alone, but its
last check only asked whether *some* filler count put the header and a
body row together: a table that carried a body row over although the
page had room for it passed.

The check now measures the room left under the header on the first page
and requires the body row to be placed there whenever it fits, which is
the position the sweep could not see. The table is built once and read
two ways (rows per page, and the cells' frames).
A first-page or even-page header or footer is shown only while Different First Page or
Different Odd & Even is on, and a text box linked from another shows the first box's text
instead of its own. Comments in those stories were still listed and reached by Next and
Previous, and typing then went into text nobody sees. shown_stories now picks each
section's versions the way layout does and leaves out linked boxes' own text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A table dragged by its move handle floats and the empty paragraph after it
moves up beside it; a click below the table then landed in a cell of its last
row (the nearest line). Pages now list their floating body tables, and
DocLayout::hit leaves out a floating table's cell lines when the point is
outside the area its cells cover, as long as other lines are left. Checked
against Word 2019 for Windows: clicks below the table and Ctrl+End go to the
paragraph beside or after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Filling in forms enforced, Word greys out the Developer tab's
content-control buttons and Properties. WordCraft refused them when
clicked (#3112) but drew them live. The ribbon now asks the same check:
Session::why_disabled combines a command's own `enabled` with what
protection refuses outright, and the tooltip says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux the run fallback loaded the script's preferred family (Noto Sans
Thai, which has no Latin letters) and stopped: every character had a face,
so an installed font mapping the whole run (IBM Plex Sans Thai) was never
tried, and a Thai + Latin run was split across two faces. Three Thai tests
failed on machines with Noto Sans Thai installed.

When no loaded face covers the run, the search now looks through the
coverage index for the first installed file mapping the whole run. It
loads that file's whole family, since path order can reach the Bold
file before the Regular. It also looks at the loaded faces again even
when it loaded nothing itself: a concurrent search may just have
loaded the font and taken its files off the index, and the miss would
otherwise be cached.

Regression test: a_run_no_preferred_face_covers_whole_loads_an_installed_font_that_does
(synthetic fonts, independent of what is installed).
Two holes in the whole-run fallback search (review on #3078):

- The coverage index keeps one coverage per file, so a font collection
  whose faces only together map a run matched it; the search loaded that
  family and stopped, returning no face even when a later font covered
  the run. It now goes on to the next file that maps the run.
- A single-character fallback may have loaded one file of a family (its
  Italic, say). The run search then treated the family as loaded and
  added only the file it reached first, so a regular run could be drawn
  in Bold although the Regular file was installed. It now loads whatever
  of the family isn't loaded yet.

Files loaded by family name are now recorded in the index too, so no
search loads a file a second time.

Tests: a split-coverage collection before a covering font; a family
whose Italic a character fallback loaded, reached at its Bold file first.
Both fail without the change (no face; Bold).
On a page of turned text (a vertical section) DocLayout::hit returned
through nearest_turned before the floating-table rule applied, so a click
outside a floating table there could still land in one of its cells.
nearest_turned now takes the same rule: the nearest turned line outside the
floating tables, else the nearest of any (as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A missing Khmer OS font drew in whichever loaded Khmer font covered the text, usually a
wider Khmer OS face, so lines wrapped and fixed-height text boxes hid their last line.
Word uses the system Khmer font; so do we now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Picture Format / Shape Format Align button ran arrange.align with
no value, which defaults to "center" — there was no way to reach Left
or Right from the ribbon short of dragging the object manually. It now
opens a menu, like Word's own Align dropdown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit (#3246): x > 0.0 alone doesn't pin Align Right down, since
Align Center's x is positive too whenever the picture is narrower than
the text column — Right could silently dispatch Center and the test
would still pass. Capture Left's and Center's x as well and assert
left_x < center_x < right_x.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A crop that kept under 5% of a picture's axis was widened to 5%, so a
header bar cropped to 2.7% of a page-sized WMF came out half as tall and
the text box over it fell below it. The rasteriser, the PDF writer and
the metafile placement now share wordcraft_metafile::crop_window, which
keeps down to 0.1% of an axis and keeps junk crops finite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/formats/src/html.rs:29-32 (decode_entity)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
installed_dictionaries() never included Russian or Czech at all,
because neither crates/proof/src/ru nor crates/proof/src/cs had an
installed_path() function for it to call (every other proofed
Hunspell language — Polish, Portuguese, Italian, Croatian, Turkish,
Indonesian — already did). File > Options > Proofing's dictionaries
list iterates installed_dictionaries() directly, so Russian and Czech
never showed up there at all, installed or not, matching the report.

Added installed_path() to both modules, mirroring the existing
pattern (crate::hunspell::find(NAMES).map(|(_, dic)| dic)), and added
both to installed_dictionaries().

The actual spell-checking dispatch (is_correct_in_with) already
routed Russian and Czech correctly — confirmed by installing the real
system ru_RU Hunspell dictionary and checking it directly
(ru::is_correct("молоко") true, ru::is_correct("малоко") false), so
that part of the pipeline isn't what's broken. The reporter's other
symptom (Russian text in the document not being checked at all) did
not reproduce with the real dictionary installed; I could not pin
that part down further without more specific repro details from
their setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fallback_for_run cached its answer under the face count read after the
search. A face loaded in between by another thread (its own search, or a
family a document names) made a miss look current: later lookups at the
new count got the stale miss (review on #3078). The search now returns
the count of the loaded faces its last choice was made from, and the
answer is cached under that; anything loaded since makes the next lookup
search again. No lock is added, so cached lookups from parallel layout
stay contention-free.
CodeRabbit (#3252): set_dictionaries_folder() reloaded every other
proofed language but not Russian or Czech, so after a folder change
installed_dictionaries() would report a path from the new folder
while spelling kept using the stale cached dictionary.

Also added Czech's test fixture to the shared dictionaries-folder
test's temp folder (the same content czech_proofing_through_the_
review_commands installs directly): since that folder-change test
now reloads Czech too, it was clearing a concurrently running Czech
test's installed dictionary with nothing to replace it, intermittently
emptying it mid-test. Same fixture content either way keeps the race
harmless, exactly as it already does for Polish/Portuguese/Indonesian.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With 0.1% the whole source of a 4000 pt frame spanned up to 4,000,000 pt,
past the ±1,000,000 pt limit Picture::place uses to leave out items far
off the page, so a metafile cropped that narrow lost what the crop shows.
At 0.5% it spans at most 800,000 pt (CodeRabbit on #3247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Find box and the Navigation pane's Results list draw the typed
query and document excerpts around each match with the interface
faces (Inter, Source Sans 3), which cover Latin plus the embedded CJK
faces — nothing else. Georgian (and Armenian, Thai, …) text typed into
the search box, or found in the document and shown in a result's
context snippet, was drawn as boxes.

Added ensure_find_fonts(), following the exact pattern already used
for the window caption (#565) and comment balloons/the Comments pane:
scan the query and the visible results' surrounding text for
characters the interface faces can't draw, and borrow a document
font's fallback face for them. Cached by (query, result count) so it
only rescans when either changes.

Verified live: built the app, inserted Georgian text, searched for a
Georgian word, and confirmed both the search box (typed query) and
the Results list (match context) now render the glyphs instead of
boxes — matching the issue's screenshot, now fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Word for Mac spaces Symbol lines at 1.222 em (measured at 11 and 20 pt)
even with Apple's Symbol installed, whose own line is one em. Symbol's
metrics now apply when the resolved face is one em tall as well as when
the font is substituted (fonts::word::symbol_line).
CodeRabbit (#3252): installed_path() searched the filesystem
unconditionally, so a dictionary given directly with install_dictionary
(a user-picked one, or a test fixture) was reported as the file of a
same-named dictionary that happened to also be installed, even though
spelling was actually using the in-memory one, not that file.

Added a DICT_IN_MEMORY flag to both modules, set in install_dictionary
and cleared in reload_dictionary; installed_path() now returns None
immediately when it's set, before even searching the filesystem.

Tried adding a dedicated test that calls install_dictionary() and
asserts installed_path() is None. It made dictionaries_folder_is_
searched_first flaky (~15-35% of runs across repeated local testing):
ru's and cs's own test modules already call install_dictionary()
directly in several tests of their own, sharing the same process-wide
static, and dictionaries_folder_is_searched_first's exact-path
assertion for Russian/Czech could observe whichever one last touched
it. Reverted the dedicated test; the production fix alone never
touches that shared state (it's a pure read-only check) and caused no
flakiness by itself across 25 repeated runs. Instead, narrowed
dictionaries_folder_is_searched_first's Russian/Czech assertion to the
language only, not the exact path, since unlike the other four
languages, installed_path() for these two is now sensitive to
concurrently-running tests' legitimate in-memory installs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The engine.execute row listed the UI commands with unescaped "|" inside
code spans ("system"|"on"|"off", true|false, ...). GitHub-flavoured
Markdown splits table cells on every unescaped pipe, code spans
included, so the row came out as 32 cells in a 3-column table and was
cut off. A second, older engine.execute row duplicated it with a
shorter command list.

The two rows are now one: the current row, with ui.contextMenu (only in
the older row) added. Every "|" inside its code spans is escaped as
"\|", which GitHub shows as "|". Every row of the file's tables now has
its header's number of cells. Nothing else changes.
With Different First Page on, a section's default header was listed and reached by Next
before its first-page header, so Next went to page 2 before page 1. A section's header and
footer versions now go in the order of the first page each one shows on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit (#3257): the cache key (query, result count) didn't change
when an edit added Georgian text right beside an existing match
without touching the query or the (stale, not recomputed by typing
alone) result count — the Results list's context window would show
the new text, but ensure_find_fonts never looked at it again.

Added the document revision to the key, matching ensure_comment_fonts'
existing (document_id, rev) pattern for the same kind of problem.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit (#3252): reload_dictionary() wrote DICT and DICT_IN_MEMORY
under two separate locks. If install_dictionary() ran between them,
the final state could hold an in-memory dictionary with the flag left
false, so installed_path() would report a same-named disk file
instead of None.

Combined both into one DictState struct behind a single RwLock in
both ru and cs, so install_dictionary() and reload_dictionary() each
update the dictionary and its origin in one critical section.

Didn't add a dedicated concurrency test: genuinely proving a race's
absence needs real multi-threaded stress testing, and the previous
commit on this PR already found that tests directly exercising
install_dictionary()/reload_dictionary() concurrency in this crate's
shared global state cause real, reproducible flakiness in unrelated
tests. The fix is correct by construction (one lock acquisition around
both field writes) and all 222 existing proof tests, run 15 times in
a row, stay green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
contour::picture_outline turned the crop into a pixel window on its own,
so a sliver or overlapping crop gave an empty window and Tight/Through
wrapping fell back to the frame while the renderers draw a 0.5% sliver.
It now uses wordcraft_metafile::crop_window, with the upper pixel edges
rounded up. A PDF test covers the bitmap and metafile paths of the export
(CodeRabbit on #3247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #3239

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in Word, the caret's table shows its move handle past the top-left
corner and its resize handle past the bottom-right one wherever the
pointer is, not only while the pointer is over the table.
table_mouse::caret_handles gives them per page; the app paints them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reference link was not read at all. `[text][label]`, `[text][]` and
`[text]` all came through as literal text, and the `[label]: url`
definition line became a paragraph of its own in the document -- so the
markdown syntax was visible twice over.

Definitions are now taken out of the lines before parsing and collected
into a map, which is threaded to the inline parser beside the formatting
it already carries. The link scanner, which required a `(` after the
closing bracket, now also accepts the three reference forms and looks
their label up.

Labels match as CommonMark compares them (section 4.7): case-folded,
with every run of white space one space, so `[  REF1  ]` finds
`[ref1]`. The first definition of a label wins. A destination may be
wrapped in `<...>`, and a definition may carry a title in quotes or
parentheses, which is read and discarded.

An undefined label stays text, so an ordinary bracketed phrase is
unaffected. A definition inside a fenced code block is code and defines
nothing. A definition written across lines, which CommonMark allows, is
still text -- no worse than before, when none of them were read.

The other two constructs in #3238, task lists and footnotes, are not
touched here: the ticket bot asked for one problem per ticket, and these
are separable.

Part of #3238.
Each template tile drew its thumbnail as an unnamed click area with the
caption as a separate label, and each Recent row was an unnamed click area,
so a screen reader found no button and no name to announce. The thumbnail
and caption are now one button named by the caption (painted as before, so
the page looks the same), and each Recent row is a button named by its file
name.
Two Recent entries ending in the same file name had the same accessible name,
so a screen reader couldn't say which one each button opens. A repeated file
name now adds the path painted under it; unique names are unchanged.
A replaced document's temporary file takes the original's Unix mode before
it replaces it. A GVFS SMB mount (smb:// in the Linux file manager) refuses
every mode change with ENOTSUP, so every save of an existing file there
failed and the close prompt's Save never closed. A folder that keeps no
Unix modes has no mode to keep, so the save goes on; any other failure to
set the mode still stops it and keeps the original.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/table_formula.rs:728-734

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A repeated header row or a row running on to the next page no longer puts them on every page,
and they come from the same geometry as the handles under the pointer, so they can be grabbed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taking the definitions out before the blocks are parsed read each line
without the structure around it, and two kinds of line were deleted that
the author wrote as text.

A link reference definition has to start a block (CommonMark 4.7: one
cannot interrupt a paragraph), so

    Status update
    [Warning]: unstable
    more text

came through as "Status update more text". The line is taken only after
a blank line, at the start of the document, after a heading or thematic
break, or after another definition. Anywhere else it stays and reads as
text, which is what it did before definitions were understood at all: a
missed definition shows the link's own text, while deleting a line that
was part of a paragraph loses it with nothing to show.

Fences opened and closed by their own rules, which were not the block
parser's, so a line that is code there was a definition here:

    ```
    ```rust
    [foo]: /bar
    ```

An info string means ```rust does not close a fence, but this pass
thought it did and deleted the next line from the code block. Four
spaces of indentation is code rather than a fence, on both sides, so an
indented ``` no longer swallows the definitions that follow it.

Both halves have a control in the test: without the block rule the
paragraph comes back as "Status update more text", and without the
fence rules the code block comes back as "```rust\nSee foo.".
Found while testing crates/doc/src/encoding.rs:82 (CP1255 table, entry for byte 0xCA)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The status bar painted the message in full, so a long one (the path of a file that isn't there,
followed by "The system cannot find the path specified. (os error 3)") ran over the Focus and
view buttons and was drawn on top of them.

The controls on the right are laid out first now, and the message is shortened to the room they
leave: `fit_text` (new, next to `text_width` in `theme.rs`) cuts the text to the width it has and
ends it with an ellipsis, keeping its beginning. The whole message is in the tooltip.

The chrome test asserts that a long message stops before the Focus button and that it is shorter
than the whole text; the theme test asserts the shortening itself.
Roll back the new preference on final command rejection, including protected-region validation after handler success. Cover prior/default-free preferences, document and undo rollback, serialization and restart. Qualify interface-language fallback documentation and add missing function documentation.
Found while testing crates/doc/src/props.rs:49-56 (Rgb::parse)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/formats/src/model.rs:1215-1223 (percent_decode)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`now_ms` in layout, `now_unix` in engine, formats and pdf gated
`js_sys::Date::now()` on `target_arch = "wasm32"`, which also matches
wasm32-wasip1 and wasm32-wasip1-threads. There js-sys has no host to call,
so it panics ("cannot call wasm-bindgen imported functions on non-wasm
targets"), and since `layout()` times itself with `now_ms`, every document
laid out on WASI aborts. `std::time` works on WASI, so these now use the
browser clock only on wasm32-unknown-unknown, the gate wordcraft-docx
already uses.
Fixes #3317

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add the two missing regional exceptions and regression coverage for parsing, grouped numbers, formatting, and Spain controls.
Update the progress date, preserve the current status, and add the narrow locale-number correction to the revision history.
Record the narrow regional separator correction without changing localization readiness or effort estimates.
Record the regression evidence, preserve broader localization readiness, and update the touched parity document status and history.
Close Header and Footer (Esc, the ribbon button, MCP) always put the caret
at the start of the document, so the view jumped to page 1. The session now
remembers the body selection when a command moves the caret into a header
or footer, and closing puts it back, as Word does. A double-click on the
body still puts the caret where clicked.

Fixes #3331.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A num's startOverride for a level applies when that level of that num is first used, and only
there: applying all of them on the num's first use reset the shared counters' other levels and
renumbered every following chapter.
The tip shown while dragging the vertical scrollbar (#2551) broke into two
or three lines ("Page 10 of" / "40") as the page number grew: the egui area
keeps last frame's width and the label wrapped to it, narrowing the tip
each time. The label no longer wraps.

Tested by the developer: the tip stays on one line in the app.

Fixes #3341.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
review.spelling (the Review tab's Spelling & Grammar button and the Home
tab's Editor button) only moved the selection to the next issue, with no
way to see or apply a suggestion — just the generic selection toolbar, so
it looked broken.

Running it now also opens the same suggestions menu Shift+F10 and a
Control-click already open for the word at the caret, via the
WordApp::run() chokepoint (not the ribbon call sites: keytips call
app.run() directly, bypassing a button's own .clicked() check).

Exposing this path also surfaced two latent i18n-audit gaps: the
suggestions popup's dictionary/grammar-rule words aren't interface text
(added an Exempt case, as the audit already does for font names and the
user dictionary), and "Possible spelling mistake" was missing from the
ru/tr/bg catalogs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No production code changed: the reported symptom (a typed reply
disappearing instead of posting) doesn't reproduce on current main.
Checked out the exact UI path the report describes (click a balloon's
Reply button, type, click away) via a kittest harness, and it correctly
posts the reply through review.reply.

The fix was apparently already in place — CommentDrafts/commit_reply
(crates/ui-egui/src/panes.rs) land the draft on focus loss, and
deselect_balloon (crates/ui-egui/src/canvas.rs) covers it again when the
balloon itself is deselected — but nothing exercised this specific
balloon-Reply-button path before, unlike comment editing which already
has coverage (a_refused_comment_edit_keeps_the_draft and others).

Reverse-verified: temporarily disabling both commit_reply call sites
reproduces the exact reported symptom (the reply silently never posts,
0 replies instead of 1) and fails this test with that change in place,
confirmed, then restored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while testing crates/formats/src/latex.rs:2526-2545 (raw_text)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
"- ", "1. " and the other list triggers only turned into a bullet or
numbered item when they were the paragraph's entire content (caret at
the paragraph's end). Typing one at the start of a paragraph that
already has more text after the caret — the common case of going back
to bulletize an existing line — left it as literal text instead.

Checked in Word for Windows: it triggers on the paragraph's first
characters regardless of what follows, keeping the rest of the text
after the new list marker.

Fix: match the pattern against the text up to the caret (not the whole
paragraph), and strip only that matched prefix, not the paragraph's
entire text.

Regression test reverse-verified: fails with the old whole-paragraph
check reinstated (the old behavior literally reproduces the report —
"- " stays as typed text instead of becoming a bullet), passes with the
fix. Full cargo xtask ci passes except the pre-existing, machine-
specific a_thai_run_with_latin_falls_back_to_one_face failure (also
fails identically on a clean upstream/main checkout).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit review: list_autoformat()'s numbering guard only checked
para.props.numbering (direct formatting). A heading numbered through
its style's linked Multilevel List preset (#1793) carries no direct
numbering at all — it's resolved from the style — so the guard missed
it, and typing "- " at the start of such a heading replaced its real
number with a fresh bullet instead of staying as plain text.

Fix: check the paragraph's resolved numbering (direct formatting over
the style chain), which reflects the list a heading actually has
whichever way it got it.

Reverse-verified: reverting just this guard reproduces the corruption
(the heading's "1.1" becomes "-"); passes with the fix restored. Full
engine suite (790 tests) and cargo fmt/clippy clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The link tooltip (URL + "Ctrl/⌘+Click to follow link") stayed open when the
context menu opened. Tooltips draw above menus, so the URL box covered the
menu's first items (Cut, Copy, Paste, Font…) and the menu looked truncated.

The tooltip now stands down while the context menu is open, as the mini
toolbar already does, and while the secondary button is opening it.
Regression test: hover a link (tooltip shown), right-click, assert the menu
is open and the tooltip is gone.
Clicking the AutoSave label in the title bar did nothing; only the 30x16 switch
responded. The label was also selectable chrome: double-clicking it highlighted
"AutoSave" and Cmd/Ctrl+C copied it.

The label is now a clickable, non-selectable part of the switch, with the same
hover tooltip and pointing-hand cursor, and clicking either toggles AutoSave.
Regression test: clicking the label turns AutoSave off and on again.
`graphics.rs` allowed dead code only on macOS (where the Windows items are unused). Since #2845
the two indirect-validation functions are called only on macOS, so on Windows and Linux
`cargo clippy --workspace --all-targets -- -D warnings` stopped with "function is never used".
They now carry `allow(dead_code)` off macOS.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 18c7c1d114)
The importer ignored table:table-column and style:column-width and gave every
table equal columns, so widths set in LibreOffice (or changed in WordCraft and
exported) were lost on the next open.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The toolbar and the command panel share one popup whose justified layout stretched the panel
to the toolbar's width. The panel now takes the width egui measures in the popup's sizing
pass, so it matches the menu shown without the toolbar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doc conflicts (ROADMAP.md, docs/gaps.md, docs/ui-parity.md) keep both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TOC whose heading isn't a TOCHeading is written with an empty heading paragraph inside the
control. Keeping the control around it on reading left that paragraph non-empty, so each save and
Update Table added a blank line (toc_menu_offers_update_field_after_save_and_reopen, #2182). The
reader now compares against the TOC the writer would actually wrap (written_toc_span), which also
keeps the original control when another block control covers the TOC heading.
Word lays out an automatic table as its control around a TOCHeading, the entries with the field
opening in the first, and a last paragraph holding only the field's end. That paragraph isn't a
TOC entry, so the TOC was kept in its control with the markers in the body; Update Table then left
the heading out of the TOC and wrote a second control around the entries alone. The reader checks
that TOC with the end moved into the last entry and hands it over as the range read, as before.
A thin outline as wide as the equation and as tall as its line, a grip over
its top-left corner and an options strip inside its right edge that opens the
equation menu; the argument the caret is in is shaded. Replaces the faint grey
shading around the equation alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones
- The list used the registry order, which is the order the languages were added
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows/Linux, the back arrow in the File (Backstage) page's side
panel was clickable across the whole 200px-wide row, not just the
chevron icon drawn at its left edge. Clicking anywhere in that row —
plain sidebar background to a user's eye — closed the File page the
same as clicking the arrow itself, with no visual cue it was
clickable.

Scope the click region to a narrow band around the chevron, the same
width already used for the macOS chevron fix (#2972). The row still
reserves its full 200x40 space for layout; only the interactive rect
shrinks.

Updated two tests that relied on the old oversized hit box to click
at the chevron's actual position, and added a regression test
asserting a click beside the arrow leaves the File page open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The renderers draw a crop window whose edges fall within pixels (crop
fractions are f32 too: 0.7 is 0.69999...). The outline stretched the
whole pixels it covered over the frame instead, so a narrow window came
out up to a pixel too wide at each edge (a 2.5-5.5 px window put pixel 2
over a quarter of the picture, not a sixth), and a crop of 0.7 took in
the neighbouring column. Grid points now map through the window itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word reads a table style without w:tblStyleRowBandSize / w:tblStyleColBandSize
as bands of zero and leaves its banded rows and columns unshaded (MS-OE376
§2.7.5.7, MS-OI29500 §17.7.6.5); ECMA-376 says one, which is what we did. Our
writer only wrote a size a style set, and our built-in styles set none, so a
table in Grid Table 4 saved by WordCraft had shaded rows here and none in Word.

- An unset or zero band size now means no bands, as in Word. PartSet::of_cell
  takes row and column band sizes; column bands get a size of their own
  (col_band_size).
- Our banded built-in styles and new table styles set both sizes to one, and
  the writer writes them (and zero), so Word bands them too.
- The reader reads both sizes, zero included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backstage's File > Save (unnamed document) > Save As page > Browse…
flow, and AutoSave's own Save As, discarded the write error when
file.save failed (no permission, disk full, a bad path): the page
just sat there with nothing to explain why, and the document stayed
unsaved with no visible cause. The Save Changes prompt's own Save As
already surfaced its error; this brings the same handling to the
plain Save As path.

Added a regression test for the silent failure, and one exercising
the full Backstage Save > Save As page > Browse… flow with an
asynchronous native dialog answer end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit caught it on PR #3434: self.status(...) alone is invisible
while Backstage is open, since its render loop skips the status bar
(only backstage::show runs). A failed Save As (no permission, disk
full, a bad path) left the Save As page sitting there with a Browse…
button and no explanation, same silent failure as before, just moved.

fail_save_as now also stores the message in the egui temp data
save_as_page reads and renders inline, cleared once File closes or a
later attempt actually saves. Replaced the status_msg-only regression
test with one that drives the real UI (Save > Save As page > Browse…
> a write that fails) and asserts the same text is rendered on the
page, not just sitting in status_msg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Saving a document with many content controls was quadratic in the number of
controls: 32,000 of them took four seconds to save, and doubling that took
four times as long again.

Two loops in the writer cost that. The plan that decides where each control
is written collects the inline markers of every control in one set, and then,
for each paragraph, looks for that paragraph's markers by filtering the whole
set — so one paragraph's answer costs the size of the document's marker set,
and the save costs paragraphs times markers. And to find the block-level
controls that clash, every control was compared with every other control.

The markers are now grouped by paragraph once, so a paragraph's markers are a
lookup, and the block-level controls are grouped by the block they end in,
each group keeping the two earliest closes, which answers the clash question
the same way in one sweep.

What is written does not change: the packages written for ten shapes of
control nesting — inline, around paragraphs, chained, nested — are byte for
byte the same as before.

Tests: crates/docx/tests/content_control_save_cost.rs. The two timing tests
fail without the fix (16.1x and 13.0x the time for four times the controls,
against a bound of 8.0) and pass with it (4.6x).

Fixes #3437.

Co-Authored-By: opencode <noreply@opencode.ai>
The repository's convention is that landed work updates the gap document, the
parity document and the progress log, with each touched document's status line
and revision history bumped. This does that for the fix in the commit before:

- docs/file-format-parity.md: a section says what saving many content
  controls costs, with the measurement before and after (32,000 controls:
  3.6 s -> 0.1 s), and names the tests.
- docs/gaps.md: gap 20 (performance on large real-world documents) is
  narrowed by it.
- ROADMAP.md: the progress log and its revision history gain the entry.

Fixes #3437.

Co-Authored-By: opencode <noreply@opencode.ai>
CodeRabbit nitpick on PR #3434: the AutoSave switch's own Save As
(chrome.rs, clicked while blocked on Unsaved) runs the same
AfterPick::SaveAs arm under AfterSave::AutoSave instead of
AfterSave::Nothing, and had no coverage of its own — a regression
removing fail_save_as from that arm would have passed the suite.

Added a_failed_autosave_switch_save_as_says_so_and_leaves_autosave_off,
asserting the failure reaches status_msg and AutoSave stays off
(autosaves(), not the raw autosave flag, which defaults to true and
isn't by itself what governs coverage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A list label is shaped with `Dir::Guess`, so a label of digits and brackets has
no run the shaper sees as right to left; the clusters were then reordered for
the right-to-left paragraph and the brackets drawn unmirrored, so "(%1)" came
out ")1(", as #2916 reports.

The label is shaped with the bidi levels of its own text now, as the paragraph
body and the atomic objects already are, so UAX #9 L4 mirrors the brackets of
the runs the levels report as right to left; the visual reorder the paragraph
direction asks for stays. A "1." label is unchanged.
CodeRabbit catch on PR #3434: stamp_save() ran before the fallible
save_path_with(), so a failed write (no permission, disk full, a bad
path) still bumped the document's modified time, revision, author and
zoom, as if it had actually been saved. A later successful save would
then persist those stale stamps for a write that never happened.

Snapshot the stamped fields before stamp_save() and restore them if
save_path_with() errors, before propagating the failure.

Added a_failed_save_leaves_the_save_stamps_untouched in
crates/engine/src/tests.rs. cargo test -p wordcraft-engine --lib: 789
passed. clippy -p wordcraft-engine --lib --tests -D warnings: clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit catch on PR #3434: a_failed_save_leaves_the_save_stamps_untouched
named its scratch directory by pid alone and removed it on a pre-existing
path, which (if that path somehow already existed) could delete content the
test never created. Matching the pid+nanos pattern already used elsewhere
(backstage.rs's recovery tests): the name is unique enough that there's
nothing to pre-clean, and `dir` itself stays uncreated, which is what makes
the write fail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Create the East Asian decoder without BOM handling: the code page is named by
  \ansicpg, \cpg or \fcharset, so a GBK/Big5/UHC run that starts EF BB BF is text,
  not a UTF-8 BOM that switches the rest of the document to UTF-8 (#2549 review).
- Decode raw 8-bit runs through the streaming decoder in one pass instead of one
  byte at a time: ASCII body text in a Japanese/Chinese/Korean document and picture
  hex skip the per-byte path, and a single \'hh byte decodes into a stack buffer
  (no String allocation per byte). Picture hex no longer goes through the code page
  decoder at all.
- Decode loop: decode_to_string writes only into spare capacity, so drain the stack
  buffer and keep decoding until the run is consumed (the old one-call version could
  truncate a run longer than the reserved capacity).
- docs/file-format-parity.md: keep main's format rows (the PR had replaced them with
  stale copies that lost ODT keep-with-next #2813, TXT #2853 and the HTML blockquote
  and MathML details), merge the East Asian sentence into the one RTF row — which also
  removes the duplicate RTF row main carries and restores "export writes every
  section" and the tracked paragraph-mark wording — restore the rolled-back header
  change list, and date the new revision row 2026-10-21.

Validation: cargo fmt --all -- --check; cargo clippy -p wordcraft-formats -p wordcraft-doc
--all-targets -- -D warnings; cargo test -p wordcraft-formats -p wordcraft-doc
→ 200 + 174 passed, 0 failed (doctests 0).
- ODT export reads the tracked insertion/deletion from a Ruby inline's formatting
  too (Item::marks and the paragraph's tracked check): an inserted or deleted
  phonetic guide keeps its change range around the base text instead of being
  written as untracked text.
- HTML export computes the paragraph-start state before the ruby's formatting tags,
  so a leading space in the base text becomes &nbsp; instead of a space HTML can
  collapse.
- docs/file-format-parity.md: restore main's format rows (the PR had replaced them
  with stale copies that lost ODT keep-with-next #2813, TXT #2853 and the HTML
  blockquote/MathML details) and add the ruby sentence to the retained .htm/.html
  row; restore the rolled-back header change list.
- docs/localization-parity.md: restore the header change list; date the new
  revision-history rows 2026-10-21.
- Tests: html_ruby_at_the_paragraph_start_keeps_a_leading_space and
  an_inserted_ruby_keeps_its_tracked_insertion.

Validation: cargo fmt --all -- --check; cargo clippy -p wordcraft-formats
--all-targets -- -D warnings; cargo test -p wordcraft-formats -> 204 passed,
0 failed (doctests 0).
Word sizes a superscript or subscript's line at the full font size and
leaves out the vertical alignment's raise or drop; a w:position raise or
lowering still grows the line. WordCraft added the drop to the line's
descent and used the shrunk size, so lines with a subscript came out taller
and lines of sub/superscript alone shorter than Word's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve w:lang/@w:eastAsia when reading and writing DOCX documents.
Previously, only w:val and w:bidi were handled, causing w:eastAsia to be
dropped on save.

Fixes #3457
The test that the controls are written where they were counted the controls
and looked at the paragraphs they were in, so a change in the order or the
place of a control's markers inside a paragraph would have passed it.

The writer's output for a document with two inline controls in one paragraph
and a control around a paragraph is now pinned: the body of the document part,
byte for byte. The golden was taken from the writer before it was made faster,
so it says that the faster writer writes the same thing. It has teeth: a
writer that returns a paragraph's markers in the opposite order fails it.

Co-Authored-By: opencode <noreply@opencode.ai>
The body of the document part is pinned, so the writer's output is compared
byte for byte; the parts of the package are pinned as well, so a part that is
added or lost shows up here too.

Co-Authored-By: opencode <noreply@opencode.ai>
Ensure Ctrl+Alt+Q (German shortcut for @, or AltGr+Q) is not treated as a command shortcut or default quit shortcut, preventing unexpected app closure and allowing the @ symbol to be typed.

Fixes #3424
Fixes #3449
The font name box and the font size box announced themselves as bare "edit"
controls, and the dropdown arrows beside them as bare "button" controls: a screen
reader had nothing to say about them.

The combo helper names its field with the hint it already shows, and its arrow
with the same "{name} options" the split buttons' arrow uses; the split button's
arrow now says whose options it opens. No new strings: every name is one the
widget already shows.

Measured the way #3172 measured it — every keyboard-focusable element of the
Home tab and its name: 43 of the 111 had no name before, 31 after.
RLA-ES, the Spanish Hunspell dictionaries in LibreOffice and most Linux
distributions, writes one flag as U+260E followed by U+FE0F, in the rules
(PFX ☎️ Y 3) and on stems (banco/S☎️). parse_aff rejected it as "not one
character", so the whole dictionary failed to load. The variation selector
is now dropped wherever flags are read, with a regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Word's date-time instructions accept either case for day and year.
Uppercase codes now format instead of being copied literally.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Word's lowercase hh instruction shows a leading zero, just like HH.
Single-digit 12-hour values now format as 05 rather than 5.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
On macOS, AltGr does not produce Ctrl+Alt and Command-Q is the native application close chord. Restrict the Windows/Linux AltGr Q regression test to non-macOS platforms as requested by review, preserving the existing macOS Command-Q close test coverage (#3424).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In table.cellMargins, resolve existing margins from the table, its style, or defaults and only overwrite the sides explicitly provided in the command parameters.

Closes #3490
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In bookmark_name_ok, validate length by Unicode character count (at most 40 characters) rather than byte count, require an initial alphabetic letter, and restrict characters to alphanumeric and underscore.

Closes #3491
In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485
Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/geom/src/lib.rs: main already rejects the overflow; kept main's check and added the PR's test.
crates/engine/src/cmd/insert.rs: date_pictures test keeps both PRs' assertions.
crates/formats/src/markdown.rs: kept both sides' new tests
contributors/people.toml: union of both sides' new entries.
crates/formats/src/markdown.rs: kept both sides' new tests
crates/ui-egui/src/lib.rs: union with #2407 (recent_fonts_for and find_fonts_for fields, both ensure_* fns and calls)
crates/layout/src/tests.rs: both sides' appended tests kept.
docs/layout-parity.md: main's table rows kept, #1721 sentence added to the autofit row; both log rows kept.
crates/formats/src/latex.rs: kept both sides' new tests
crates/ui-egui/src/keys.rs: kept main's single keymap lookup (bound) and added #2211's Outline-only guard inside it.
crates/proof/src/lang.rs: main's English-dialect helpers kept; code() doc lists `de`.
crates/proof/src/lib.rs: hostile-input test loops over main's languages plus De.
crates/ui-egui/src/options.rs: language_name maps EnGb/EnCa and De to no dictionary name.
ROADMAP.md: main's status lines; both progress-log and revision rows kept.
docs/gaps.md: main's status line; gap 8 row is main's with German hyphenation added; revision rows unioned.
docs/layout-parity.md: main's status line, Tabs and Drop caps rows; the PR's Hyphenation row; revision rows unioned.
docs/localization-parity.md: main's status line; revision rows unioned.
crates/docbin/src/tests.rs: union (main's sprmTCellWidth test, then the PR's two tests)
ROADMAP.md, docs/gaps.md, docs/layout-parity.md: status line keeps main's and adds the PR's entry; log rows unioned.
ROADMAP.md, docs/file-format-parity.md: status lines keep main's with the PR's item added; log rows union.
ROADMAP.md,docs/ui-parity.md: status lines keep main's with the PR's item added; tables and lists union.
crates/zotero/src/tests.rs: both new #898 tests kept.
crates/fonts/src/fontdb.rs: PR's new test added before main's renamed run_fallback_is_cached_per_char_set
docs/typing-parity.md: status line kept main's, #2388 added as a previous change; both revision rows.
docs/ui-parity.md: status line kept main's, #2388 added as a previous change; Keytips row keeps main's text plus the AltGr clause; both revision rows.
ROADMAP.md: main's status line kept; both progress-log lines added.
docs/target-app-parity.md: main's status line and Tables row kept, #1596 sentence added to the row; log row added.
crates/docx/src/units.rs: union of test assertions (main's line_multiple, the PR's emu/case ones)
ROADMAP.md: both progress-log rows, #2692's under main's newest 2026-10-17 row.
docs/gaps.md: status line kept main's, #2692's change added as a previous change.
ROADMAP.md: main's status lines; both progress-log and revision rows kept.
docs/file-format-parity.md: main's status line and rows; the PR's HYPERLINK sentence added to both of main's Fields rows; revision rows unioned.
ROADMAP.md: kept main's status line; progress-log and change-log rows from both sides
docs/ui-parity.md: kept main's status line; change-log rows from both sides
crates/ui-egui/src/canvas.rs: kept both sides' new tests as separate functions
crates/ui-egui/src/objects.rs: kept both new functions (dragged_off_page from main, drop_point from the PR).
ROADMAP.md, docs/ui-parity.md: status lines keep main's with the PR's item added; log rows union.
ROADMAP.md,docs/gaps.md,docs/layout-parity.md: status lines keep main's with the PR's item added; tables and lists union.
ROADMAP.md,docs/gaps.md,docs/ui-parity.md: status lines keep main's with the PR's item added; tables and lists union.
crates/docx/tests/fixtures.rs: both sides' appended tests kept.
crates/ui-egui/src/lib.rs: kept main's nav_pane field next to the new nav_collapsed.
crates/ui-egui/src/panes.rs: nav_heading keeps main's wrapping label (#3015) and gains the PR's collapse triangle and (toggled, response) return.
crates/doc/src/numbering.rs: kept #3417's per-(num, level) seen map and its single-level override; the override now sets that level's LevelFlags { started: false, first: Some(start) } from #2343 instead of start - 1 (the reviewed integration fix for #2343).
ROADMAP.md: main's status lines; both progress-log and revision rows kept.
docs/ui-parity.md: main's status line; revision rows unioned.
crates/ui-egui/src/lib.rs: union, keeps both print-page tests (#2958 and #3210).
crates/docx/tests/fixtures.rs: both sides' appended tests kept.
docs/file-format-parity.md: one status line naming this change first, then main's
crates/formats/src/odt.rs: kept main's tests and added the PR's test after them.
ROADMAP.md: main's status lines; both progress-log rows kept.
docs/gaps.md: main's status line; revision rows unioned.
docs/localization-parity.md: main's status line; revision rows unioned.
ROADMAP.md: status line kept main's, #3234's change added as a previous change; both progress-log and revision rows.
docs/ui-parity.md: status line kept main's, #3234's change added as a previous change; Ribbon row keeps main's text plus the forms-protection sentence; both revision rows.
crates/ui-egui/src/backstage.rs: side bar fills with t.nav (main dropped the recovery branch and APP_COLOR)
crates/layout/src/display.rs: border_stroke maps Dotted, Dashed and DotDash to the PR's Border* strokes and keeps main's Triple; Stroke::dashes lists Triple as undashed.
crates/pdf/src/tests.rs: kept both new tests.
ROADMAP.md, docs/ui-parity.md: main's status lines kept; the PR's log rows added.
ROADMAP.md,docs/layout-parity.md: status lines keep main's with the PR's item added; tables and lists union.
ROADMAP.md,docs/file-format-parity.md: status lines keep main's with the PR's item added; tables and lists union.
ROADMAP.md,docs/ui-parity.md: status lines keep main's with the PR's item added; tables and lists union.
docs/layout-parity.md: status lines keep main's with the PR's item added; tables and lists union.
crates/engine/src/cmd/references.rs: Update Field keeps both the DOCPROPERTY (#1851) and FILENAME arms.
crates/layout/src/tests.rs: main's #2048 tests and the PR's cache tests kept.
docs/layout-parity.md: one status line naming this change first, then main's; history rows union
crates/ui-egui/src/i18n/sv.tsv: union by key, no duplicates.
docs/ui-parity.md: status line kept main's, #3176 added as a previous change; both revision rows (the #3234 row moved above the #2388 row, newest first).
crates/ui-egui/src/objects.rs: main's tests kept; the PR's rotation_window helper and two rotation tests appended after them.
ROADMAP.md, docs/ui-parity.md: main's status lines kept; the PR's log rows added.
docs/gaps.md: main's status line and row 10 kept, #2277 clause added to the row; log row added.
crates/ui-egui/src/i18n/it.tsv: union by key, no duplicates.
docs/control-protocol.md: one engine.execute row, main's current text (the template save command, focusRibbon) with the PR's \| escapes and its ui.contextMenu entry
crates/formats/src/html.rs: both sides' appended tests kept.
ROADMAP.md, docs/ui-parity.md: status lines kept main's, #3341 added as a previous change; both sides' progress-log and revision rows, newest first.
crates/formats/src/odt.rs: Body::new from #873 with main's xml_space_end; parse_tracked returns main's document font as 4th element and #873's page parts as 5th (the reviewed integration fix for #873).
docs/file-format-parity.md: status line, ODT row and change log carry both sides.
ROADMAP.md: status line once (this change first, then main's); progress-log and history rows union in date order
docs/layout-parity.md: status line once (this change first, then main's); history rows union in date order
ROADMAP.md: kept main's status lines; progress-log entries from both sides
docs/gaps.md: kept main's status line
docs/ui-parity.md: kept main's status line; change-log rows from both sides
crates/formats/src/mathml.rs: menclose keeps main's box-with-strikes reading (the writer emits "box horizontalstrike") and adds the PR's radical, actuarial, cross and case-insensitive notations.
ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: main's status lines kept; the PR's log rows added.
crates/ui-egui/src/lib.rs: kept main's removal of the Focus Escape block from logic() (Escape handling moved in #1733); took #3434's comment on the Recent/Save As error reset.
ROADMAP.md: main's status line kept; the PR's log row added.
crates/layout/src/tests.rs: both sides' new tests kept.
crates/layout/src/display.rs: main's Paint path fill and fill.as_ref() with the PR's Option<GraphicKind> figure and described-shape figure.
crates/pdf/src/lib.rs: main's holds_picture and rotated_pictures arm kept beside the PR's figure_alt.
crates/pdf/src/tests.rs: union; the PR's shape test builds its fill with Fill::solid (main's shape fill type).
crates/layout/src/tests.rs: both sides' appended tests kept.
ROADMAP.md: status line, progress log and revision history carry both sides.
docs/file-format-parity.md: status line and change log carry both sides.
ROADMAP.md: status line once (this change first, then main's); progress-log rows union in date order
docs/file-format-parity.md: status line once; VML row merged word by word (main's #1500 fill text plus the PR's signature-line text)
ROADMAP.md: status line, progress log and revision history carry both sides.
crates/metafile/src/lib.rs: re-export both TextLayout (#1915) and crop_window.
ROADMAP.md, docs/layout-parity.md: main's status lines kept; the PR's log rows added.
docs/file-format-parity.md: status line once (this change first, then main's); history rows union
ROADMAP.md, docs/gaps.md, docs/localization-parity.md: main's status lines kept; the PR's log rows and gaps note added.
ROADMAP.md: main's status lines, with this change added to the newest.
docs/file-format-parity.md: main's status line with this change added; PDF export row keeps main's outline note and adds the lam-alef note.
crates/layout/src/math.rs: main already centres &-less rows (#2617); kept main's code and added the PR's two tests.
ROADMAP.md: main already has the #2617 log row; the PR's duplicate row dropped.
ROADMAP.md: progress log carries both sides.
docs/file-format-parity.md: ODT row keeps #873's headers and footers and adds #3193's formula objects.
ROADMAP.md: status line once (this change first, then main's); progress-log entries and history rows union
docs/file-format-parity.md: status line once; history rows union in date order
docs/gaps.md: status line once (this change first, then main's)
crates/formats/src/odt.rs: StyleInfo keeps both cell_valign (#2416) and column_width.
ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: main's status lines kept; the PR's log rows added.
crates/layout/src/tests.rs: both sides' new tests kept.
docs/file-format-parity.md: main's status line with this change added.
docs/file-format-parity.md: main's status line with this change added; both progress-log rows, this one first.
docs/localization-parity.md: main's status line with this change added.
crates/render/src/lib.rs: imports keep main's Paint and take the PR's page_layers (page_display moves to the test imports).
Doc status lines keep main's and add the PR's entry; log rows unioned: ROADMAP.md
ROADMAP.md: status line and revision history carry both sides.
docs/gaps.md, docs/localization-parity.md: status lines carry both sides.
crates/pdf/src/tests.rs: both sides' new tests kept.
ROADMAP.md, docs/gaps.md, docs/typing-parity.md: status lines and revision histories carry both sides.
ROADMAP.md: main's status lines with this change added to the newest; progress-log rows from both sides in date order.
crates/ui-egui/src/i18n/{de,et,ja,pt-br,sr,sr-latn,uk,zh-hans,zh-hant}.tsv: union by key, no duplicates.
docs/localization-parity.md: main's status line with this change added; both progress-log rows.
crates/docx/src/read/story.rs: main's vml_opacity with the PR's box size.
crates/pdf/src/tests.rs: both sides' tests kept; the glyph_frames helper both #3155 and #3156 add stays once.
Doc status lines keep main's and add the PR's entry; log rows unioned: docs/layout-parity.md
docs/gaps.md: status line keeps main's and adds the PR's entry; main's last log row kept above the PR's new section.
Doc status lines keep main's and add the PR's entry; log rows unioned: docs/ui-parity.md
ROADMAP.md, docs/layout-parity.md: main's status lines with this change added to the newest; log rows from both sides in date order.
ROADMAP.md, docs/ui-parity.md: main's status lines with this change added to the newest; log rows from both sides in date order.
crates/docx/tests/fixtures.rs: union, keeps main's shape-gradient test and the PR's band-size tests.
ROADMAP.md, docs/file-format-parity.md: main's status lines with this change added to the newest; log rows from both sides in date order.
Docs: status lines combined (t02's entries first); log rows unioned; file-format-parity .rtf row from t02 (#1653, duplicate .rtf row dropped as t02 did), .odt row word-merged.
# Conflicts:
#	ROADMAP.md
#	crates/formats/src/html.rs
#	docs/gaps.md
#	docs/ui-parity.md
crates/formats/src/odt.rs: t01's Body::new/take_blocks kept; take_blocks finishes tables with t04's TableB::finish (column widths). Test files: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/layout-parity.md
contributors/people.toml: union. crates/layout/src/tests.rs: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
crates/ui-egui/src/backstage.rs: t03's bidi_display on the Recent names over t09's borrowed name/path. people.toml: union. Tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
crates/layout/src/lib.rs: #1800's border room in the keep-with-next lookahead, with #2431's list counters passed to next_first_line.
crates/ui-egui/src/canvas.rs: #2984's context-menu open logic, opened also by #3419's equation options strip (eq_strip).
crates/ui-egui/src/ribbon.rs: main's Share button (already named) kept; the » button's "More tabs" name added in main's restructured strip; both sides' tests kept. i18n: "More tabs" rows added after main's.
Test-font pins: #3389's kept, #2500's dropped (layout tests.rs, tests_pages.rs, render lib.rs). crates/fonts/src/fontdb.rs: #2500's PostScript-name match moved into #3078's family_paths.
ROADMAP.md: the PR's status entry prepended to main's first status line. docs/parity-checklist.md: main's side, regenerated after the merges. ko.tsv: union.
ROADMAP.md: the PR's status entry prepended to main's first status line. i18n catalogs: union by key.
crates/ui-egui/src/i18n/mod.rs: registry holds nl, da (#2722) and ro (26 languages). tests.rs: both sides' tests kept.
i18n: registry holds 27 languages (sl after nl, da, ro); audit.rs and the Polish test list sl beside pl and ro; tests: both sides kept. Docs: language counts and lists name ro, sl, nl and da; status entries prepended to main's lines.
Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.
Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.
crates/ui-egui/src/backstage.rs: #3125's t.nav fill kept, without the recovery black. Docs: status entries prepended to main's lines; the recovery paragraph placed after the status line.
docs/typing-parity.md: status entry prepended to main's line.
Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).
clippy::useless_conversion failed cargo xtask ci.
Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.
polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.
every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.
#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.
Integrates the backlog round's ready set from aiongg:next.
Header and footer: a click in the other band edits it there (#3557)
Some checks failed
nix.yml / Header and footer: a click in the other band edits it there (#3557) (pull_request) Failing after 0s
sonarcloud.yml / Header and footer: a click in the other band edits it there (#3557) (pull_request) Failing after 0s
nix.yml / Header and footer: a click in the other band edits it there (#3557) (push) Failing after 0s
sonarcloud.yml / Header and footer: a click in the other band edits it there (#3557) (push) Failing after 0s
CI / cargo xtask ci (push) Has been cancelled
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 (windows-latest) (pull_request) Has been cancelled
CI / cargo xtask ci (pull_request) Has been cancelled
Printing / Print (macos-latest) (pull_request) Has been cancelled
Printing / Print (ubuntu-24.04) (pull_request) Has been cancelled
8e986366c7
alf merged commit 8e986366c7 into main 2026-10-22 09:39:16 +00:00
alf deleted branch chore/sync-upstream-20261022 2026-10-22 09:39:16 +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!6
No description provided.