Swedish (Finland) is built at run time, so no catalog row can match it. One helper
(ribbon::display_name) now translates the language, the region and the {language} ({region})
template separately and joins them; the Set Proofing Language list, the Language menu, the status
bar and the dialog all use it. English output is unchanged and matching never uses a name.
gap 8 no longer describes the missing-pack bar, which is in its own branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
* #3867: review — header repeats, picture outlines and linked-box widths bounded the same way every layout
- Header repeats: a Clip (floating Drawing Canvas) counts its members, depth-bounded, so a 1000-member canvas no longer counts as one item (CodeRabbit thread 4373103686); the budget is per layout (Ctx), not per table.
- Tight-wrap outlines: each cache entry stores its picture's pixel cost; a cached hit is charged to the layout's decode budget like a decode, so cold and warm layouts (and the CLI) wrap alike.
- Linked boxes: widths are budgeted by laid-out work (MAX_FLOW_WORK), not 16 widths, so ordinary stories keep exact widths for every box.
- header_rows.contains -> index compare; layout-parity doc revision.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: review — a picture that fails to decode still costs its pixels when cached; header repeats restored on retry
- contour: Alphas records the pixels each picture charged (costs map), also when the body then fails to decode, so the cache entry's cost is right and warm layouts charge it like cold ones.
- Snap saves and restores ctx.header_repeats so balance_pass retries don't keep their charge.
- Tests: truncated large PNG cold vs warm; canvas header row end to end through place_table; linked-chain bound <= 8; vertical box's layout differs from every other box's.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: CodeRabbit — header and linked-box budgets count every clip descendant
placed_cost counts iteratively (explicit stack, stops past the limit) and is shared by flow_work.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: docs — progress log, gap 20 note for layout resource caps and linear-time passes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
* #3867: review — roadmap gives each cap its unit: header items, wrap-cache pixels, linked text box lines and items
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3867: CodeRabbit — the vertical-box check runs past the work cap too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — a linked chain's work counts the story it lays out, not only the lines it shows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — header-row cost import follows main's explicit layout imports (#4501)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3867: review — a test pins the header repeats that column balancing gives back; docs keep main's caps row and both gap status lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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
* #3382: a long comment's card and balloon stop at ten lines
A comment or reply field, in the Comments pane and in a selected balloon,
is at most ten lines tall and scrolls beyond that, keeping its caret in
view. A balloon that isn't selected shows the first ten lines of the
comment and of each reply and cuts off the rest; clicking it opens the
scrolling field. The buttons and the next comment stay in view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3382: record the ten-line comment cap in the UI parity doc; test the drawn rows
The Comments entry of the UI parity doc notes that a long comment's card,
balloon and fields stop at ten lines. A new test checks what an unselected
long balloon paints: its first ten rows inside the card's clip and none of
the rows after them, in Latin and Arabic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #3382: check the shown balloon rows start inside the clip
The drawn-rows test now also checks that each of the first ten rows starts
at or below the top of the balloon body's clip, so a clip that hid the
first rows would fail it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(docbin): keep one override per table cell instead of a copy per repeated sprm
* DOC import: vertical alignment reads only the cells a row can hold, like the other cell-range sprms
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>
* Arabic interface: mirrored right-to-left window and Arabic UI fonts
Adds the Arabic (`ar`) interface language's code: its catalogs are craft-i18n's
`locales/ar`, so this changes no catalog.
- `ar` is registered in `i18n::LANGUAGES` and `Lang::rtl()` says it is written
right to left; its translations are used as written (egui's text layout
orders each line's bidi text itself). A ribbon location reads from the right
with a leftwards mark.
- `rtl.rs`: the window is mirrored. Containers open in `rtl::root`; columns,
grids, checkboxes, radio buttons, menus, rows, panels, the dialog tab strip
and the ribbon's scrolling row follow the language's direction. Title bar,
status bar, ribbon tabs and groups, File pages (including Print, with the
settings on the right and a solid scrollbar gutter), dialogs, menus and the
task panes (their docking edge swaps) are laid out from the right. egui's
`Grid` panics in a right-aligned layout, so grids go through `rtl::grid`.
- Arabic UI font fallback: the craft-fonts Arabic face, else an installed
Arabic font, loaded when the interface is Arabic.
- The "Auto (200%)" size label keeps its percent sign inside the isolated
number.
- Tests: Arabic locale, saved preference and lookup order on fixture catalogs;
an interface audit (skipped without the craft-i18n Arabic catalog) that lays
the app out right to left at several widths, checks tab, group, pane, Print
and Go To positions and that nothing panics.
The Arabic work builds on Abdelhaq El Amraoui's mirrored-interface PR (#737).
Co-Authored-By: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: pin craft-i18n at a commit with the Arabic catalog; review fixes
- craft-i18n.rev and the Flatpak manifest pin 19358491 (craft-i18n main), the
first commit line that has locales/ar/wordcraft.tsv. The old pin had no
Arabic catalog, so `ar` was registered but not embedded and two i18n tests
(the_language_setting_is_a_ui_command_and_persists,
the_languages_offered_are_the_embedded_ones_of_the_list) failed in CI.
- The Arabic font fallback is requested for any right-to-left interface
(`Lang::rtl()`), not only after the language picker sets the flag, so an
automatic or saved Arabic gets the installed Arabic face.
- The file browser, the update window and Reading Mode's toolbar run their
bodies in `rtl::root`, so their far-side buttons and rows are mirrored.
- A group in a right-to-left ribbon row is measured from the right edge: the
cursor's left edge is negative infinity there, so its width was NaN and the
ribbon planner dropped every group, never compacting a narrow Arabic window.
A narrow window now folds groups into drop-downs.
- The mirrored checkbox and radio marks are labelled by their text, so screen
readers name them.
- Tests: group width in a right-to-left row; the Arabic interface audit checks
Reading Mode's toolbar.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: ask for the installed Arabic font separately from the CJK one
A previous commit made every right-to-left interface request the installed CJK
fallback font (`system_cjk`) so that the installed Arabic font would load. That
also made every Arabic launch read a large system CJK font file when no
craft-fonts input is embedded (the Flatpak build has none).
`install_ui_fonts` now passes a separate `system_arabic` flag (`Lang::rtl()`):
the installed Arabic font is read for it, and `system_cjk` is again set only
by a CJK interface or by CJK text on screen. `WordApp::fonts_system_arabic`
remembers what was installed, so a language change reinstalls the fonts.
Tests: the_installed_arabic_font_is_asked_for_by_arabic_alone (the Arabic flag
adds the installed font only when none is embedded, and the CJK request alone
adds no Arabic font). Checked a headless screenshot of the Arabic interface
without craft-fonts: the installed Arabic font draws the labels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix the AutoSave switch knob: on is at the right, mirrored in Arabic
The title bar's AutoSave knob was placed with `lead_x(rtl == on, ..)`, which
reverses main's mapping in both directions: left to right it drew the knob at
the left when the switch was on, and right to left at the right. main draws it
at the right when on and at the left when off, so the mirrored interface must
draw it at the left when on: `rtl != on`.
The position is now `autosave_knob_x(rtl, on, track)`, pinned by a test for all
four cases. A headless screenshot of the off switch shows the knob at the left
in English and at the right in Arabic.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: file names and paths in right-to-left lines; kerning row order
Tester feedback on the Arabic interface:
- File > Open's Recent list, the built-in file browser and the title bar drew
file names and paths raw, from the left edge. In an Arabic line the marks of
a name ("(نهائي) 2.docx") and the folders of a path with Arabic parts were
scrambled. Rows now start at the side reading starts from (`rtl::lead_x`),
names go through `i18n::isolate` (a first-strong isolate, with a left-to-right
mark before the extension so `2.docx` stays `2.docx`) and paths through
`i18n::isolate_path` (a left-to-right isolate with a mark after every
separator, so neighbouring Arabic folders do not swap). Other languages get
the text untouched; saved and spoken text never carries the marks.
- Font dialog, Kerning row: the dialog's grid is drawn left to right inside a
right-to-left window, so "Points and above" came after its number; in an
Arabic interface it now comes first, so the line reads "14 نقطة فأكثر" from the
right. (The grid's columns themselves still keep their left-to-right order.)
Tests: names_and_paths_are_isolated_in_a_right_to_left_interface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic interface: mirror through craft_ui::direction and Style::right_to_left
The suite's craft-ui now has the direction helpers (`craft_ui::direction`) and
its egui fork a `Style::right_to_left` that mirrors egui's own chrome. Use them
instead of our parallel wrappers:
- `rtl::sync(ctx)` applies the interface direction every frame the language's
changes (`direction::apply`): it sets the shared direction state and
`Style::right_to_left` on every theme, which draws check boxes and radio
buttons with the mark at the reading start, mirrors submenus, scroll bars,
title bars and the combo boxes' arrows (the Print page's choice lists, the
font list) and opens combo menus from the right.
- `rtl::checkbox`, `radio_value`, `radio` and `indeterminate_checkbox` are
gone: about 200 call sites use plain `ui.checkbox`, `ui.radio_value`, ... and
egui does the mirroring (and names the control by its text, as a plain widget).
- `lead_x`, `lead_center`, `lead_top`, `start_panel` and `end_panel` delegate to
`craft_ui::direction`. `root`, `flows`, `island`, `grid`, `menu`, `vertical`,
`far_side` and the other layout helpers stay: the shared crate has no
equivalent that fits them.
Visual changes: the combo boxes' drop-down arrows and lists are mirrored in
Arabic; nothing changes in left-to-right languages (checked headless
screenshots of the English main window, Font dialog and Print page).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Recent files and the file browser: full path on hover, long names keep their start
Review feedback:
- Each row of File > Open's Recent list shows the document's full path as its
tooltip (the path line under the name can be longer than the row).
- A long name in the built-in file browser is laid out in one row, with an
ellipsis at its end, from the side reading starts from, instead of being
clipped at the row's edge: in a right-to-left interface the old text anchored
at the right edge cut the end of an Arabic name and the start of a Latin one.
The whole name is the row's tooltip when it was shortened.
Checked headless screenshots of the file browser with a long English and a long
Arabic name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Font combos in a right-to-left row: the arrow joins its field on the left
The ribbon's font name and size boxes are drawn from a field and a separate
arrow box that share one outline: the field is square and the arrow's box is
rounded on the side where the arrow goes, painted a border width under the
field. That assumed the arrow is right of the field. In a right-to-left row
(the Arabic ribbon) the arrow comes after the field on its left, so the field
was square on the wrong side and the arrow box rounded on the side facing the
field: two boxes with a gap-like seam, the arrow looking outside the frame.
`arrow_box_shape(rtl, ..)` mirrors the radii and the underlap, and the field's
square corners are chosen by the row's direction (the nw/sw pair in right to
left). Left-to-right rows are drawn exactly as before (English before/after
crops are identical).
Test: the_arrow_box_is_rounded_away_from_its_field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n 4612850 (merged Arabic WordCraft rows, craft-i18n#76)
The pin and the Flatpak manifest commit move from 1935849 to 4612850, which is on
craft-i18n main and adds the 70 Arabic rows this branch relies on.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Pin craft-i18n cac2cf0 (Arabic number agreement and status-bar rows)
Moves the pin and the Flatpak manifest commit from 4612850 to the current craft-i18n
main, which includes craft-i18n#95 (48bed03) and #81 (ef58ff3).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: isolate paths shown beside labels; mirror the Print page zoom
File > Info shows Location as its own left-to-right path label (cut short with the full path on hover, not wrapped), and the open-failure, Exported and dictionary-file messages isolate their paths. The Print preview zoom puts minus at the slider's left end and plus at its right in Arabic, as the status bar does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Arabic: mirror the label/field rows of the Font, Info, Paragraph, Page Setup, Insert Table and Options forms
rtl::Table lays a form out as an egui Grid in a left-to-right interface (same id, columns and spacing, so the output is unchanged) and, where the interface is mirrored, with each row's cells running from the right, every column as wide as its widest cell. File > Info puts Properties at the right and Statistics after it, and the Font dialog's kerning row reads 14 points and above.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs: record Arabic mirrored interface landing (#1195)
Update the language counts, Arabic parity row, gap #11, and UI/ROADMAP
status so they match the craft-i18n catalog and mirrored shell already
on this branch.
* Fix Organizer Copy arrow in RTL and refresh roadmap status.
Point the Copy glyph at the Document column when the shell is mirrored,
and bump docs/roadmap.md status/revision history for Arabic UI (#1195).
* fix(ui): keep pane dock left/right physical in RTL
ui.paneDock and view.stylesPaneDock now store and report physical window sides; only unset defaults still open on the reading-start edge. Also wrap the language-packs grid for RTL, document the contract, and load a system Arabic face for Arabic names in LTR UIs.
* docs: clarify mirrored default pane edges in RTL
Start-default panes open on the physical right in Arabic; end-default
panes open on the physical left — not every never-docked pane on the right.
* refactor(ui): one PaneState for a saved pane place
The four saved-dock arms built the same PaneState; only the place and
whether it was set explicitly differ.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style(ui): rustfmt the panes re-exports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: The Legend Of MTS Dev <tlo.mts.tsh@gmail.com>
Co-authored-by: Abdelhaq El Amraoui <abdelhaqelamraoui@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: email <email@email.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.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>
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>
- 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>
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>
- 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
- 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
craft-proof moves to 0675d58: an English no longer takes another English's
SCOWL words (en-US flagged colourblind, en-GB colorblind), British English
keeps -ize, and English suggestions drop junk splits and names.
craft-i18n.rev and the Flatpak source move to storytold/craft-i18n#114's merge
commit (059cbf9: judgment/judgement right in every English).
No wordcraft test depended on the dialects crossing
(engine 1120 + 43, layout 578 pass). localization-parity's English row says so.
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
- 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
* Markdown export: a list inside a block quote stays in the quote
`> - one / > - two` was imported as quoted list paragraphs (the quote kind on each item) but
the list branch of the exporter ignored that kind and wrote `- one / - two`, so every save as
Markdown moved the list out of its quote. A quoted list item now takes `> ` on each of its
lines, nested levels and continuation lines included. The same paragraphs come back from a
.docx (Quote style with numbering), so the quote now survives that round trip too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Markdown export: a list after a quoted list stays outside the quote
* fmt
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word draws an arrowhead 2, 3 or 5 times the line's width long and wide
(small, medium, large; `a:headEnd`/`a:tailEnd` `len`/`w`), a line under
2 points as a 2-point one. Every arrowhead was drawn one size and saved
as a medium triangle: the kind, width and length are now read (DrawingML
and VML), kept and written back, and the size is drawn as Word does.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #5178: the line between columns is drawn for a section starting mid-page
Columns for selected text are a continuous section in the middle of a page, and
the line between columns was only added when a page started, down to the page
bottom. The lines are now drawn per section on each page, from the top of its
text there to the bottom of its tallest column, when the page or the section ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the line between columns spans the text's lines, not its paragraph spacing
Review: the line started above the section's first line (its space before) and
ended below its last (its space after). It now runs from the highest text line or
table cell of the section on the page to the lowest, falling back on the flow's
extent when there are none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the column lines find the section's text by place, and draw nothing without it
Review: the section's items were the page's items after an index, which objects
behind the text (inserted at the front) shift; the flow's extent drew a line
where the section had only spacing; overflow lines of exact-height cells counted.
The text is now the page's top-level body lines and table cells below the
section's top; lines in cells count by their cell's box; no text, no line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5178: the column lines take the section's text by its blocks
Review: a floating table of an earlier section placed lower on the page would
pass a filter by place. The section's text is now the page's items of top-level
blocks from the section's first block on the page.
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>
* #5066: .doc default font is the stylesheet's standard font
The .doc reader took the default character font from the font table's
first entry. A Word 365 .doc keeps Times New Roman there and names Aptos as
the stylesheet's standard font (Stshif.rgftcStandardChpStsh), so its text
came out in Times New Roman. The defaults are now the standard ASCII, East
Asian and complex-script (ftcBi) fonts, else entry 0 as before; runs'
sprmCRgFtc1 and sprmCFtcBi are read too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5066: the complex-script standard font is read only when the header has it
ftcBi is optional in the Stshi, and the latent style data that follows it starts
with cbLSD (4): a header that leaves ftcBi out no longer makes cbLSD the
complex-script font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5066: a header ending with ftcBi = 4 keeps it
Review: a 4 at ftcBi's offset was taken for cbLSD even when the header ends
there, so a complex-script standard font of index 4 was lost. It is read as
left out only when more data follows.
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>
When a row's first part on a page that already holds something would leave
a cell empty because that cell's inline picture (one line, never split) or
first line runs past the room, the row moves on whole instead of leaving the
other cells' text beside the empty cell. An at-least row grown taller than its
minimum by its picture used to pass the #1742 check (room >= declared minimum)
and then split, the picture going on alone. At the top of a page such a row
still splits.
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>
Measured in Word for Windows: when a table row breaks across pages, a cell
aligned to the middle or bottom whose text all fits in the part of the row
left on the page is aligned within that part, as tall as the other cells'
text there (with its space after), and a cell whose text doesn't fit runs
from the top of the cell with the rest on the next page. WordCraft kept the
offset the whole row's height gave the cell, so a short centred label
beside a long cell went to the top of the next page and left its cell empty
on the first, and a long centred cell started far down the page.
The vertical offset each cell's content got is kept with the row, so a split
moves the content back to the top before it decides what fits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layout: tab leader dots spaced and placed as in Word
Measured in Word for Windows with six fonts at four sizes: Word spaces leader
dots by the dot's advance at the font size in whole 600-dpi pixels (the size
itself in whole pixels), so a TH SarabunPSK dot at 16 pt (2.59 pt) is drawn
every 2.64 pt and a Tahoma one at 22 pt every 6.6 pt. The dots sit on the
page's grid of that pitch, from the first place after the text before the
tab to the last one that ends before the text after it. WordCraft drew them
at the unrounded advance, half a dot after the text and 1.2 dots short of
the number, so a table of contents line had a dot or two more than Word's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: each leader dot's place comes from its grid index
The leader's dots were placed by adding the pitch to the last place, so on
a long leader the rounding of each addition added up. From 72 to 528 pt at
2.64 pt it came to more than the 0.001 pt allowed at the end, and the last
dot, which ends exactly on 528 pt, was left out. `leader_xs` now works out
each place as its grid index times the pitch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: name the leader slack and glyph limit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout: leader dots use the font size rounded to whole pixels
Word rounds the font size to the nearest 600-dpi pixel before it rounds the
dot's advance (Arial 14 pt: 116.7 px, so 117, dots every 3.96 pt); taking the
floor drew them 3.84 pt apart, as measured at 11, 14, 17, 20 and 26 pt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* leader_xs: saturate the glyph range end
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Lists: a level above counts its first number at the first deeper item
Word counts a list level that hasn't been used yet as started at its first
number when a deeper item comes first: after 10.1-10.4 with no level-1 item
before them, the next level-1 item is 11. WordCraft showed 10 in the deeper
labels but didn't count it, so the next level-1 item repeated it ("10.
Qualifications", then "10. Penalties" in a tender document whose sub-items
come first).
Counters::next_label now starts each unstarted level above the one it counts,
at that num's start override if it has one, else the level's start. Measured
in Word with ListFormat.ListString: 1.1, 1.2, 2., 3.; (1), (2), 2. when the
deeper text doesn't show level 1; 1.1.1., 1.2., 2. from level 3.
a_list_s_own_level_definitions_win expected the old count; its level 0 item
after level 1 items is now 2., as in Word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lists: record the counting fix in the progress log and gaps
AGENTS.md asks for the progress log and the gap list when work lands. The
ROADMAP progress log has the entry; gaps.md records that no ranked gap
changed. Each with its status line and revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lists: test a num's start override for the level above
The deeper-item-first test now also gives the num a start override for
level 0 (10) under level-1 items: 10.1., 10.2., then 11. It fails if the
override is ignored for the level above (1.1., 1.2., 2.).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test a start override of 5 and a restart after a level-1 item
Review: 5.1., 6., 6.1. with the num's start override, and level 2 restarting below the next level-1 item.
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>
* #5036: paragraph side borders lie beyond their distance from the text
push_para centred the left and right rules on the distance, so a 3 pt border
at no distance reached 1.5 pt into the text and, drawn after the lines, covered
the first letter. In a heading whose border matches its shading the letter
looked cropped, on the page and in the ribbon's style tiles. All four lines now
lie outside the distance, as the top and bottom ones did. Top and bottom reach
the corners and the shading fills out to the outer edges, so no seam shows
where rules and shading meet.
* #5036: pin the right border on its own in the side borders test
The test only compared the right border with the end of the top rule, and both come from the same value, so a wrong right edge still passed. Check it against the line's own bound.
* docs: progress lines for #5088
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* T280: anchor header drawings below tall headers
* #2485: share rendered header boundary with pins and obstacles
* docs: layout-parity status line for #2485
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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.