Word brings a document's alternative format chunks (w:altChunk) in where they
stand when it opens the file; WordCraft left them out, so a document made of
one (as generators and mail-merge tools write them) opened empty.
- docx: read_with/ReadOptions with a ChunkImporter for formats other than
WordprocessingML; nested Word documents are read here (at most 4 deep,
256 chunks and 512 MB of chunk bytes a file, XML depth counted on).
- doc: Document::adopt (moved from paste) brings another document's media,
lists, styles, notes, comments and revisions along under new ids; pictures
in groups and charts are relinked. Insert > Text from File uses it and now
keeps the file's pictures and lists.
- engine: chunks in MHT, HTML (in their charset), RTF and plain text go
through the format importers.
- formats: MHT web archives open (MIME, quoted-printable/base64, charsets,
pictures by Content-Location or cid). HTML reads style element rules
(simple selectors, cascade, budgets), paragraph margins, indents, line
height, cell widths and borders; an opened page takes Word's web-page
defaults (Times New Roman 12 pt, single lines, black headings, automatic
spacing). html::decode knows the single-byte Windows code pages.
docxide-pdf suite: croatian_regulations_altchunk goes from one empty page to
Word's 8 pages, SSIM 2.7% -> 78.7%; no other document changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- craft-proof now names ja, zh and th in missing_dictionary: packs::missing_dictionary hides it
for a pack language when no pack for it is listed (status bar and Options).
- de-CH and pt-PT are pack languages in craft-proof (de-ch, pt-pt): names, hyphenation and
grammar stay German and Portuguese (tests); pack names fall back to the region-aware name.
- About › Language data shows craft_proof::scowl::notice() when SCOWL is embedded (test).
- Flatpak manifest's craft-i18n commit follows craft-i18n.rev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- proof.packs reports index_override; the CLI prints the test source; the bar's Install tooltip
says it turns on downloading of fonts and Language packs, and names a test source.
- Docs: Install works once language-packs-1 is published (until then only a test index); with a
test index the sha256 only catches a broken download.
- About › Language data keeps its size: a fixed-height scrolling list, licence texts read again
only when the dictionaries change.
- The engine's Cancel is the font store's own flag (no snapshot), so a cancel stops a fetch or a
stalled download too.
- Index text shown without control, bidi or format characters; long names cut; the bar works out
what to offer again when the list changes (index generation, not its length).
- ui_shot's store is a temporary folder (or storeDir); tests reset the backend even on panic; a
fixture pack proves per-run proofing and hyphenation from a pack.
- The default-build check follows craft-proof's carry-over (de, pl, pt, it, ru embedded until packs
ship; CRAFT_PROOF_EMBED=en is English only); the Polish hyphenation check runs again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- Install, Show available languages and Refresh turn on the font store's downloading only on the
person's own click (input_from_control, as in #959); a faked click says why instead.
- proof.installPack and proof.packs {refresh} run on the engine's thread in the app ("started"),
with progress, results and errors in proof.packs; the CLI and headless MCP stay synchronous.
- A session's layout is keyed on the dictionaries' generation, so parked documents proof again.
- proof.removePack refuses while that pack installs; pack commands that need a locale are left out
of Search; options text names Refresh; licence tooltip says Source; errors go through tl!;
the bar rescans at most once a second while typing; English names for every pack language and
sr-latn; About has one Close button; xtask doc comment back on cmd_i18n_report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- `review.issues` and `file.options` report a run's language with `ProofLang::tag()`, so a
Pack language reports its locale, not "".
- The test-source warning reads craft-font-store's `language_pack_index_overridden()`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- craft-proof (benandez/craft-libs 6ae4f03, storytold/craft-libs#485) and craft-font-store /
craft-font-catalog (630e42f, #486) by git rev instead of local paths.
- ProofLang::Pack: the bar offers every language with a pack (zxx/und never).
- Options › Language warns when CRAFT_LANGUAGE_PACK_INDEX supplies a test index.
- Pack `source` (upstream link) when the index gives it, else the archive.
- The bar's close button is the code-drawn close icon: "✕" had no glyph in the interface fonts
and egui panicked drawing it.
- `cargo xtask language` allows our own ru-extra, which stays embedded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- engine `packs`: the packs Backend (the font store on the desktop and CLI, a fixed list in tests
and ui_shot), the pinned index, installs and removals through craft-font-store's
`language-pack` kind, craft-proof reading the store's `language-packs/` folder at once.
Commands open nothing and never turn downloading on (only an Install click in the app does);
confined automation can't write the packs folder outside its write root.
- ui-egui `language_packs`: File › Options › Language › Language packs (name, size, licence,
source, Install/Remove, progress, errors), the missing-pack bar (dismissible per document,
never for a language without a pack), About › Language data (full licence texts).
- CP4.4: `cargo xtask language` (in `ci`): the default build embeds only en-* Language data.
- CP4.5: docs (help, MCP, control protocol, gaps #8, localization parity, roadmap).
- Path deps to Lane 4's worktrees while developing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
craft-proof moves to storytold/craft-libs#483's head (e6e948f, on #482's
9e5862a): each English checks against its own SCOWL rel-2026.02.25 dictionary
through the affix engine (ordinals via COMPOUNDRULE), with en-variants.txt as
the dialect policy. Lookup order: dictionaries folder, [English pack], SCOWL,
system Hunspell, the built-in Moby list; Moby stays for hyphenation.
craft-i18n.rev and the Flatpak source move to storytold/craft-i18n#109's merge
commit (efb1127), which adds the SCOWL files.
Typed-text tests pin British spellings in en-GB (#40), modern words (#520),
Canadian/Australian/NZ/Indian English (#1281, #1045) and ordinals; they skip
without the Language data, as the other data tests do. Docs: typing-parity,
localization-parity, ROADMAP progress log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
CodeRabbit on #5202:
- AGENTS.md, ATTRIBUTION.md, README.md, docs/architecture.md: without
CRAFT_I18N_DIR English spelling falls back to an installed Hunspell English
or craft-proof's built-in Moby list, and only pattern hyphenation is off
(Bulgarian, Turkish and Russian hyphenate by rule); they said English
wasn't spell-checked. (CLAUDE.md carries the same sentence; left for the
maintainer, as agents don't edit it.)
- NOTICE: craft-i18n's patterns are language-data/<lang>/hyphenation.pat,
not en-us.pat, pl.pat, pt.pat, de.pat, it.pat.
- docs/spelling-dictionaries.md: the Flatpak's dictionaries folder is
~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries; the host's
folders are outside its sandbox.
- printing.yml: the repository checkout doesn't persist credentials.
- tests.rs no_language_data: also probes the American/British variant table,
so a craft-i18n checkout missing some files skips the dialect tests
instead of failing them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Polish/Portuguese/Czech review commands, the custom dictionary file and the two typing squiggle
tests pass without CRAFT_I18N_DIR at 48ed6f8. English dialects, regions' English and Hyphenation
Options still need the variant table and patterns, so they keep the skip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- wordcraft-proof is now `package = "craft-proof"` from craft-libs (local
path for now; TODO in Cargo.toml for the git+rev pin after the craft-libs
PR merges). `wordcraft_proof::` paths are unchanged.
- crates/proof and assets/{spelling,hyphenation} deleted with their
ATTRIBUTION.md rows; one row now points at craft-i18n's Language data;
NOTICE paths updated.
- craft-i18n.rev (and the flatpak source) moved to the craft-i18n commit that
adds language-data/ (CI sets CRAFT_I18N_REQUIRED=1, so the pin must have it).
- xtask layers: proof removed. Engine tests carry their own copy of the
synthetic Czech fixture (they reached into crates/proof/src).
- Docs: architecture.md, CLAUDE.md/AGENTS.md layer table, README build note
(CRAFT_I18N_DIR), localization-tr path, ROADMAP progress log.
Before/after wordcraft-cli on a 17-language proofing document: issues,
suggestions, manual hyphenation, PDF text and PDF bytes identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Tests and notes for rules that #4129, #4113, #4109 and #4101 added and
left uncovered:
- A border line at the top of a story with formatting left out of the
Track Changes markup is not a formatting change, and Reject All keeps
it.
- The author's own earlier formatting change on that paragraph stays the
one change.
- Reject All keeps the table of a "+---+" line.
- A border line that a content control's lock stops Backspace from
deleting stays as it is, and Enter is refused, untracked too.
- With tab indents on, Tab in a table cell moves to the next cell; with
them off, Backspace before an indented paragraph joins it.
- AutoCorrect and the spelling check's web and e-mail addresses after a
narrow no-break space and a figure space.
docs/typing-parity.md describes each, and lists a no-break space typed
after a word, which does not finish the word for AutoCorrect, as a
known gap.
A hyperlinked TOC wrote each entry's page number as plain text inside the
link. Word writes it as a PAGEREF _Toc… \h field, so Update Field in Word
moves the number with its heading; with plain text the numbers stay what
they were when the file was saved.
Entries that link to a bookmark now end with that field (result: the page
number). Entries without a link and \n levels are unchanged. The DOCX span
test looks for the TOC's own end, the last one, since PAGEREF fields now end
inside it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found while testing crates/engine/src/cmd/mailings/recipients.rs:6-18 (csv_separator)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4734: the print job's PDF leaves out link annotations
craft-print refuses to fit a page with annotations to the paper ("flatten page annotations
before using shrink-to-fit"), and the desktop app always prints with ShrinkToFit, so any
document with a hyperlink or a TOC (its entries are links) failed to print. Links print
nothing; `PdfOptions::links` (default on) lets the print job leave them out. Exported PDFs
keep them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: review — the print note is in every desktop column; gaps and progress log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: doc wording: the print PDF omits link annotations; hardware still to validate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4734: only the printer's PDF drops links; Save these pages as PDF keeps them
Session::print_job_pdf is what ui.printSubmit sends to the spooler, without
link annotations; Session::print_pdf (file.printPdf) keeps them again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): saturate page parameters on 32-bit and wasm (#3676)
The file.exportPng and edit.goto paths cast a u64 page to usize and subtracted one. On 32-bit and wasm, page 2^32 truncated to 0 and the subtraction wrapped to page 1 (a debug panic).
Add p::page_index, which saturates instead of wrapping, and use it in both engine paths. The MCP render path already has the same guard on main; the new tests also pin that behavior.
Regression tests cover the helper, Go To, PNG export and the MCP render path.
* fix(engine): guard caret.set page cast too (#3676)
caret.set takes a 0-based page; pg as usize truncated on 32-bit and wasm and could select page 0. Use the same checked conversion and add a regression test.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #3631: index laid-out objects during connector rerouting
* docs: keep the gaps.md note in the status line and revision table
* docs: drop a stray blank line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4848: corporate authors in bibliographies
A Word source whose author is an organisation (Create Source › Corporate Author)
stores it as `b:Corporate`, which the reader ignored: the source opened with no
author, citations fell back to the title and bibliography entries started with
the year. A bare name typed into Author was split into "Organization, W. H.".
- Source gains `corporate`, read from and written as `b:Corporate` (in place of a
name list: a role holds one or the other, ECMA-376 CT_NameOrCorporateType).
- Citations and bibliography entries write it as it is in every style; it gives
the source's tag and its place in an author-sorted list, and untagged sources
are matched by it.
- APA puts its full stop after a group author ("World Health Organization.
(2021).") instead of relying on the initials for it.
- Create Source has a Corporate Author box under Author; Edit Source opens with
it checked when the source has one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6DxS4tKmWSLCiDPvb4G
* Match untagged sources by corporate author before personal author
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M12: preserve custom-property bookmark links (#4767)
* M12: preserve bookmark links across property renames (#4767)
* M12: align custom-property preservation status dates (#4767)
* docs: keep the gaps.md note in the status line and revision table
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Row, column and cell settings applied to the caret's row, column and cell only, so
selecting the whole table and unticking "Allow row to break across pages" changed
one row and the others still split across pages.
- table.properties applies row height and rule, allow break and header row to every
selected row, the column width to every selected column, and the cell width and
vertical alignment to every selected cell (the caret's with nothing selected), in
one undo step.
- The dialog shows "Allow row to break" and "Repeat as header row" as mixed when the
selected rows differ; left alone they change nothing, clicked they set every row.
- The row-height rule box is named by "Specify height" for screen readers.
Claude-Session: https://claude.ai/code/session_01LvG6DxS4tKmWSLCiDPvb4G
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M2 #4903: step each selected run from its own font size
* docs: keep the gaps.md note in the status line and revision table
* docs: drop a stray blank line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #4411: captions in a style based on Caption are cross-reference targets
Cross-reference and Table of Figures found captions only by the exact
Caption style, so a caption restyled with a style based on Caption (a
label's own "Caption Table") was left out: of five tableX captions,
only the two still in Caption were offered. A caption is now a
paragraph whose style is Caption or based on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4411: test captions two styles from Caption and the Table type
Covers a style based on a style based on Caption, a Table caption in
such a style offered under the built-in Table type, and a caption
label used only in a style not based on Caption staying out of the
cross-reference types.
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>
* #4086: highlighting at a caret leaves the word alone
* #4086: the highlight toggle reads pending caret formatting; word formatting updates it
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Backspace after a floating shape's anchor joins paragraphs and keeps the shape (#4592)
* wc-upkeep-6: protect Backspace joins and markers
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 4757: keep Arabic base letters when removing marks
* docs: keep the gaps.md note in the status line and revision table
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1744: WIP
* #1744: Select Table copies and cuts the whole table
Copy of a whole-table selection used to keep only the cells' paragraphs
(copy_blocks flattened a cross-cell range), and Cut only cleared the text.
Now Copy puts the table on the clipboard and Cut removes the table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: selected table rows and cells copy as a table; Cut clears their text
Copy of whole rows, cells of a row or a block of cells puts a table of just
those cells on the clipboard (Table::slice). Cut of rows or cells clears their
text and keeps the rows and cells, as in Word (Word check A).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: review fixes
Table::slice widens every row to a merged cell partly selected (no ragged
copy); an empty selected table or cell copies and cuts; a text selection over
exactly a nested table copies it; one selected_table per Cut; tests for
slice merges/widths/controls, tracked and locked cuts, and the text route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1744: Cut clears every cell Copy takes; Spike stays off for empty cell selections
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #1744: a copied part of a percentage-width table keeps a point width
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: tidy merged header
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: keep only the #1744 rows over main's version
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: remove conflict markers; keep only the #1744 rows over main's version
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #1744: rename the range Cut helper clear_range_cells (main added clear_cells)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Remove conflict markers from ROADMAP.md; leave main's header line alone
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* cargo fmt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Import Path in table structure after merging 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>
The shared dictionary slot reports no file while a dictionary given
with install_dictionary is in use, so this test had accepted null
alongside the folder's file, for installs from tests running at the
same time. That also let it pass if file.options stopped reporting the
folder's file at all. The tests' install helpers and this test now take
turns on one lock, and each language must report the folder's file
again. An install from before the test doesn't matter: changing the
folder reloads every slot.
main split Czech's suggest_in into one helper per round for the
cognitive_complexity limit (#4501). On this branch that code is the
shared affix_suggest::suggest_in (36 of 30), so cs/suggest.rs stays the
thin wrapper and the shared function gets the same split. The candidates
are offered in the same order; a dump of 13,081 calls over every
language's test dictionary matches the old engine except one: the typo
round counts the lower-cased letters, so "İ" (two letters lower case)
now gets "I" in Polish, where the old count stopped one letter short.
Merge main 436d444ef0. Port proofing selection/navigation and app/control/pane hooks to extracted modules, retain per-window state and stale-target guards, preserve upstream menus, and extend preview path filters. Keep the original pane implementation and targeted tests, with explicit imports after test relocation.
Workspace formatting passes. Local cargo xtask ci is blocked during Clippy by missing dlltool.exe in the Windows GNU toolchain; no local test success is claimed. Hosted navigation, UI, rendering and full checks on this tree remain required.
A paragraph with keep with next measures the paragraph after it before it is
placed (next_first_line). That measurement called layout_para directly, so
every paragraph following a heading was laid out twice in each layout pass:
once to measure it and once, through the cache, when it was placed.
The measurement now goes through the layout cache under the key
para_labelled uses (Ctx::para_ahead) when its inputs are the same: no
proofing, no typed word, not inside a text box, no inline text boxes that
resize, the body's tracked-deletion view. Otherwise it is laid out on its
own as before. It counts nothing (list labels, equation numbers and the
page-total flag are restored), so pagination is unchanged.
Adds crates/engine/examples/export_bench.rs, which saves a long generated
document (the sample document 200 times, 259 pages) through io::save_bytes
and prints wall times:
cargo run --release -p wordcraft-engine --example export_bench -- pdf 200 9
PDF export on that document, interleaved with main (Ryzen, 16 threads):
median 286 ms -> 256 ms (-10.6 %). The PDF bytes are identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>