Commit Graph
217 Commits
Author SHA1 Message Date
Valentin KleinandClaude Opus 5.5 4e710a920b Footnote and Endnote options per section (#385) (#390)
- Model: NoteProps (pos, numFmt, numStart, numRestart) in the settings and
  each section; Document::note_options resolves section > document > defaults.
- DOCX: w:footnotePr / w:endnotePr read and written in settings and every
  w:sectPr in schema order; hostile numStart clamped (ECMA-376 §17.11).
- Layout: footnotes below the text, endnotes at the end of each section,
  numbering restarted per section or per page (from where notes land, at
  most three passes), per-section number formats.
- Commands: references.noteOptions takes kind/pos/numFmt/numStart/
  numRestart/scope; references.convertNotes (footnote/endnote/swap, one
  undo step); references.footnote/endnote take a custom mark.
- UI: Footnote and Endnote dialog from the Footnotes group launcher;
  strings in every catalog.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:10:17 -04:00
Valentin KleinandClaude Opus 5.5 f8f1d80c38 Shape Format › Text: direction, alignment and linked text boxes (#369) (#380)
- Model: TextBody on text box parts (vert, anchor, next link); chain
  resolution ignores cycles, self-links and second claimants and caps
  chains at 256 boxes; pruning a box clears links to it.
- Layout: text boxes lay out in their own frame and turn like table
  cells (rotate 90°/270°), stacked text one letter to a line, Align Text
  top/middle/bottom; linked boxes split the first box's story by line,
  each at its own line length.
- DOCX: wps:bodyPr vert/anchor, wps:txbx id and wps:linkedTxbx id/seq
  read and written (ECMA-376).
- Commands shape.textDirection, shape.alignText, shape.link,
  shape.breakLink; ribbon Shape Format › Text group with drawn icons;
  Create Link waits for a click on the target box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:13:07 -04:00
09921dfae8 Right-to-left tables and sections (#362) (#365)
Tables marked right to left (w:bidiVisual) now lay out mirrored: the first
column stands at the right edge, start alignment and indent are measured
from the right margin, and each cell's start margin and border sit on its
right. Merged cells, turned text and nested tables follow; clicks and the
caret follow the drawn geometry while Tab keeps the logical cell order.
Right-to-left sections (w:bidi) run their newspaper columns from the right,
with the line between columns mirrored.

The table direction is read and written in .docx (section direction already
round-tripped). New commands table.rtl and layout.sectionRtl, a "Table
direction" choice in Table Properties ("rtl" in table.properties) and a
"Section direction" choice in Page Setup, translated in every catalog.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 14:00:53 -04:00
c010b73428 Documents keep their own footnote separators and continuation notice (#361)
* Keep and draw a document's own footnote separators and continuation notice (#352)

The docx reader used to drop the special footnotes and endnotes (w:type
separator, continuationSeparator, continuationNotice), so a document's own
separator lines and continuation notice were lost on screen, in PDF and on
save. They are now kept on the document as paragraphs, with the separator
lines themselves as run objects.

Layout draws the document's separator above a page's footnotes and its
continuation separator above notes continued from the page before: the
separator objects become a short line or one across the column, and any
other text or formatting is shown as given. The continuation notice goes
under the part of a note that continues on the next page. The endnote
separator is used above the endnotes. Documents without their own keep the
default lines as before. The separators and notice can't be clicked into.

On save the special notes are written back with Word's ids -1, 0 and 1 (the
notes follow them, starting at 2 when there is a notice) and listed in the
settings' footnotePr/endnotePr. Documents without them keep the default
output.

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

* Merge fix: NoteSeps is Clone (the layout context that holds it is cloned)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:53:08 -04:00
72e59f9f0b Drawing Canvas: insert a canvas and draw shapes, pictures and text boxes inside it (#344) (#356)
Insert > Illustrations > Shapes > New Drawing Canvas (and Draw > Drawing Canvas)
inserts a 6" x 3" canvas in line with text. While it is selected, Pictures,
Shapes and Text Box put the new object inside it. Fit, Expand and Scale Drawing
change its frame, Shape Fill and Shape Outline colour its background and border,
and canvas.member moves, resizes or removes a member.

The canvas is the group object with a canvas style: its frame is independent of
its members, and members outside it are clipped on screen and in PDF (new
Draw::Clip / Placed::Clip). DOCX reads and writes wpc:wpc with wpc:bg,
wpc:whole and wps:wsp / pic:pic members.

A canvas turns and flips as a whole, like a group (#332): its background,
outline and clipped members are drawn inside its turn (Draw::Rotated around
Draw::Clip; Placed::Clip carries the spin, its members sit in the unturned
frame), and hit testing follows the turned frame. The canvas schema has no
transform of its own, so a turned canvas keeps its rot/flipH/flipV as an
a:xfrm in a wpc:extLst extension (urn:wordcraft:canvas-xfrm), which Word skips.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:45:13 -04:00
908a6de2c7 Balance newspaper columns before a continuous section break (#350) (#353)
When a multi-column section is followed by a section that starts
continuous, the columns on its last page are laid out again with the
shortest common height that still holds the text (a binary search on
the column height over at most 12 passes of that page), filled left to
right, and the next section starts right below the tallest column.
Keep-together, keep-with-next, widow/orphan control and rows that can't
split still apply, so the columns may come out slightly uneven. Pages
with an explicit column break, sections ending with a next-page,
even- or odd-page break, and one-column sections are not balanced.
Unequal column widths work. A continuous section now always starts
below the tallest column on the page, not the last one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:34:40 -04:00
589e82e050 Mail merge recipients from Excel and OpenDocument spreadsheets (#334) (#341)
Select Recipients › Use an Existing List… now reads .xlsx, .xlsm and .ods
workbooks besides CSV. A new read-only SpreadsheetML / ODF spreadsheet
reader in wordcraft-formats lists the sheets and turns one sheet into rows
of display strings: shared, inline and rich strings, numbers, booleans,
cached formula results, dates and times as ISO 8601 from the cell's number
format, sparse cells, ODS value types, repeated rows/columns and covered
cells. Zip entries, rows, columns, cells, string lengths and repeat counts
are capped, and nothing recurses.

mailings.recipients takes "data" (base64) and "sheet" and "headers"; a
workbook with several sheets and no sheet answers with the sheet names, and
the app shows a Select Table dialog with "First row of data contains column
headers". The desktop picker, dropped files and the web inbox route
spreadsheets to the recipient list.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:31:25 -04:00
8a2a8bb60d Insert > Icons: an original icon library, a searchable picker and Icon Color (#309) (#318)
Adds wordcraft_render::icon_lib, 61 monochrome icons in ten categories
(Arrows, Communication, Office, People, Nature, Weather, Shapes, Symbols,
Technology, Transport). They are original WordCraft artwork: simple
geometric line drawings designed on a 24x24 grid as path data in code, with
no icon files and nothing copied from another icon set.

insert.icon {"id", "color"?, "size"?} renders the chosen icon with vello_cpu
to a transparent PNG at about 300 ppi and inserts it at the caret through the
ordinary picture path, so it saves to .docx and every other format. Without
an id it lists the library (narrowed by query and category) and asks the
front end for the picker. picture.iconColor repaints a one-colour picture
in a new colour and keeps its transparency.

Insert > Illustrations > Icons opens a dialog with a search box, category
chips and a grid of icons drawn live by the egui painter from the same path
data; Insert or a double click places the selection. Picture Format > Adjust
gains an Icon Color menu. New strings are translated in every catalog.

Catalog parity 393/434 -> 394/434 (90.8%).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:27:42 -04:00
a5b17eb3a4 Enclose Characters: put a character in a circle, square, triangle or diamond (#301)
* Home › Font › Enclose Characters: circle, square, triangle or diamond around a character (#297)

New command format.enclose {"shape": "circle|square|triangle|diamond" | null,
"style"?: "shrink|enlarge", "text"?: string} puts one full-width or up to two
half-width characters in an enclosure, changes its shape, or (null) takes the
text back out. It is stored the way ECMA-376 describes it: an EQ field with the
overstrike switch, eq \o\ac(○,字), with the text as its plain-text result.

Layout now draws EQ \o fields: the arguments overstruck as one cluster
(centred, \al or \ar). An enclosure symbol among them is drawn as a vector
shape around the text in the text's colour, so it never depends on the font
having ○ □ △ ◇; "shrink text" scales the text into a one-em shape, "enlarge
symbol" grows the shape around text at its own size. Screen and PDF both get
it through the display list. Imported Word documents with enclosed characters
used to show nothing (the field has no result); they now render.

.docx: written as Word stores enclosed characters (begin, the code split so the
symbol and the text have their own run sizes, end, no result); the reader
recovers the style from those sizes. The style lives in the model as a trailing
\* enlarge on the code, never written to a file.

Home › Font gets an Enclose Characters button that opens a dialog (Style:
None / Shrink text / Enlarge symbol, Text, Enclosure list, a drawn preview);
scripts with a shape never see it. Strings in every interface catalog.

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

* Merge fix: keep both sides (enclosures and ruby; tests); regenerate parity checklist

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:20:45 -04:00
3042e5251e Phonetic Guide: ruby text (furigana, pinyin) over base text (#296)
* Phonetic Guide: ruby text over base text (#288)

Home › Font › Phonetic Guide (format.phonetic) sets small ruby text
(furigana, pinyin) over the selected text. The ruby is one inline object
anchored at U+FFFC whose plain text is the base, so selection and the
caret treat it as one unit and exports read the base text.

- Model: InlineObject::Ruby with base, ruby, alignment (ECMA-376
  ST_RubyAlign), ruby size, raise, base size, language and the ruby's own
  formatting.
- Layout: the base is drawn in its run's formatting, the ruby above it by
  alignment (centre, distribute letters/space, left, right); the wider of
  the two sets the width and the line grows to fit the ruby. Drawn in the
  shared display list, so screen and PDF both show it.
- DOCX: w:ruby read and written with w:rubyPr (rubyAlign, hps, hpsRaise,
  hpsBaseText, lid), w:rt and w:rubyBase runs with their formatting.
  Previously only the base text was kept.
- Command format.phonetic {"ruby", "align"?, "size"?, "offset"?}: sets,
  edits or (null/"") removes the ruby, undoable; without "ruby" it opens
  the dialog and returns the selection's phonetic guide.
- UI: Phonetic Guide button in Home › Font and a dialog (base text, ruby
  text, alignment, offset, size, Remove), strings in every catalog.

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

* Merge fix: keep both sides' tests

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:12:45 -04:00
dba6e66d53 Choose which side of an object text wraps on (#364)
* Tight and Through wrapping follow the object's outline (#354)

Text now wraps around a Tight or Through object's shape instead of its
bounding box. The outline is the object's wrap polygon (wp:wrapPolygon,
in the 21600-unit space of its frame) when it has one; otherwise it is
derived: a shape's geometry (ellipse, triangle, star, heart... flattened),
the convex hull of a picture's opaque pixels (alpha threshold on a coarse
grid, at most 64 points), or the rectangle. The outline turns and flips
with the object, keeps the left/right and top/bottom distances from text,
and is built once per placed object; each line asks it which stretches of
its band it covers. Through also lets text into gaps inside the outline
that are wide enough.

DOCX reads and writes wp:wrapPolygon (with edited) for wrapTight and
wrapThrough; a shape without one gets its outline written, other objects
their rectangle, since the schema requires a polygon. Square, top and
bottom, behind and in front of text are unchanged.

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

* Choose which side of an object text wraps on (#354)

Square, Tight and Through objects now have a wrap side, like Word's
"Wrap text" choice: both sides (the default), left only, right only, or
the largest side. Left keeps the band to the object's right empty on
every line beside it, Right the band to its left, and Largest picks the
wider side line by line. It works with contour wrapping and with turned
objects, and right-to-left paragraphs mirror it.

The setting is Float::wrap_text, read from and written to wrapText on
wp:wrapSquare, wp:wrapTight and wp:wrapThrough (previously always
written as bothSides). The new arrange.wrapText command
{"side": "bothSides|left|right|largest"} sets it on the selected object
as one undo step. The ribbon's Wrap Text button is now a menu with the
wrap styles and a Wrap Text Sides submenu, translated in every
interface language.

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-11 12:39:42 -04:00
860f4ca260 Tight and Through wrapping follow the object's outline (#354) (#360)
Text now wraps around a Tight or Through object's shape instead of its
bounding box. The outline is the object's wrap polygon (wp:wrapPolygon,
in the 21600-unit space of its frame) when it has one; otherwise it is
derived: a shape's geometry (ellipse, triangle, star, heart... flattened),
the convex hull of a picture's opaque pixels (alpha threshold on a coarse
grid, at most 64 points), or the rectangle. The outline turns and flips
with the object, keeps the left/right and top/bottom distances from text,
and is built once per placed object; each line asks it which stretches of
its band it covers. Through also lets text into gaps inside the outline
that are wide enough.

DOCX reads and writes wp:wrapPolygon (with edited) for wrapTight and
wrapThrough; a shape without one gets its outline written, other objects
their rectangle, since the schema requires a polygon. Square, top and
bottom, behind and in front of text are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:10:48 -04:00
34511263a7 Customize Ribbon and Quick Access Toolbar (#379) (#384)
* Customize Keyboard dialog (#368)

Tools > Customize Keyboard (tools.customizeKeyboard): pick a category and
a command, see its keys, press a combination to see which command uses it,
then Assign / Remove / Reset All. Also reachable from File > Options
(Keyboard shortcuts: Customize...) and command search.

- engine: KeyMap (assigned key -> command, removed built-in keys), consulted
  before the registry's built-in shortcuts; keys compared in one canonical
  form. Saved maps read leniently: junk entries and unknown commands are
  dropped, never failing the rest of the preferences.
- tools.customizeKeyboard: with params (assign/remove/reset/command/key) it
  only changes or reports the map; without params it opens the dialog.
- ui: key dispatch and menu/tooltip shortcut text use the user map; the map
  persists in ui.json; more keys (Q, F1-F12, 4/6/7, Insert) can be bound.
- i18n strings in every catalog; docs/ui-parity, gaps #24, ROADMAP log.

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

* Customize Ribbon and Quick Access Toolbar (#379)

Tools > Customize Ribbon (tools.customizeRibbon): one dialog with two
pages. Ribbon: commands by category next to the ribbon's tabs; show or
hide and reorder tabs, add custom tabs and custom groups (on built-in or
custom tabs) holding any commands, rename, remove, reset. Quick Access
Toolbar: add, remove and reorder its commands, reset.

- engine: RibbonLayout (tab order and visibility, custom groups, the
  toolbar's commands). Saved layouts read leniently: junk, duplicates,
  reserved names and unknown commands are dropped.
- tools.customizeRibbon: with params (hide/show, newTab, newGroup, add,
  remove, rename, move, qatAdd/qatRemove/qatMove, reset, layout) it only
  changes or reports the layout; without params it opens the dialog.
- ui: the tab strip and ribbon follow the layout (custom groups draw the
  commands' own icons, else a lettered tile); the Quick Access Toolbar
  shows the user's commands; right-click any ribbon button to add it to
  (or remove it from) the toolbar or open the dialog; the toolbar's ...
  button opens a menu of common commands and More Commands... (#376);
  File > Options has a second Customize button; layout persists in
  ui.json under `ribbon`.
- Open and Print icons drawn in code; i18n strings in every catalog;
  docs/ui-parity, gaps #24, ROADMAP log, parity checklist.

Fixes #376.

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-11 12:06:17 -04:00
9cf3b6862f Customize Keyboard dialog (#368) (#378)
Tools > Customize Keyboard (tools.customizeKeyboard): pick a category and
a command, see its keys, press a combination to see which command uses it,
then Assign / Remove / Reset All. Also reachable from File > Options
(Keyboard shortcuts: Customize...) and command search.

- engine: KeyMap (assigned key -> command, removed built-in keys), consulted
  before the registry's built-in shortcuts; keys compared in one canonical
  form. Saved maps read leniently: junk entries and unknown commands are
  dropped, never failing the rest of the preferences.
- tools.customizeKeyboard: with params (assign/remove/reset/command/key) it
  only changes or reports the map; without params it opens the dialog.
- ui: key dispatch and menu/tooltip shortcut text use the user map; the map
  persists in ui.json; more keys (Q, F1-F12, 4/6/7, Insert) can be bound.
- i18n strings in every catalog; docs/ui-parity, gaps #24, ROADMAP log.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:00:30 -04:00
41bbaf51bf i18n: add Russian (ru) interface translation (#300)
* I18N.et: add Estonian interface translation

* I18N.et: record the verified headless fork push step

* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

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

* i18n: add Russian (ru) interface translation

- ru.tsv: 1,068 entries covering every interface string, written clean-room
  from the English labels and reviewed by a native speaker. `ru`, `ru-RU`,
  `ru_RU.UTF-8` and other regions resolve to it; File > Options picks it by hand.
- Built-in style names (Normal, Heading 2, TOC 3, Strong…) show in the
  interface language in the Styles gallery, the Styles pane and the Style
  Inspector. Display only: documents, commands and saved files keep the
  English names, and styles the author made keep theirs.
- Rendered previews use the interface language: the Styles gallery samples
  (`АаБбВвГг`) and the Design tab's style-set tiles.
- Catalogs gain a `preview` context for sample text that must fit a small
  tile (`Title` is «Заголовок» as a style but «Пример» in the tiles); it falls
  back to the plain entry, and unknown contexts are still rejected.
- Tests: locale detection and saved preference, full coverage, Cyrillic glyphs
  in the bundled interface fonts, style-name display, catalog contexts.

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

* i18n(ru): translate everything the interface audit finds

Adds 356 Russian entries: the equation gallery (categories, templates and
built-in equations), equation options, themes, watermark presets, paper
sizes, table styles, greetings, outline levels, the strings #313 routes
through the catalog, and the new AutoSave messages. The two rows #305 made
obsolete (📂 Browse…, ➕ New comment) are gone.

The interface audit now also fails when a string it sees translated has no
Russian entry, so Russian stays complete as the interface grows.

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

* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

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

* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

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

* i18n audit: start every ribbon click from a fresh app; translate the table style preview

The sweep pressed Escape after each click, and in an equation Escape leaves it,
so the Equation tab vanished after its first control and the rest of its menus
(Convert, Justification, …) were never opened. Each control now gets a fresh app
set up the same way. The narrower allowance for short text (only icon lettering
and initials) also catches short words.

It found the sample table in the Table Style dialog (Mon, Tue, North, Total, …),
now translated.

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

* #313: catalog rows for the newly translated interface text

Rows in all eight catalogs for the strings #313 now sends through the
catalog (cell alignment tooltips, credits columns and summary, theme names,
context-menu items, table picker, pt suffix, Document1, the Font dialog
sample) and for the Table Style preview's sample table, which the
untranslated-text audit flagged after main's table style editor landed.
The audit runs headless (no host services: no file pickers or speech) in
about 20 s, so it stays enabled.

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

* i18n audit: click the ribbon's bottom row too

The sweep only clicked controls centred between y = 52 and 125, which took in
the tab row and left out each group's bottom row (Equation › Convert and
Justification among them). The band is now taken from the layout: below the
tab row and above the document area.

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

* i18n audit: lay the app out wide enough that no ribbon tab overflows

At 1440 pt the Equation tab ran past the window, so its last controls
(Justification among them) sat behind the ribbon's scroll arrow and were never
clicked. The audit window is now 2560 pt wide.

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

* i18n: add Russian (ru) interface translation

- ru.tsv: 1,068 entries covering every interface string, written clean-room
  from the English labels and reviewed by a native speaker. `ru`, `ru-RU`,
  `ru_RU.UTF-8` and other regions resolve to it; File > Options picks it by hand.
- Built-in style names (Normal, Heading 2, TOC 3, Strong…) show in the
  interface language in the Styles gallery, the Styles pane and the Style
  Inspector. Display only: documents, commands and saved files keep the
  English names, and styles the author made keep theirs.
- Rendered previews use the interface language: the Styles gallery samples
  (`АаБбВвГг`) and the Design tab's style-set tiles.
- Catalogs gain a `preview` context for sample text that must fit a small
  tile (`Title` is «Заголовок» as a style but «Пример» in the tiles); it falls
  back to the plain entry, and unknown contexts are still rejected.
- Tests: locale detection and saved preference, full coverage, Cyrillic glyphs
  in the bundled interface fonts, style-name display, catalog contexts.

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

* i18n(ru): translate everything the interface audit finds

Adds 356 Russian entries: the equation gallery (categories, templates and
built-in equations), equation options, themes, watermark presets, paper
sizes, table styles, greetings, outline levels, the strings #313 routes
through the catalog, and the new AutoSave messages. The two rows #305 made
obsolete (📂 Browse…, ➕ New comment) are gone.

The interface audit now also fails when a string it sees translated has no
Russian entry, so Russian stays complete as the interface grows.

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

* i18n(ru): native-speaker review corrections

Add Note, Move Past Citation, equation display/inline, accents, stacks,
operators, Column Selection, Reading Highlight, ASAP and Tools, as reviewed;
the Accent gallery button wraps onto two lines.

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

* i18n(ru): translate what the improved audit and today's main add

The audit now opens every ribbon menu (bottom rows and the Equation tab's last
controls included), which surfaced Equation › Convert and Justification, and
the Table Style dialog's sample table. Also covers password protection, Manage
Styles, the clipboard pane, Shape Effects and Asian typography from main.

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

* i18n: translate the Table Layout 'Draw Table' ribbon label in every catalog

#308's two-line ribbon label had no catalog rows; #300's Russian audit
(ru covers everything the interface looks up) found it. Rows in all ten
catalogs keep et == es and ru covering zh-hans.

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

* fmt

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

* i18n: Borders and Shading line widths through the catalog ({n} pt)

#313's untranslated-text audit flagged main's new dialog (#327).

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

* land-294: Estonian rows for main's newer dialogs

Define New Multilevel List and Track Changes Options (#342), rotation (#332),
Tabs and Borders and Shading (#327), Columns, Symbol and Field (#335): et
keys equal es again.

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

* i18n audit: run on demand (#[ignore]) until its rare failure under load is understood

It failed once in ~46 runs on a heavily loaded machine and passed 35 straight reruns,
so CI shouldn't depend on it yet. Run it with --ignored.

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

* i18n(ru): restore the review corrections a merge reverted; narrow and hide labels

A merge of older copies of this branch's commits put back the pre-review
wording of 20 entries (Add Note, Tools, the equation accents and operators,
Column Selection, Reading Highlight, ASAP, …) and dropped six that are still in
use (Equation › Convert, Centered as Group, the hyphenation menu's ✓ None).
They're back as reviewed. Also: 1/4 Em Space → «Узкий пробел», and the Draw
tab's Hide Ink button reads «Скрыть».

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

* i18n(ru): restore the reviewed wording a merge rewrote

The same merge reworded 24 reviewed entries (password protection, Shape
Effects, Asian typography, the clipboard pane, the table style sample);
they're back as a native speaker reviewed them. Clipboard Pane keeps
«Область буфера обмена», like the other panes.

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

---------

Co-authored-by: Henno Täht <henno.taht@gmail.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: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:53:41 -04:00
Dylan M. Taylor dded39accf ci: refactor workflows to use storytold/craft-common-actions (#639)
* ci: refactor workflows to use reusable artcraft-common-actions

* ci: point reusable workflows to storytold/craft-common-actions

* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly

* ci: use @main (unpinned) for craft-common-actions with explanation comment
2026-10-11 11:50:38 -04:00
adcbf52611 Move paragraphs, promote/demote, Go Back and Alt+X like Word (#343) (#351)
- para.moveUp / para.moveDown (Alt+Shift+Up/Down, Ctrl+Shift+Up/Down on macOS): the
  paragraphs the selection touches swap with their neighbour in the same story or cell,
  keeping style and list membership; selection follows; one undo step; refused at the
  container edges, next to a table and across a section break.
- para.promote / para.demote (Alt+Shift+Left/Right, Ctrl+Shift+Left/Right on macOS):
  heading level up/down (Heading 1..9), promoted body text takes the level of the heading
  before it, list items change level like Shift+Tab/Tab.
- edit.goBack (Shift+F5): cycles the caret through the last four edit places.
- text.toggleUnicode (Alt+X; unbound on macOS): hex code <-> character, undoable. The
  canvas no longer types the key's letter after an Alt shortcut ran.
- Catalog rows, translations in every interface catalog, typing/UI parity docs, tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:49:02 -04:00
39ab3b5433 Field codes like Word's field keys (#339) (#349)
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.

New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.

The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:22:25 -04:00
eb068d3823 Add Word's common keyboard shortcuts (#330) (#338)
Bind Word's documented default shortcuts that were missing to existing
commands, plus a few small new commands: Reset Paragraph Formatting
(Ctrl+Q), Reset Character Formatting (Ctrl+Space, which used to clear all
formatting), em dash, copyright, registered, trademark and ellipsis, DATE /
TIME / PAGE fields (Alt+Shift+D/T/P), Print Layout / Outline / Draft /
Split views, Word Count (Ctrl+Shift+G), Bookmark, Alt+F7, Ctrl+Page Up/Down
find browsing, Shift+F12 / Ctrl+F12 / Ctrl+Shift+F12 / Ctrl+F2 / Ctrl+F4,
Mark Index Entry, Mark Citation and the mail merge keys.

Where macOS claims Word's key (Cmd+Q quits, Cmd+Space is Spotlight, Cmd+Shift+G
is Find Previous, Option+letter types symbols), the binding follows Word for
Mac or is left off there. F4 Repeat, Q and F2 now reach the registry from the
keyboard, and an Alt shortcut no longer also types its key's character.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:19:59 -04:00
6129bfeee5 Docs: collapse duplicate rows left by parallel merges (#372)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:18:54 -04:00
8fde69767a i18n audit: full ribbon sweep, and the table style sample in the interface language (re-lands #313's last three commits) (#411)
* i18n audit: start every ribbon click from a fresh app; translate the table style preview

The sweep pressed Escape after each click, and in an equation Escape leaves it,
so the Equation tab vanished after its first control and the rest of its menus
(Convert, Justification, …) were never opened. Each control now gets a fresh app
set up the same way. The narrower allowance for short text (only icon lettering
and initials) also catches short words.

It found the sample table in the Table Style dialog (Mon, Tue, North, Total, …),
now translated.

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

* i18n audit: click the ribbon's bottom row too

The sweep only clicked controls centred between y = 52 and 125, which took in
the tab row and left out each group's bottom row (Equation › Convert and
Justification among them). The band is now taken from the layout: below the
tab row and above the document area.

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

* i18n audit: lay the app out wide enough that no ribbon tab overflows

At 1440 pt the Equation tab ran past the window, so its last controls
(Justification among them) sat behind the ribbon's scroll arrow and were never
clicked. The audit window is now 2560 pt wide.

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

---------

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-11 11:18:37 -04:00
2c8439b24c i18n: add Turkish (tr) interface translation (#494)
* chore(lints): satisfy nonminimal_bool on Rust 1.96 Clippy

Four boolean expressions rewritten into their equivalent minimal forms
(De Morgan / is_none_or) so `cargo xtask ci` passes `clippy -D warnings`
on current stable. No behaviour change.

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

* i18n(tr): Turkish interface (Türkçe)

- crates/ui-egui/src/i18n/tr.tsv: complete catalog, all 1,307 keys of
  es.tsv, written clean-room from the plain meaning of the English
  labels; placeholders, escapes and "…" kept. A noun after a number
  stays singular in Turkish, so the context-free format needs no
  plural forms.
- Registry entry `tr` / "Türkçe": tr-TR, tr-CY and tr_*.UTF-8 locales
  resolve by the generic prefix match; saved preference and English
  fallback use the existing machinery.
- Command search folds case the Turkish way (İ→i, I→ı) for translated
  labels and locations, while English labels and ASCII command ids keep
  their existing Unicode case-insensitive matching ("italik" finds
  "İtalik", "INSERT.TABLE" still finds insert.table).
- The Font dialog's preview sample is "AaBbYyZz ĞğİıŞş": it has no
  lowercase i, so the All Caps preview (to_uppercase) gets the Turkish
  capitals right.
- Tests: catalog key and placeholder parity, locale resolution, saved
  preference, Turkish folding, the sample's capitals, a real egui
  command-dialog regression, and ÇçĞğİıÖöŞşÜü in the bundled UI fonts.
- Docs: README, ROADMAP, localization-parity, control-protocol, new
  localization-tr.md (terminology, scope); ATTRIBUTION row for tr.tsv.

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

* test(paste): an absolute picture path that is absolute on Windows too

`/tmp/b.JPG` has no drive, so `Path::is_absolute` is false on Windows and
`paste_inserts_clipboard_pixels` failed there; the test uses `C:\tmp\b.JPG`
on Windows. Test-only.

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-11 11:16:22 -04:00
c5af6d4154 i18n(es): consistent tú register, fix swapped Page Up/Page Down, wording fixes (#514)
- Every instruction now uses tú (the catalog mixed usted and tú), and
  «añadir» replaces the remaining «agregar».
- Page Up / Page Down labels were swapped (Av Pág / Re Pág).
- Mistranslations: Block Authors, Grab bag, Do Not Show in TOC,
  Capitalize Each Word, Set Caret, Switch Modes (and its mention in the
  dark-page hint), Space before/after, At least/Exactly.
- Spanish quotation marks in the command box hint.

Translated from the English labels in this repository only; no other
product's localisation consulted.


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

Co-authored-by: Diego Ospina <diegozpy@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:15:25 -04:00
1581f30176 Import/export fixes: TXT encodings and list labels, RTF Unicode, ODT styles and cell colours, HTML borders and pre spaces, LaTeX alignment groups (#433–#440) (#482)
- TXT: the engine opens and saves .txt through wordcraft-formats (BOM/unmarked
  UTF-16 sniffing, Windows-1252 fallback; list labels and a tab on export)
  instead of its own narrower decoder and body plain text.
- RTF: \uN and \ucN are handled in document info, bookmark names and field
  instructions, not only in body text.
- ODT: common paragraph styles (and common parents of automatic styles) give
  their text formatting, except the style that sets the paragraph kind;
  table-cell-properties fo:background-color becomes cell shading.
- HTML: tables without style or visible borders export without cell borders;
  white-space:pre paragraphs keep leading/trailing spaces.
- LaTeX: brace groups restore the paragraph alignment set inside them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:15:08 -04:00
5078d9dea5 Equation fixes: numbering removal, control colour, undo for navigation edits, LaTeX text spaces (#444, #446, #447, #448) (#487)
- equation.number off strips the number from every row; a single row is unwrapped,
  several rows stay an equation array (was: only the first row survived).
- equation.move works out AutoCorrect/build-up on a copy; plain navigation no longer
  rewrites the equation (keeps imported OMML such as m:ctrlPr colours), and a real
  build-up is checkpointed as "Build Up" and marks the document dirty.
- LaTeX tokenizer keeps white space inside \text/\textrm/\mbox/\textnormal/\textbf/\textit
  arguments (collapsed to one space), so \text{for all x} stays "for all x".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:14:51 -04:00
2666031c35 Spelling: common words like "desktop" are recognised (#520) (#522)
The English speller only knew the 1990s Moby word list plus a small
hard-coded EXTRA array, so everyday modern words (desktop, laptop,
download, upload, spreadsheet, database, screenshot, inbox, offline,
workplace…) were flagged. Inflection, doubled-consonant and prefix rules
also only looked stems up in Moby, so even supplemented words failed in
inflected forms (blogging, uploader).

- New supplement assets/spelling/en-extra.txt: ~220 lowercase base forms,
  our own list (MIT OR Apache-2.0), replacing the EXTRA array.
- All stem lookups (inflections, doubled consonant, prefixes) consult the
  dictionary and the supplement; suggestions include supplement words.
- Regression tests: common modern words and inflections accepted,
  misspellings still flagged, "dekstop" suggests "desktop"; the list
  stays sorted and lowercase.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:14:31 -04:00
5c2fbda5e2 Dialogs: switching tabs keeps the window's top edge in place (#507) (#511)
Every dialog was anchored CENTER_CENTER, which egui re-applies each frame,
so a tab of a different height split the change between top and bottom and
the tab strip jumped. Dialogs are now centred on the frame they open, then
anchored CENTER_TOP at that top edge until they close; only the bottom edge
moves. egui still keeps the window on screen.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:14:13 -04:00
18e472e3de .doc import fixes: piece bold, section kinds, orientation, list overrides, style inheritance, alternate font names (#458–#463) (#484)
Follow [MS-DOC] in the Word 97-2003 reader:
- Prm: bit 0 is fComplex; Prm0 isprm in bits 1-7, Prm1 Prc index in bits 1-15 (#458)
- sprmSBkc operands 0-4 map to continuous/new column/new page/even/odd (#459)
- sprmSBOrientation 1 = portrait, 2 = landscape (#460)
- negative sprmPIlfo (0xF802-0xFFFF) references the negated LFO index (#461)
- StdfBase: istdBase shares the word with stk and is zero-based; the
  whole base chain is applied, bounded and cycle-safe (#462)
- SttbfFfn entries advance by cchData, covering the optional xszAlt (#463)

One regression test per issue, built in code from the spec.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:13:20 -04:00
e5c55c0fc5 CLI: a closed stdout ends quietly instead of panicking (#508) (#510)
`println!` panics when stdout is a closed pipe, so `wordcraft-cli commands
--json | head` aborted with exit 101. Every stdout write now goes through a
locked handle with `writeln!` and propagates `io::Error`; a BrokenPipe becomes
`Failure::StdoutClosed`, which `main` treats as a normal exit 0. The MCP stdio
server's BrokenPipe takes the same path, so a client that hangs up ends the
server cleanly. Real errors still go to stderr with exit 1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:13:01 -04:00
b20d43707c Backstage: the side menu is fixed width, no dead resize handle (#502) (#504)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:12:45 -04:00
1d4f741f23 Citations: eight more bibliography styles (#383) (#388)
Citation and bibliography styles are now data: a CiteStyle table entry
per style (in-text form, author name format, ordering, entry templates
for books and journal articles). Adds Harvard (Anglia), ISO 690
author-date and numerical, Turabian, GB/T 7714, GOST (name sort and
title sort) and SIST02 next to APA, MLA, Chicago and IEEE. Numbered
styles count in order of first citation (GOST: in its sorted order) and
renumber when a citation is inserted. Word's style names and
style-sheet file names select the matching style.

The References > Style button is now a menu listing every style with
the current one checked; its label shows the current style.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 01:55:01 -04:00
Valentin KleinandClaude Opus 5.5 51e927028a Options: interface size setting (#475) (#480)
File › Options › General gets an Interface size choice (80–200 %), kept in
ui.json and applied as egui's zoom factor, so it multiplies the display
scale the system reports instead of replacing it. Junk saved values read
as 100 % and out-of-range ones are clamped. Agents use ui.interfaceSize
{"percent"}. Document zoom (view.zoom) is untouched; egui's keyboard zoom
stays off, so Word shortcuts never change the interface size.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:51:56 -04:00
Valentin KleinandClaude Opus 5.5 5528eff2ac Mail merge: SKIPIF, NEXT, IF in preview, header/footer fields (#427, #428, #429, #430) (#478)
Merge fields are now evaluated in document order with a record cursor,
shared by Preview Results and Finish & Merge:
- SKIPIF drops the merged copy when its condition holds (#427).
- NEXT/NEXTIF move later fields to the next record; the next copy
  starts after it, and records past the end are blank (#428).
- IF rules on merge fields preview their true/false text (#429).
- Header and footer stories are previewed and merged; with merge fields
  in them each letter becomes its own section with its own copies (#430).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:51:15 -04:00
Valentin KleinandClaude Opus 5.5 aa7b41d8ef Groups keep rotated members' orientation; group resize; ink erase after rotation (#441, #442, #443, #445) (#474)
- Group keeps each member's own turn (and composes a merged group's turn
  into its members); the group box covers the members' turned bounds and
  the new group starts unturned (#441).
- Size (picture.size) resizes a selected group, scaling its members
  through the group's child space like Move or Resize (#442).
- Ungroup decomposes a turned group: each member's centre is turned about
  the group's centre and its turn becomes its own within the group's (#443).
- The eraser tests the point turned back into the stroke's unturned frame,
  so turned ink is erased where it is drawn (#445).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:51:00 -04:00
Valentin KleinandClaude Opus 5.5 5e84270e60 Hyperlinks cover only their own text (#413) (#449)
Linking a whole paragraph also linked its paragraph mark, and Enter
copied the link at the split into the new paragraph's mark, so everything
typed in the paragraphs Enter made (above or below) became the link, and
hovering anywhere near the end of the text showed the link's tooltip.

- doc: a paragraph mark never takes a hyperlink (format_range, split_off)
- engine: typing at either end of a link isn't linked; inside it is (Word)
- ui: hover tooltip and Ctrl+click only on the linked text itself

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:50:48 -04:00
Brandon ThomasandClaude Opus 5.5 e4b7d155f1 Bump version to 0.5.0; add Linux riscv64 release build (#394)
- release.yml: new `linux-riscv64` job ported from GridCraft. Cross-compiles
  wordcraft and wordcraft-cli on ubuntu-24.04 with riscv64 libs from
  ports.ubuntu.com (riscv64 variants of the Linux job's -dev packages),
  packages a tar.gz, and smoke-tests the CLI under QEMU. The draft-release
  job needs it, so its tar.gz is uploaded and covered by SHA256SUMS.
- packaging/linux/package.sh: CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE/EMULATOR
  support (as in GridCraft).
- README and docs/releasing.md list the riscv64 tarball.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.5.0
2026-10-10 19:41:30 -04:00
Valentin KleinandClaude Opus 5.5 972df1e0c7 Track Changes: formatting revisions (w:rPrChange/w:pPrChange) (#41) (#399)
Formatting changes are tracked like insertions and deletions:
- Model: runs and paragraph marks carry the formatting before a tracked
  change (CharProps::fmt_change), paragraphs their old properties
  (ParaProps::fmt_change) and a list numbering change; tables, rows,
  cells and sections their old properties.
- DOCX: w:rPrChange (runs and paragraph marks), w:pPrChange,
  w:numberingChange, w:tblPrChange, w:trPrChange, w:tcPrChange and
  w:sectPrChange are read and written back, each last in its parent
  (ECMA-376 17.13.5). Nested changes are ignored; ids are renumbered.
- Recording: with Track Changes and Track Formatting on, Session::run
  compares the paragraphs a command changed with the document before it,
  so every formatting command records without per-command code. An
  author's later changes keep the formatting before the first one;
  formatting set back to the original stops being a change.
- Accept/Reject (at the caret, selection, all) keep or restore the
  formatting; Next/Previous and the Reviewing Pane list format changes
  with a "Formatted: Bold, Size: 14" description.
- Markup: change bars beside formatted lines and "Formatted: ..." balloons
  in the markup area, per Track Changes Options.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:32:59 -04:00
loganjandClaude Sonnet 5.5 4c6dd898c2 Tables: Delete Columns never leaves a table with no rows (#397)
Delete Cells removes a cell without shrinking the grid, so a row can be shorter than the
grid. Delete Columns then removed that row's only cell, dropped every row left without
cells, and kept the now empty table block (`rows: []`), an invalid state that anything
indexing the first row would turn into a panic. When no rows are left, remove the table, as
deleting the only column already does.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 19:30:36 -04:00
loganjandClaude Sonnet 5.5 144ddb7f2d Layout and render: extreme page numbers and wave borders no longer overflow or run away (#393)
- A page-number start near u32::MAX (.docx accepts up to 4294967295) or a line-number start
  near u32::MAX (open through .json and commands) overflowed when the next page or line was
  counted: a panic whenever overflow checks are on, silently wrapped numbers otherwise. The
  three additions now saturate.
- The wave stroke used by wave borders and underlines looped forever, pushing curves until
  memory ran out, once its x coordinate passed about 1e16 and `x + step == x` (a wave page
  border with a 1e20 `space`, reachable through .json, the control channel and MCP). The
  path is built in `wave_path`, which stops as soon as a step makes no progress.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 19:30:07 -04:00
loganjandClaude Sonnet 5.5 da07540f31 Layout: a drop cap in a font over 1000 pt no longer panics (#389)
The drop-cap letter's size was `clamp(rc.size, 1000.0)`, and `f32::clamp` panics when the
minimum is above the maximum, which happens as soon as the paragraph's text is larger than
1000 pt (Word allows up to 1638). Layout, PDF export and saving run outside `Session::run`'s
panic guard, so the desktop app panicked on every frame, and `wordcraft-cli convert` failed.
Use `.min(1000.0).max(rc.size)`, which never panics and keeps the letter at least as large as
the text.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 19:29:27 -04:00
4615cef878 Rotated OLE objects keep their turn when saving DOCX (#333 follow-up to #332) (#370)
#333 writes OLE objects back from their kept markup, resized to the
object's current size, but #332's rotation and flips weren't carried into
that markup: a turned or flipped OLE object came back unturned (DrawingML)
or with its old turn (VML).

The VML shape's style now gets the object's rotation and flip, and a
DrawingML picture's a:xfrm gets rot/flipH/flipV and its size, with the
drawing's wp:effectExtent covering the rotated bounds like other drawings
(added when missing). Inline VML shapes now read their rotation and flips
too, so the turn survives a WordCraft round trip. Charts and diagrams can't
be turned, so their frame needs nothing new.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:13:08 -04:00
a6b23683c1 Font box: type to find a font (#359) (#367)
Typing in the ribbon's or the mini toolbar's font box now lists the
installed families that match, under the field: exact name first, then
names starting with the text, then names containing it, ignoring case
("roman" finds Times New Roman). Up/Down move the highlight, Enter or a
click applies it ("arial" applies Arial), Escape restores the current
font. Text that matches no family is still applied as typed, as Word
allows fonts that aren't installed.

The search is opt-in (widgets::font_combo); the font size box keeps
literal entry. Matches are computed only when the text changes and the
list stays virtualised (#121).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:12:01 -04:00
afb4720644 Add Estonian interface translation (#294)
* I18N.et: add Estonian interface translation

* I18N.et: record the verified headless fork push step

* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

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

* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

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

* #313: catalog rows for the newly translated interface text

Rows in all eight catalogs for the strings #313 now sends through the
catalog (cell alignment tooltips, credits columns and summary, theme names,
context-menu items, table picker, pt suffix, Document1, the Font dialog
sample) and for the Table Style preview's sample table, which the
untranslated-text audit flagged after main's table style editor landed.
The audit runs headless (no host services: no file pickers or speech) in
about 20 s, so it stays enabled.

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

* fmt

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

* i18n: Borders and Shading line widths through the catalog ({n} pt)

#313's untranslated-text audit flagged main's new dialog (#327).

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

* land-294: Estonian rows for main's newer dialogs

Define New Multilevel List and Track Changes Options (#342), rotation (#332),
Tabs and Borders and Shading (#327), Columns, Symbol and Field (#335): et
keys equal es again.

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

* i18n audit: run on demand (#[ignore]) until its rare failure under load is understood

It failed once in ~46 runs on a heavily loaded machine and passed 35 straight reruns,
so CI shouldn't depend on it yet. Run it with --ignored.

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: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:06:45 -04:00
25b025b1ea i18n: send interface text that skipped translation through the catalog (#313)
* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

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

* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

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

* i18n: send interface text that skipped translation through the catalog

A test-only pseudo-language wraps every catalog lookup in ⟦…⟧ marks; a new
audit test lays the app out in it (every tab, File page, dialog and pane,
the right-click menu, and a hover and a click on every ribbon control,
including the Table and Equation tabs) and fails on any visible text
outside the marks. The document is drawn as an image, so only interface
text is checked; document text, font and style names, shortcuts, icon
lettering, contributor handles and command error messages are allowed.

What it found, now translated in every language that has the strings:
- Right-click menu items (Cut, Paste, Font…, Paragraph…, Link…, New Comment).
- Paragraph dialog: the alignment choices showed code values (`left`);
  the label now sits before the dropdown.
- Table Design › Table Style Options checkboxes.
- Table Layout › Alignment tooltips showed code values (`topLeft`).
- Design › Fonts: the theme's name is translated, its font names are not.
- `pt` units, the Insert › Table grid label, the default watermark text,
  the font dialog's preview sample, and the untitled document's name.
- Contributors and Models dialogs: headings, sort and name choices, summary.

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

* i18n audit: start every ribbon click from a fresh app; translate the table style preview

The sweep pressed Escape after each click, and in an equation Escape leaves it,
so the Equation tab vanished after its first control and the rest of its menus
(Convert, Justification, …) were never opened. Each control now gets a fresh app
set up the same way. The narrower allowance for short text (only icon lettering
and initials) also catches short words.

It found the sample table in the Table Style dialog (Mon, Tue, North, Total, …),
now translated.

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

* #313: catalog rows for the newly translated interface text

Rows in all eight catalogs for the strings #313 now sends through the
catalog (cell alignment tooltips, credits columns and summary, theme names,
context-menu items, table picker, pt suffix, Document1, the Font dialog
sample) and for the Table Style preview's sample table, which the
untranslated-text audit flagged after main's table style editor landed.
The audit runs headless (no host services: no file pickers or speech) in
about 20 s, so it stays enabled.

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

* i18n audit: click the ribbon's bottom row too

The sweep only clicked controls centred between y = 52 and 125, which took in
the tab row and left out each group's bottom row (Equation › Convert and
Justification among them). The band is now taken from the layout: below the
tab row and above the document area.

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

* i18n audit: lay the app out wide enough that no ribbon tab overflows

At 1440 pt the Equation tab ran past the window, so its last controls
(Justification among them) sat behind the ribbon's scroll arrow and were never
clicked. The audit window is now 2560 pt wide.

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

* fmt

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

* i18n: Borders and Shading line widths through the catalog ({n} pt)

#313's untranslated-text audit flagged main's new dialog (#327).

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

* i18n audit: run on demand (#[ignore]) until its rare failure under load is understood

It failed once in ~46 runs on a heavily loaded machine and passed 35 straight reruns,
so CI shouldn't depend on it yet. Run it with --ignored.

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: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:06:30 -04:00
e71e2d1c52 Draw tab: draw with pen, pencil and highlighter, erase ink, and hide ink (#315)
* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

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

* fmt

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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-10 18:50:37 -04:00
0e2e4cde80 Draw tables and split or merge cells with a pen and eraser (#308)
* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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-10 18:50:20 -04:00
pkibiego 5c970f90bd Bump quick-xml to 0.41: the namespace-declaration DoS was reachable (RUSTSEC-2026-0195) (#363)
`crates/docx/src/xml.rs` parses untrusted document XML through
`quick_xml::reader::NsReader`. In 0.38.4 that is RUSTSEC-2026-0195 (CVSS 7.5): the
namespace resolver pushes a binding for every `xmlns` / `xmlns:*` declaration on a
start tag into an unbounded `Vec`, with no limit on the number of declarations.

None of the reader's existing caps bound that path - MAX_PART (256 MiB), MAX_TOTAL
(512 MiB), MAX_ENTRIES, MAX_DEPTH and MAX_ELEMENTS bound part size, entry count,
nesting and element count, not declarations per tag.

Measured before this change, `wordcraft-cli convert` on a crafted .docx whose
`word/document.xml` is one start tag with 21,845,333 `xmlns:a="u"` declarations:

  508 KB on disk -> 262 MB decompressed (under MAX_PART) -> 1.19 GB peak RSS,
  exit 0: the document opened and converted without any warning.

At the MAX_PART/MAX_TOTAL ceiling that is roughly 2.4 GB of resident memory from
about a megabyte of download, i.e. an OOM kill with unsaved work open. Nothing
panics, so the `Session::run` guard does not catch it.

0.41.0 caps namespace declarations per start tag at 256
(`DEFAULT_MAX_DECLARATIONS_PER_ELEMENT`, raisable through
`NamespaceResolver::set_max_declarations_per_element`) and returns
`NamespaceError::TooManyDeclarations` beyond it. The same fixture now fails in
about a second with a clear error and no namespace allocation:

  wordcraft-cli: malformed XML: start tag declares more than 256 namespace
  bindings; raise the limit with NamespaceResolver::set_max_declarations_per_element

Two API adaptations for 0.41 (0.x bumps are allowed to break):
- `resolve_attribute` moved off `NsReader` on to the resolver:
  `r.resolve_attribute(key)` -> `r.resolver_mut().resolve_attribute(key)`.
- `Attribute::unescape_value` is deprecated in favour of `normalized_value`, which
  also applies XML attribute-value normalization as the spec requires; attribute
  values in a Word part are UTF-8 here (UTF-16 parts are transcoded first).

Regression test: `crates/docx/src/xml.rs::too_many_namespace_declarations_is_error`
asserts a tag with 10,000 declarations is an error. It fails on 0.38.4 and passes
on 0.41.0.

This also clears the second quick-xml advisory, RUSTSEC-2026-0194
(quadratic duplicate-attribute scan). That one was not reachable here - the reader
already passes `with_checks(false)` at xml.rs:257, and a 60,000-attribute tag parses
in the same 65 ms as a 20,000-attribute one - but `cargo audit` is clean afterwards.

Verified: `cargo xtask ci`; `cargo audit` (no vulnerabilities left, the two
unmaintained-crate warnings are `krilla`'s `rustybuzz`/`ttf-parser` and are
unrelated); 32 docx tests plus the malformed-input suite; and 21 crafted hostile
documents through `wordcraft-cli convert` with zero panics before and after.

One residual, unchanged by this commit: the fixture above still costs about 509 MB,
because MAX_PART lets one part decompress to 256 MiB and every part is held in memory
while the package is read. That is the existing, intended bound; what this commit
removes is the namespace allocation on top of it.
2026-10-10 18:04:11 -04:00
15f2f01475 Tabs and Borders and Shading dialogs (#327)
* Add Tabs and Borders and Shading dialogs (#320)

Tabs: lists the paragraph's tab stops with position, alignment and leader,
Set / Clear / Clear All and the default interval. Opens from the Paragraph
dialog's Tabs… button, by double-clicking a tab marker on the ruler, or
para.tabs without params. OK is one para.tabs call, which now takes
set / clear / clearAll / default (clearing a style's stop records a clear
stop over it).

Borders and Shading: Borders (None / Box / Shadow / 3-D / Custom, style,
colour, width, paragraph or text, preview with clickable sides), Page
Border (same, whole document or this section) and Shading (fill, paragraph
or text) tabs. Opens from Home › Paragraph › Borders › Borders and
Shading… and Design › Page Borders. OK runs only the changed parts as one
undo step. para.borders and design.pageBorders take exact "sides" (and
design.pageBorders a style and applyTo). Shadow and 3-D are drawn as
heavier sides because the model has no border shadow flag; shading
patterns are omitted because the model keeps a solid fill only.

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

* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Columns, Symbol and Field dialogs (#321)

Layout > Columns > More Columns... opens a Columns dialog: One/Two/Three/
Left/Right presets, number of columns, per-column width and spacing,
Equal column width, Line between, a page preview and Apply to (whole
document, this section, this point forward). OK is one layout.columns
call, which now takes unequal widths and an apply target (forward adds a
continuous section break first).

Insert > Symbol > More Symbols... opens a Symbol dialog: font picker,
Unicode subset list that jumps through a grid of only the characters the
font has (drawn in that font), recently used symbols, a hex character
code, and a Special Characters tab. insert.symbol takes a font for the
symbol alone; typing after it goes on in the text's font. The ribbon's
Symbol gallery shows recently used symbols first.

Insert > Quick Parts > Field... opens a Field dialog: categories and the
fields WordCraft computes, date pictures, number formats, bookmark /
sequence / merge field names, preserve formatting, a live field code and
a Field Codes view to type the code directly; OK runs insert.field.

Also: date pictures read correctly when switches follow them, and am/pm
and seconds work in date and time pictures. Strings in every catalog;
gaps #7 and ui-parity updated.

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

* #321: Field dialog descriptions in our own words (clean room)

Reword the one-line field descriptions and the empty-selection prompt so
none reads like Word's; translations keep their meaning under the new keys.

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

* #320: test that a default-tab-only para.tabs change undoes and redoes

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

* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Columns, Symbol and Field dialogs (#321)

Layout > Columns > More Columns... opens a Columns dialog: One/Two/Three/
Left/Right presets, number of columns, per-column width and spacing,
Equal column width, Line between, a page preview and Apply to (whole
document, this section, this point forward). OK is one layout.columns
call, which now takes unequal widths and an apply target (forward adds a
continuous section break first).

Insert > Symbol > More Symbols... opens a Symbol dialog: font picker,
Unicode subset list that jumps through a grid of only the characters the
font has (drawn in that font), recently used symbols, a hex character
code, and a Special Characters tab. insert.symbol takes a font for the
symbol alone; typing after it goes on in the text's font. The ribbon's
Symbol gallery shows recently used symbols first.

Insert > Quick Parts > Field... opens a Field dialog: categories and the
fields WordCraft computes, date pictures, number formats, bookmark /
sequence / merge field names, preserve formatting, a live field code and
a Field Codes view to type the code directly; OK runs insert.field.

Also: date pictures read correctly when switches follow them, and am/pm
and seconds work in date and time pictures. Strings in every catalog;
gaps #7 and ui-parity updated.

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

* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field

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

* Add Tabs and Borders and Shading dialogs (#320)

Tabs: lists the paragraph's tab stops with position, alignment and leader,
Set / Clear / Clear All and the default interval. Opens from the Paragraph
dialog's Tabs… button, by double-clicking a tab marker on the ruler, or
para.tabs without params. OK is one para.tabs call, which now takes
set / clear / clearAll / default (clearing a style's stop records a clear
stop over it).

Borders and Shading: Borders (None / Box / Shadow / 3-D / Custom, style,
colour, width, paragraph or text, preview with clickable sides), Page
Border (same, whole document or this section) and Shading (fill, paragraph
or text) tabs. Opens from Home › Paragraph › Borders › Borders and
Shading… and Design › Page Borders. OK runs only the changed parts as one
undo step. para.borders and design.pageBorders take exact "sides" (and
design.pageBorders a style and applyTo). Shadow and 3-D are drawn as
heavier sides because the model has no border shadow flag; shading
patterns are omitted because the model keeps a solid fill only.

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

* cargo fmt after merging main

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 17:56:54 -04:00
8204c5cb49 Columns, Symbol and Field dialogs (#335)
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Columns, Symbol and Field dialogs (#321)

Layout > Columns > More Columns... opens a Columns dialog: One/Two/Three/
Left/Right presets, number of columns, per-column width and spacing,
Equal column width, Line between, a page preview and Apply to (whole
document, this section, this point forward). OK is one layout.columns
call, which now takes unequal widths and an apply target (forward adds a
continuous section break first).

Insert > Symbol > More Symbols... opens a Symbol dialog: font picker,
Unicode subset list that jumps through a grid of only the characters the
font has (drawn in that font), recently used symbols, a hex character
code, and a Special Characters tab. insert.symbol takes a font for the
symbol alone; typing after it goes on in the text's font. The ribbon's
Symbol gallery shows recently used symbols first.

Insert > Quick Parts > Field... opens a Field dialog: categories and the
fields WordCraft computes, date pictures, number formats, bookmark /
sequence / merge field names, preserve formatting, a live field code and
a Field Codes view to type the code directly; OK runs insert.field.

Also: date pictures read correctly when switches follow them, and am/pm
and seconds work in date and time pictures. Strings in every catalog;
gaps #7 and ui-parity updated.

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

* #321: Field dialog descriptions in our own words (clean room)

Reword the one-line field descriptions and the empty-selection prompt so
none reads like Word's; translations keep their meaning under the new keys.

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

* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Columns, Symbol and Field dialogs (#321)

Layout > Columns > More Columns... opens a Columns dialog: One/Two/Three/
Left/Right presets, number of columns, per-column width and spacing,
Equal column width, Line between, a page preview and Apply to (whole
document, this section, this point forward). OK is one layout.columns
call, which now takes unequal widths and an apply target (forward adds a
continuous section break first).

Insert > Symbol > More Symbols... opens a Symbol dialog: font picker,
Unicode subset list that jumps through a grid of only the characters the
font has (drawn in that font), recently used symbols, a hex character
code, and a Special Characters tab. insert.symbol takes a font for the
symbol alone; typing after it goes on in the text's font. The ribbon's
Symbol gallery shows recently used symbols first.

Insert > Quick Parts > Field... opens a Field dialog: categories and the
fields WordCraft computes, date pictures, number formats, bookmark /
sequence / merge field names, preserve formatting, a live field code and
a Field Codes view to type the code directly; OK runs insert.field.

Also: date pictures read correctly when switches follow them, and am/pm
and seconds work in date and time pictures. Strings in every catalog;
gaps #7 and ui-parity updated.

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

* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field

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-10 17:54:23 -04:00
a7c809e63b Charts, SmartArt and OLE objects are no longer lost when you save a .docx (#333)
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field

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-10 17:48:51 -04:00