* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
Drawings carry an angle and horizontal/vertical flips on their placement
(Float::rot, flip_h, flip_v), read and written as DrawingML a:xfrm rot,
flipH and flipV for pictures, shapes, text boxes and groups; the written
effect extent covers the rotated bounds and reading takes that overhang
back out. Layout draws them turned about their centre (screen and PDF),
square wrap and inline lines keep clear of the rotated bounds, and hit
testing follows the turned shape. arrange.rotate now turns the frame
(90 degrees, flips) instead of rewriting pixels; arrange.rotation sets an
exact angle. The canvas frame has a rotation handle (Shift snaps to 15
degrees) and turned frames resize along their own axes; Picture Format
and Shape Format get a Rotation box and a Rotate menu. Text inside a
rotated text box stays upright for now.
Groups turn and flip as a whole (wpg:grpSpPr a:xfrm): members keep their
own turn inside the group's, are carried round its centre, and a group's
text boxes move with it. Shape effects are drawn inside the turn; a
shadow's direction follows DrawingML rotWithShape (kept on the model,
true when absent; Word's presets write 0, so their shadows keep their
page direction).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* System interface theme keeps following the OS; Dark page leaves the interface alone (#311, #312)
With Interface theme set to System, every frame resolved the OS appearance
to Light or Dark and passed that to egui's set_theme. egui then sent
SetTheme(Light/Dark) to the window, which on macOS sets an explicit
NSAppearance; winit ignores appearance changes while one is set, so later
OS switches never reached the app.
Both palettes are now installed as egui's light and dark styles (with the
same spacing and text styles), and System leaves egui's preference on
System with a light fallback, so egui picks the matching style from the
OS appearance it reports and the window keeps following the OS. Manual
Light and Dark still pin the window.
Dark page (View > Switch Modes, Options > Dark page) also forced the
interface dark; it now only changes how pages are drawn and the interface
follows the Interface theme setting alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Theme: update #314's test to the new theme::apply(ctx, Appearance) signature
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>
Since Windows defaults to DirectX 12 only (#50), PCs whose graphics chip has
no DirectX 12 driver (Intel 4th generation on Windows 11) fall back to WARP,
Microsoft's software renderer, and the app lags. Before eframe creates its
instance, WordCraft now probes DirectX 12 with a short-lived instance: a
hardware adapter that meets eframe's device limits keeps DirectX 12 only;
WARP only enables OpenGL next to DirectX 12 (GPU preferred, WARP as last
resort); no adapter, or only GPUs below the limits, uses OpenGL alone.
WGPU_BACKEND still overrides everything. The choice is logged, and the
decision is a pure function with unit tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Clipboard pane: collect the last 24 copies and cuts and paste any of them again
Copy and Cut now also keep each item (rich fragment plus plain text) in a
session list of up to 24, newest first. Items over ~4 MB (estimated) are
skipped and the list stays under ~24 MB in total; copying the same thing
twice in a row adds it once. The system clipboard is untouched.
New commands: edit.clipboardPane (toggle the pane, also the Clipboard
group's dialog launcher), edit.clipboardItems (list with previews),
edit.pasteClipboardItem {index}, edit.pasteAllClipboard,
edit.deleteClipboardItem {index} and edit.clearClipboard. Pasting goes
through the normal paste path, so it is one undo step and respects Track
Changes. The pane lists the items with a short preview; clicking one pastes
it at the caret, and each has Delete, plus Paste All and Clear All.
Parity: 378 -> 379 of 428 features.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout > Arrange > Group: group pictures, shapes and text boxes into one object
Shift+click (or Ctrl+click) adds pictures, shapes and text boxes to the
selection; Group (arrange.group) combines the selected floating objects
into one group object that moves, wraps and resizes as one, with its
members keeping their places and scaling with the group. Ungroup
(arrange.ungroup) splits it back, where the group showed them, and
leaves them selected so Group puts them back together.
- Model: InlineObject::Group with the members' coordinate space (like
DrawingML a:chExt), so resizing the group scales its members.
- Layout draws the members inside the group's frame; hit testing
selects the group; text boxes in a group show their text.
- DOCX writes wpg:wgp with a:chOff/a:chExt and each member's a:xfrm,
and reads Word's groups (including inside mc:AlternateContent and
nested groups, flattened).
- select.addObject makes multi-selection scriptable (pos or Selection
Pane index).
Parity 378 -> 379 of 428 (88.6%); floor raised to 88.5%.
Fixes#265
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paragraph: Asian Typography settings, with kinsoku and mid-word Latin wrapping in layout (#269)
Paragraphs gain the Asian Typography flags (line-breaking rules, wrapping
Latin text in the middle of a word, hanging punctuation, compressing
punctuation at the start of a line, and automatic space between Asian and
Latin text and between Asian text and numbers). They inherit through styles,
round-trip through .docx pPr, are set by the new para.asianTypography
command and appear on a new Asian Typography tab of the Paragraph dialog.
Layout applies the kinsoku lists (no line starts with closing East Asian
punctuation or ends with opening punctuation), breaks between any East Asian
characters when kinsoku is off, and breaks Latin words anywhere when word
wrap is off. Text without East Asian characters breaks as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: list, preview, delete and show or hide styles
Home › Styles › Manage Styles (styles.manage) opens a dialog from the
Styles pane listing the document's paragraph, character, linked and table
styles, sorted as recommended or alphabetically, with a preview line and a
description (type, formatting it sets, what it is based on). From there:
Modify… and New Style… reuse the existing dialogs and come back, Delete
removes custom styles, and two checkboxes show the style in the Styles
gallery (w:qFormat) or hide it from style lists (w:semiHidden).
styles.manage returns the list for scripts (name, type, builtIn,
inGallery, hidden, basedOn, priority, own formatting). styles.delete now
moves paragraphs, runs and tables that used the style to its base (in
every story), rebases styles built on it and takes a custom linked
character half with it; built-in styles are refused and it is undoable.
New styles.setVisibility {style, gallery?, hidden?} sets the flags, which
round-trip through .docx.
Fixes#266
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Create and modify your own table styles, saved in the .docx (#146)
New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.
- engine: table.newStyle / table.modifyStyle with documented params
(regions take borders, fill, bold, italic, colour, size, font; bandSize;
para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
style tiles redraw when a style changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table styles: every region, a live preview, delete, custom styles first (#146)
Follow-up to the custom table styles work: the New/Modify Table Style
dialog now covers every region a table style formats, shows what the
style will look like, and table styles can be deleted.
- engine: table.newStyle / table.modifyStyle take lastRow (total row),
firstColumn, lastColumn and bandedColumns regions with the same props
as the others. New table.deleteStyle removes a custom table style:
built-in ones are refused, styles based on it are re-based on its own
base with its formatting folded in (so they look the same), and tables
using it fall back to Table Grid. Undoable like any command.
- doc/layout: table styles gain fill and cell borders for the first and
last column and total row, and fill, text and borders for column
bands; layout applies them in the standard order (column bands, row
bands, first/last column, header/total row).
- docx: firstCol/lastCol/lastRow/band1Vert shading and cell borders are
read and written.
- ui: the dialog's "Apply formatting to" lists all seven regions and a
small live preview table shows the pending changes; Styles menu gets
Delete Table Style (disabled for built-ins); the gallery shows the
document's own table styles first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paste pictures from the system clipboard (screenshots, Copy Image, copied files) (#45)
egui's paste event only carries text, so a clipboard holding just a picture
sent no paste at all on Ctrl+V, and the ribbon/context-menu Paste only knew
WordCraft's own copy ("the clipboard is empty").
Desktop: the app reads the clipboard's picture through a new
Services::clipboard_picture (arboard, already in the build via egui-winit):
copied files first, else image pixels. Mod+V whose release arrives without a
paste event, and Paste from the ribbon or menus, insert that picture through
insert.picture (one undo step); text naming copied picture files inserts the
pictures. Pixel sizes are hostile: at most 16,000 px a side and 50 Mpx, the
buffer must match, files at most 200 MB.
Web: a document paste listener reads image files from the paste event when
there's no text and delivers them through the inbox like dropped pictures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shape Format: Shape Effects — shadow, glow and soft edges (#275)
Shapes and text boxes get an effects model (outer shadow with colour,
transparency, blur, distance and angle; glow with colour, size and
transparency; soft-edge radius), the shape.effects command (presets,
custom values or null per effect, applied to every selected shape,
undoable, hostile numbers clamped), screen and PDF rendering with the
same banded approximation of a blur, DOCX a:effectLst read/write
(outerShdw, glow, softEdge) with a matching wp:effectExtent, and a
contextual Shape Format tab with Shape Fill, Shape Outline and Shape
Effects (Shadow, Glow, Soft Edges submenus) plus Arrange.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open and save password-protected Word documents (#55)
Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.
- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
PasswordRequired / WrongPassword / Encryption errors; hostile files are
bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
{"password": string|null} sets or removes the save password (pure, so it
never lands in Repeat or recorded macros); a document opened with a
password is saved with it again; file.info reports "encrypted".
Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
error), File > Info > Protect Document with Encrypt with Password (typed
twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
zeroize.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table styles: Table Grid and Normal Table can't be deleted even when a .docx marks them custom
A .docx can set w:customStyle on any style, which made Word's built-in table styles look custom
and deletable. table.deleteStyle and the ribbon's Delete Table Style now also recognise them by
id and name, so no table is left on a style that's gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: deleting a table style runs Table Design's table style deletion
styles.delete on a table style now calls table_style::delete (#263): its tables take Table Grid,
styles based on it keep their look, and Word's built-in table styles are refused even when a
.docx marks them custom (the Manage Styles list reports them built-in too). Document::restyle no
longer retargets tables' own styles, so there is one place that does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paste pictures: Mod+Shift+V (Paste Formatting) no longer pastes a clipboard picture
The key-release fallback that pastes the system clipboard's picture treated every Mod+V release as
Paste. It now requires plain Mod+V or the Paste key (no Shift, no Alt), with a unit test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Asian typography: test that kinsoku keeps a manual line break before a closing bracket
Manual line breaks end the line by cluster kind, not by break opportunity, so kinsoku's filter
can't drop one before 」; pin that with a layout test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Encrypted saves: the data-integrity HMAC key is hashSize bytes
MS-OFFCRYPTO §2.3.4.14 makes the HMAC salt as long as the hash (64 bytes for SHA-512); it was
16. Reading already accepted both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: password dialog and File › Info strings in every catalog
Rows for the Password and Encrypt with Password dialogs, the Protect Document section of
File › Info and the password status messages in zh-hans, zh-hant, ja, pt-br, es, uk, sr and
sr-latn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open and save password-protected Word documents (#55)
Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.
- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
PasswordRequired / WrongPassword / Encryption errors; hostile files are
bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
{"password": string|null} sets or removes the save password (pure, so it
never lands in Repeat or recorded macros); a document opened with a
password is saved with it again; file.info reports "encrypted".
Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
error), File > Info > Protect Document with Encrypt with Password (typed
twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
zeroize.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reland: catalog coverage 390/431 (90.5%) in ROADMAP and target-app parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: mark table styles with ⊞, which the interface fonts have (▦ has no glyph, #305's check)
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>
The original squash merges of these six PRs were empty. Integrated and tested together on current main.
Co-authored-by: Valentin Klein <vangel1976@googlemail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AutoSave writes only a file saved in this session, in a format that keeps
everything (.docx, .docm, .dotx, .dotm, .odt, .rtf, .json), and never in the
browser, where saving is a download. The title-bar switch still showed on for
a saved .md file (#196) and AutoSave looked available in the web app (#176),
so edits stayed unsaved with no explanation.
WordApp::autosave_block() now says why AutoSave can't cover the document
(browser, never saved, a format that can't keep everything, opened but not
saved here). The switch is greyed out with that reason as its tooltip, and
clicking it offers Save As (desktop) so saving as .docx turns AutoSave on.
In the browser, File > Options' AutoSave checkbox is disabled with the same
explanation. file.autosave (scripts, MCP, the switch) now sets the app's
AutoSave and returns the reason as an error when asked to turn it on where it
can't save; turning it off always works. New strings in every catalog.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Table sizing: Height/Width boxes, real AutoFit Contents, Table Properties dialog (#44)
- Table Layout > Cell Size gets Height and Width boxes (inches) for the caret cell's row
and column; each edit runs table.rowHeight / table.columnWidth, a drag is one Undo.
- AutoFit Contents now measures every column's narrowest (longest word) and widest
(unwrapped text) content with the layout's text shaping, shares the text width the way
Word does, and writes the widths into the table grid and cell widths.
- Table Properties opens a dialog (table width, alignment, indent; row height and rule,
break across pages, header row; column width; cell width and vertical alignment). OK
applies only what changed as one table.properties step; with settings the command still
works headlessly, and without them it reports the current values.
- table.columnWidth clears the table's preferred width so the column keeps the new size;
table.rowHeight takes an optional exact rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Progress docs: Table Properties dialog and AutoFit Contents landed (#44)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table sizing: saturating column-span sum in sync_cell_widths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fix: one region_text (main's) after #287 met the landed table-style code
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>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, layout, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
select.column (Mod+Shift+F8) selects the same x range on a run of lines,
from two positions, two page points (Alt+drag), or by toggling column
mode where the arrow keys extend the block. Copy/Cut give one line per
row, Delete/Backspace remove only the block, character formatting and
Change Case apply per row, typing/paste replace the block. Any ordinary
caret move drops it. F8 keys now reach their shortcuts in the UI.
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>
styles.inspector shows/hides the Style Inspector pane and returns the
paragraph style, character style and the direct formatting that differs
from each; styles.inspect is the pure query; styles.inspectorClear
resets one level (paragraphStyle, paragraphFormatting, characterStyle,
characterFormatting) as a single undo step. The pane opens from the
Styles pane, with a clear button per level. Parity floor raised to 87%.
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>
New edit.pasteSpecial command ({"as"?, "text"?, "html"?, "rtf"?}). Without "as" it
lists the clipboard's formats and asks the front end for the dialog; with "as" it pastes in
that format. HTML/RTF go through the wordcraft-formats importers, with their lists, styles and
images merged into the document under fresh ids. Respects Track Changes (now also inside pasted
tables) and caps payloads at 16 MB. UI: Paste Special dialog (radio list + result hint),
Mod+Alt+V and Paste menu entry; strings in all four catalogs. Parity 355/405, floor 87.5%.
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>
Home › Editing › Find › Advanced Find (edit.advancedFind) opens the Find
and Replace dialog. With params it returns every match in the body, or
only inside the selection ("in": "selection"), and "highlight": true
turns on Reading Highlight, which marks all matches on the page and
follows edits until it's turned off. The ribbon's Find button becomes a
menu with Find, Advanced Find and Go To; the Find dialog gets a Reading
Highlight toggle. Parity 355/405.
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>
New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.
- engine: table.newStyle / table.modifyStyle with documented params
(regions take borders, fill, bold, italic, colour, size, font; bandSize;
para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
style tiles redraw when a style changes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(doc,docx): complex-script run properties and RTL OOXML round-trip
Model the formatting Word applies to Persian, Arabic and Hebrew text:
cs, font_cs, size_cs, bold_cs, italic_cs and lang_bidi on CharProps,
resolved with Word's fallbacks (ResolvedChar::complex). Size, bold and
italic pair with their complex-script values so a run's size or bold
also covers its Persian characters. Add doc::bidi script helpers, and
keep ZWNJ and Arabic marks inside words for word selection.
DOCX: read and write w:rFonts/@w:cs, w:szCs, w:bCs, w:iCs, w:cs and
w:lang/@w:bidi in schema order; stop using the cs font as the Latin font.
Tests round-trip Persian text with ZWNJ, digits and mixed Latin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(fonts): directional shaping and Persian font fallback
Add shape_run to shape a run in a known bidi direction (Arabic joining,
mirrored brackets). Prefer Persian-capable fonts (Vazirmatn, Noto Naskh/
Sans Arabic…) for right-to-left fallback, pick bold/italic fallback faces,
and map common Persian document fonts (B Nazanin, B Lotus, B Titr,
IRANSans…) to open substitutes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(layout): Unicode Bidirectional Algorithm and right-to-left paragraphs
Run UAX #9 per paragraph (unicode-bidi), shape each level run in its
direction, keep marks and ZWNJ with their letter and a word in one
fallback face. After line breaking, apply rules L1/L2 per line and give
each cluster a visual box; right-to-left paragraphs are laid out from
their start edge and mirrored, so indents, tabs, alignment, list labels,
drop caps, floats and borders sit on the right.
Caret, hit testing and selection use visual geometry (split selections,
caret at bidi boundaries, DocLayout::visual_step for arrow keys). Display
items carry glyph-to-text ranges, which the PDF writer now uses. Plain
left-to-right lines are unchanged. No hyphenation of RTL words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(engine,ui): paragraph direction commands and RTL editing
Add para.ltr / para.rtl (Word's text direction buttons) that change only
the reading order, and make the alignment buttons visual in right-to-left
paragraphs (para.align also takes start/end). Arrow keys move visually in
bidirectional text, Ctrl+arrows follow reading order, and Bold/Italic/
Size/Font cover complex-script text. Ribbon buttons with code-drawn
icons, a mirrored ruler for RTL paragraphs, translations, catalog,
parity and roadmap updates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(contributors): add Saeed Azarehgar to people.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): para.align with an explicit value sets it, without toggling
Only the alignment buttons toggle back to the start edge when pressed
again. para.align (used by the Paragraph dialog, scripts and agents) now
sets the requested alignment, as seen on the page, in each selected
paragraph's own direction. Previously pressing OK in the dialog with
Center or Right selected reset the paragraph to the start edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ui): Direction option in the Paragraph dialog
Add Word's Direction (Right-to-left / Left-to-right) to the Paragraph
dialog's General section. The dialog now shows alignment and Left/Right
indents as seen on the page, mapping them to the paragraph's logical
start/end values on OK, so an unchanged OK no longer flips a
right-to-left paragraph. OK logic moved to apply_paragraph, with tests;
ja/zh translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(layout): allow hanging trailing spaces in the RTL display test
A wrapped right-to-left line's trailing spaces hang past its end edge
(the left margin), as left-to-right lines hang them past the right. The
test failed whenever the loaded fallback fonts made a line end there;
it now checks only visible glyphs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M5.5: Word equations (OMML) import, lay out and save like Word
Equations were flattened to plain text on import. Now OMML is read into a
math tree (fractions, scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, boxes, equation arrays, matrices,
phantoms), laid out with the OpenType MATH table (Cambria Math when
installed, else STIX Two Math and other open math fonts), drawn to screen
and PDF, and written back as the original OMML. Display equations sit on
their own centred line. insert.equation parses the linear format into
structure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M5.5: equation editor like Word's — Equation tab, in-place editing, # numbers
Insert › Equation now opens an equation for in-place editing, with the
Equation tab: ready-made equations, Unicode/LaTeX input and conversion
(professional ↔ linear), Normal Text, eight symbol sets, and the eleven
structure galleries (fraction, script, radical, integral, large operator,
bracket, function, accent, limit and log, operator, matrix) with tiles
rendered by the math engine. Typing builds up like Word (a/b, x^2, √, ∑…
then space), Math AutoCorrect turns \alpha, \sqrt… into symbols, Tab moves
between empty slots, arrows/Backspace work through structures, and the
right-click menu has structure commands (fraction type, radical degree,
limit position, matrix rows/columns, remove brackets…). A trailing #(n)
numbers a display equation with the number at the right margin; # alone
numbers it automatically. DOCX output keeps numbers Word-compatible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: equation editor strings in Spanish, Brazilian Portuguese and Ukrainian
The catalogs that landed on main after #191 was opened lacked the Equation
tab and equation command labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Equations: our own names and order for the built-in equations
Clean room: the ready-made equation list used another product's gallery entry
names and order. Same formulas (and ids), with our own descriptive labels,
grouped by subject (geometry, algebra, calculus, trigonometry, probability).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ROADMAP: equations row matches the landed equation editor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fmt after merge
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>
* fix text boxes: type into a new box, click in and out, Escape leaves it
Insert › Text Box left the caret in the body after the box and clicks inside a
box never reached its story, so its text could not be edited.
- insert.textBox puts the caret in the new box (when anchored in a top-level
body paragraph, the only place layout shows box text today).
- Layout emits a hit-test-only Placed::TextBox area; story_at prefers a box
under the point (body text wins over boxes behind it) and ignores box lines
outside their box.
- Canvas: click enters a box, clicking outside goes back to the body (not while
editing a header/footer); double-click in a box selects a word there.
- Escape in a box puts the caret just after it (Document::text_box_anchor).
- Editing a box doesn't dim the body or show "Editing header/footer".
- Tests: engine (caret in box, typing, undo, table cell unchanged), layout
(story_at inside/outside/behind), doc (anchor lookup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix pasted text boxes sharing the original's text
A copied fragment only carried the text box's part id, so a pasted box showed
the original's story: editing either changed both, edits made after copying
leaked into the paste, and pasting into another document (or Insert › Text
from File) pointed at whatever part had that id there, e.g. a header.
- Fragment carries copies of the text box stories it shows (nested boxes too).
- insert_fragment gives every pasted box a new story of its own; a box whose
story can't be found keeps none rather than showing an unrelated part.
Nesting is depth-limited and one paste creates at most 4096 stories.
- Insert › Text from File uses Document::body_fragment, which brings the
file's text boxes along.
- Tests: doc (own story, copy-time snapshot, other document with clashing
ids, box in a table, fragment without parts, self-nested box), engine
(copy/paste/type/undo through commands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count includes text boxes, footnotes and endnotes (Word's default)
The status bar and Word Count dialog counted only the body, so words typed
into text boxes (and notes) were missing.
- Document::counted_stories / word_count_including_notes: the body plus the
text boxes, footnotes and endnotes it actually shows (nested too); orphaned
stories (deleted or cut boxes), headers, footers and comments don't count.
- Session::count_notes (default on) with Session::word_count for the status
bar; review.wordCount takes {"includeTextBoxes"?: bool} and reports it.
- Word Count dialog: "Include text boxes, footnotes and endnotes" checkbox.
- Document::word_count stays body-only (docx Words property, NUMWORDS, stats).
- Tests: doc (used vs orphaned stories, nesting, self-nested box), engine
(default on, toggle sticks, deleting a box drops its words).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* canvas: select, drag to move and resize pictures, shapes and text boxes
Click a picture or shape (or a text box's border) to select it: a frame with
eight handles. Drag it to move it, drag a handle to resize it (pictures keep
their proportions from a corner, Shift toggles), arrows nudge a floating one
(Ctrl/Alt: 1 pt), Escape cancels a drag. While a text box's text is edited its
frame shows dashed with handles. Typing or Enter with a text box selected
goes into it.
Reusable pieces, no per-kind code:
- layout: Placed::Object, one hit area for every picture/shape/text box
(replaces the text-box-only item); DocLayout::object_at / object /
text_box / find_object (page-hinted, no whole-document scans per frame).
- engine: arrange.bounds {width,height,x,y,page} (floating objects keep their
anchors on the same page; inline ones float where dropped; another page
re-anchors to the nearest line) and arrange.nudge; objects::min_size shared
with the UI.
- ui: frame.rs, a document-agnostic transform frame (handles, hit testing,
drag/resize maths, painting); objects.rs wires it to document objects.
- geom: Rect::expand. control: ui.press / ui.release for half-way drags.
Rendering: nothing is laid out or rasterised while dragging. The preview is
the object's own pixels from the cached page texture (moves, picture
resizes) or a light outline (text box/shape resizes); the drop is one
command, one relayout, one undo step.
Tests: frame maths, layout hit areas, engine move/resize/nudge across
anchors and pages, hostile values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* moving an object anchors it to the paragraph it's dropped on, so text wraps
A floating object only pushes aside text from its anchor paragraph down, and a
move kept the old anchor: drag a text box above its paragraph and the text
there ran straight under it.
- arrange.bounds now anchors the object to the paragraph under its top edge
(the last one starting at or above it on that page) and positions it
relative to that paragraph's column and top, as Word does: the text it
lands on wraps, and it moves with that text. On a page where no paragraph
starts it anchors to the nearest line, positioned on the page.
- layout: Placed::Object / ObjectHit carry the paragraph origin that column-
and paragraph-relative offsets start from.
- engine: edit_obj / edit_float change an object at a position (with_obj
uses it).
- Test: a box dropped over an earlier paragraph anchors there and the lines
beside it stop short of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* text boxes hide text that doesn't fit, as in Word
Text past a text box's bottom spilled out over the page below it.
- layout: fit_box keeps what fits in a box: whole lines that fit (a line that
only partly fits is hidden too) and other items starting inside; the first
line always stays so even a tiny box shows something. Done in layout, so the
canvas, PNG and PDF agree and hidden text can't be clicked; no renderer
changes. Reusable for anything laid out into a fixed height.
- BOX_INSET_X / BOX_INSET_Y name the text box internal margins (were 7.2/3.6
literals).
- Test: overflow hidden, every shown line inside the box, a tall box shows
everything, a minimum-size box shows its first line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* square wrap flows text on both sides of a floating object
Text only used the wider side of a floating picture, shape or text box; Word's
Square wrap fills both sides when there's room.
- para: row_spans gives the spans of a row around the objects in its way (each
at least 0.5" wide, else the line goes below); a row cut in two is two Lines
sharing top and height, the second marked `beside`. Everything per line
(alignment and justification per side, hit testing, caret, selection,
drawing, PDF) works unchanged; a row is as tall as its tallest part.
- layout: a row stays on one page; line numbers count rows, not parts.
- Exclusions that don't overlap a line horizontally no longer narrow it.
- Test: text both sides of a mid-column box (same row, left then right,
hittable), a too-narrow gap stays empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* drop a deleted or cut text box's text from the document
Deleting or cutting a text box left its story in Document::parts: never shown
or saved to .docx, but listed by document.inspect and kept in memory.
- doc: prune_text_boxes removes the text box stories no box shows (nested
ones too); free without text boxes, one paragraph pass otherwise. Session
runs it after every editing command. Undo snapshots and the clipboard hold
their own copies, so undo and cut/paste are unaffected.
- doc: reachable() walks the stories objects lead to; counted_stories (word
count) and prune_text_boxes share it, and it walks blocks directly instead of
building paragraph paths.
- Tests: orphaned and nested-orphan boxes go, shown ones (body, nested, in a
header) stay; delete then undo, cut then paste.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count's include-text-boxes setting survives a restart
The Word Count dialog's "Include text boxes, footnotes and endnotes" was a
session field, so it reset to on every launch.
- engine: Prefs, the editing preferences the front end saves between runs
(count_notes moves there; Session::prefs is the one source of truth).
- ui: SavedPrefs pairs UiState with the engine Prefs; WordApp::saved_prefs /
restore_prefs. UI fields stay at the top level of ui.json, so existing files
still load (the new `editing` section defaults).
- wordcraft app: saves and restores SavedPrefs instead of UiState alone.
- Tests: an old prefs file loads with Word's default; the setting round-trips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* text boxes and floating objects outside the body; footnotes in text boxes
Text boxes in table cells, headers, footers, footnotes and other text boxes
showed an empty frame (or nothing), and floating pictures and shapes there
weren't drawn. Footnotes inserted inside a text box were never numbered or
placed.
- layout: place_objects, one routine for the pictures, shapes and text boxes
in a paragraph's lines (floating ones drawn, every one's area, text boxes'
text), used by body paragraphs and by layout_box (cells, headers, footers,
notes, box contents). Floats are positioned against an AnchorFrame (column,
paragraph, margins, page); headers and footers pass where the page is, so
page/margin-anchored objects land right (a footer is laid out again only
when it has floating objects); elsewhere they're placed in their container.
Nested boxes show MAX_TEXT_BOX_DEPTH (doc) levels deep.
- Placed::Object / ObjectHit carry their story; DocLayout::object(&Pos),
object lookups include headers and footers, header_footer_text_box_at.
A part's lines are looked up in headers and footers too (caret, typing).
- Footnotes: numbered in reading order with text boxes where they are
(Document::objects_in_reading_order); a box's footnotes go on the page of
its line (Document::notes_in_text_box), sharing the body's reservation.
- engine: insert.textBox always puts the caret in the new box; disabled
inside a text box (as in Word).
- canvas: click into a header/footer's text boxes while editing it; leaving a
box returns to where it is; body dimming computed once per frame and used
for both the render and its cache key. objects: only body objects are
selectable, only body paragraphs' objects movable; frames off the body
show without handles.
- Tests: boxes in header (inline and page-placed), footer, cell and note show
text and take the caret; footnotes in a box numbered and placed in order;
a box in a table cell takes the caret and its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* word count: a selected object counts the whole document, in the dialog too
With a picture, shape or text box selected the status bar showed the
document's total but Review > Word Count counted the selection (0 words).
- engine: cmd::objects::object_selection, "the selection is exactly one
object"; review.wordCount counts the document then. The UI's
objects::selected now uses it (one definition).
- Test: an object selection counts the document; a text selection itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: bound nested text box expansion; tidy-ups
Blocker: laying out a text box lays out the boxes inside it, which could fan
out unboundedly (a box whose shapes show itself, or chains of boxes each
showing the next many times) - 30 shapes made 1.7M layout items. Footnote
numbering's walk fanned out the same way.
- doc: BoxBudget, one bound for both: a box isn't expanded inside itself,
nesting stops at MAX_TEXT_BOX_DEPTH, an outermost box expands at most 64
boxes inside it (so one hostile box can't starve the next page's header
boxes) and a pass at most 20,000.
- layout: Ctx carries a BoxBudget for the whole layout; place_objects asks it
before laying out a box's text. DocLayout::text_boxes reports how many were.
- doc: walk_objects (objects_in_reading_order, notes_in_text_box) uses it too.
- Tests (crafted documents): a box showing itself 30 times lays out once; a
five-level chain fanning out 30 ways stays within the budget, for layout and
for the walk; a box inside a box still shows its text. Weakening the budget
makes them fail (20,000 boxes / 600,001 visits).
Tidy-ups:
- para: row spans in a VecDeque (pop_front, not remove(0)).
- fit_box: a text box's text area excludes both the top and bottom margins.
- theme: Tokens::handle / handle_shadow for the frame handles (light, dark);
the drag preview's opacity is a named constant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* box budget: no overall cap; bound the .docx writer too; tests
An audit of the previous commit found:
- The 20,000-box overall cap starved ordinary long documents: one budget per
layout, spent by body boxes first, then one per page for header boxes (two
for footers with floats), so a long document's header boxes went blank on
every page. It wasn't needed: only nesting grows exponentially, and
MAX_NESTED caps that per outermost box, keeping the work linear. Removed.
- The .docx writer still expanded a shape's story only depth-limited (a box
showing itself 30 times saved as ~28,000 nested copies, ~7 MB zipped). It
now uses the same BoxBudget, and only for text box parts.
- Layout expanded a shape's story whatever its part kind (a shape pointing
at a header showed the header's text); now text box parts only, as the
walk does. The story's blocks are cloned only once the budget agrees.
- Translations of "Count words in text boxes and notes" said footnotes only;
now footnotes and endnotes, in each catalog's own terms.
- Tests: no timing assert or leftovers; a long document with 400 boxes and a
header box shows all of them on every page; notes_in_text_box follows
nested boxes once; saving a self-showing box stays small (fails at 7 MB
without the bound).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arrange: nudging or dragging an aligned float keeps it in place first
main (#136) reads w:align into Float.h_align/v_align, which overrides the offset; the PR's
nudge only changed the offset and drag only reset the anchors. Nudge now converts an
aligned axis into a column/paragraph offset that keeps the object where it is, and a drag
clears the alignment. Test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: a lookup that races another thread's load of the same family no longer falls back
FontDb::face fell back to Source Sans 3 when its load_cataloged added nothing, which happens
when another thread loaded the family between the miss and the load. word::resolve memoises
that, so the wrong font stuck for the process; it made layout tests on main flaky
(justified_lines_shrink_spaces_in_word_2013_mode, space_shrinking_is_only_for_justified_text
failed 6 of 15 runs on origin/main). Look again after loading whatever it returns. Regression
test races 8 threads on fresh databases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout tests: text box and two-sided wrap tests measure what they mean
square_wrap_flows_text_on_both_sides compared xs.last() (the end of a trailing space, which
may hang past the edge) with the edge; compare the last non-space cluster's right edge.
text_box_hides_text_that_does_not_fit compared a tall box's lines with another document's
line count; compare with its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Weeb Labs <weeblabs@VFIEVOX3.Router>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* D0+D1: open Word 97-2003 .doc — container, FIB, piece table text (#28)
New wordcraft-docbin crate (L2) reads the OLE2 container via the cfb
crate (MIT) and parses the FIB and piece table from the [MS-DOC]
specification: text pieces decode as UTF-16LE or Windows-1252 (table
moved to wordcraft-doc::encoding), subdocument CP ranges, and Word's
special characters map to the model's (tabs, breaks, hyphens). .doc/.dot
registered in OPEN_EXTS and the I/O dispatch. Encrypted and Word 6/95
files give clear errors; every length is checked and proptest
truncation/flips never panic.
* D2: .doc formatting — stylesheet, fonts, CHPX/PAPX FKPs, sprms (#28)
Sprm decoder with operand-size rules (incl. sprmTDefTable/sprmPChgTabs
exceptions and the Prm0 isprm table), bin tables + ChpxFkp/PapxFkp page
lookup, STSH styles (StdfBase layout validated against real Word files:
bchUpe == cbStd, sti 0 = Normal) and the SttbfFfn font table in both
extended and self-delimiting flavours. The CP walk now splits runs on
formatting changes and resolves paragraph styles: bold/italic/strike/
underline/caps/hidden, colours (Ico palette + COLORREF), half-point
sizes, super/subscript, fonts, highlights; alignment, indents, spacing,
keep options, outline levels. Unknown sprms are skipped leniently;
an env-gated corpus test opens real files (styles + text asserted).
* D3: .doc structure — sections, headers/footers, tables, lists (#28)
- Sections: PlcfSed/Sepx with 16-bit measurement sprms (sprmSXaPage & co),
explicit sprmSBOrientation with a width/height fallback; even/odd/first
header and footer stories from Plcfhdd become document parts.
- Tables: sprmTDefTable grids (operand length fixed to cb+1 per the spec),
horizontal merges (sprmTMerge), vertical merges (sprmTVertMerge), cell
shading (sprmTSetShd / sprmTDefTableShd80), cell widths, valign, row
height/header/cant-split, and table borders (sprmTTableBorders80 Brc80s).
- Lists: PlfLst (LSTF + appended LVLs) and PlfLfo (LFO + LFOLVL overrides)
resolved into the numbering model; sprmPIlvl/sprmPIlfo mark paragraphs.
- Combined fixture (intro + table + list + 2 sections + header) round-trips
through Document JSON; hostile proptests now mutate the structured fixture.
* D4: .doc notes, fields, bookmarks and inline pictures (#28)
- Footnotes/endnotes: PlcffndRef/PlcfEndRef reference CPs paired with the
PlcffndTxt/PlcfEndTxt story ranges; each story walks into a Footnote or
Endnote part and the 0x02 anchor becomes a NoteRef object.
- Fields: 0x13/0x14/0x15 state machine in the walker — instruction text is
hidden, the cached result kept; HYPERLINK fields keep their result as
linked runs, everything else becomes one Field object.
- Bookmarks: SttbfBkmk names + Plcfbkf/Plcfbkl CPs become BookmarkStart/
BookmarkEnd objects at their anchors.
- Pictures: sprmCPicLocation points into the Data stream; the PICF header
gives the display size (dxaGoal/dyaGoal scaled by mx/my) and the payload
is format-sniffed (PNG/JPEG/GIF/BMP; metafiles are dropped with a warn).
- Hostile proptests now truncate/flip a fixture with all of the above.
* D5: .doc import hardening, caps and roadmap (#28)
- Cap inline pictures at 5 000 per document.
- Engine test: .doc/.dot bytes dispatch through the Word 97-2003 reader
(garbage must error, never panic or open empty).
- ROADMAP: Word 97-2003 import row (60% — text, formatting, styles,
sections, headers/footers, tables, lists, notes, fields, bookmarks,
raster pictures).
Real-corpus validation stays opt-in: WORDCRAFT_DOCBIN_FIXTURES=<dir>
cargo test -p wordcraft-docbin corpus_files_open.
* Corpus fixes: compressed-piece FC space, PAPX query point, none-level labels (#28)
Validated against two real files (kept locally under plan/word/fixtures,
gitignored): a 1 MB Lorem-ipsum sample and a 2003 styles/lists/table
sample.
- FKP bin tables and pages address the WordDocument stream in true byte
offsets; a compressed piece stores its base fc doubled (byte = fc/2),
so fc_of_cp now unwinds the doubling. Before, every PAPX/CHPX lookup in
compressed (cp1252) files landed on the wrong entry — tables collapsed
to one row, lists and heading styles were lost.
- The paragraph-properties query now uses the paragraph mark's own FC
(inside the paragraph) instead of the FC after it, which picked up the
next paragraph's FKP entry.
- Fixture papx entries re-keyed to paragraph-start CPs, the real rgfc
convention.
- Counters::next_label returns nothing for NumFormat::None levels (Word's
numFmt none shows no label; also fixes a latent docx-reader case).
- Symbol/Wingdings bullet chars map to Unicode glyphs (• ▪ ○ ●).
With this, both corpus files render headings, styles, nested and numbered
lists, the tables and the PAGE field correctly; metafile-wrapped images
and OLE objects stay dropped by policy (logged).
* docbin: bound the text walk by real text; dedupe and cap pictures (#28)
Hostile CP numbers no longer drive the work:
- header/footnote/endnote story ranges from the Table stream are clamped to their
subdocument (ccpHdd/ccpFtn/ccpEdn) with checked base additions, and every walk stops at
the last piece's CP; a story spanning [0, 4e9) used to push billions of U+FFFD;
- characters are found by binary search over the pieces (PieceTable::char_at), and an
unreadable stretch (gap, truncated or out-of-stream piece) is skipped in one step rather
than one U+FFFD per CP; the old per-character scan was O(chars x pieces);
- the piece CPs are made non-decreasing at parse, so inverted pieces are empty and binary
search stays sound; surrogate pairs now decode;
- note references are looked up by binary search; the style's CharProps is borrowed, not
cloned, per character;
- pictures are interned by fc_pic (5000 anchors on one picture share one media entry and
one copy) and the bytes copied per document are capped at 256 MB.
Each fix has a crafted-bytes regression test; against the previous code the story,
piece-claim, last-piece and many-pieces tests hang (killed at 60 s) and the shared
picture test copies the picture once per anchor (80 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docbin: checked offset arithmetic in lists, sections and table rows (#28)
- list.rs: PlfLst/PlfLfo/LVL offsets (at + 2 + cLst*28, at + 4 + lfoMac*16, off += len,
...) saturate, which matters on 32-bit targets (wasm32) where fc + n can overflow usize;
the LFO override path no longer indexes abstracts directly.
- sections.rs: the Sepx range (fc_sepx + 2 + cb) is checked; Plcfhdd is read only as far
as the stories a capped section count can use (it used to reserve lcb/4 entries).
- table.rs: sprmTDyaRowHeight = i16::MIN negated in i16 (overflow panic in debug builds);
now -(v as f32).
Regression tests: the row height test failed before (overflow panic with debug assertions, a 0 pt height without); the list test pins the saturating
offsets (it only overflowed on 32-bit targets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the merged OPEN_EXTS list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 1: range fields and custom document properties round-trip
Citation managers' ADDIN fields (Zotero, Mendeley, EndNote) are now kept as
range fields: FieldStart/FieldEnd markers with the result as ordinary,
formatted content between them, possibly spanning paragraphs (a Zotero
bibliography). Before, the result was flattened to plain text and a
multi-paragraph field lost its end.
- doc: InlineObject::FieldStart/FieldEnd, Document::field_ranges,
balance_field_ranges; CustomProp and custom_props with get/set/remove.
- docx: read ADDIN fields as ranges (field codes up to 2 MiB), write them
back as begin/instr/separate…end; orphan markers are dropped on save so
files always hold whole fields. docProps/custom.xml is parsed and
regenerated (simple types as text, vectors/blobs verbatim) instead of
passed through, so Zotero's ZOTERO_PREF_n settings are editable.
- engine: file.properties reads and sets custom properties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 2: wire-protocol client and document bridge
New crate wordcraft-zotero (L5) speaks Zotero's published word-processor
protocol (port 23116; 32-bit transaction id + big-endian length frames,
JSON payloads, ERR: replies):
- client: sends an integration command (addEditCitation,
addEditBibliography, addNote, refresh, removeCodes, setDocPrefs) and
answers Zotero's calls until Document_complete or the connection
closes (cancelled dialog). Answers go through a callback so the app can
apply them on its UI thread; run_on_session answers directly.
- Bridge: implements Zotero's Document_*/Field_* calls on a Session.
Fields are stored as Word's plugin stores them (ADDIN ZOTERO_ range
fields; prefs in ZOTERO_PREF_n, with Zotero's ReferenceMark field type
kept as Word's Field in the file), so documents stay Word-compatible.
Rich text (RTF or HTML) keeps citation formatting; bibliographies get
a Bibliography style from setBibliographyStyle; note styles create
footnotes/endnotes, and deleting a lone note citation removes its note;
placeholder links convert to fields. One transaction = one undo step.
Field ids stay stable through inserts and deletes.
- Tests: a fake Zotero over a real socket, every call, docx round trip,
hostile arguments for every method, random frames (proptest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 3: Zotero in the app and the CLI, tested with the real Zotero
- ui.zotero.<command> (addEditCitation, addEditBibliography, addNote,
refresh, removeCodes, setDocPrefs), ui.zotero.status and
ui.zotero.answer. The socket runs on a background thread; each of
Zotero's calls is answered on the UI thread against the session, and
Zotero's questions become a dialog the session waits on. Web builds
report that Zotero needs the desktop app.
- wordcraft-cli zotero <command> <file> [--save] [--trace] [--port].
- Bridge: a selection Zotero makes to show a citation is put back when
the session ends.
- ui_shot: {"wait": ms} keeps frames running for background work.
- docs/zotero.md.
Checked against Zotero 10.0.6: refresh of a citation and bibliography
(APA), its "modified citation" question answered in the app's dialog,
RTF results with italics, Bibliography style, prefs round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 4: switching between in-text and note styles moves citations
Document_convert and Field_convert now move a citation between the
text, footnotes and endnotes, as Zotero asks when the document's style
changes between an in-text and a note style: into a new note at the
same place, or back into the text where the note mark was (removing a
note that held only the citation; keeping one with other text). Field
ids survive the move, formatting is kept, and each Zotero session stays
one undo step.
Checked against Zotero 10.0.6: unlink citations (removeCodes, with its
confirmation question) and a refresh after switching the document to
Chicago notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 5: Zotero tab in the ribbon
A Zotero tab (before Help) with Add/Edit Citation, Add Note, Add/Edit
Bibliography, Refresh, Document Preferences and Unlink Citations. The
buttons are disabled while a Zotero command is running and in the web
build. Three new icons drawn in code (document preferences, unlink,
add note); the others reuse the References tab's. Labels and status
messages translated (zh-hans, zh-hant, ja).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: Add/Edit Bibliography inserted nothing
Zotero reads a newly inserted bibliography field back before filling
it, and unlinks a field whose text is empty, taking it for a
bibliography the user deleted. Our new fields were empty, so Add/Edit
Bibliography ended with Field_removeCode and nothing in the document.
New fields now hold "{Citation}" until Zotero writes the result.
Found by tracing the session with Zotero 10.0.6; regression test added.
wordcraft-cli zotero takes --cmd (run before the Zotero command, e.g.
caret.docEnd) to reproduce sessions from a given caret position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: log each command's outcome and Zotero's calls to the app log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Aloud player: speed, previous/next sentence, skips citations
Read Aloud now speaks sentence by sentence with the system speech program
(say / espeak / Windows SAPI) on a worker thread, so it can pause, skip
and change speed. A floating player window has previous / play-pause /
next / stop, a 0.5-3x speed slider and "Skip citations & bibliography"
(on by default: Zotero, Mendeley, EndNote and Word citation fields).
The spoken sentence is highlighted and the caret follows it.
New commands: readAloud.playPause/next/previous/stop/speed/
skipCitations/status, and caret.pastCitation (Zotero > Move Past
Citation). Speed and skip preference persist with the UI prefs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cargo.lock: wordcraft-zotero at the workspace version (0.4.0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Aloud: no unused BASE_WPM warning on wasm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Aloud: fuzz review.readAloud in hostile_params_never_panic
#190 makes the engine's speech backend silent (Hold) under cfg(test), so the
#75 skip is no longer needed; assert the backend instead so a test can
never start the system synthesiser.
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>
* Zotero 1: range fields and custom document properties round-trip
Citation managers' ADDIN fields (Zotero, Mendeley, EndNote) are now kept as
range fields: FieldStart/FieldEnd markers with the result as ordinary,
formatted content between them, possibly spanning paragraphs (a Zotero
bibliography). Before, the result was flattened to plain text and a
multi-paragraph field lost its end.
- doc: InlineObject::FieldStart/FieldEnd, Document::field_ranges,
balance_field_ranges; CustomProp and custom_props with get/set/remove.
- docx: read ADDIN fields as ranges (field codes up to 2 MiB), write them
back as begin/instr/separate…end; orphan markers are dropped on save so
files always hold whole fields. docProps/custom.xml is parsed and
regenerated (simple types as text, vectors/blobs verbatim) instead of
passed through, so Zotero's ZOTERO_PREF_n settings are editable.
- engine: file.properties reads and sets custom properties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 2: wire-protocol client and document bridge
New crate wordcraft-zotero (L5) speaks Zotero's published word-processor
protocol (port 23116; 32-bit transaction id + big-endian length frames,
JSON payloads, ERR: replies):
- client: sends an integration command (addEditCitation,
addEditBibliography, addNote, refresh, removeCodes, setDocPrefs) and
answers Zotero's calls until Document_complete or the connection
closes (cancelled dialog). Answers go through a callback so the app can
apply them on its UI thread; run_on_session answers directly.
- Bridge: implements Zotero's Document_*/Field_* calls on a Session.
Fields are stored as Word's plugin stores them (ADDIN ZOTERO_ range
fields; prefs in ZOTERO_PREF_n, with Zotero's ReferenceMark field type
kept as Word's Field in the file), so documents stay Word-compatible.
Rich text (RTF or HTML) keeps citation formatting; bibliographies get
a Bibliography style from setBibliographyStyle; note styles create
footnotes/endnotes, and deleting a lone note citation removes its note;
placeholder links convert to fields. One transaction = one undo step.
Field ids stay stable through inserts and deletes.
- Tests: a fake Zotero over a real socket, every call, docx round trip,
hostile arguments for every method, random frames (proptest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 3: Zotero in the app and the CLI, tested with the real Zotero
- ui.zotero.<command> (addEditCitation, addEditBibliography, addNote,
refresh, removeCodes, setDocPrefs), ui.zotero.status and
ui.zotero.answer. The socket runs on a background thread; each of
Zotero's calls is answered on the UI thread against the session, and
Zotero's questions become a dialog the session waits on. Web builds
report that Zotero needs the desktop app.
- wordcraft-cli zotero <command> <file> [--save] [--trace] [--port].
- Bridge: a selection Zotero makes to show a citation is put back when
the session ends.
- ui_shot: {"wait": ms} keeps frames running for background work.
- docs/zotero.md.
Checked against Zotero 10.0.6: refresh of a citation and bibliography
(APA), its "modified citation" question answered in the app's dialog,
RTF results with italics, Bibliography style, prefs round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 4: switching between in-text and note styles moves citations
Document_convert and Field_convert now move a citation between the
text, footnotes and endnotes, as Zotero asks when the document's style
changes between an in-text and a note style: into a new note at the
same place, or back into the text where the note mark was (removing a
note that held only the citation; keeping one with other text). Field
ids survive the move, formatting is kept, and each Zotero session stays
one undo step.
Checked against Zotero 10.0.6: unlink citations (removeCodes, with its
confirmation question) and a refresh after switching the document to
Chicago notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 5: Zotero tab in the ribbon
A Zotero tab (before Help) with Add/Edit Citation, Add Note, Add/Edit
Bibliography, Refresh, Document Preferences and Unlink Citations. The
buttons are disabled while a Zotero command is running and in the web
build. Three new icons drawn in code (document preferences, unlink,
add note); the others reuse the References tab's. Labels and status
messages translated (zh-hans, zh-hant, ja).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: Add/Edit Bibliography inserted nothing
Zotero reads a newly inserted bibliography field back before filling
it, and unlinks a field whose text is empty, taking it for a
bibliography the user deleted. Our new fields were empty, so Add/Edit
Bibliography ended with Field_removeCode and nothing in the document.
New fields now hold "{Citation}" until Zotero writes the result.
Found by tracing the session with Zotero 10.0.6; regression test added.
wordcraft-cli zotero takes --cmd (run before the Zotero command, e.g.
caret.docEnd) to reproduce sessions from a given caret position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: log each command's outcome and Zotero's calls to the app log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cargo.lock: wordcraft-zotero at the workspace version (0.4.0)
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>
* i18n #85: add Ukrainian interface translation
Translate the existing 771-entry interface catalog and register Ukrainian
for locale detection and persisted selection. Check full catalog coverage,
count-neutral labels, preference persistence, and bundled Ukrainian glyph
coverage. Validate the translation against Ukrainian dictionaries.
Co-authored-by: Codex <noreply@openai.com>
* i18n: Ukrainian rows for strings added on main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Ukrainian label for Character Border (lands after #60)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* LaTeX (.tex) import and export (#7)
Open and save LaTeX source as a document format, next to Markdown and
HTML. Export writes a standalone article (fontenc, ulem, xcolor,
multirow, amsmath, hyperref, loaded only when used): unnumbered sections,
itemize/enumerate, quote, center/flushright, verbatim, tabular with the
document's column widths and merged cells, equations as $..$ / \[..\],
character formatting, colours and links; metadata goes to \hypersetup.
Import reads the everyday subset: preamble \title/\author, sectioning
(\part/\chapter shift levels), paragraphs, lists with labels, quote,
alignment environments, verbatim-like environments, tabular with
\multicolumn/\multirow/\cellcolor and p{} widths, inline and display
math, formatting commands and declarations, colours, links, refs,
citations, footnotes, accents, TeX ligatures and special characters.
Unknown commands keep their arguments' text; layout and definition
commands are dropped with their arguments. Nesting is depth-limited and
input is capped, so hostile files can't overflow the stack.
The flow model gains an Equation inline so equations survive LaTeX round
trips; the other text formats write their linear text as before.
Wired into open/save dispatch, the file dialog filter, Backstage Export,
the web app, CLI/MCP help, docs and the Linux desktop entry.
Fixes#7.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FczntrDNYd6DupyBdpbL5
* #7: bound LaTeX column specs and equation export (review)
- spec_widths: `*{n}{…}` repeats recurse with a depth argument and stop at
MAX_NEST, so a deeply nested column spec no longer overflows the stack or
exhausts memory. Column counts saturate and widths stop at MAX_COLS.
- Column specs read from the source are capped at MAX_SPEC (4096) chars.
- balanced() looks groups up in a table of closers built in one stack pass,
so linear_to_latex no longer rescans to the end of the string for every
unmatched `(` (quadratic: 50k took seconds). spec_widths uses it too.
- env_name restores the position when `\begin{` / `\end{` has no `}`
within 64 chars, so the `{` is no longer lost.
- Regression tests: the 100k-deep `*{1}{` spec and a 200k-`(` equation in
deep_nesting_is_bounded; unclosed_begin_keeps_its_brace and
spec_widths_repeats_are_bounded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD
* #7: linear-time labels, brackets and \begin{; no commands from the document in exports
Applies @HaringIbon's review suggestions:
1. Parser::anchor keeps at most MAX_ANCHOR_RUN (64) bookmarks in a row with
no text between them; text() looked back past every one, so a file of
labels imported in quadratic time. Labels separated by text are all kept.
2. linear_to_latex_at checks for the `/` of a fraction before copying the
group out, so deeply nested matched brackets export in linear time.
3. env_name searches only the next 65 bytes for `}`, so a run of unclosed
`\begin{` imports in linear time.
4. An equation's `\name` is written as a command only when it is a known
symbol, function or font/spacing command (MATH_COMMANDS); anything else
becomes `\backslash name`. Two carets are never written side by side in
equations or anchor names, since TeX reads `^^5c` as a backslash. Compiling
an export can no longer `\input` files or `\write18` named by the document.
Regression tests: three cases added to deep_nesting_is_bounded, plus
latex_export_carries_no_commands_from_the_document and
latex_labels_in_a_row_are_read_in_linear_time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD
* Merge follow-ups: FTable.borderless (from #102) in the LaTeX importer; rustfmt
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>
* M2: styles.apply (Apply Styles) command; parity 355/405
Add Home > Styles > Apply Styles as a pure command. With a style name
or id it runs para.style (one undo step); with no params it opens the
styles pane and returns {"pane": true}. Non-string, empty or
whitespace style values return CmdError::Params. Move Mod+Shift+S
from para.style to styles.apply. Add 8 tests, ja/zh labels, and
regenerate docs/parity.md.
* i18n: pt-BR label for Apply Styles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
771-entry pt-br.tsv catalog written clean-room from the English labels,
one LANGUAGES row, locale/persistence/coverage/font regression tests,
and README, ROADMAP, control-protocol and ATTRIBUTION updates.