Commit Graph
2926 Commits
Author SHA1 Message Date
batistalucasv c4d5f70c1d DOCX: resolve LibreOffice font fallback lists to first installed family (#5843) (#5851)
* #5843: resolve LibreOffice semicolon-separated font fallback lists to first installed family

* #5843: address review comments on font fallback list resolution

* #5843: verify fallback list text on cache hit and accept Times New Roman in tests

* #5843: bound the fallback lists read from a DOCX and reuse their classification

A document could name thousands of fallback families in `w:ascii`/`w:hAnsi`.
Nothing bounded that value, and the name-based rules (roman classification,
Thai family, named substitutes) then scanned every candidate for each shaped
piece, so opening the document took seconds.

`w:ascii` and `w:hAnsi` now keep the leading families within MAX_FONT_NAME
bytes, cut at a `;` so no name is split, as `w:cs`/`w:eastAsia` already bound
their names. The family a real list would pick is kept: a LibreOffice list
names a few families, well under the bound.

The Thai-family and named-substitute answers are worked out once per list and
kept in the fallback-list cache, keyed by the list text, so shaping no longer
rescans candidates for every piece. Resolution still reads every candidate.

A 500-paragraph document whose font names 20,000 candidates laid out in 10.8 s;
bounded on read it takes 17 ms.

Tests:
- a hostile fallback list keeps its leading families, cut on a `;`
- a real LibreOffice list is read whole
- a later Thai or named fallback still classifies, and lists are cached
2026-11-08 05:55:24 +01:00
Helen Kwok de4ca29309 wordcraft-web: window.wordcraft, the page command API from craft-web-api (#5860)
The web shell queues the page's calls as control requests (the channel the desktop control server uses) and answers them on the next frame. The API is installed in every mode; opening, saving, the file name and the unsaved flag stay with the host messages.
2026-11-08 05:53:01 +01:00
neilcawse b8a7e2acdb docx: normalize TOCHeading gallery content controls with trailing FieldEnd paragraph (#5952)
* docx: normalize TOCHeading gallery content controls with trailing FieldEnd paragraph

* test(docx): assert TOC entries survive round-trip in section-break TOC control test
2026-11-08 05:50:41 +01:00
Jhong-Ken ChenandMartin Ball f0fb31c0c9 XML: normalise line ends in text and CDATA (XML 1.0 §2.11) (#5897) (#5898)
Text nodes were decoded without line-end normalisation, so a file
saved with Windows line ends kept the CR of each CR LF: a line break
inside w:t read as two spaces. Text and CDATA now go through
quick-xml's xml_content, which reads CR LF and a lone CR as one line
feed; a CR written as 
 stays a CR. Attribute values were already
normalised.

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:48:20 +01:00
David PrietoandMartin Ball 4cd6ffa564 doc: preserve following formatting when deleting an empty paragraph (#5929)
* doc: preserve following formatting when deleting an empty paragraph (Leaf #152)

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

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

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

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

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

* test: retain structured controls across an empty paragraph join

* docs: explain empty paragraph engine regressions

* docs: explain the empty heading keyboard regression

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:48:03 +01:00
Senura DissanayakeandMartin Ball 52934ef110 docbin: brace and bracket shapes are unfilled unless the file fills them (#5943) (#5945)
A .doc bracket or brace AutoShape (MSOSPT 85-88, 185, 186) is drawn as
its box, and with no fFilled property it got the default white fill, an
opaque block over the text behind it. Word's shapetypes for these shapes
are unfilled by default (VML filled="f"), so they now get a fill only
when fFilled is set.

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:45:35 +01:00
3291be0fbf Layout: an empty paragraph after a leading floating table takes no space before (#5440)
Word drops the space before of an empty paragraph that follows a document's
leading floating table: its PDFs of docxide-pdf's leading-empty-before-float-0
and -12 put the text after the empty anchor (beside the table) at the same
place for 0 pt and 12 pt before.

Flow::gap_before (crates/layout/src/flow.rs) kept it: a floating table
takes no room in the flow and clears `pb.prev`, so the paragraph after it
was laid out like a document's first paragraph, with its full space before.
It now drops it for body block 1 when block 0 was placed floating on this
page (the page's float_tables), the page is fresh and the paragraph holds
nothing but markers (bookmarks, comment and permission ranges). It
doesn't check that the paragraph sits beside the table, so below a
full-width leading float an empty paragraph loses its space before too;
that case is untested. Later pages and columns, and paragraphs with text,
keep their space before.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:45:17 +01:00
Kevin JosephandMartin Ball 5dfb3acf15 picture.change: keep the width and take the new picture's aspect ratio (#5761)
* picture.change: keep the width and take the new picture's aspect ratio

Change Picture swapped only the media key, so a replacement of another
shape was stretched into the old frame (a revised figure came out ~2%
taller in a real manuscript). Keep the display width, set the height
from the new picture's pixel size, and drop the old crop, which
described the old picture. Regression test included.

* docs: progress log and parity entries for picture-change-aspect

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:40:51 +01:00
4259dd1b2e Layout: before Word 2013 layout a floating table moved below another keeps its own left edge (#5442)
A floating table with tblOverlap=never that is moved below another floating
table steps in to its distance from the text's left edge only in Word 2013
layout (compatibility mode 15). clear_of_float_tables read the mode from the
legacy shift, the first cell's left margin before mode 15, being zero, so a
mode 12 table whose first cell has no left margin was taken for Word 2013
layout and moved 9 pt right (docxide displaced-anchor-*-compat12-overlap0-*:
Word 72 pt, WordCraft 81 pt). Pass the mode itself.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:31:43 +01:00
5c0a62a736 Layout: no line break between a full stop and an opening parenthesis (#5437)
UAX #14 allows a break between a full stop (IS) and an opening
parenthesis (OP), so `break_positions` (crates/layout/src/para/paragraph.rs)
let "29.05.2023г.(оформление ЗУ)" and "2023.(Example detail)" wrap after
the full stop. Word keeps such a token whole and wraps it at the space
before, as the docxide-pdf dot-open-token* Word references show. The
break between the two is now dropped (before the kinsoku pass, so
break-anywhere wrapping still applies).

Test: layout::tests::a_full_stop_before_a_parenthesis_is_no_break.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:18:09 +01:00
855a0fe9bd Layout: a nested table's Can't Split row moves on with its outer row (#5456)
A row is split across pages from its flattened items, so a row of a nested
table marked w:cantSplit was cut between its lines like any other text in
the outer cell. Word leaves such a row whole: the outer row moves to the
next page whole when that row comes first in it (docxide-pdf
nested-first-row-prefix300/330-cant1), and otherwise splits above it
(inferred).

table_flow now moves the cut up to just below the top of the highest nested
Can't Split row it runs through (nested_keep_cut): the outer row's bottom
band below that top, so the text and nested rows ending there stay on the
page. A row runs through the cut when it ends below the room left for text,
as in the split itself; one walk over the rows from the lowest top up moves
the cut until none does. A vertically merged cell doesn't stretch its row's
band, and a cell aligned to the middle or bottom is measured from its top, as
the split runs it. This applies to the first part of a row and to the kept-chain and
header-row checks. On a fresh page, where moving on gains nothing, a nested
row taller than the room still splits at the room rather than running into
the bottom margin.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 05:03:54 +01:00
Ashok Ramandashokram-si 5f1db14954 Proofing: Check spelling as you type is one setting for every open document (#5740) (#5766)
* Proofing: Check spelling as you type is one setting for every open document (#5740)

Each open document's view kept its own copy of the setting. Turned off with one document active, it stayed on in the others, and the settings saved for the next run took the value of whichever document was active then, so it came back on. A document made active now takes the saved preference, as Read Mode's page colour already does.

* Contributors: credit ashokram-si as Ashok Ram

* Proofing: every view made active follows the app-wide settings, a window's view put back too (#5740)

put_view restored a window's view with the Check spelling as you type value it had when it was taken, after switch_to had applied the shared preference, so the settings saved for the next run could take the old value again. One ViewState::follow_prefs now applies the app-wide settings a view keeps a copy of (Read Mode's page colour and Check spelling as you type) wherever a view is made active: switch_to, a new document's view, set_document and put_view.

* test(ui): Check spelling off in a second window stays off in every window (#5740)

The whole sequence through the app: View › New Window, spelling turned off in the second window, the first window's own view put back (put_view), a new document's window, back to the second window, then the saved settings read by a new app. Fails if put_view doesn't follow the setting.

---------

Co-authored-by: ashokram-si <347905169+ashokram-si@users.noreply.github.com>
2026-11-08 05:01:43 +01:00
Bardan GhimireandBardan b016ecc26c fix: reject non-regular files when reading template titles (#5904)
Co-authored-by: Bardan <bardan.ghimire.10@gmail.com>
2026-11-08 04:59:16 +01:00
batistalucasv 625f598b64 Fix #5750: style-set gallery follows RTL interface direction (#5857) 2026-11-08 04:56:55 +01:00
Bardan GhimireandBardan fa791d29b2 test(mcp): gate the names helper to unix, where its only test runs (#5879)
Co-authored-by: Bardan <bardan.ghimire.10@gmail.com>
2026-11-08 04:56:38 +01:00
Senura Dissanayake 796b39dde6 docbin: open Word 97-2003 files in compatibility mode 11 (#5905)
Word 97-2003 documents were laid out with the model default of mode 15. Word opens them in mode 11, which changes justification and line breaking.

Test: a_doc_opens_in_word_97_2003_compatibility_mode
2026-11-08 04:54:06 +01:00
33fa77a892 Track Changes: resolving away an empty paragraph with a section break keeps the next paragraph's properties (#5667) (#5734)
Rejecting an inserted paragraph (or accepting a deleted one) whose mark carries a section break
left it empty; remove_empty_para refused it for the break, so the next paragraph was joined into
it and took its properties (style, numbering, indents). The empty paragraph now goes whole and
its section joins the following one first, as deleting the break does (remove_section_break).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:51:36 +01:00
efd476cbc7 #5197: the ruler shows the spaces between text columns (#5261)
In a section of several text columns the horizontal ruler was one white
strip from margin to margin. The space between each two columns is now
shaded like the margins, so the ruler shows where each column ends and
the next starts.

The edges of a space can be dragged, through layout.columns, one undo
step per drag, to change the spacing. Equal columns stay equal, so every
space changes with it, as in Word for Mac. Between unequal columns only
the column at that edge and the space change. As in Word for Mac, the
middle of a space has no grip of its own. Columns stay at least a
quarter inch wide and spaces 0-300 pt, as the Columns dialog allows,
snapped to 1/16 inch. Right-to-left sections are read from the right
edge.

Outside the main text, outside Print Layout and in a table the ruler is
unchanged. A selection across sections shows no spaces, since a drag
would set the columns of each section it touches; nor does a page the
caret isn't on. Columns from a file that the command wouldn't take back
(over 12, a column under a quarter inch, a space over 300 pt, wider than
the text) are shaded but not dragged. A tab stop's marker keeps its own
drag where it sits on an edge. The indent markers still follow the page
rather than the caret's column; that is #2196 (PR #2215).

Fixes #5197.

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

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:51:18 +01:00
d3fcf10bcc Layout: a picture in a paragraph whose hidden mark closes a section keeps its room (#5288) (#5347)
* Layout: a picture in a paragraph whose hidden mark closes a section keeps its room (#5288)

A paragraph with a hidden mark and no plain text was a quiet mark: laid out
with no room, its drawing kept. An in-line picture adds no plain text, so a
hidden-mark paragraph holding only a picture qualified, and the text after it
(the next continuous section in the report) was drawn over the picture. Such
a paragraph, or one holding a note reference mark, now keeps its line; one
with only floating objects stays quiet.

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

* Layout: a legacy check box in a hidden closing mark keeps its room too (#5288)

A FORMCHECKBOX field has an empty result, so a hidden-mark paragraph holding
only a check box still counted as quiet and the next continuous section was
drawn over the box. Fields now count as visible content when the paragraph
layout shows them as something other than their result: their code, a legacy
check box, or a drop-down's entry (the same test as the layout's `field`).

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

---------

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:48:48 +01:00
b9936609dc Output counts locked SEQ fields, and not tracked-deleted ones, as Word does (#5594)
* Count locked SEQ fields and not tracked-deleted ones in output

Exporting, Word counts a locked SEQ, its \r too, though the field keeps its result, and a SEQ in a tracked deletion keeps its result and doesn't count, with markup shown or hidden (measured in Word for Windows). Output counted it the other way round: a locked SEQ was skipped and a deleted one counted.

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

* Docs: locked and tracked-deleted SEQ fields in layout parity

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

* count_seq walks the deleted ranges with one cursor

The runs come in order and the objects by offset, so a forward cursor
replaces the per-field scan of every deleted range.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:38:32 +01:00
7152143e45 Layout: a line can break after a hyphen before a slash, like Word (#3408) (#5348)
Word wraps "Dataprotection-/Secrecyviolation" as "Dataprotection-" /
"/Secrecyviolation". UAX #14 (LB13) forbids a break before the slash, so
the compound had no break opportunity: lines moved it whole and auto-fit
table columns widened to fit it, taking width from their neighbours.
Add the break after U+002D when a slash and a letter follow, next to the
existing break before a digit (#2712); Word keeps "fitness-/1589" whole,
so a slash before a digit gains no break. Always sort the added breaks.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:36:10 +01:00
c40719f431 Layout: a turned inline picture with no recorded room for the turn keeps its line to its frame, as in Word (#5448)
A file that turns an inline picture but writes no wp:effectExtent (or a zero
one), as other tools do, makes Word keep the picture's line to its frame and
let the turn overhang it, cut at the line's top and bottom: the frame as it
is in Word 2007 layout (compatibilityMode < 15), with width and height
exchanged from 45 to 135 and 225 to 315 degrees in Word 2013+ layout.
WordCraft always reserved the exact rotated bounds (Float::spin_pad), which
is what Word's own files encode through their effect extents, so the text
after such a picture sat up to 34 pt too low (docxide-pdf
standalone-picture-compat12-* / compat15-angle45/135).

The docx reader now marks such objects (Float::bare_turn, cleared when the
object is turned again); the inline room comes from Float::line_pad, which
display uses too to place the frame in it, and the line clips the picture's
overhang. The writer keeps writing no turn overhang for them, so a save
round-trips. Word-saved pictures are unchanged.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:34:00 +01:00
Harrison Booth 6d0bd73127 Preserve paragraph text direction in DOCX (#5392)
* WC6-07: preserve paragraph text direction

* Resolve gaps summary merge

* WC6-07: retain paragraph direction variants

* WC6-07: preserve strict paragraph direction

* Update lockfile for DOCX test dependency

* Close paragraph direction implementation

* Format paragraph direction imports
2026-11-08 04:31:23 +01:00
Dylan M. TaylorandMartin Ball cd81d5da49 XLSX import: index worksheet relationships by id (#5424)
* fix(formats): index XLSX worksheet relationships by id

* style: rustfmt

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:28:57 +01:00
b7c2fe8dff DOCX: a VML text box keeps its text direction (layout-flow) (#5193) (#5346)
Compatibility-mode documents store text boxes as VML only. The reader
ignored v:textbox's text direction, so a header text box turned 270
degrees was drawn level. It now reads it as Word for Windows does:
mso-layout-flow-alt:bottom-to-top is vert270 whatever layout-flow says;
otherwise layout-flow:vertical or vertical-ideographic is vert; anything
else stays horizontal.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:28:39 +01:00
Marc 586f85df96 fix(ui): the File page scrolls across its full width, with the bar at the window edge (#5282) (#5320)
* fix(ui): the File page scrolls across its full width, with the bar at the right edge (#5282)

Turn off horizontal auto_shrink on the File page ScrollArea so the scroll area expands across the full CentralPanel width. This positions the vertical scrollbar at the right edge of the window and allows mouse wheel scrolling over the entire panel width.

* fix(ui): the File page's scroll area reaches the window edge (#5282)

The side padding moves from the panel into the pages, so the scrollbar sits at
the edge and the wheel also works in the rightmost 40 points.

* test(ui): assert the File page's scrollbar sits at the window edge (#5282)
2026-11-08 04:26:26 +01:00
Dylan M. TaylorandMartin Ball bd8bc96cb5 Layout: look at the text after a decimal tab once, not once per cluster (#5343)
* Layout: look at the text after a decimal tab once, not once per cluster

Text after a decimal tab is shifted after each of its clusters, and each
shift walked the text again from the tab to find its first '.'. A long run
without a '.' (soft hyphens take no room, so it stays on one line) made a
single paragraph's line breaking quadratic. The scan now carries on from the
cluster it reached.

* address review: layout progress doc for decimal-tab scan

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:15:42 +01:00
Fin WarmanandMartin Ball 7830333c34 Layout: a page break before applies at a continuous section's top mid-page (#5287)
* Layout: a page break before applies at a continuous section's top mid-page

A continuous section moves the column top to the current line, so at_top() was true mid-page and the paragraph's page break before was skipped. Check fresh() instead, as Word does.

* 1485: record continuous-section page-break parity

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:15:24 +01:00
Dylan M. TaylorandMartin Ball e349439bb6 fix(pdf): leave the author out when personal information is removed (#5452)
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:13:03 +01:00
ReesandMartin Ball 4c8c1f1268 Find: match smart single quotes from a keyboard apostrophe (#5388) (#5401)
* M2.1: find smart-quoted words with a keyboard apostrophe (#5388)

* M2.1: align Find parity status and regression docs (#5388)

---------

Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:10:50 +01:00
caeb87c754 Shapes in Strict Open XML documents open (#3548) (#5697)
A Strict document opened with its text and no shapes at all. Strict puts
shapes, groups and canvases (wsp, wgp, wpc) and their parts in its one
WordprocessingDrawing namespace, which the reader maps to wp:, while the
drawing reader looks for Word 2010's wps:, wpg: and wpc: names. Word also
writes a Strict text box's text in wne:txbxContent.

The XML reader now names those elements as Transitional documents do,
from the element and its parent (xml::element_prefix), and reads a text
box's content from wne:txbxContent and from the schema's own
txbxContent. Saving writes them as Transitional.


Claude-Session: https://claude.ai/code/session_01LvG6E1XTfHtZDbfYwfREhR

Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 04:08:36 +01:00
0f5a701171 #5469: Size keeps the side it isn't asked to change (#5474)
* #5469: Size keeps the side it isn't asked to change

Typing only the width of a straight line (height 0) gave it a 1 pt
height: picture.size fitted both sides into 1-4000 pt, also the one the
call never set. A side the call leaves as it was now keeps its size,
a line's 0 and a side past 4000 pt from a file too.

Sides set in proportion (the original size, or a scale or one side
typed with the proportions locked while both sides have a length) still
fit the limits together, even when one of them comes out as it was;
any other side the call sets fits on its own.

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

* #5469: the table test checks exact sizes

Where both sides follow each other the test only checked that they
stay within 1-4000 pt and keep their proportions. The rule gives one
size there (the one factor nearest 1 that fits both), so each case now
checks that size, worked out by hand.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:57:17 +01:00
Valentin KleinandClaude Sonnet 5.5 677e610886 Tests: the file name field updates on Save As and Rename (#3796) (#5308)
Pin that FILENAME and FILENAME \p in a header show the new name and path after Save As, that a Word-style FILENAME \* MERGEFORMAT field drops its cached name, and that Rename is picked up by Update Field. No production code changes.

Refs #3796

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 03:55:02 +01:00
d2a2709e14 Pictures: a linked picture shows its link on hover and follows it on Ctrl+click; DOCX keeps the link on the drawing (#5267) (#5295)
* #5267: a linked picture shows and follows its link; DOCX keeps it on the drawing

A link put on a picture from its context menu was stored, but nothing showed
it: hovering the picture gave no tip, and Ctrl+click picked the picture for a
copy instead of following the link. Now the hover tip and Ctrl/Cmd+click work
on a linked picture, shape or text box, in line or floating, as on linked text.

DOCX: Word keeps a picture's hyperlink on the drawing (wp:docPr/a:hlinkClick,
ECMA-376 Part 1 20.4.2.5). It is now read (a w:hyperlink around the run wins)
and written beside the w:hyperlink, sharing one relationship. A link to a place
in the document stays the w:hyperlink w:anchor only.

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

* #5267: ink and Office 2016 charts take their drawing's link; Ctrl+click over text on a Behind Text picture stays a text click

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

* #5267: rustfmt

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

* #5267: a chart or ink takes the link of the choice it was read from; Ctrl/⌘+click in the docs

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

* #5267: the decoy choice in the chart-link fixture needs a namespace no reader knows

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

* #5267: inside a linked text box, hover and Ctrl/⌘+click take the box's link

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

* #5267: the picture-link test sets the image's fx after merging main

Main gave InlineObject::Image picture effects (fx); the round-trip test this PR adds built
an image without them and no longer compiled.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:50:38 +01:00
050b6b0da3 Arabic letters join across a change of formatting (#4093) (#5691)
* Arabic letters join across a change of formatting (#4093)

Each run of a paragraph was shaped on its own, so a letter at a run's edge
took the form of a word's start or end. A coloured letter stood apart from
its word, and a word edited with Track Changes on read with a space in it
(ال ذكاء).

Runs are now shaped with the text shown on either side of them, through the
shaper's pre- and post-context (fonts::ShapeContext). Runs left out of the
layout (hidden text, deletions in a final view) are skipped, so letters join
what is on the page. Only paragraphs with a joining script do the work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk

* #4093 review: no letter joins a drop cap formatted on its own

A drop cap letter in a run of its own (coloured, say) was still passed to
the next run as the text before it, so that run's first letter joined the
drop cap, which is drawn apart from the word. The shown text now has a floor
past the drop cap, and no run takes context from before it. The drop cap
test also covers the letter in a run of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk

---------

Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:44:01 +01:00
Dylan M. TaylorandMartin Ball 6b163b33dc xtask: no symlink walks, exclusive test dir; Paste Special picture files must be regular files (#5471)
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:39:35 +01:00
ReesandMartin Ball 4ba444a342 Table Sort: validate explicit columns (#5531) (#5533)
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:39:17 +01:00
4821a13ee0 Font menus: entries are drawn from a few shared textures, so Windows no longer shows them on black, white or red strips (#5105) (#5111)
* #5105: font menu entries are drawn from a few shared textures

Each font menu entry was a texture of its own, created and freed as the list
was browsed: on Windows (DirectX 12) the entries showed other textures' memory,
black strips in light mode, white or red ones in dark mode, as the Equation
tab's pictures did before #1002. They now share an atlas written in place, the
one the equation pictures use (MathAtlas, now PreviewAtlas).

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

* #5105: font menu entries are drawn again in every window once fonts change

The font atlas remembers the font database generation its entries were drawn
with and starts over when it changes, so another window's entries and a removed
font's entry (Recently Used Fonts) don't keep an old face.

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

* #5105: the atlas test doesn't compare with the shared font generation

Review: tests running alongside add and remove fonts, so the database's
generation may change between the draw and the assertion.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:36:54 +01:00
852e67d587 A template opened from the file manager starts a new document (#3433) (#5698)
A Word template (.dotx, .dotm, .dot) the platform hands the app, from a
double-click in the file manager, the command line or the Finder, opened
as itself: the window called it saved, AutoSave was on, and Ctrl+S wrote
over the template. Word's default verb for a template makes a new,
untitled document based on it.

WordApp::open_from_platform sends a Word template to file.newFromTemplate
and opens anything else as File > Open does; main.rs (also for a name that
isn't valid Unicode) and the Apple Events handler use it.
file.newFromTemplate reads the template as file.open reads a file
(password, data, an empty file) and, with replaceBlank, takes an untouched
blank document's place, so a launch shows one window. The Password box
retries the command that asked for it, so a protected template still
starts a new document. File > Open and drops still open a template itself.


Claude-Session: https://claude.ai/code/session_01LvG6E1XTfHtZDbfYwfREhR

Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:25:17 +01:00
9b6e9564a2 Lists: a numFmt none level shows its literal text, so RESPONSE: labels the paragraph (#4985) (#5055)
* #4985: a numFmt none list level shows its literal text

Word labels a paragraph whose level has numFmt none and lvlText RESPONSE:
with RESPONSE:. Counters::next_label returned no label for any none level;
now only a none level of placeholders shows nothing. Its own %n stays
empty, and it still doesn't count. Numbering::shows_label agrees.

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

* #4985: rustfmt

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

* #4985: a "none" list level counts, and only its own number is empty

Taken from MeHail-02's numbering-literal-none branch (46405e66), checked in Word
for Windows: a none level is an ordinary level whose own number reads as
nothing. It counts and restarts the levels below it, stays empty in legal
numbering, and an empty label still tabs the text to the indent unless it
already ends there (the Chapter 1 list's Heading 2 stays at the margin).

Co-Authored-By: MeHail <mihailsmetanin15@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #4985: STYLEREF in full context keeps a none-level heading's parent

Review: a none level now has a label, so its headings reach the style index,
which leaves the none level out of the levels it keeps; \w and \r took the last
kept level for the heading's own and dropped the parent. Levels now carry their
index, and the levels above are picked by it.

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

* #4985: STYLEREF number switches on a none-level heading

Review: in full context an empty label of its own added a trailing period (3.),
and the suppress switch gave a none level's label (Note), which holds no number,
back whole. An empty own part is left out, and a none level's label with no
number is suppressed to nothing.

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

* #4985: STYLEREF \s and \t on none-level labels

Review: \s took a none level's label (Note) for a chapter number; the heading's
count stands in, as for an unnumbered heading. \t matched any ancestor's number
inside a word (the a of Chapter); only the numbers the label's template shows
count now.

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

* #4985: STYLEREF takes a label's numbers by their spans

Review: \s took the last word of a label (Response of "3 Response") and fell
back on the heading count for a none level showing its parent's number (%1);
\t kept text attached to a number (Section3). A label's numbers are now the
spans of the values its template shows, with their delimiters (a letter number
only standing apart from other letters); \s takes the last of them.

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

* #4985: tests edit the numbering through Arc::make_mut after merging main

Main shares the numbering between undo steps (#2694), so `Document::numbering` is an
`Arc<Numbering>`; the tests this PR adds wrote to it directly and no longer built.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: MeHail <mihailsmetanin15@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:20:49 +01:00
eb100edfd6 Linux: ignore an AppImage environment inherited from another program (#5653) (#5690)
* Linux: ignore an AppImage environment inherited from another program (#5653)

An AppImage app that opens a document through xdg-open passes its APPIMAGE,
APPDIR, ARGV0 and OWD on to the WordCraft it starts. The self-update trusts
APPIMAGE, so an installed WordCraft replaced the other app's file with its own
AppImage.

The app now starts over without those variables, first thing in main, unless
its executable is under APPDIR. The update then goes through the package or
plain file this copy really is, and a real AppImage keeps its variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk

* #5653 review: turn the self-update off when starting over fails

If the process can't start again without another program's AppImage
variables, it still carries APPIMAGE, and an accepted update would replace
that program's file. The app now turns its self-update off for that
process: no update is offered, and Help > Check for Updates says why. The
app still opens, so the document the user asked for isn't lost.

Also documents the test helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk

---------

Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:09:52 +01:00
c38dc17ba9 Search: the title bar's box takes the typing in place, its results dropping down under it, as in Word (#5398) (#5475)
* #5398: the title bar's Search box takes the typing in place, its results dropping down under it

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

* #5398: Search in Focus opens the dialog, and Search whose box is gone doesn't keep Escape

Review: Focus hides the title bar's Search box, so Alt+Q on the frame Focus comes on opened
Search in a box that wasn't drawn, and that hidden Search kept Escape from leaving Focus.

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

* #5398: Search takes the keys from its first frame; Enter on a result isn't also the new dialog's OK

Review fixes:
- The Search box is focused before it reads the frame's input, so keys typed on the frame
  right after Alt+Q go into it (they were lost). Alt+Q's own text is still swallowed: Alt is
  read from the shortcut's key event too.
- Enter that runs a result is used up, so a dialog the result opens (Word Count) doesn't take
  the same Enter as its OK and close at once.
- The Focus test opens Search with the command Alt+Q runs, so it also runs on macOS, where
  Option+Q types a character (the test failed on the macOS runner).

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

* #5398: Tab moves from the Search box to its results, and Enter runs the focused one

Review: the box took the focus back every frame, so Tab couldn't reach a result, and Enter
always ran the first hit. The box now takes the focus back unless it is on one of its results
(read from last frame's rects, so the page taking it is still undone), and runs the first hit on
Enter only while it has the focus; a focused result takes Enter or Space itself. Either way the
Enter that ran a result is used up, so the dialog it opens doesn't take it as its OK.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:07:24 +01:00
5997aef2f9 ODT: a file's font declarations are kept (gap 19) (#5720)
* ODT gap 19: a file's font declarations are kept

WordCraft declared each font itself, guessing its kind, in place of the file's
declaration. A reader without the font picks its substitute by that, so on a
computer without it a document came back in LibreOffice from a save with no
edits reflowed, 70 pages instead of 83. A font the file declares stays declared
as the file does.

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

* ODT: font declarations in single quotes and their name spaces are kept (gap 19)

Single-quoted names weren't recognised, so the declaration wasn't replaced and
could be added twice. A prefix only the declarations' container declared was left
unbound in the saved part. Found by CodeRabbit on this PR.

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

* ODT: a font name spelled another way in XML is the same declaration (gap 19)

Declarations were matched on their raw text, so O'Brien and O&#39;Brien counted as two fonts and one was declared twice. They are now compared as they read.

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

* ODT: a face named other than its family gives that family the file's declaration (gap 19)

A face named F1 of the family Mystery: text written from the model names Mystery, and WordCraft's declaration of it kept a guess at the font's kind. It now says what the file's says. Faces are matched in one walk, and namespace prefixes are tracked in a set, as a hostile file with many on its top-level elements could stall import.

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

* ODT: font faces are found by parsing and renamed at their own attribute (gap 19)

The walk matched text, so a kept script reading like a declaration could be rewritten. It now takes the declarations the parser finds. A rename uses the position of style:name itself, not a search that another attribute with that in its name could mislead.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:05:10 +01:00
Zinadin Zidan 73356ed82a Styles gallery: reads from the right in a right-to-left interface (#5566) (#5708)
In the mirrored Arabic interface the Home tab's Styles gallery still laid its
tiles out left to right with the arrows on the right, so Normal sat at the
far end. Tiles now start at the right edge and the up/down/more arrows sit at
the left; left-to-right is unchanged. The "more" button gets the Styles Pane
label it opens, so screen readers can name it.
2026-11-08 03:02:42 +01:00
6d3b51fab1 Decode HTML character references written without ';' (#4820)
Found while testing crates/formats/src/html/entity.rs:250-251 (unescape)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 03:00:23 +01:00
TheRenegadeDevandClaude Opus 5.5 750e26657e Navigation pane: test that the tabs fit the narrowest pane in every language (#4482) (#4883)
* #4482: test that every Navigation tab fits the narrowest pane in every language

The new kittest draws the Navigation pane body at the 200 pt minimum width with the
longest translation of each tab from craft-i18n (Überschriften, Страницы, Eredmények)
as a fixture language, so it runs in CI without the catalogs. With the catalogs built
in, it also checks every interface language. Each tab must lie inside the 178 pt body.

Main already wraps the tabs onto a second row, so the test passes there. It fails on
a single-row tab strip that doesn't wrap, such as the one in #4480.

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

* #4482: draw the tab test in the app's style; catch a stale fixture

The test now applies the app theme with the fonts. With the catalogs built in, it
also fails when any language's tab label is wider than the fixture's, so a
craft-i18n bump that brings a longer label asks for the fixture to be updated.

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

* #4482: the tab test fails when a tab elides its label to fit

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

* #4482: the tab test puts English back even when an assertion fails

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

* #4482: the stale-fixture failure says which label to put in the fixture

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:00:05 +01:00
f7e060ac3f Escape a heading's trailing '#' on Markdown export (#4808)
Found while testing crates/formats/src/markdown/export.rs:377 (write_blocks, Kind::Heading arm)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 02:57:32 +01:00
b61aee61ba Styles pane: the scroll bar no longer covers the a / ¶ markers (#4753) (#4882)
* #4753: Styles pane scroll bar keeps clear of the a / ¶ markers

The Styles pane list used egui's default floating scroll bar, which is drawn
over the right edge of the rows; on hover it widens to 10 pt with a translucent
track and covers the right-aligned "a" and "¶" style-type markers. The list now
uses a solid scroll bar, which takes a column of its own beside the rows (as in
the TOC options dialog and vectorcraft#2232's menus). Its handle keeps the text
colour: the solid preset fills it with the button colour, which is white on the
pane's white track and would leave the handle invisible.

The regression test draws the pane with the app theme, hovers the bar and
checks that no scroll bar rectangle meets a marker and that the handle stands
out from the track.

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

* #4753: Styles pane scroll bar test hovers the bar and checks dark too

The test now takes the hover point from the pane's own right edge instead of
the harness size (kittest insets the UI, so the old point sat just outside the
bar), asserts the pointer is on the bar, and runs in the dark appearance as
well as the light one.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 02:55:02 +01:00
bc0f691c13 Layout: a picture lowered past its height lowers its line as far, as in Word (#5796)
* Layout: a picture lowered past its height lowers its line as far, as in Word

A picture lowered further than its own height had its lowering cut at that
height. Word keeps its top on the baseline but lowers the line as far as the
position says: a 6 pt picture lowered 30 pt makes a 30 pt line. Exact spacing
keeps the line's baseline. Closes the gap noted with #3984: all 156 measured
lines now match Word.

* Lowered pictures: an exact-spacing test; gaps and progress log

An exact-spacing line keeps its baseline when a picture is lowered past its
height (the test fails without the cap). Gap 4 and the progress log note the
change.

---------

Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 02:54:47 +01:00
cac121fd89 Shading on text selected inside a paragraph shades the text, so No Color takes off an orange shading (#4685) (#4702)
* #4685: Shading on text selected inside a paragraph shades the text

Home › Paragraph › Shading always set the paragraph's shading. Word shades the selected
text when the selection is inside one paragraph (w:shd in the run properties), and the
paragraph for a caret or a whole paragraph. So an orange text shading from a .docx (which
Text Highlight Color › No Color leaves, in Word too) couldn't be taken off from the ribbon.

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

* #4685: review — a paragraph's whole text is text unless it's the last; a selected object isn't text

A selection with a paragraph's mark ends at the next paragraph's start, so 0..len of a
paragraph that isn't the last of its story, cell or box is text only and No Color clears its
shading. A selected text box or picture (its object character) shades the paragraph as before,
instead of every line of the box.

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

* #4685: review — a selected field is text; Apply to: Paragraph shades the paragraph

Only a selected picture, shape or text box (`object_selection`) keeps Shading on the
paragraph; a field's object character is text, so No Color clears its shading. The Borders
and Shading dialog's Apply to: Paragraph passes `paragraph: true`, so it shades the
paragraphs even for text selected inside one.

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

* #4685: ui-parity wording (CodeRabbit): text shading applies to part of one paragraph

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

* #4685: ui-parity wording (CodeRabbit): removes text shading imported from a file

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

* UI parity: status line after merging main

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

* #4685: a test of Shading in a cell's last paragraph

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-08 02:52:26 +01:00