Another ui-egui test's frame runs language_packs::tick, which puts installs on the engine's
thread while this test's backend is set; start_install then returned Ok and the test failed
in the full suite (it passed alone). Red on main since the language packs merge (#5224).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
When a region is chosen in File › Options, start-up replaces the first blank document with one
on that region's paper through `session.set_document`. That changes the session's active
document, but the window still showed the old one, so on the first frame `sync_document` took
the change for a switch of document and cleared the dialog. A password-protected file named on
the command line (a double-click in the file manager) had just asked for its password with
`file.open`, so the Password box was dropped before it was drawn and a blank Document1 stayed.
`WordApp::replace_startup_document` swaps the document and lets the window follow at once; the
start-up code uses it. Before the file is opened, so nothing is open to lose.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The WOFF2 decoder rebuilds every collection font's table directory (16 bytes
per listed table) before checking its output size, while the file spends as
little as one byte per listed table. A small collection listing the same
tables over and over made it allocate hundreds of megabytes twice.
Preflight the collection directory before decoding: the rebuilt directories
must fit 16 MiB and no font may list a table twice.
A name record can hold 64K characters, and every face copies its family into
each localized name and every variable-font named instance (with their
localized names again), so a small font could claim hundreds of megabytes of
name strings before any later check.
name() now keeps at most MAX_NAME_CHARS characters, the limit localized names
already used, so every copy stays small.
The TIFF guard read the first Compression tag, but the tiff crate keeps the
last copy of a tag. An allowed first value (none, LZW...) could hide a JPEG
compression after it, and the JPEG strips are then decoded by a nested
decoder with its own limits, outside the caller's budget.
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>
* DOCX: VML horizontal rules take their line
A web page's <hr> pasted into Word is a VML rule: `v:rect` with
`o:hr="t"`, often `width:0`. WordCraft took the width as written and
drew a 1 pt dot where Word draws a line across the page.
Measured on Word with a synthetic document: a rule with no width and
no `o:hrpct` spans the line inside the paragraph's indents; `o:hrpct`
is a share of it in tenths of a percent (500 = half); `o:hrpct="0"`
keeps the shape's own width; `o:hralign` places a shorter one (left,
centre, right); `fillcolor` and `stroked` apply as to any VML shape.
WordCraft now matches each case within 0.1 pt.
- wordcraft-doc: ShapeExtra::hr (HorizontalRule: share, alignment,
Word's standard look).
- wordcraft-docx: `o:hr`, `o:hrpct`, `o:hralign`, `o:hrstd` read; a rule
is written back as the same VML rule (DrawingML has none), and Word
opens the re-saved file with the same lines.
- wordcraft-layout: a rule takes its whole line and is drawn at its
share; measured for a table column it asks for no width, so it
follows its column. (Word draws a rule in a cell at the page's text
width, centred on the cell and past its edges; it stays in the cell
here.)
docs/file-format-parity.md is updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: only an inline, shown rule is one
Review: the rule arm of the writer ignored the shape's placement, so a
rule imported with position:absolute or visibility:hidden came back
inline and visible. Only a rule in the text and shown is read as a rule
now (a pasted <hr> is one); a positioned or hidden one stays a plain
shape, which keeps both when saved. The writer checks the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: a see-through fill is kept; column measuring asks for the right width
Review: `write_hr` wrote only the fill's colour, so a rule with `v:fill
opacity` came back opaque after a save; the opacity now goes back as `v:fill`.
Review: measured for a table column, every rule asked for no width, so one of
its own width (`o:hrpct="0"`) could end up clipped by a narrow column; it now
asks for its width. AutoFit to contents measures on a line of its own width
that the rule took for a real line, so a rule sharing its line made the
column as wide as it could go; any measuring width now counts.
`Placed::Shape` boxes its extras: with the rule, `Placed` went past clippy's
large_enum_variant limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: placed in what the line has left; rotation and flips kept on save
Review: a whole-line rule ignores a first-line indent or a line narrowed by a
float. Measured in Word, it does too: the width stays the share of the
paragraph's width and the rule runs past the margin, or under a float on its
right. What did differ was the placement: Word centres and right-aligns a rule
in what the line has left, with a first-line indent moving the end of that room
along, and a right-aligned rule beside a float was drawn off the page here. The
display now places it that way (`ParaLayout::rule_room`).
Review: a save dropped a rule's rotation and flips; they now go back in its
style, as Word writes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: VML horizontal-rule revision row at the top of the history
---------
Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Shapes: a solid fill keeps its transparency
A shape's fill colour can carry a:alpha, its opacity. WordCraft dropped
it, so a translucent white wash laid over a picture was drawn opaque and
hid the picture. The transparency is now read into the shape, drawn on
screen and in the PDF, and written back.
* Fix#1600 freeform and soft-edge fill opacity
* #1600: wrap canonical caret sampler signature
* #1600: format canonical integration
* #1600: assert canonical DOCX alpha round-trip
* #1600: document canonical shape opacity API
* Adapt alpha fixture to rectangle render bounds
* docs: one file-format-parity row for soft-edge fill opacity, status line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #3660: refuse a package that declares too many zip entries before zip parses them
zip::ZipArchive::new parses and keeps every central directory header
the end record declares before its caller can look at len(). A ZIP64
end record can declare one per 46 bytes of the file: a 47 MB package of
one stored file listed 1,000,000 times takes zip 3.0.0 about half a
second and a large transient allocation, and len() then says 1, since
the copies share a name, so the docx reader's MAX_ENTRIES cap never
fires. ODT and spreadsheet packages had no cap at all.
wordcraft_doc::zip_guard::check finds the end records the way zip finds
them (backwards; the ZIP64 record from the locator's offset on; a plain
record when it has no entries or a central header before it) and refuses
a package declaring more than the reader's limit. The docx package
reader, the ODT reader, the spreadsheet reader (mail-merge data) and the
template-title probe call it first, with 20,000 entries as the limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* formats: keep the extensions doc line on EXTENSIONS
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Count the runs proofing text walks, instead of timing it (#5185)
`proof_text_walks_the_runs_once` was the one linear-time test here that timed
its walk, and the ratio crept over its 24x limit when the crate's tests ran in
parallel -- 28.2, 26.4, 25.6 -- from cache and memory contention rather than
from any regression.
The file's own module doc says why the others don't have this problem: "The
cost is the steps the lookups' walks count (crate::work), not a timing, so a
busy machine can't make a linear lookup look quadratic or the other way round."
It then says this one is timed because "proofing text, whose walk is the
document model's" counts no steps -- but `proof_text` does the walk itself,
advancing a runs iterator alongside the text, so it can count the runs it looks
at like every other walk here. Nothing in `wordcraft-doc` had to change.
before 8x the runs took 28.2x the time (limit 24), under load
after 50000 steps for n, 400000 for 8n, ratio 8.0 (limit 12)
The count is the same on any machine under any load, so the flakiness is gone
rather than made less likely: raising the limit to 32x would have left the same
failure available on a busier machine. It is also about 100x faster -- 0.03 s
against three rounds of eight repetitions -- and `work::steps` is `#[cfg(test)]`
inside, so release builds are untouched.
The test still catches what it was written for. Putting the quadratic walk back
(the run for each character found from the first run, every time) gives:
proof_text: 1250025001 steps for n, 80000200001 for 8n, ratio 64.0
panicked: 8x the input took 64.0x the steps: not linear
Verified under the reported condition: three runs of the whole crate suite in
parallel, 559 tests, all green.
* linear tests: proofing text is counted now, not timed
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
An alternative format chunk holding a Word document with w:matchSrc keeps its
own styles: a style the document already has comes along under a new id, the
chunk's document defaults (and Word's, where they say nothing) go into its
base styles, and its unstyled paragraphs use its own Normal. Without
w:matchSrc the document's styles win, as before. Adopting another document
now also renumbers table styles and the revisions of table, row and cell
property changes.
A document whose chunk shares style names with it (Normal with an exact line
height, an italic Subtitle) ran a page longer than in Word: the chunk's tables
took the document's styles. It now keeps its own.
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
CodeRabbit on #5205:
- NOTICE: the SCOWL English dictionaries (en_US, en_GB-ise, en_CA) with
Kevin Atkinson's SCOWLv2 notice, Benjamin Titze's notice and Geoff
Kuenning's Ispell BSD licence for the affix files, as their licences ask of
distributions.
- docs/gaps.md: gap 8's evidence says each English checks its own SCOWL
2026 dictionary, with the #40 and #520 regression tests; #40 (closed) is no
longer listed as open. Status line and revision history.
Also: docs/localization-tr.md's craft-proof path change (3110dcf02, #5202)
and docs/architecture.md's no-data wording (b7dad64d9) get their status lines
and revision rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2