Commit Graph
519 Commits
Author SHA1 Message Date
417896301b M1: preserve native Word ink through DOCX saves (#592)
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:30:49 -04:00
b30cda1535 Persist the custom dictionary across sessions (#40) (#796)
* Persist the custom dictionary across sessions (#40)

* Review fixes for #796: session-only Ignore All, desktop-only persistence, capped atomic I/O

- review.ignoreAll no longer adds to (and saves) the custom dictionary: ignored words go to a
  separate session-only list, as Ignore All does in Word.
- Only the desktop app wires the dictionary file. Headless MCP and `wordcraft-cli run` keep
  words in memory, so an agent never reads or changes the person's dictionary (and nothing
  writes outside an automation write root, #769).
- Loading reads through `take(1 MB)`, so a huge file is never read whole; lines that aren't a
  single word (spaces, control characters, invalid UTF-8, over 64 characters) are skipped.
- review.addToDictionary applies the same word rule (proof::custom_word), so a word added is
  never dropped on the next load.
- Saving merges the words on disk first (another process's additions survive; words just
  removed stay out), creates the folder, and replaces the file atomically (temp + rename).
  The file I/O lives in the proof crate, so the engine doesn't touch std::fs itself.
- Tests for each.

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-12 07:23:51 -04:00
64319e0039 MCP: confine file access with --automation-read-root and --automation-write-root (#769) (#798)
* Engine: check automation read and write roots on every path a command reads or writes (#769)

* MCP: confine the headless session to the automation roots (#769)

* CLI: add --automation-read-root and --automation-write-root to mcp (#769)

* Docs: document --automation-read-root and --automation-write-root for mcp (#769)

* Review fixes for #798: confine file.printPdf; guard all of engine/src

- print.rs::print_pdf_to_file (file.printPdf, added on main) wrote with std::fs::write
  directly, so a confined agent could print a PDF anywhere. It now goes through
  io::write_file, which checks the automation write root.
- The guard test now walks every file under crates/engine/src (not only cmd/), except
  io.rs, file_access.rs and the tests*.rs modules, and also flags OpenOptions.
- Tests: an engine test that file.printPdf is refused outside the write root, works inside
  and is unchanged without roots; the MCP integration test covers it too.

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

* Review fixes for #798: list file.printPdf among the confined writes in docs/mcp.md

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

* Font files added by path go through io, inside the automation read root (#769)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:18:00 -04:00
691e887004 i18n: complete Turkish interface catalog with 2,170 entries and full … (#774)
* i18n: complete Turkish interface catalog with 2,253 entries and full parity

* Review fixes for #774: Turkish entry count after merging main (2,325)

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-12 07:04:50 -04:00
325e5a6157 M8: Truncate long Navigation headings in resizable pane (#759)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:59:54 -04:00
62a17b841f Bidi: auto-detect RTL paragraph direction when typing Kurdish, Arabic and Hebrew (#713)
* Bidi: auto-detect RTL paragraph direction and keep typing caret advancing smoothly

When typing in Kurdish, Arabic or Hebrew in a new document, default
paragraph direction (bidi: None) was resolving to LTR. In an LTR
paragraph, visual reordering pinned newly typed characters and the
caret to the left margin (x = 72.0), resetting to the start of the line
with every keystroke.

- Add first_strong_is_rtl() following UAX #9 rules P2/P3.
- Auto-resolve unassigned paragraph direction to RTL in layout when the
  paragraph starts with an RTL script.
- Auto-promote paragraph to RTL (bidi = Some(true)) when typing text
  starting with an RTL character.
- Add regression test in wordcraft-engine.

* Review fixes for #713: RTL only when typing into an empty paragraph

- Drop the layout fallback that drew any paragraph without w:bidi as RTL when its first
  strong character is Arabic/Hebrew: opened DOCX paragraphs keep their direction, and a
  style that sets bidi=false is respected.
- type_text promotes a paragraph to RTL only when it had no letters before the insert and
  nothing (paragraph, style chain, document defaults) sets its direction; typing into a
  paragraph that already has text no longer flips it.
- docs/typing-parity.md: Text direction section; test in tests_typing.rs.

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-12 06:51:03 -04:00
e22ce1d05e i18n(hu): complete the Hungarian catalog again (#842)
* i18n: add Hungarian (hu) interface translation

- crates/ui-egui/src/i18n/hu.tsv: complete catalog, all 1,572 keys of
  es.tsv in the same order, written clean-room from the plain meaning
  of the English labels; placeholders, escapes and trailing ellipses
  kept. Commands are nouns, messages use the informal second person,
  and only the first word of a label is capitalised. A noun after a
  number stays singular in Hungarian, so the context-free format needs
  no plural forms.
- LANGUAGES gains `hu` (Magyar); `hu`, `hu-HU` and `hu_HU.UTF-8`
  resolve to it.
- The Font dialog's preview sample is "AaBbYyZz ŐőŰű", so All Caps
  shows the double acute capitals.
- Tests: locale resolution and saved preference, key parity with the
  Spanish catalog, sample translations, count wording, the sample's
  capitals and Hungarian letters in the bundled interface fonts.
- Docs: new localization-hu.md (conventions, terminology, scope);
  README, ROADMAP, localization-parity and control-protocol list the
  new language; ATTRIBUTION row for hu.tsv.

* i18n(hu): built-in style names, equation gallery and citation styles

- hu.tsv now carries every key of the Russian catalog: built-in style
  names (Normal, No Spacing, Subtitle, TOC {n}…), the equation gallery
  and structure labels, paper sizes, watermarks and the Backstage
  greetings, plus the citation style names (1,946 entries).
- The interface audit (untranslated_interface_text) checks Hungarian
  alongside Russian and reports every language's gaps in one run.
- Tests: Hungarian keys cover the Russian catalog; built-in style
  names and numbered style templates show in Hungarian.
- Docs: entry counts and scope in localization-hu, localization-parity
  and ROADMAP.

* Review fixes for #731: Hungarian stays a partial catalog

- The i18n audit requires completeness from Russian only again: hu.tsv lacks ~280 strings of the
  dialogs that landed since (Compare Documents, Math AutoCorrect, …), so it can't claim to be
  complete. The catalog test no longer asserts it holds every Russian/Spanish key.
- 43 Hungarian rows for tab and command labels that reached main since (charts, content controls,
  Developer tab, templates and the styles organizer, WordArt), which every_tab_and_command_is_translated
  requires.
- Docs say partial, with the entry count (1,989); localization-parity's header keeps its quote
  marker and the #505 note.

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

* Review fixes for #731: Hungarian rows for the Outlining tab

24 rows for Outline view's tab and commands, which landed on main meanwhile; 2,013 entries.

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

* Review fixes for #731: ROADMAP header keeps main's SmartArt note

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

* i18n(hu): complete the Hungarian catalog again

- hu.tsv: 374 new rows for the strings that reached main while #731 was
  in review (Compare Documents, Restrict Editing, Math AutoCorrect,
  charts, SmartArt, the Options sections, the print page and more).
  The catalog now holds every key of ru.tsv again: 2,397 entries.
- The completeness checks are back: the interface audit requires
  Hungarian alongside Russian, and the catalog test asserts hu.tsv holds
  every key of ru.tsv and es.tsv. The docs describe a complete catalog.
- Seven review rows follow the catalog's existing patterns: Text Pane
  (Szöveg panel, like the other panes), Design Mode (Tervezőmód, one
  compound), Picture and Date Picker Content Control (clearer compounds),
  Promote to Heading 1 / Demote to Body Text (style name after a colon,
  no case ending) and Reset Graphic (Ábra visszaállítása, shorter).

* Hungarian: translate interface strings added on main since this PR

Adds the 10 strings main gained with Polish proofing (spelling and grammar
messages, Do Not Check Spelling or Grammar, comment/browse buttons), so
hu.tsv again covers every key of the other catalogs (2,419 entries), and
drops the stale "Wildcards (regex)" row; updates the documented counts.

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>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 06:45:30 -04:00
feaff0f22b i18n: add French (fr) interface (#729)
* i18n: add French (fr) interface

A complete 1813-entry catalog with the same key set and order as
ru.tsv (the most complete reference), written from the meaning of the
English source. Registered in LANGUAGES after Turkish. The
locale-resolution tests that used fr-FR as the example of a language
without a catalog now assert the French mapping and use Italian,
which has no catalog, to keep testing the unsupported path.

* i18n: complete the French catalog and apply review fixes

Rebase onto main (1805 uk.tsv keys, up from 1812 rows at the original
PR): 357 new translations for the charts, Draw, content controls,
Caption/Index/TOC dialogs and formatting features that landed after
the PR was opened. Also applied the reviewer's five fixes: Bring
Forward → Avancer d'un niveau, Print Layout → Mode Page, Not used by
any command → N'est utilisé par aucune commande, Through → Au travers,
and consistent U+2019 apostrophes throughout.

* Review fixes for #729: attribution, French tests, language lists

- ATTRIBUTION row for fr.tsv, in the form the other catalogs use.
- French locale/saved-preference test and bundled-font coverage test (accents, œ, « », ’, and the
  no-break spaces in Inter).
- Tests that need a language without a catalog use a private-use tag (qaa-QM, qtz) instead of
  it-IT/de-DE, so they keep passing as real languages land.
- 11 French rows for commands that landed on main since (templates, styles organizer, Overtype,
  Move with Text), which every_tab_and_command_is_translated requires.
- fr in docs/control-protocol.md, README and localization-parity.

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

* Review fixes for #729: French rows for Outline view and SmartArt

34 rows for the Outlining and SmartArt Design tabs and their commands, which landed on main
meanwhile and every_tab_and_command_is_translated requires; 1,850 entries.

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

* French: translate interface strings added on main since this PR

Adds 174 French entries (fr.tsv now covers every key in the other
catalogs, 2,023 entries) and drops the stale "Wildcards (regex)" row;
updates the documented entry count.

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

* French: translate interface strings added on main since this PR

Adds the 10 strings main gained with Polish proofing (spelling and grammar
messages, Do Not Check Spelling or Grammar, comment/browse buttons): 2,033
entries; documented count updated.

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

* French: strings main added since the last merge (default app, numerals, KeyTips, Reading Mode)

---------

Co-authored-by: sanches <cfic34@ya.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 06:38:53 -04:00
241877d666 Add paginated Reading Mode with smooth column navigation (#740)
* M8: reflow reading mode into viewport-sized pages

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: navigate reading columns with wheel, keys and page controls

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: keep Outline in current view and add transient reading layout controls

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: replace reading ribbon and align navigation to columns

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: animate reading column turns and retain visible text through resize

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: name reading navigation Outline and complete its translations

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: consolidate Reading Mode progress notes

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: keep reading clicks from scrolling the selected column

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: reveal reading caret only when outside the viewport

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

* M8: hide floating formatting toolbar in Reading Mode

Co-Authored-By: ChatGPT (GPT-6) <noreply@openai.com>

---------

Co-authored-by: ChatGPT (GPT-6) <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:33:19 -04:00
7cccdf0f38 KeyTips: Quick Access Toolbar badges, saved option and native Alt fix (#620) (#709)
* M8.1: Quick Access Toolbar KeyTips (#620)

* M8.1: Handle native Alt events for KeyTips (#620)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:24:36 -04:00
af622ac843 Layout › Paragraph: fields fit above the title and line up (#654) (#703)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

* #653: large-button labels end above the group title row

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

* #654: Layout › Paragraph fields fit above the title and line up

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:16:15 -04:00
4a1acb6c3f Ribbon: large-button labels end above the group titles (#653) (#699)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

* #653: large-button labels end above the group title row

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:13:17 -04:00
3c8840556a Menu buttons grey out while their command is disabled (#658) (#698)
* #659: one drop-down marker (the dropdown icon) on every ribbon button

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

* #658: menu buttons grey out while their command is disabled

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 06:09:16 -04:00
c1e1c06297 #659: one drop-down marker (the dropdown icon) on every ribbon button (#697)
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-12 06:05:30 -04:00
d4d28e01f4 Skip non-English runs in proofing instead of flagging them (#100) (#741)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:55:40 -04:00
dd66d5171b Dark theme: gallery previews no longer bright white (#663) (#706)
* #656: dialog buttons, Discord pill, eraser and search highlight use theme tokens

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

* #663: gallery previews dimmed in the dark theme; dark page under Switch Modes

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 05:42:58 -04:00
d510d9a41a #656: dialog buttons, Discord pill, eraser and search highlight use theme tokens (#705)
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-12 05:39:06 -04:00
de8a3daf31 Fix paragraph alignment focus (#690)
* Fix paragraph alignment focus

Fixes #686

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Review fixes for #690: every ribbon command button returns focus to the document

Instead of four hard-coded alignment ids, big, small and split buttons and ribbon menu
items hand keyboard focus back to the canvas after running any engine command (ui.*
commands open dialogs and panes, which take focus themselves). The test also clicks Bold.
The ui-parity.md note moves into the change log.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:32:58 -04:00
25048c8b9c M7: ignore ellipses in sentence capitalization proofing (#742)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:29:57 -04:00
1c188ba140 M1: preserve independent complex-script DOCX formatting (#366)
Keep inherited complex-script size, bold and italic independent from Latin
properties and retain explicit values during DOCX round trips. Preserve
ordinary formatting-command behavior and add regression coverage.
Resolve eight existing Clippy warnings without changing behavior so all
six required CI gates pass.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:55:44 -04:00
ba9bbb48dd Open Word documents with WordCraft by default (file associations + Options button) (#304)
* Open Word documents with WordCraft from the system (#295)

Register WordCraft for Word, RTF and ODT files on Windows and Linux without
taking over anyone's defaults, and add File > Options > General > "Make
WordCraft the default for Word documents..." where the system allows it.

- Windows MSI: a ProgId per type (.docx .docm .dotx .dotm .doc .dot .rtf
  .odt, plus .md and .txt as Open With candidates only) with the app icon
  and `"wordcraft.exe" "%1"`, OpenWithProgids, Applications\wordcraft.exe
  SupportedTypes, and Capabilities + RegisteredApplications so WordCraft
  shows in Settings > Default apps. New component with its own GUID.
- Linux: the desktop entry declares the macro-enabled, template and legacy
  Word types and text/rtf; AppStream lists the media types. deb, rpm,
  AppImage and both Flatpak manifests use this same file.
- macOS: Info.plist also lists legacy .doc.
- The Options button runs `ui.makeDefaultApp` through a new
  Services::make_default_app hook: Windows opens
  ms-settings:defaultapps on WordCraft's page; Linux and BSD run
  `xdg-mime default ai.storyteller.wordcraft.desktop <types>` (refused
  inside Flatpak or without an installed desktop entry); macOS shows the
  Finder Get Info hint; the web shows nothing. The status bar reports the
  outcome. Strings translated in every catalog.

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

* Review fixes for #304: keep Make default out of programmatic dispatch

ui.makeDefaultApp is refused when run programmatically (control channel, MCP, macros,
scripts); only a click on File > Options > General > Make WordCraft the default… reaches
the host's hook, so no agent can change the user's system default apps or open system
settings. Test: programmatic_calls_cannot_change_the_default_apps.

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

* Default-app strings in the newer catalogs; the button's test looks for a General-pane label

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 04:30:57 -04:00
199ac88a2e i18n: add Czech (cs) interface translation (#387)
* i18n: add Czech (cs) interface translation

Translate the 1,246-entry interface catalog clean-room from the English source labels (cross-referenced with PdfCraft, VectorCraft, and PhotoCraft Czech catalogs) and register Czech (Čeština) for system locale detection and persisted selection. Add tests for Czech locale resolution, full catalog key parity, count-neutral word count formatting, and bundled UI font glyph coverage for Czech diacritics.

# Conflicts:
#	ROADMAP.md

* Czech: translate interface strings added on main since this PR

Adds 221 Czech entries so cs.tsv again has the full zh-hans key set
(2,021 entries) and drops the stale "Wildcards (regex)" row; updates the
documented entry count.

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>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 04:09:20 -04:00
1fa5333b26 M15: Polish interface, and Polish spelling, grammar and hyphenation (#346)
* M15: Polish interface, and Polish spelling, grammar and hyphenation

Interface
- Polish is the tenth interface language ("Polski", pl.tsv, 1,045 entries);
  pl, pl-PL and pl_PL.UTF-8 system locales select it automatically.
- Right-click menu labels now go through translation in every language.
- Review > Language is a menu (English US/UK, Polski, Do not check spelling
  or grammar); the status bar shows the language at the caret.
- With a Polish interface, new blank documents and opened .txt/.md files
  default to pl-PL.

Proofing
- Each run is checked in its own language (w:lang, resolved through
  styles): squiggles, F7, review.issues, suggestions and hyphenation.
  Text in languages without proofing tools is no longer flagged as English.
- Polish spelling from the SJP.PL Hunspell dictionary (Apache-2.0 option),
  read by a new Rust affix engine (crates/proof/src/affix.rs); 875 KB asset,
  ~4.5M word forms. Regenerate with the pl_dictionary example.
- Polish suggestions: case, missing diacritics, rz/ż, ó/u, ch/h confusions,
  missing spaces (napewno -> na pewno), edit distance.
- Conservative Polish grammar rules: repeated words, spacing, doubled
  punctuation, sentence capitals, missing comma before że/który/ponieważ/
  aby/żeby/jeśli…, we/ze forms.
- Polish hyphenation with the hyph-utf8 hyph-pl.tex patterns (MIT option);
  pattern keys widened to 20 letters, TeX exceptions parsed; the heuristic
  no longer treats ł, ż, ś… as vowels.

review.language validates and normalizes tags, can set noProof alone and
the document default; format.state reports lang and noProof.

Tests for every part, docs and ATTRIBUTION.md/NOTICE updated;
cargo xtask ci passes (fmt, clippy, test, assets, layers, wasm).

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

* Layout: rustfmt the test module order left unformatted by #391

cargo fmt --check failed on main after #391 added mod tests_grid.

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

* Review fixes for #346: read Polish spelling from the installed Hunspell dictionary

- Polish spelling uses the Hunspell pl_PL dictionary installed on the
  system (OS packages, Homebrew, LibreOffice, ~/.local/share/hunspell, or
  WORDCRAFT_DICTIONARIES) instead of a bundled 870 KB SJP.PL file;
  without one, Polish text gets no spelling marks, like Word without the
  language's proofing tools. .aff/.dic are size-capped and decoded from
  UTF-8 or ISO 8859-2 (crates/proof/src/hunspell.rs).
- The dictionary can be replaced at runtime (install_dictionary) or
  re-searched (reload_dictionary); an empty one accepts every word.
- Tests use small synthetic dictionaries written for them, so they don't
  depend on what the machine has installed.
- The bundled pl.dic, its ATTRIBUTION/NOTICE entries and the generator
  example are removed; docs describe the installed dictionary.

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

* M15: Polish catalog covers the interface text merged from main

The merge of main (98456ac) brought ~575 new interface strings (charts,
SmartArt, content controls, Developer tab, AutoCorrect, outline view,
protection, compare, print, accessibility, references and Language/
Hyphenation dialogs, Draw additions). pl.tsv lacked them, so
polish_covers_every_key_of_every_other_catalog and
every_tab_and_command_is_translated failed. All are now translated.

cargo xtask ci: all 6 steps pass.

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-12 04:04:03 -04:00
55f7b894c6 Add German (de) interface language (#574)
* Add German (de) interface language

New catalog crates/ui-egui/src/i18n/de.tsv (1,307 entries), key set
byte-identical to zh-hans.tsv; original translations written from the
meaning of the English UI strings in standard German word-processor
vocabulary; MIT OR Apache-2.0.

- LangInfo for de (Deutsch) registered in i18n::LANGUAGES
- Tests: German locale resolution (de, de-DE, de-AT, de-CH,
  de_DE.UTF-8@euro), saved-preference/document-content isolation,
  catalog key-set parity with zh-hans, count-neutral wording,
  bundled-font glyph coverage for ÄäÖöÜüß
- Count strings use count-neutral wording (Wörter: {n}), the
  convention the other catalogs' tests pin
- Docs: localization-parity.md, gaps.md, roadmap.md, ROADMAP.md,
  README.md, control-protocol.md; ATTRIBUTION.md row for de.tsv

cargo test -p wordcraft-ui-egui i18n: 28 passed; --lib: 162 passed.

* rustfmt after merging main

* German: translate interface strings added on main since this PR

Adds 636 German entries so de.tsv again has the full zh-hans key set
(2,021 entries) and drops the stale "Wildcards (regex)" row; updates the
documented entry count.

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

* Fourteen interface languages with German and Italian

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
2026-10-12 03:55:05 -04:00
99fbd1861a i18n: add Italian interface (it) (#598)
* i18n: add Italian interface (it)

New it.tsv catalog with 1,378 entries: every key of the existing catalogs plus 51 strings that go through tl! but were in no catalog yet (File › Info properties, File › Home greeting, list levels, Equation tab gallery and tooltips, Draw Table, About tabs). Ribbon tabs, groups and command names use the names Italian Word users know; all other text is written from the meaning of the English.

Registers it in LANGUAGES (it, it-IT, it-CH resolve here), adds locale, saved-preference, coverage and bundled-font tests, and updates README, control protocol, localization parity and roadmap. Refs #594.

* i18n(it): shorter Wrap Text Sides label

Lati del testo a capo matches its parent menu, Testo a capo.

* i18n(it): translate the 590 strings main added since the rebase

Options, accessibility, alt text, content controls, document grid, templates, index, citations and the other labels every catalog gained on main. The Italian coverage test (every key of the other catalogs) passes again.

* i18n(it): Issue is Fascicolo in the bibliography source form

* i18n(it): translate the 351 strings main added (equation gallery, built-in styles, paper sizes, animation settings, file browser)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:46:44 -04:00
6801d89065 .doc import: inline pictures, floating pictures and basic shapes (#551) (#558)
* .doc import: inline pictures, floating pictures and basic shapes (#551)

Word 97-2003 files kept their text but dropped every picture and drawing.
Pictures and drawings are now read from the OfficeArt records ([MS-DOC],
[MS-ODRAW]):

- Inline pictures: the PICF's OfficeArtInlineSpContainer (MM_SHAPE /
  MM_SHAPEFILE) and its FBSE/BLIP, with crop and alt text from the shape
  properties; the older raw-image path stays as a fallback.
- Floating pictures and shapes: PlcSpaMom/PlcSpaHdr anchors joined with
  the shapes of the OfficeArtContent drawings (fcDggInfo), placed by the
  SPA bounds, bx/by or posrelh/posrelv/posh/posv, wrap type and
  fBelowText; rotation and flips; pictures from the BLIP store.
- Shapes: rectangle, rounded rectangle, ellipse, diamond, triangles, star,
  block arrows, heart, lines/connectors, with fill/line colour and width;
  groups flattened into one group space; text boxes linked to their
  PlcftxbxTxt story parts by spid.
- BLIPs: PNG, JPEG, TIFF as-is, DIB converted to PNG, EMF/WMF inflated
  (the metafile renderer draws them), PICT kept as a sized placeholder.
- Hostile input: record lengths clamped to their parent, nesting depth
  and record counts capped, decompression size-limited, picture bytes
  capped per document as before.

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

* Review fixes for #558: miniz_oxide as a workspace dependency

Restores the change @echelon pushed in 04b59287, which a rebase dropped: miniz_oxide is declared once in the root [workspace.dependencies] and docbin uses it with workspace = true.

Co-Authored-By: Claude Sonnet 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-12 03:42:18 -04:00
Rees 4611afba16 SCRUM-28: preserve imported TOC result ranges (#617) 2026-10-12 03:29:32 -04:00
ad8c264420 Tables: resize by dragging column borders, row borders and the corner handle (#217) (#600)
Like Word: the pointer turns into a resize arrow over a column border or a
row's bottom border, and a small handle shows past the table's bottom-right
corner. Dragging shows a dashed guide; releasing runs the new
`table.resizeDrag` command (page coordinates), so a drag is one undo step
and the selection is kept. Esc cancels a drag.

- Column border: the right neighbour gives or takes the difference, so the
  table keeps its width; Shift, or the table's right edge, changes only the
  column on the left. Columns stay at least 6 pt wide.
- Row border: sets the row's "at least" height; the margins and border bands
  the layout adds are measured and taken off, so the row is drawn at the
  dragged height.
- Corner handle: scales every column and the rows on that page.
- Widths come from the drawn layout, so autofit and percentage widths don't
  make the result jump; a merged cell hides the border it covers.
- Borders are hit-tested in the existing canvas mouse handler, only within a
  few pixels of a border, so clicks in cells still place the caret.

Engine: crates/engine/src/cmd/table_resize.rs (7 tests).
UI: crates/ui-egui/src/table_resize.rs, wired into canvas.rs; kittest tests
drive real pointer events through the whole window (one undo step per drag,
selection kept, clicks in cells still move the caret, Esc cancels).
The command label is translated in all nine interface catalogs.
Progress docs updated: gaps, ui-parity, target-app-parity, ROADMAP.
`cargo xtask ci` passes (fmt, clippy, test, assets, layers, wasm).

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:38:05 -04:00
skylar taylor-barrick a38caa4b15 Print: preserve native workers across upstream platform shutdown changes 2026-10-12 08:37:53 +02:00
ReesandBrandon Thomas b0f3d52d80 SCRUM-26: preserve custom XML stores through saves (#612)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:29:16 -04:00
c08d9f8598 Accessibility Options: Animation effects setting that follows the system (#791)
* Options: Animation effects setting that follows the system

The app eased wheel scrolling, Ctrl+wheel zoom and its fades whatever the
system's animation setting was: with Windows' Animation effects off, the
page still smooth-scrolled.

File > Options > Accessibility > Animation effects: Use system setting
(the default), On or Off; `ui.animations` for agents; kept in ui.json.

- Off: a wheel notch scrolls the page its whole distance at once, egui's
  own scroll areas get the notch whole instead of eased in, Ctrl+wheel
  zoom steps at once, and egui's fades and scroll-to glides are off.
  Touchpad momentum and the caret blink are unchanged.
- Use system setting asks the host (`Services::animations_enabled`) at
  start-up and when the window regains focus. Windows: the Windows
  Runtime's UISettings, a safe API of the `windows` crate already in the
  build. Web: `prefers-reduced-motion`. macOS and Linux/BSD aren't read
  yet (docs/gaps.md); there it means on.
- The eight new strings are in all eleven interface catalogs.

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

* Animation effects off: zoom steps land finished in one frame

Follow-up to the Animation effects setting: zooming could still look
animated with the setting off.

- Ctrl+wheel zooms in whole steps, each at once. A mouse-wheel notch is
  two steps in its frame, the same zoom as before. Turns that arrive in
  small pieces (a touchpad pinch that Windows reports as Ctrl+wheel, a
  high-resolution or free-spinning wheel) add up to a step of about 10%
  first, where each piece used to zoom a little and the zoom glided.
- A page re-rendered for a zoom step is drawn on the pixel grid in that
  frame. It used to be drawn off the grid, slightly soft, and snap sharp
  a frame later.
- The frame a zoom changes in asks for the next one at once, so the
  status bar's percentage and slider follow in the next frame. They
  could lag until the caret's next blink, up to half a second.

The last two apply whatever the setting.

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

* Animation effects: follow the system's setting on macOS, Linux and the BSDs

"Use system setting" only read Windows and the browser; elsewhere it
meant on.

- macOS: System Settings > Accessibility > Display > Reduce motion,
  through NSWorkspace (a safe call in objc2-app-kit 0.3, already in the
  build).
- Linux and the BSDs: the XDG settings portal over D-Bus (zbus, already
  in the build through AccessKit). Three settings say whether animations
  play, and any one saying no turns them off: the cross-desktop
  reduced-motion preference, GNOME's enable-animations and KDE Plasma's
  animation speed. A thread reads them at start-up and follows the
  portal's change signal. Where no portal answers, or it knows none of
  them, KDE's kdeglobals and GTK's settings.ini are read instead.
- The UI now asks the host every frame instead of at start-up and on
  focus, so a change in the system's settings is followed while the app
  runs. Hosts answer from memory: Windows and macOS are asked again at
  most once a second, the web keeps its media query, and Linux/BSD is
  kept current by its thread.

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

* Animation setting on Linux: ask every setting again when one changes

KDE's portal announces a new animation speed only as the cross-desktop
reduced-motion preference, not under its own kdeglobals name. Keeping
each setting's last announced value left KDE's own one stale: after
going from instant back to normal speed, animations stayed off.

Every setting is now read afresh on any relevant change, and the
settings files' answer stands whenever the portal has none.

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

* Caret glide follows Animation effects; its canvas test turns the setting off, then on

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:24:58 -04:00
d69c94efcf Linux: open/save works without a portal file dialog (#576) (#583)
* Linux: built-in file browser when the portal file dialog is unavailable (#576)

rfd 0.17 on Linux/BSD asks the xdg-desktop-portal FileChooser and falls back to
zenity; with neither (labwc with only xdg-desktop-portal-wlr, no zenity) it
returns None, the same as Cancel, so Open/Save As/Insert Picture silently did
nothing. The dialog thread now checks first (zenity on PATH, or the portal's
FileChooser interface via dbus-send) and answers with an error when neither is
there; the app then finishes the job in its own egui file browser (folder
field, Up/Home, folders and files of the chosen type, file name, type filter,
replace confirmation) with a hint on how to get the system dialog.
File > Open > Browse in WordCraft... (Linux/BSD) and `ui.fileBrowser` reach
it at any time, even while a system dialog never showed up;
WORDCRAFT_FILE_DIALOG=builtin forces it. Native only (not wasm32).

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

* Turkish and Russian strings for the file browser (#576)

Co-Authored-By: Claude Sonnet 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-12 02:19:04 -04:00
cbd9291909 Caret: glide and fade like Word for Windows; File › Options › Advanced › Animation turns it off (#570)
* Add arcanii to the contributor names

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

* Caret: glide and fade like Word for Windows; File › Options › Animation turns it off

The caret no longer jumps and switches on and off. A key or typing glides
it to its new place in 100 ms along cubic-bezier(0, 0, 0, 1), the same for
one character and for a whole line; a second key before it arrives makes
it snap to where it was heading and set off from there. Clicks, and moves
made by scripts or the control channel, don't glide. The blink is a pair
of 100 ms fades (530 ms on, 530 ms off), and a key or a click turns the
caret on from however visible it was.

These are Word for Windows' numbers (16.0.20326, 100% zoom, 144 Hz),
measured black-box: a synthetic document in a separate Word instance, its
window captured frame by frame while key and mouse messages moved the
caret. Fourteen consecutive frames match the curve to 0.003. Word for Mac
was not measured.

Nothing glides when the view changed (zoom, a resized window, scrolling,
another document), when the caret wasn't drawn the frame before, or when
a move starts or ends off screen.

File › Options › Animation › "Animate the caret (it glides and fades)"
turns it off: ui.caretAnimation {"value"} for agents, kept in ui.json, on
by default, translated in the nine catalogs. Off, and whenever egui's
animations are off (the headless harness, so ui_shot and scripted
screenshots), the caret jumps and blinks exactly as before.

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-12 02:15:02 -04:00
ReesandBrandon Thomas 1f09547f4e SCRUM-34: protect original files during native saves (#621)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:11:13 -04:00
Eduardo SoaresandBrandon Thomas 833d0b00c2 #755: Line Numbers and Hyphenation measure in the Options unit (#795)
File > Options > Advanced > Measurement units (#485) missed these two dialogs:
the Line Numbers "From text" distance and the Hyphenation zone read and
sent inches whatever the preference was. They now convert through
options::unit() like every other dialog; params still send points.

Reported by @Ledity.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:32:44 -04:00
a520fb362d Track Changes: show deletions in balloons when 'Show revisions in balloons' is on - balloons now work (#734)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:08:26 -04:00
1490d4e594 Dark theme: readable text on the accent (#655) (#704)
* #657: input borders reach 3:1 contrast in both themes

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

* #655: dark-theme accent controls use dark on_accent text (6:1)

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:51:53 -04:00
skylar taylor-barrick 1d0dff03a4 Print: preserve page planning and pass native copy collation to spooler 2026-10-12 06:50:19 +02:00
b2491a28dd #657: input borders reach 3:1 contrast in both themes (#696)
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-12 00:43:19 -04:00
219b125eeb #661: Table of Authorities buttons labelled, with Update Table (#693)
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-12 00:41:03 -04:00
1b96e04b2c Add fonts at run time: Add Font…, drop a font file, font.add (#582) (#668)
* Add fonts at run time: Add Font…, drop a font file, font.add (#582)

Rebased onto main as one commit (the branch had merge commits from main).

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

* #582: font test no longer depends on the system's fallback font

The FreeBSD runner has no system fonts, so a missing family fell back to
Source Serif 4, the font the test compared against. The test now adds a
renamed JetBrains Mono under a name without Serif/Sans/Mono (which picks a
fallback of that kind), with narrow-letter sample text: monospaced, it
measures unlike any proportional fallback.

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 00:38:20 -04:00
85698c4d79 Security: pictures travel in the reply, exported links keep only safe targets (#678, #679) (#681)
- `ui.render` and `ui.screenshot` without a `path` return the PNG as base64 `png` in the
  reply instead of writing a fixed name into the shared temporary folder, where another
  user could have planted a link to one of this user's files; the MCP bridge's
  `render_page` and `screenshot` use that and no longer round-trip through
  `temp_dir()/wordcraft-mcp-*.png`. A given `path` still works as before (#678).
- One allow-list, `wordcraft_doc::link`: `is_web_or_mail_link` for Ctrl/⌘-click and
  `is_safe_href` (web, mail, `#bookmark`, relative path; schemes read the way browsers
  do, so `java\nscript:` is a script) for the HTML and Markdown exporters and the HTML
  importer. Scripts and other schemes export as plain text (#679).
- Tests: the allow-list, exporters with hostile links, the MCP bridge against a fake app
  (no `path` ever asked for), the control channel's in-reply PNGs.

Co-authored-by: petycomppety <30414255+petycomppety@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:57:08 -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
381c475edc M-print: File > Print preview, page range, odd/even, copies and collate (#641) (#674)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:30:58 -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
3fd97c3e58 Complex scripts: shape Myanmar, Khmer and Indic text; caret and line breaks respect syllables (#604) (#652)
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:22:57 -04:00
2544372a22 #611: DOCX diagrams: theme-styled shapes and connectors, VML lines (#671)
Shapes and connectors that take their fill and outline from the theme
(wps:style a:fillRef/a:lnRef) now show; scheme, preset, system, scRGB and
HSL colours resolve. Callouts and flowchart boxes draw as the nearest
shape. Legacy VML lines (with arrowheads), rectangles and ovals are read.
A DrawingML line runs top-left to bottom-right, as the preset does.

Rebuilt on main, which now has connectors, arrowheads and canvases (#416,
#344): those parts of the earlier branch were dropped in favour of main's.

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:15:28 -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
ReesandBrandon Thomas 0eea21c2c8 M1: preserve omitted DOCX paragraph defaults (#647)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 22:47:03 -04:00