Commit Graph
1028 Commits
Author SHA1 Message Date
Fin WarmanandBrandon Thomas 6528f972db Shapes: draw flowchart OR spokes (#1644)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:21:49 -04:00
8b0194bc23 M12: preserve document properties when importing legacy DOC and DOT (#1640)
* M12: preserve document properties when importing legacy DOC and DOT

M12: preserve document properties when importing legacy DOC and DOT


Integrate current upstream while retaining import behavior and embedded-font loading.

* docbin: as_chunks for fixed-size records (clippy::chunks_exact_to_as_chunks on Rust 1.99); rustfmt

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

---------

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:21:35 -04:00
718eb02b89 DOCX: keep row-level w:tblPrEx when saving (#1637)
Co-authored-by: Larry Hymes <19394645+lhymes@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:21:23 -04:00
87410b4e3f DOCX: keep w:formProt when saving (#1636)
Co-authored-by: Larry Hymes <19394645+lhymes@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:21:11 -04:00
skylar taylor-barrick c3ba9011a2 fix(pdf): preserve shared writer across main updates (PDFW)
Retain the shared writer and its regressions alongside the current PDF text
behavior and the equivalent bounded find-pattern condition.

PR #1558.
2026-10-14 19:18:05 -06:00
Fin WarmanandBrandon Thomas 2c26fa58a8 Draw arrowheads on open freeform paths (#1645)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:04:33 -04:00
Larry HymesandLarry Hymes ed980e4df7 DOCX: keep an explicit horizontal w:textDirection when saving (#1635)
Co-authored-by: Larry Hymes <19394645+lhymes@users.noreply.github.com>
2026-10-14 21:04:15 -04:00
Larry HymesandLarry Hymes 98cda1ea50 DOCX: keep a bare w:pgNumType when saving (#1634)
Co-authored-by: Larry Hymes <19394645+lhymes@users.noreply.github.com>
2026-10-14 21:04:04 -04:00
116e7697a1 View Gridlines outlines header and footer tables (#1628) (#1633)
* View Gridlines outlines header and footer tables (#1628)

Table Layout › View Gridlines drew only the body's table cells; a table
in a header or footer showed no gridlines. Walk the page's header and
footer items too.

Fixes #1628

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test: Placed::Cell names its clip flag (#1617)

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:03:53 -04:00
6eeb6d10db A floating object anchored after a page break stays on the page its paragraph starts on (#1632)
Word's cover pages are one paragraph holding a page break and then the
page-anchored text box with the title and the approval table. Word
draws a floating object on the page its anchor paragraph starts on;
WordCraft drew it with the line holding the anchor, which after the
break is the first line of page two, so the cover was blank and the
title box sat over the body text of page two.

`place_objects` now places a paragraph's floating objects from its first
piece, whichever of its lines holds the anchor, and none from later
pieces; inline objects still go with their own line. The wrap areas were
already registered from the paragraph's first page, so nothing changes
for text flowing around the object.

Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 21:03:41 -04:00
a8c1c228d7 i18n(it): the interface audit checks Italian; Privacy Settings wording; envelope label test (#1627)
- Envelopes: the size list was drawn as English text (`#10   4.13 × 9.5 in`)
  with a decimal point in every region. The label is now a catalog template
  (`{name}   {height} × {width} in` / `mm`) filled with the region's decimal
  separator; ru, hu, tr, bg, pl and it carry it. Regression test included.
- Italian: the 18 strings the interface audit looks up that it.tsv lacked
  (Reading Mode bar, Go To, command search, status and command messages);
  `Privacy Settings` no longer says "current".
- The audit's catalog-completeness check covers Italian too.
- docs/localization-parity.md: status line, Italian note, revision history.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:43:13 -04:00
Yasas SriandBrandon Thomas 4ad625eef1 Escape selects a text box or WordArt, so Delete removes it (#1538) (#1604)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:39:24 -04:00
63ed4daab2 Fonts: a Bidi theme slot names the theme's complex-script font (#1619)
w:asciiTheme="minorBidi" (and majorBidi, and w:cstheme) read as the
theme's Latin font, so a template's titles came out in Calibri where
Word draws Arial. Measured in Word: the slot names the theme's a:cs
font, and an empty a:cs names nothing, so the font inherited from the
style applies.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:35:37 -04:00
74a279d5f2 Fonts: a missing roman font is drawn in Cambria, as in Word (#1613)
WordCraft chose a substitute for a font that isn't installed from its
name alone, so an unknown brand font always came out sans. Word reads the
font table instead: measured with the same missing font, it draws it in
Cambria when w:family is roman and in Calibri when it is swiss or auto.
A report whose font table calls its brand font roman got a sans footer
where Word draws serif.

The font table's roman fonts are read into the settings, and layout looks
a missing one up as Cambria (Caladea where Cambria isn't installed). The
other classes keep the existing substitutes, which already start with
Calibri's metric clone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:31:42 -04:00
fe9d560b49 Layout: paragraph spacing adds up when HTML auto spacing is off (#1612)
space after and the second's space before, as Word does by default. Word
adds them instead when the document's compatibility options include
w:doNotUseHTMLParagraphAutoSpacing. Measured in Word with five paragraphs
of 12 pt before and after: 24 pt between them with the option, 12 pt
without, the same in compatibility modes 14 and 15. A 60-page report with
the option lost 6 pt per paragraph, about 60 pt on its cover.

The option is read, laid out in the body and in boxes, and saved back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:29:43 -04:00
f3f110b442 Layout: an inherited footer sizes its percentage tables by its own section (#1620)
A header or footer a section inherits (Same as Previous) keeps the
percentage widths of its tables relative to the section that defines it.
Measured in Word: a 100%-wide footer table defined by a portrait section
stays 451 pt wide, centred, on landscape pages that inherit it, and spans
the landscape width only when that section names the footer itself.
Paragraph alignment in the same footer follows the landscape page.

Found on a report whose landscape calculation pages drew their footer
table full width where Word draws it at the portrait width.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:25:49 -04:00
6d5cc9ef84 Layout: an empty section mark stays on a full page (#1614)
* Layout: an empty section mark stays on a full page

An empty paragraph that carries a section break, when the next section
starts on a new page, needs no room of its own in Word. Measured: 50 lines
filling a page, then that paragraph, then the next section makes two pages
in Word; WordCraft made three, the middle one blank. A plain empty
paragraph in the same place still moves on, in both.

Found after a landscape drawing page in a 60-page report.

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

* Layout: drop an unneeded mut in the section mark test

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:23:15 -04:00
1b60773196 Tables: a row with less room than its minimum height moves on whole (#1618)
* Tables: a row with less room than its minimum height moves on whole

Measured in Word with about 49 pt left on a page: a table row of eight
lines splits there, but the same row with an at-least height of 63.6 pt
goes to the next page whole, and so does one holding a nested table.
Without the height both split, as before. WordCraft split them all, so a
report's title row (a logo beside a small nested table) started at the
foot of its cover page.

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

* Tables: preserve splitting for oversized minimum-height rows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:19:27 -04:00
fee9b984eb Tables: text running past an exact-height row is cut off (#1617)
* Tables: text running past an exact-height row is cut off

A row with an exact height (w:trHeight w:hRule="exact") keeps that
height whatever its cells hold. Word cuts the text that doesn't fit at
the cell's edge; WordCraft drew it over the rows below, so a certificate's
identification block came out as overlapping lines.

The cell is marked as clipped in layout (Placed::Cell clip), and display
draws its lines inside that cell's clip, on screen and in PDF. Hit
testing is unchanged: the lines stay where they are in the layout.

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

* Tables: keep row clipping compatible with Rust 1.90

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:15:39 -04:00
2444e64a25 Layout: a numbered paragraph opening with a page break takes its number along (#1616)
A heading whose text opens with a page break had its list number drawn on
the line holding the break, so the number stood alone at the bottom of one
page and the heading text started the next. Word draws the number with the
text. The label now goes on the first line after a leading break.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:11:50 -04:00
241743b4ab Layout: a bookmark after a page break takes no line (#1615)
Word writes the next heading's TOC bookmark start into the paragraph that
ends with a page break. The line after the break then held that marker,
so it wasn't the empty mark kept with the break (#932): it went onto the
next page with the paragraph's spacing and pushed the heading 17 pt down.
Lines holding only zero-width markers (InlineObject::is_marker) now count
as empty there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:08:02 -04:00
c2872f23c3 Layout: a page break followed by another page start makes one page (#1611)
A paragraph holding only a page break keeps its mark on the page before
(#932), but its space after still went onto the new page, so a heading
with page break before no longer stood at the top and broke again: a
blank page. The space after now stays with the mark. A next-page section
break carried by such a paragraph also started a second page; the empty
page the break started is now used instead.

Found on a 60-page inspection report: two blank pages Word doesn't have.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:04:14 -04:00
Brandon ThomasandClaude Opus 5.5 3b16face49 CI: the rtTriangle VML fixture draws a right triangle
Follow-up to #1900: the fixture test vml_preset_shapes_are_drawn also
expected the isosceles triangle for VML shape type 6 (#1599).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:00:19 -04:00
Brandon ThomasandClaude Opus 5.5 4570952069 CI: VML shape type 6 is a right triangle in the preset-mapping test
#1599 made VML spt 6 (rtTriangle) a RightTriangle; #1442's mapping test
still expected the isosceles Triangle, so it failed on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:54:31 -04:00
64e430354c Tabs left of the margin stay on a default stop they already sit on (Word) (#1631)
Left of the left margin Word's next default tab stop is the first
multiple of the interval *at or after* the current position, not after
it: its arithmetic truncates toward zero. So a tab that already sits on
a stop, a second tab in a row or a leading tab on a line that starts at
-72 pt, doesn't advance; a tab preceded by text or a space, or a line
starting between stops, moves to the next multiple as usual, and right
of the margin nothing changes.

Measured on Word with synthetic documents (40 positions): "a\t\tX" at
-60 pt puts X at -36 pt where the old rule gave 0; "\tX" starting at
-72 pt leaves X at -72, at -60 moves it to -36; "\t \tX" at -60 reaches
0. Letterheads that line a footer up with spaces and tabs under a
negative indent rely on it: one had its registration number drawn 72 pt
too far right, over a vertical rule. It now lands within 0.2 pt of Word.

Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 19:51:35 -04:00
Marco Arnulfo 39af5b17ca Equations: a multi-letter subscript stays whole after build-up (#1623) 2026-10-14 19:51:24 -04:00
90d8844e44 Shapes: a right triangle keeps its right angle (#1599)
DrawingML's rtTriangle was read as an isosceles triangle, so a cover
page's white corner cut-outs pointed up into a peak instead of cutting a
V into the picture above them. It is now its own shape kind, drawn with
the right angle at the bottom left, wrapped and glued along that outline,
and saved back as rtTriangle. The .doc reader maps msosptRightTriangle (6)
to it too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:51:13 -04:00
952eb57688 Respect the image byte limit during DOC DIB conversion (#1590)
* Engine: the Word 2003 format check reads through io, so commands still never touch files themselves

is_word_2003_file (#1081) opened the file with std::fs, which the file-access guard test
(commands_do_not_touch_the_file_system_themselves) forbids outside io.rs; main failed it.
A new io::read_file_head reads the first bytes inside the read root.

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

* UI: a chrome click counts the page as focused at once, so the next keystroke isn't dropped; the ribbon-focus test passes again

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

* fix: DOCのDIB画像変換でバイト上限を守る

* Drop the duplicate read_file_head left by the merge with main

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: haranishi <223786623+haranishi@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:50:54 -04:00
a3ec498f2a Italian proofing: po', qual è, È and un/un' grammar rules; more elisions in spelling (#1585)
* Italian proofing: po', qual è, È and un/un' grammar rules; more elisions in spelling

- grammar (works without a dictionary): un pò / un po -> un po'; qual'è -> qual è;
  E' -> È; un'altro -> un altro and un altra -> un'altra (also nessun, ciascun,
  qualcun, alcun), gender from the ending with masculine -ista/-ma/-cida... left alone;
  more emphatic doubles allowed (pian piano, poco poco, così così...)
- spelling: more elisions (dov'è, cos'è, anch'io, senz'altro, nessun'altra, n'ha...),
  truncations po'/mo'/be' accepted, pò and qual'è left to grammar (no double marks)
- tests: rule tests, grammar proptests, Italian suggestion proptest, It in mixed-span proptest

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

* Contributors: credit lupant by their GitHub profile name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:50:35 -04:00
2c5249d13e All Caps draws ß as SS, as Word does (#1581) (#1583)
* #1581: WIP

* #1581: All Caps and Small Caps draw ß as SS, one character for the caret

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

* #1581: exact caret tests, small-caps scaling, symbol runs never expand

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

* Docs after rebasing on main

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:50:23 -04:00
da0370c30b Comments: deleting a comment leaves no dangling anchors or replies (#1345) (#1565)
review.deleteComment removed anchors from the body only, so a comment
anchored in a header, footer, note or text box left its CommentStart and
CommentEnd behind, pointing at a deleted definition. It also collected only
direct replies, so a reply to a reply survived with its parent gone.

Anchors are now removed from every story, and the deleted set is the whole
thread below the comment. The thread is collected from a parent-to-replies
map built once, with a seen set, so a parent cycle from a hostile file ends
and a long reply chain doesn't take quadratic time. Undo restores everything.

Regression tests: a header comment (anchors checked in the header story), a
three-level reply chain (with its text parts and Undo), a parent cycle, and a
50,000-reply chain.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:50:04 -04:00
AlikandBrandon Thomas b70ab1a6e7 Save Changes prompt takes the keyboard from the page (#1597) (#1602)
Save got focus only when nothing else had it, but the page keeps focus while typing and claims Tab and the arrow keys, so the prompt opened without a focused button and the keys moved the caret behind it. Save now takes the focus whenever none of the prompt's buttons has it.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:38:54 -04:00
ba1e28382d Layout: page numbers can restart at 0 (#1601)
w:pgNumType w:start="0" was read but laid out as 1: the page counter
was set to one less than the restart and could not go below zero. A
report numbering its cover 0 read "Page 2" on the page Word calls 1.
The restart is now the next page's number itself.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:38:36 -04:00
e9e8af517c Layout: a table's percentage width past 100% is kept (#1598)
* Layout: a table's percentage width past 100% is kept

Word keeps a preferred width over 100% of the text column (w:tblW
w:type="pct" above 5000). A cover-page footer table at 131% with the
left margin as a negative indent spans the page edge to edge in Word;
WordCraft capped it at 100%, so it ended three-quarters of the way across.

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

* Contributors: add Fin Warman

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:38:18 -04:00
rabis7575andClaude Opus 5.5 4175d68be5 Text boxes in tables nested deeper than 16 survive edits (#1339) (#1591)
The model's paragraph walk (each_para / each_para_mut) stopped 16 tables
down, while a .docx opens with tables nested up to 24 deep and pasting can
nest them further. Text box pruning, which runs after every edit, took a
box shown only from deeper for one shown nowhere and dropped its story,
so an unrelated edit lost the box's text. The same walk decides which
boxes a copy takes along and which pasted shapes get stories of their
own, and serves reading order, the next drawing id and note conversion.

The walk now keeps its own stack instead of recursing, so it reaches
every paragraph however deep tables nest, in the same document order and
with no call stack to run out of; a paragraph on its own still needs no
allocation. The signature is unchanged (the depth argument is now
unused), so every caller builds as before.

Tests: a box 17 tables down in an opened .docx survives an unrelated
edit and a save, while a box shown nowhere still goes; boxes 17 and 40
tables down, in a box and in a header are kept, counted in reading order,
and new drawing ids go above the deep shape's; a copied deep table
pastes into another document showing its own box's text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:37:59 -04:00
rabis7575andClaude Opus 5.5 663de991df Flat OPC: reading bounded everywhere in the file, CDATA base64 read (#1353) (#1580)
The Word XML Document (Flat OPC) reader counted only the parts it kept:
a repeated or nameless part, a child of the package that isn't a part
and anything nested inside one were read without being counted, nesting
had no cap, and a part's inline XML was copied before its size was
checked. Every child of the package now counts against the part limit
and every part's data against the part and package size limits, kept or
not; nesting and the elements of each child are capped as the XML tree
reader caps a part's; and data is checked before it is copied.

Base64 in pkg:binaryData was taken from plain text only, so CDATA and
character references were dropped and a picture written that way lost
bytes. Both now read as the same text, and an entity there is an
invalid base64 error.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:37:48 -04:00
AlikandBrandon Thomas 2a1cdbb17f Lists: Russian letter numbering draws а) б) в) instead of digits (#1577)
russianLower/russianUpper showed 1), 2), 3). They now follow Word's 29-letter sequence (no ё, й, ъ or ь; ы kept), then repeat the letter (аа, бб, ааа), as observed black-box in Word.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:37:37 -04:00
8fd4951d76 Layout: widow control moves a three-line paragraph whole (#1575)
With room for two of its three lines at the bottom of a page, a paragraph was
split two and one, leaving a widow; taking a line along would leave an orphan,
so Word moves the paragraph whole. A paragraph continued from the page before
may still take one line along from two (no orphan there).

Co-authored-by: przemek-arch <263159043+przemek-arch@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:37:18 -04:00
rabis7575andClaude Opus 5.5 1b05f9a668 .doc import: notes, headers and text boxes read their text once (#1319) (#1572)
The story tables of footnotes and endnotes (PlcffndTxt, PlcfendTxt),
headers and footers (Plcfhdd) and text boxes (PlcftxbxTxt) were read
one range at a time, and each range only had to run forwards: a table
stepping back to an earlier CP after every story pointed many notes,
header slots or text boxes at the same text, and each was read into a
part of its own (2,000 note references on one story made 1,001 copies
of it). The CPs of a well-formed PLC ascend, so a story that starts
before the end of the last one kept is now dropped (a header or footer
slot is left empty, which links it to the previous section's): every
subdocument CP becomes at most one story, and the stories together
never hold more than the file's text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:36:59 -04:00
divyanshu2074 21f2b938e2 fix(engine): trim arguments when parsing sample text formulas (#1571) 2026-10-14 19:36:47 -04:00
divyanshu2074 8ba285cdef fix(engine): handle multi-byte UTF-8 characters before em-dash in dash_autoformat (#1570) 2026-10-14 19:36:36 -04:00
3cc0c16302 A file name that isn't valid Unicode no longer crashes wordcraft-cli or the app (#1440) (#1444)
* A file name that isn't valid Unicode no longer crashes wordcraft-cli or the app (#1440)

Both binaries read std::env::args(), which panics on a non-Unicode
argument such as a Latin-1 file name. The CLI now reads args_os(): the
file a command opens keeps its real name and opens (convert, info, text,
inspect, render, zotero, run --file), and any other such argument is an
error naming it, exit 1. The app leaves such a file out with a warning,
as it does a file that fails to open, since file.open takes its path as
text; a --control port that isn't valid Unicode exits 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* The app opens a file whose name isn't valid Unicode too (#1440)

file.open takes its path as text but also accepts the file's bytes
under a name, as dropped files use. The app reads such a file, opens
its bytes under the lossy name and keeps the real path on the session,
so Save writes back to it. A unit test covers the open and the save.

The CLI test now checks that every command in OPENS_FILE reaches its
open with the file, and no longer pins `pos` counting a `--page` value.
ROADMAP says which saves go back to the file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1440: skip non-UTF-8 file name tests on macOS

APFS refuses file names that aren't valid UTF-8, so the fixtures fail
with "Illegal byte sequence" there, and the app-start test keeps running
because macOS opens a window without a display variable. The unit test
for the CLI's argument checks and the control-port test still run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:32:48 -04:00
bbdcc7c62f Proofing: status bar flags a language whose dictionary isn't installed (#1167) (#1441)
* Proofing: status bar flags a language whose dictionary isn't installed (#1167)

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

* vi: missing-dictionary status strings

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:32:30 -04:00
aec3f50582 macOS: Word for Mac's keys for Save As, Insert Endnote, New Comment, Replace and the ribbon (gap #24) (#1438)
On macOS five of Word for Mac's default shortcuts were missing or unreachable:

- Cmd+Shift+S is Save As (it opened Apply Styles, which keeps Ctrl+Shift+S
  on Windows and Linux).
- Cmd+Option+E inserts an endnote. macOS takes Cmd+Option+D to show and
  hide the Dock, so the old key never reached the app.
- Cmd+Option+A adds a comment (Cmd+Option+M still works).
- Control+H opens Find and Replace (Cmd+H still works).
- Cmd+Option+R collapses or expands the ribbon (ui.collapseRibbon).

Keys on Windows, Linux and the web are unchanged. Tests pin the bindings
on each platform.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:32:13 -04:00
81b5602bf0 insert.picture and insert.icon leave the caret after the picture (#965) (#1437)
* contributors: add Nael-Nathanael

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* insert.picture and insert.icon leave the caret after the picture (#965)

A script that typed right after inserting a picture replaced it, so the
picture was missing from the saved file. Commands now leave the caret
after the new picture; Picture Format commands still act on it. The app
still selects a picture the user inserts, as in Word.

Fixes #965

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Select a pasted picture again, keep pictures inserted in a row (#965)

Desktop paste inserts through execute, so after the caret change it
left a pasted picture unselected. WordApp::select_inserted_picture
selects the picture just before the caret; run and the paste path call
it, paste once after the last picture so a multi-file paste keeps them
all. Tests for paste selection and for two inserts in a row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:31:55 -04:00
9c57b950b9 Page Setup: Justified vertical alignment (#1320) (#1435)
* #1320: justified vertical page alignment, in DOCX, layout.verticalAlign and Page Setup

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

* #1320: a section's last page takes its own vertical alignment; pin space-after and multi-column cases

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

* Docs after rebasing on main

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:31:37 -04:00
e4e389cdb3 Free part, list and comment ids after a file's largest ones (#1338) (#1428)
New parts, lists and comments took "largest id + 1": after an id of
u32::MAX (a .json model or a .docx can carry one) that overflowed, a
panic in debug builds and a wrap to 0 in release, and list.define's
saturating_add reused the taken id. free_id gives one past the largest,
or the lowest free id once that would overflow; try_add_part,
try_add_list and try_add_levels report ids running out and add nothing,
and the commands that add parts and lists use them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:31:25 -04:00
3370fd1ba2 JSON open repairs hostile run lengths (#1337) (#1412)
Paragraph::normalize now uses saturating sums, moves a run end that splits
a character to the next boundary, and makes the runs cover the text
exactly. Opening a .json document normalizes every paragraph in every story.

Co-authored-by: justinjohn0306 <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:31:14 -04:00
rabis7575andClaude Opus 5.5 4d2649738e Markdown import: brackets no longer turn later links in the paragraph into plain text (#1452) (#1453)
* #1336: Markdown looks up the backtick run closing a code span instead of rescanning the text

Code spans and link brackets searched the rest of the text for the run
of backticks closing a code span. Every run nothing closes (one per
length) cost a scan to the end, and the bracket scan repeated those
searches for each `[` over them, outside its look-ahead budget: 2 MB of
such runs took 2-18 s. The runs are now indexed by length once per text
and found with a binary search; what reads as a code span or a link is
unchanged.

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

* #1452: Markdown brackets no longer turn later links in the paragraph into plain text

The link and image bracket scans share a look-ahead budget per text, so
hostile input stays linear, but it was charged more than they read: a
`[` nothing closes took the whole budget, and every bracket pair and
every link took the rest of the text, as if a destination had been read
to the end. An interval like `[0, 1)`, a dozen references in a long
paragraph or ten links were enough to make every later link plain text.

A scan now pays for the bytes it reads and records the pairs it passes,
and the brackets still open at the end of the text, so later brackets
are looked up. A destination is read, and paid for, only when `(`
follows the `]`; one that parses costs its own length.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:09:07 -04:00
spiritbearlabsandClaude Opus 5.5 a4b335f6a3 Canvas: a redrawn page goes to egui as rendered, without a round trip through straight alpha (#1450)
Each time a page was redrawn (every keystroke on it, a zoom step, a page
scrolled into view) its raster was un-premultiplied (`Rendered::to_straight`)
and handed to `ColorImage::from_rgba_unmultiplied`, which premultiplied it
again. Both `Rendered` and egui's `Color32` hold premultiplied RGBA8, so the
round trip gave back the same pixels after two more passes over the whole
page.

The raster now goes across with `ColorImage::from_rgba_premultiplied`. A
test checks every premultiplied pixel value (each channel from 0 to its
alpha, for each alpha) and every page of the sample document against the
old path: identical.

Measured on an Apple Silicon MacBook Pro (120 Hz, 2x, 100% zoom, a
1632x2112 px page), typing into the sample document: the canvas redraw per
keystroke goes from 17.2 to 11.4 ms on page 1 and from 13.0 to 7.5 ms on
page 2.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:08:56 -04:00