Commit Graph
67 Commits
Author SHA1 Message Date
8bd3427012 View: Hide White Space between pages in Print Layout (#645) (#684)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:34:30 -04:00
f7a84fbfb8 Print Markup: comments in PDF, print, images and text copies (#589) (#667)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:38:29 -04:00
39f51ca292 More Colors… and a Colors dialog for custom colours (#606) (#666)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:27:34 -04:00
ab49e4f5fa Open playlists and other text files (.m3u, .m3u8, .log, .ini, …) (#569) (#648)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:07:41 -04:00
14080c614c Drag after a double/triple-click extends the selection by words/paragraphs (#549) (#649)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 22:58:11 -04:00
alexanderalberandBrandon Thomas f91ce21f90 Find: each Find Next in the dialog moves one match (#468) (#556)
The Find and Replace dialog ran edit.find and then edit.findNext on every
click (and Enter). edit.find already selects the first match after the
caret, so the step after it skipped one: with four matches only the
second and fourth were ever reached, with two only the second.

edit.findNext and edit.findPrevious now take the same optional search
options as edit.find (text, matchCase, wholeWord, regex), and the dialog
sends one edit.findNext with its options. Calls without options behave
as before (F3, Mod+G).

generated with Claude Code

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 22:02:37 -04:00
baeb8d9c6c Open and save Word XML Document (Flat OPC); open Word 2003 XML (#548) (#560)
- docx: Flat OPC (`pkg:package`) read by unpacking the parts into an
  in-memory package for the DOCX reader; written from the DOCX writer's
  parts (XML inline, binary parts in base64). Part count, part size,
  total size and base64 length are capped.
- docx: Word 2003 XML (`w:wordDocument`) imported by rewriting it as the
  corresponding package (hyphenated names, w:hlink, w:listPr, w:lists,
  inline headers/footers, notes, aml annotations, w:binData pictures,
  o:DocumentProperties) and reading that with the DOCX reader.
- `.xml` told apart by root element; other XML is refused with
  "not a Word XML document". File > Export gets "Word XML Document (*.xml)"
  (all 9 catalogs); .xml saves keep the document like .docx.
- Tests: Flat OPC round trip equals DOCX round trip, hand-written Flat OPC
  package, Word 2003 import, hostile inputs, engine io.

Fixes #548

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:59:00 -04:00
dab5092e83 Font dialog: separate Asian and Latin text fonts (#505) (#536)
* Font list: installed Chinese fonts are listed, by their Chinese names in a Chinese interface (#505)

The font database read only the English family name of each font, so a
Chinese interface listed SimSun, Microsoft YaHei and WenQuanYi Zen Hei
by their English names and never as 宋体, 微软雅黑 or 文泉驿正黑, and a
document naming a font by its Chinese name fell back to another font.

- fonts: keep each face's localized family names (name IDs 16 and 1,
  every face of a collection, read during the scan without loading the
  file); FontDb::families_in(lang) lists them in place of the English
  names for a matching interface language (zh-hans, zh-hant, ja, ko);
  face, has_family and styles accept either name.
- ui: the ribbon and mini toolbar font boxes show the interface
  language's names and store the English name.
- East Asian font: choosing a Chinese, Japanese or Korean font also sets
  it as the East Asian font (w:eastAsia), read and written in DOCX;
  layout draws CJK text in a run's installed East Asian font.

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

* Font dialog: separate Asian and Latin text fonts (#505)

Word's Font dialog sets the font of Chinese, Japanese and Korean text
apart from the font of Latin text; WordCraft's had one font box.

- Font dialog: an "Asian text font" box (the installed Chinese, Japanese
  and Korean fonts, by their interface-language names, type to find)
  above the "Latin text font" box, shown for a Chinese or Japanese
  interface, East Asian text in the selection or a selection with an
  Asian font. OK sets only the fonts changed, in one undo step.
- format.font takes "eastAsia" (w:eastAsia) besides "name"; either or
  both; format.state reports fontEastAsia.
- fonts: the scan reads each font's OS/2 code pages, so
  FontDb::east_asian_families lists CJK fonts without loading them.
- Tests: dialog sets each font on its own; layout sets CJK text in the
  run's Asian font and Latin text in its Latin font (skipped without an
  installed CJK font); command and undo; the scan finds a CJK font.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:42:56 -04:00
7484a6bd11 Find and Replace: special characters (^p, ^t, …) and wildcards (#543) (#553)
- Find what understands Word's ^ codes (^p ^t ^? ^# ^$ ^^ ^l ^m ^n ^b ^+ ^= ^s ^~ ^- ^w ^g ^% ^v,
  ^nnn / ^unnnn); Replace with adds ^& (found text) and ^c (clipboard). `special: false` reads
  the text literally.
- Use wildcards: ? * [abc] [a-z] [!x] {n} {n,} {n,m} @ < > ( ), \1-\9 in Replace with; always
  matches case, like Word. Patterns are translated to the linear-time regex engine (no
  catastrophic backtracking), length and counts capped, bad patterns are errors.
- ^p matches paragraph marks between sibling paragraphs; a ^p in the replacement splits the
  paragraph. Replace All stays one undo step.
- Match prefix / suffix, Ignore punctuation / white-space characters.
- Dialog: Use wildcards checkbox, Search Options with the new options and a Special menu that
  inserts codes into the focused field (a shorter list with wildcards); strings in all 9 catalogs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:15:31 -04:00
46d5e99b3e Styles: Update to Match Selection takes the selection's formatting (#555) (#562)
styles.updateToMatch copied only the direct run/paragraph formatting at the
caret onto the style, so text whose look came from its own style (Normal,
Title, ...) changed nothing; it then wiped that paragraph's direct formatting
without applying the style, so the selection lost its look too.

Now the style takes the selection's effective formatting (paragraph style
chain, character style chain, direct formatting) as a diff against what the
style currently gives, keeping its outline level and numbering. The selected
paragraphs take the style and drop only the direct formatting it now carries.
The Home styles gallery tiles get the right-click menu (Update to Match
Selection, Modify...).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:06:14 -04:00
6a22d7257b Insert › SmartArt: create and edit list, process, cycle, hierarchy, pyramid, radial, matrix and venn graphics (#497) (#503)
- Model (doc::smart_art): layout, items (text + level), colour variant; capped at 100 items,
  255 characters, 8 levels.
- Layout algorithms of our own (docx::smart_art) place rounded rects, circles, bands, arrows and
  connectors; WordCraft draws the graphic from the same dsp:drawing it saves.
- DOCX: data (dgm:dataModel points and connections), our own layout/quick-style/colour
  definitions under the built-in identifiers, the drawing part, relationships and content types.
  A diagram reads back editable only when its data part is exactly what WordCraft writes.
- Commands: insert.smartArt, smartArt.items/addShape/promote/demote/layout/colors/reset/textPane,
  one undo step each; picture.size relays out a SmartArt graphic.
- UI: layout picker with previews drawn in code, SmartArt Design tab, Text Pane; strings in all
  9 catalogs.
- Diagram reader draws `line` presets top-left to bottom-right (was the other diagonal).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 20:09:32 -04:00
900c0f4412 Outline view: Outlining tab, levels, collapse/expand, Show Level (#493) (#499)
* Outline view: Outlining tab, levels, collapse/expand, Show Level (#493)

- View › Outline is its own view mode: the body on one long sheet, each paragraph indented
  by its outline level behind a symbol drawn in code (circled plus for a heading with content,
  circled minus for one without, a dot for body text); paragraph indents and alignment hidden.
- Collapsed headings hide their subtree and get a wavy line; Show Level 1-9 / All; Show First
  Line Only; Show Text Formatting off draws everything in the Normal style. All of it is view
  state (ViewState.outline), not saved; collapsed headings are found again by their text after
  edits, and a caret entering hidden text moves on to the next shown paragraph.
- Commands: outline.promoteToHeading1/promote/demote/demoteToBody/level/moveUp/moveDown,
  outline.expand/collapse/toggle/selectSubtree, outline.showLevel (+ showLevel1-9, showAll),
  outline.firstLineOnly, outline.showFormatting, outline.close, outline.state. In Outline view
  Move Up/Down and Promote/Demote take a collapsed heading's hidden content along.
- Keys: Alt+Shift+Plus/Minus expand/collapse, Alt+Shift+1-9 and Alt+Shift+A Show Level.
- Outlining tab (next to File while in Outline view): level box, promote/demote, move,
  expand/collapse, Show Level, the two check boxes, Close Outline View. Clicking a symbol
  selects the heading with its content; double-clicking expands or collapses it.
- Catalog: Outlining tab rows (421/453, 92.9%); translations in all nine catalogs; docs.

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

* Docs: drop the stale shortcut and dialog rows left by the rebase (#493)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:54:56 -04:00
968c66a13e Fixes: tracked paste marks, plain paste blank lines, ^p in Replace, Delete/Split columns and cells, Font dialog keeps untouched formatting (#417, #422, #423, #431, #432, #466) (#479)
- Paste under Track Changes marks the paragraph marks it adds as inserted, so Reject All
  joins the paragraphs again (#417).
- Keep Text Only / Paste Special text paste treat paragraph separators as text, not Enter:
  an empty list item between them no longer ends the list (#422).
- Untracked Replace All splits paragraphs at CR / U+2029 in the replacement like a single
  Replace does (#423).
- Delete Columns deletes every grid column the selected cells cover (#431).
- Split Cells refines the grid inside the split cell only; other rows' cells widen over the
  new grid columns, keeping the table width (#432).
- Font dialog sends only the properties the user changed and shows small caps, all caps,
  hidden and spacing from the selection (format.state reports them) (#466).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:36:30 -04:00
50505d9e62 Editing fixes: Center range, undo grouping, Change Case runs and offsets, Bold undo at caret (#418, #419, #420, #421, #424) (#477)
- Paragraph formatting skips the paragraph a non-empty selection ends at the start of
  (Select Paragraph's end), via Document::para_paths_in (#418).
- A non-mutating command that moves the selection closes the typing group (#419).
- Change Case converts char by char and replaces only changed stretches inside one run,
  keeping each run's formatting (#421) and mapping the selection to the new offsets (#420).
- Undo/redo steps carry the pending caret formatting (#424).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:13:50 -04:00
4b5ed293d7 Compare, Restrict Editing and AutoCorrect Options dialogs (#412) (#476)
* Compare, Restrict Editing and AutoCorrect Options dialogs (#412)

- Compare / Combine Documents dialog: original (this document or a file) and
  revised document, label changes with, More >> comparison settings, word or
  character level, show changes in. review.compare honours case changes,
  white space, formatting (tracked formatting changes), tables (compared and
  kept, no longer dropped) and character level; the LCS no longer drops text
  past its size cap. Unsupported settings are accepted and reported.
- Restrict Editing pane: formatting restrictions (locked styles,
  w:formatting), editing restrictions, exceptions for everyone
  (w:permStart/permEnd), Start Enforcing Protection with an optional password
  (SHA-512, salt, spin count 100000; ECMA-376 §17.15.1.29) and Stop
  Protection. Enforced in Session::run: outside the exceptions nothing may
  change; undo can't lift protection; passwords never land in Repeat/macros.
  DOCX round trip of protection, hash attributes, locked styles and ranges.
- AutoCorrect Options dialog: replacement list (add/replace/delete, built-in
  entries too), capitalization options with exceptions, AutoFormat As You
  Type switches, Math AutoCorrect list. Saved per user in ui.json; damaged
  values read as defaults. Names of days are capitalised.

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

* Restrict Editing: verify Word's legacy protection hash; refuse unsupported forms (#412)

- Stop Protection checks Word's legacy w:documentProtection hash
  (w:cryptAlgorithmSid/w:hash/w:salt/w:cryptSpinCount) like Word: the password
  becomes the legacy 32-bit key (ECMA-376 Part 4 §14.8.1 initial code array and
  encryption matrix), its reversed bytes as an upper-case hex string are hashed
  with the salt prepended and the iteration number appended. A wrong password
  is rejected; the form no longer unlocks with any password.
- Unsupported or damaged hashes (MD2/MD4/MD5 and other SIDs, w:algIdExt,
  unknown algorithmName, spin counts above 10,000,000, bad base64) refuse with
  "This password type isn't supported" and the protection stays on. A
  protection without a hash still stops without a password.
- Known-answer tests: the specification's "Example" key 0x64CEED7E and an
  independently computed SHA-1 hash; pane message in all 9 catalogs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:10:02 -04:00
Valentin KleinandClaude Opus 5.5 fd014ea9a1 Insert › Chart: create and edit column, bar, line, pie, area, scatter and doughnut charts (#409) (#415)
* Insert › Chart: create and edit column, bar, line, pie, area, scatter and doughnut charts (#409)

- Chart model (wordcraft_doc::chart::ChartSpec): type (clustered/stacked column and bar, line,
  area, pie, doughnut, scatter), categories, series, title, legend, data labels; sanitized
  (255 series, 4000 points, 100k cells, 255-char text, non-finite values dropped).
- DOCX: charts with a model are written as word/charts/chartN.xml (c:chartSpace, ECMA-376
  §21.2, data as c:strLit/c:numLit, no c:externalData) with relationship and content type.
  On read, a chart part becomes editable only when writing its model again gives the same
  markup; every other chart stays read-only and kept as before (#319).
- Drawing reuses read/chart.rs (the model is drawn from its generated part); data labels
  (c:dLbls showVal) are now drawn for bar, line, area, scatter, pie and doughnut charts.
- Commands: insert.chart, chart.type, chart.editData, chart.title, chart.legend,
  chart.dataLabels (one undo step each); picture.size redraws an editable chart at its size.
- UI: Insert › Chart gallery with previews drawn in code, Chart Design contextual tab, Edit Data
  grid dialog; strings in all 9 catalogs; catalog 408/439.

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

* #409: refresh catalog count and parity checklist after rebase

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:26:44 -04:00
6526b134be Document grid: lines per page and characters per line (#391) (#401)
- Model: `SectionProps::doc_grid` (`w:docGrid` type, line pitch, charSpace) and the
  paragraph `w:snapToGrid` flag; DOCX read and write (ECMA-376 §17.6.5).
- Layout: with a line grid, lines of snapping paragraphs take whole line pitches with
  their text centred; with a character grid, East Asian characters take whole cells of
  the character pitch (all text with snapToChars), Latin text stays proportional.
  Hostile pitches and charSpace are clamped. Headers and footers don't snap.
- Command `layout.documentGrid` (type, linesPerPage, charsPerLine, linePitch,
  charSpace), one undo step, section scope; catalog row under Layout › Page Setup.
- Page Setup dialog: Document Grid section; View › Gridlines draws the document
  grid of a section that has one. UI strings in every catalog (incl. et, sr, sr-latn).
- Tests: DOCX round trip, line pitch and lines per page, character pitch placement,
  command with clamping and undo.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 15:29:13 -04:00
Valentin KleinandClaude Opus 5.5 4e710a920b Footnote and Endnote options per section (#385) (#390)
- Model: NoteProps (pos, numFmt, numStart, numRestart) in the settings and
  each section; Document::note_options resolves section > document > defaults.
- DOCX: w:footnotePr / w:endnotePr read and written in settings and every
  w:sectPr in schema order; hostile numStart clamped (ECMA-376 §17.11).
- Layout: footnotes below the text, endnotes at the end of each section,
  numbering restarted per section or per page (from where notes land, at
  most three passes), per-section number formats.
- Commands: references.noteOptions takes kind/pos/numFmt/numStart/
  numRestart/scope; references.convertNotes (footnote/endnote/swap, one
  undo step); references.footnote/endnote take a custom mark.
- UI: Footnote and Endnote dialog from the Footnotes group launcher;
  strings in every catalog.

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

---------

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

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

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

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

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 19:30:36 -04:00
15f2f01475 Tabs and Borders and Shading dialogs (#327)
* Add Tabs and Borders and Shading dialogs (#320)

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

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

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

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

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

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

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

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

* Columns, Symbol and Field dialogs (#321)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Columns, Symbol and Field dialogs (#321)

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

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

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

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

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

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

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

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

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

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

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

* cargo fmt after merging main

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 17:56:54 -04:00
4b450f1b00 Open and save password-protected Word documents (#55) (#281)
* 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>
2026-10-10 16:16:35 -04:00
29e978d6d8 Re-land #264, #263, #270, #271, #267, #280: clipboard pane, table styles, Manage Styles, Asian Typography, Group, Shape Effects (#323)
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>
2026-10-10 16:09:04 -04:00
08e8d4ade0 Table sizing: Height/Width boxes, AutoFit to contents and a Table Properties dialog (#287)
* 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>
2026-10-10 14:52:08 -04:00
c0bbcbac24 Charts and SmartArt: draw them instead of dropping them (#292)
A chart or SmartArt diagram in a DOCX was dropped on import: it took no
room, so the text after it moved up (five of the six chart documents in
docxide-pdf's fixtures came out a page short), and nothing was drawn.

They are now an InlineObject::Graphic: it takes its room, floats and
wraps like a picture, and draws a list of neutral items (shapes, paths,
one-line texts, pictures) that layout scales to the object's box.

- Charts (read/chart.rs) are drawn from the values the chart part
  caches: clustered, stacked and 100% bar and column charts, line, area,
  pie, doughnut and scatter charts, with title, legend, gridlines, axis
  labels and Excel number and date formats.
- SmartArt (read/diagram.rs) is drawn from the drawing Word stores
  beside the diagram: preset shapes, theme colours, text and pictures.
- Theme colour handling is shared (read/drawing_color.rs).
- Each chart or diagram part is read once, only through a relationship
  of its type, under a per-file budget of items.
- Tagged PDF tags them as figures with their alternative text, and the
  accessibility check reports charts without one.

Not yet: charts and diagrams aren't written back when saving a DOCX
(they weren't before either), so the editor doesn't let you move or
resize them. Radar, stock, 3-D and combination charts draw their first
supported chart type only; SmartArt pictures aren't clipped to their
shape.

docxide-pdf bench (365 documents, vs main + #135): page counts right
320 -> 326 (all five chart documents and one SmartArt document), Jaccard
passes 208 -> 215, SSIM passes 76 -> 79; eight documents better, none
worse.

Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:39:41 -04:00
Valentin KleinandClaude Opus 5.5 4a75c57f58 Comment balloons edit in place, and the ruler drags page margins (#167) (#289)
Clicking a comment balloon in the markup area selects it like Word: its text
becomes editable in place, with Reply, Resolve/Reopen and Delete, and replies
are shown (and editable) under the comment they answer. Each edit is written
back through a new review.editComment command, one undo step per editing
session; the Comments pane uses the same path (it used to write the text
without an undo step) and gains a Reply button.

The margin edges on the horizontal ruler (left/right) and the vertical ruler
(top/bottom) can be dragged to set the section's margins through
layout.margins, one undo step per drag, snapped to 1/16 inch and clamped so
at least an inch of text remains. Indent markers keep priority where they
overlap an edge. Setting margins by number no longer switches off mirrored
margins; only a preset decides that.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:06 -04:00
88ad402581 Gridlines: the document's own grid spacing, and table gridlines on by default (#69, follow-up to #243) (#283)
View › Gridlines now draws the drawing grid at the document's own spacing:
DOCX keeps it in settings.xml as w:drawingGridHorizontalSpacing and
w:drawingGridVerticalSpacing (ECMA-376 §17.15.1.44/45), which WordCraft now
reads (clamped) and writes (only when it differs from the default), with a
round-trip test. Documents that don't say use the spec's 180 twentieths of
a point (1/8 inch) instead of a fixed 0.5 cm; across and down can differ.

Table Layout › View Gridlines (table.viewGridlines) is on by default, as in
Word, so tables without borders still show their cells.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:31 -04:00
8ac57f0312 i18n: add Serbian, both scripts (sr, sr-latn) (#250)
Serbian is officially digraphic — Cyrillic and Latin are both in
everyday use, with Cyrillic the default per BCP 47 when no script is
given. Adds both as separate catalogs, same as the existing
Simplified/Traditional Chinese split handles script rather than
region:

- sr-latn.tsv: the actual translation, written clean-room from the
  plain meaning of each English label (never from Microsoft Word's or
  another product's Serbian localisation), covering the full existing
  catalog (874 entries, same key set as zh-hans.tsv).
- sr.tsv: mechanically transliterated from sr-latn.tsv by a small
  script (not committed — a one-off; the mapping is the standard
  digraph-aware Serbian Latin→Cyrillic table). Brand/product/format
  names (WordCraft, PDF, Discord, Zotero, SmartArt, file extensions…)
  and `{placeholders}` are protected from transliteration, same names
  that stay in Latin letters in every other catalog. Hand-editing this
  file isn't the workflow — change sr-latn.tsv and re-transliterate.
- mod.rs: registers `sr` (Cyrillic) and `sr-latn`. No special-casing
  needed in `candidates()` for the script split, unlike Chinese's
  region-implies-script fallback — `sr-Latn-RS` already produces
  `sr-latn` as a candidate before falling back to plain `sr`, and a
  bare `sr`/`sr-RS` (no script tag, the common case) resolves straight
  to `sr` since that's the registered default-script code.
- tests.rs: locale-matching coverage for both scripts, full-catalog
  parity with the reference language, and bundled-font glyph coverage
  for both Cyrillic and Serbian Latin's extended letters (š č ć ž đ),
  mirroring the existing Ukrainian/Portuguese tests.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:47:38 -04:00
15f5ad274e Column Selection: Alt+drag selects a block of text (#237)
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>
2026-10-10 13:47:06 -04:00
e71c2b6c5b Style Inspector: styles.inspector pane, styles.inspect query and per-level clearing (#236)
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>
2026-10-10 13:46:39 -04:00
8bc4e6c999 Paste Special: paste the clipboard as formatted text, RTF, HTML or unformatted text (edit.pasteSpecial) (#235)
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>
2026-10-10 13:46:21 -04:00
9c3ad84d33 Advanced Find: every match, Reading Highlight, search within a selection (#234)
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>
2026-10-10 13:45:35 -04:00
d8f71959d3 Create and modify your own table styles, saved in the .docx (#146) (#256)
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>
2026-10-10 13:45:11 -04:00
19e2e37ca6 View › Gridlines shows the drawing grid; Table Layout › View Gridlines outlines cells (#69) (#243)
View › Show › Gridlines only outlined table cells, so with no table on the
page it seemed to do nothing. It now draws a light 0.5 cm grid over the
page's text area, inside the margins, in Print Layout.

Table Layout › View Gridlines gets its own command, table.viewGridlines,
with its own view flag, and draws the table cell outlines as before. The
ribbon button, keytip and parity catalog point at it.

Both are screen-only overlays drawn by the canvas; print, PDF and image
exports never show them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:44:09 -04:00
69eaf9925c View tab: Zoom In/Out buttons that step from the shown zoom (#67) (#232)
Zoom In/Out stepped from the stored manual zoom, which goes stale while a
fit mode (Page Width, One Page, Multiple Pages) sizes the page: from a 163%
Page Width, Zoom In jumped to 110%, i.e. zoomed out. The canvas now keeps
view.zoom equal to the shown zoom while a fit mode is on, so Zoom In/Out,
the Zoom dialog and view.state all start from what the user sees.

The View › Zoom group gains Zoom In / Zoom Out buttons (code-drawn icons;
the catalog already lists them there), and the Zoom dialog's slider is
logarithmic and wider so a short drag no longer jumps from 100% to 480%.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:43:42 -04:00
8b49ea7546 Table Layout › Text Direction turns the cell's text (#226) (#245)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:05:04 -04:00
302528d67e Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)
#214 gave now_iso the browser's clock on wasm; the other clock helpers were
still stubs there: Insert Date's weekday was always Thursday, the PDF
creation date was left out, and the layout and UI millisecond clocks
returned 0, so browser AutoSave's timer never fired, the caret didn't blink
and status messages never cleared.

The engine's clock is now one now_unix() (std on native, js_sys::Date in the
browser, as #214 does) that now_iso and the weekday share; the pdf, layout
and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:20:56 -04:00
Valentin KleinandClaude Opus 5.5 4c4dc14a54 Reject All removes paragraph breaks inserted with Track Changes on (#229) (#244)
Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:19:52 -04:00
6565a0f0ce Headless: file.setAuthor takes a name; review.trackChanges lists its value param (#90) (#224)
file.setAuthor now documents {"name": string} in the command list, trims the
name, strips control characters, caps it at 255 characters and refuses a blank
name. wordcraft-cli run and headless mcp take --author NAME. review.trackChanges
lists {"value"?: bool} so scripts find the explicit, idempotent form.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:43:05 -04:00
919d74bee6 Shift+F3 runs Change Case again, not Find Previous (#186) (#221)
* Shift+F3 runs Change Case again, not Find Previous (#186)

edit.findPrevious and format.changeCase were both bound to Shift+F3.
Registry::by_shortcut returns the first match, so Find Previous won
and Change Case never ran from the keyboard. Find Previous keeps
Mod+Shift+G. A new test fails if any two commands share a shortcut.

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

* Contributors: credit vangel76 by their GitHub profile name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:27:11 -04:00
a9eb233a9d Track changes: keep other authors' deletions, credit paragraph splits, exact edit snapshots (split from #54) (#125)
* Track changes: a tracked delete keeps other authors' deletions

Deleting a range with Track Changes on marked every run in it as deleted
by the current author, also text another author had already deleted.
That author's deletion was lost: the change list showed it under the
wrong name, and rejecting the second author's changes brought back text
the first author had deleted. Text that is already deleted now keeps its
deletion and its author, like Word.

* Track changes: a tracked paragraph split credits the new mark to its author

Pressing Enter with Track Changes on inserts a paragraph mark by the
current author, but the paragraph after the split took its mark's
revisions from the text at the caret. Inside or at the end of another
author's insertion, the original paragraph mark then showed as inserted
by that author. The paragraph after the split now keeps the original
mark's revisions; only the new mark is the splitting author's.

* Engine: edit snapshots that take commands back exactly

Session::edit_snapshot and Session::restore let a caller run a command
and take it back without a trace: the document, the selection, the undo
and redo stacks, the open typing group, a pending join_next_undo and
the dirty flag come back as they were, and the layout is recomputed.
Edit > Undo cannot do this: it leaves a redo step, loses the redo stack
the command cleared, and closes the typing group.

At the undo limit (500 steps) a command pushes the oldest step out. The
snapshot keeps the oldest steps when the stack is near the limit and
restore puts them back, so the stack is exact there too (up to four
commands between snapshot and restore).

Restore is exact for commands that add undo steps; edit.undo and
file.new/file.open in between are outside its contract (documented).
The snapshot destructures every Session field, so a new field does not
build until someone decides whether a snapshot must cover it.

Session::undo_depth and the snapshot's doc/dirty/undo_depth accessors
tell a caller whether the command changed anything.

* Tests: hostile_params_never_panic leaves out review.readAloud

Read Aloud hands the document's text to the system's speech synthesizer
(say, PowerShell, spd-say or espeak), so running the test made the
computer speak the sample document. Like the file commands, it is left
out of the sweep.

* Undo: evictions at the limit are counted everywhere edit snapshots rely on them

Merging main brought two paths #125's eviction counter didn't know: push_undo (typing parity, #204) dropped the oldest step without counting it, and a failed command (#153) put the whole stacks back but left the count raised. Either made restore() bring back a step that had never left. Both now keep the count right; restore_is_exact_at_the_undo_limit covers each (both fail without the fix).

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

---------

Co-authored-by: lpgn <2556951+lpgn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:07:32 -04:00
e4456efb84 Text boxes and floating objects: editing, drag/resize, wrapping, overflow, footnotes (#46)
* 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>
2026-10-10 08:51:38 -04:00
663904a2e2 Import Word 97-2003 .doc files (#28) (#145)
* 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>
2026-10-10 08:36:38 -04:00
fe7e79ad70 Read Aloud: player with speed control, sentence skipping, skips citations (#190)
* 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>
2026-10-10 08:36:19 -04:00
43c0be5735 Zotero integration: cite while you write, with a Zotero tab (#189)
* 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>
2026-10-10 08:35:59 -04:00