While landing community PRs, a few rows in gaps.md, layout-parity.md and
file-format-parity.md ended up twice (an older and a newer version).
Each pair is now one row: the newer text, plus what only the older one
said (section note options #385; allowSpaceOfSameStyleInTable).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Clicks on ribbon chrome that isn't a command button (Styles gallery tiles, tab
names, empty ribbon space, View check boxes, status-bar items) left the page
without keyboard focus. The click was caught at the end of the frame, after the
page had been drawn: want_focus was only picked up next frame and the page's
focused flag, which gates the keys, not recomputed until then either, so the
keystroke right after the click went nowhere.
Claim the keyboard in the same frame a ribbon command button does, using the
page's widget id from the frame just drawn.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word for Windows (checked by hand): Shift+Backspace erases like
Backspace, Shift+Delete cuts the selection, and Shift+Delete with nothing
selected does nothing. On Windows egui-winit already turns Shift+Delete
into a Cut event, so WordCraft matched. On macOS and Linux it arrives as
the Delete key with Shift, no shortcut was bound to it, and it did
nothing, even with text selected.
The canvas now runs Cut for Shift+Delete when there is a selection,
through the same clipboard path as Mod+X, and does nothing otherwise.
Shift+Backspace already erased (#707).
Tests: Shift+Backspace erases, Shift+Delete without a selection changes
nothing, Shift+Delete on a selection cuts it to the clipboard (fails
without the change); the key check accepts Shift alone.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A table with repeated header rows that started near the bottom of a page left the header rows
there alone when the first row after them didn't fit, then repeated them on the next page above
that row. Word starts such a table on the next page.
place_table now checks, before the first row, whether the row after the header rows can start
under them (whole, or split when it may split). If it can't and the page already holds
something, the table starts on the next page. A table at the top of a page keeps its header
there as before.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The active ribbon tab was saved to ui.json and restored verbatim at startup,
so relaunching resumed whichever tab was last used (Review, Help, Draw).
Word always opens on Home. apply_prefs now forces DEFAULT_TAB on load.
Going File and back to the editor still returns to the tab that was showing,
contextual tabs included; tab is still written to ui.json, so the UiState JSON
contract (MCP/control) is unchanged. resolve_tab uses the same DEFAULT_TAB so
the startup default and the contextual-tab fallback can't drift.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* engine: store replacement images under their true format extension
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Update Table on a TOC read from a file set the page-number tab at the text width of the
caret's section (after Select All, the last one: a landscape appendix pushed level-one
leaders past the margin), kept the style's number tab stops so a short entry or the last
line of a wrapped one stopped there without its leader, and left the paragraph that held the
field's start empty under the TOC heading. The width now comes from the TOC's own section,
entries without a tab of their own clear the style's left/centre/decimal stops before the
page-number stop, and a range that filled its own paragraphs under a TOC heading puts the
field at the end of that heading, where Table of Contents puts it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With no system fonts (a headless CLI), Arabic and Persian text drew as
empty boxes: the document font database only loaded the craft-fonts
Japanese and Chinese faces. Load every non-Japanese craft-fonts face.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* RTF import: Mac OS Roman text (`\mac`, `\fcharset77`) (#1653)
A Macintosh RTF document (`{\rtf1\mac …}`) had its `\'xx` bytes read as
Windows-1252: LibreOffice's fdo85889-mac.rtf showed "ñòó" instead of
"ÒÚÛ". Mac OS Roman (code page 10000, with the euro sign at 0xDB) joins
the single-byte code pages, and the reader uses it for `\mac` documents,
fonts with the Mac charset (`\fcharset77`) and `\cpg` / `\ansicpg` 10000.
Tests: the code page table (accents, euro, quotes) and two RTF cases, the
issue's file and a Mac-charset font in a Windows document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* RTF import: DOS code pages (`\pc`, `\pca`, OEM `\cpg`) (#1653)
With the OEM tables from #1146 now on main, the RTF reader decodes DOS
text too: `\pc` documents as code page 437, `\pca` as 850, a font's
`\cpg` 437/737/850/852/857/866 and `\fcharset254` (PC 437).
`encoding::code_page_byte` decodes a byte of document text in any code
page RTF names (Windows, OEM or Mac OS Roman; ASCII below 0x80 stays
ASCII), and `is_byte_code_page` says which those are.
`SINGLE_BYTE_CODE_PAGES` stays Windows-only, so `text.typeCode`'s `ansi`
pages are unchanged.
A `\cpg` / `\ansicpg` number out of the u16 range is now ignored; before,
`pv as u16` truncated it to another code page (65973 read as 437).
Tests: document text in DOS, Mac and Windows pages; `\pc`, `\pca` with a
`\cpg437` font, and an out-of-range `\ansicpg`.
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>
A shape anchored in a text:p (draw:custom-shape, draw:rect, ...) had its
own text:p read as a body paragraph. That closed the paragraph the shape
sits in, and the text after the shape was dropped. LibreOffice's
MadeByLO7.odt lost three of its seven body paragraphs.
Drawing shapes are now skipped with their text, as text boxes already
are. Groups are still walked, so the pictures inside them are read.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
* 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>
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>
- 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>
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>
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>
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>
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>
* 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>
* 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>
* 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>
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>
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>
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>
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>
#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>
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>
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>
* 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>
* 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>
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>
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>
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>