* Gap 4: flag document fonts that aren't installed
The font box (ribbon, mini toolbar, both Font dialog boxes) shows a
warning mark when the current font isn't installed; its hover note
names the font drawn instead. The status bar says "N fonts not
installed" and lists each with the faces drawn instead, plus Add Font….
Agents get `font.missing` and `format.state.fontShownAs`.
Layout records, where it picks a face per text segment, the fonts the
shown text asks for that aren't installed and the faces drawn instead
(ParaLayout::substituted, DocLayout::substituted_fonts), so hidden
text, unused styles and orphaned stories never count; list labels,
field results and East Asian and complex-script text do, and so does a
missing roman font drawn in Cambria. Metric-compatible substitutes and
symbol fonts drawn as Unicode aren't flagged. Bounded for hostile
files: 64 pairs a paragraph, 1024 a document, names kept to 256
characters, 20 rows in the popup.
Ported from alehel/wordcraft#1 onto current main; English strings only
(translations belong in storytold/craft-i18n).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: missing-font review fixes
- Judge a padded font name by the face layout drew for it, and skip blank names.
- Name the face CJK text is drawn in for a missing East Asian font, not Cambria.
- Keep the missing-fonts list per window, so several windows don't rebuild it every frame.
- Expose the font box's warning to screen readers as its description.
- Count missing fonts with craft-i18n plurals (`i18n::tn`).
- One table of metric twins (`fonts::word::METRIC_TWINS`): a missing font is drawn in an
installed twin first, and the warning reads the same table.
- Find the status-bar item by part of its label in the click test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: warn for a font picked at the caret; drop fonts of hidden list items
- At a collapsed caret the font box warns for a missing font typing will use, also at the end
of text in another font (it waited for the first letter).
- A list item that shows nothing (#3982) reports no missing font: its number was shaped, and
its font recorded, before layout dropped it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: say where Add Font… is, and that it needs the font file
The font box's note said "Add Font… installs it": Add Font… opens a file picker at the end of
the list, so it only helps with a font file at hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: docs say the popup lists 20, and when the font box warns ahead
CodeRabbit review: the status-bar popup lists the first 20 missing fonts, then a count; the font
box also warns at a collapsed caret for a font no shown text uses yet; `fontShownAs` is the
font's substitute, fallback faces aside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Font dialog warns for a missing font over a selection
CodeRabbit review: the dialog's boxes show the fonts OK applies, which no shown text may use
yet (an Asian text font over Latin text, a font typed into the dialog), so they name the
substitute whatever is selected. The ribbon's box over a selection still reads the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: the Asian font box names the face CJK text gets
CodeRabbit review: the status bar's list is by font name, so for a font Latin text also uses
it named the Latin text's substitute. The Asian text box now always looks up the face layout
draws Chinese, Japanese and Korean text in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md status on the current line, missing-font scope and twin wording
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: gaps.md says which missing fonts are flagged and what font.missing lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 4: font.missing lists what the current view lays out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
row_names uses the shared display_name helper, so a built "Language (Region)" name is translated in
parts. The dictionaries-folder comments in prefs.rs and tests.rs say File > Options > Language.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
The Dictionaries folder moves here from Proofing with the Dictionary files list (#1063) kept. Set as
default sets only the saved default proofing language (the new pure review.language.preferred command), and
proof.packs returns the languages rows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
With no list of packs yet the bar offers a language with no dictionary and Install fetches the
list, then the pack. The caret's typing language counts, the spelling context menu heading is in
the interface language, and closing the bar is remembered per document, pack locale and tag.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Every published pack is named in English, Valencian is ca-ES-valencia, and rows are matched by
tag (language, script, region, variant), never by the name shown. One region helper
(packs::is_region, packs::without_region) next to pack_for_tag serves the engine and the UI.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
Word's TOC over entries in columns keeps its TOC Heading title in a one-column
section above a blank paragraph that ends it; Update Table moves the field into
that blank paragraph. Replace (references.toc with settings) put the new title
under the break, inside the two-column section, and Remove kept the blank
paragraph and its break.
Observed black-box in Word for Mac 16.114.3 on a synthetic document: Insert
Table of Contents keeps the title, the blank paragraph and the break and only
changes the field; Remove Table of Contents takes the title, the blank
paragraph and its break with the entries.
Replace now keeps the title above the break (renamed when a title is given)
and rebuilds the field and entries in place; Remove takes the title and the
blank section-ending paragraph with the table. A paragraph of another style
above the break stays.
Co-authored-by: Harrison Smith <harrison@hsmith.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #5290: Page Up/Down move by a page in Outline, Draft and Web Layout
These views lay the document out as one long page, so caret.pageUp/pageDown looked for a previous or next page, found none and left the caret where it was. Move by the text area height of the caret's section instead, the same distance Print Layout moves.
* #5290: Page Up/Down pick the line past the caret, and page from a hidden outline selection end
Choose the destination among the lines past the caret by height first (DocLayout::line_toward), so narrow right-indented paragraphs can't keep the caret on its own line when the step is short. The above/below test is shared with vertical().
In Outline view, a selection that ends in text a collapsed heading hides pages from where the outline shows it (outline::shown_pos, pulled out of after_command), keeping the anchor. Avoid f32::clamp, which panics on a NaN page height.
Building the queue checked every character of the document against every
citation field, so a document with many citations made Read Aloud do
characters x citations comparisons before playback started.
The citation spans are now sorted and merged once, and each position is
looked up by binary search.
- PACK_INDEX: language-packs-2 index.json, sha256 b35c7aef…, built from craft-i18n 20467b63
- craft-proof pinned at storytold/craft-libs 47c9f625 (flag-based compounding, #497; review follow-ups and en-AU, #496)
- Options › Language: ProofLang::EnAu has no Hunspell dictionary name of its own (English uses SCOWL)
- docs: Danish, German, Dutch, Swedish, Norwegian, Hungarian, Estonian, Croatian, Persian and Esperanto are packs; Mongolian and Thai stay hidden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>