Commit Graph
462 Commits
Author SHA1 Message Date
.CooCoo c419985c54 Tests: the balloon anchor test counts work steps instead of timing (#5608)
anchors_walk_dense_unrelated_markers_in_linear_work compared CPU times of
a few microseconds, so cache and timer noise could push the ratio past its
24x limit on a linear walk (43.52x in a CI run).

wordcraft-doc now counts, in tests only, the characters its paragraph
object lookups walk (objects_before, object_offsets and
objects_with_offsets), through a work::count helper behind the existing
test-support feature. The test asserts that 8x the markers take at most
12x the steps, as the layout linear-time tests do: the walk takes 2001 ->
16001 steps (8.0x), and the object_offsets + object_at walk it replaced
takes 2003001 -> 128024001 (63.9x).
2026-11-05 01:32:10 +01:00
Dmitrii KhlapovandClaude Sonnet 5.5 1c7b5013fa Drop Cap follow-ups: whole selection, script fonts, margin gap, menu keeps options (#4077)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 01:26:57 +01:00
Aleksander HelgakerandClaude Opus 5.5 650a97b623 ODT: remove the display settings; one choice for each open case (#5609)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:08:09 +01:00
NetterSilverandClaude Haiku 5.5 0802b4fe70 Layout: the caret on the empty line after a trailing line break follows the paragraph's alignment (#4293)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 21:57:34 +01:00
mmazzieriandClaude Opus 5.5 75fc50fb77 DOCX: picture brightness, contrast and transparency (VML washout, a:lum, a:alphaModFix) (#1961)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:47:44 +01:00
FredApps 8218dea5bd Fix #5559 follow-ups: generated list prefix, hidden-text objects, turned clipped lines, kept-markup checks once per export
- update_generated keeps what precedes a wrapped list's field (r4468529243)
- from_doc keeps objects in hidden text when hidden text is written (r4469018820)
- clipped cell lines in a turned frame are drawn once, clipped, turned (r4469018900)
- kept::while_writing works out reply map and page fingerprint once per export (r4468529227)
- odt_charts fixture declares its namespaces (r4468529259)
- guard test for character styles based on a missing parent (r4469018843, not a bug)
2026-11-04 17:43:46 +01:00
FredApps 69945e0ccb Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559
# Conflicts:
#	ROADMAP.md
#	docs/layout-parity.md
2026-11-04 16:08:39 +01:00
pakiatandClaude Opus 5.5 2df2bfc30e Show fresh SEQ, STYLEREF and REF results in PDF export and printing (#5162)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:01:58 +01:00
FredApps 6d22e36196 Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
#	docs/layout-parity.md
2026-11-04 15:54:52 +01:00
ReesandClaude Opus 5.5 958867b7e8 Walk Print Markup balloon anchors once (#3614) (#4785)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:51:39 +01:00
FredApps 3c7aa5df90 Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559
# Conflicts:
#	crates/formats/src/odt/import.rs
2026-11-04 15:24:27 +01:00
Bryan MarkandClaude Opus 5.5 b2734c28f4 Undo: steps share the style sheet and list definitions (1.2 GB → 21 MB at the limit) (#2694)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:03:13 +01:00
FredApps 8b9f726ea7 Merge main (688cce47) into ODT PR 2026-11-04 13:44:24 +01:00
FredApps b6afa0ba3c Review fixes: kept object names skipped (headers too), self-closed settings, STYLEREF/XE after a leading space, RTL padding, split cell backgrounds, PAGE in a table moved by a page break, written-as-read doc 2026-11-04 13:38:55 +01:00
midasdfandClaude Opus 5.5 2d6dff96cc Layout: 行頭禁則 lists small kana and the prolonged sound mark (JIS X 4051 §4), with the emergency-break gap documented (#2558)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:31:39 +01:00
FredApps d2f504e29a Review fixes: package-wide PBKDF2 budget, CLI keeps the ODF style, turned linked boxes, path close, centred PDF fill, doc status (gap 19) 2026-11-04 12:50:36 +01:00
FredApps 773ef21f9f Merge origin/main into the ODT branch (gap 19)
# Conflicts:
#	ROADMAP.md
#	crates/layout/src/tests.rs
#	docs/gaps.md
#	docs/layout-parity.md
2026-11-04 11:57:31 +01:00
FredApps 53bccd5442 ODT: two display styles (Microsoft Word style, LibreOffice style) and review fixes (gap 19)
- odf.preset: word (Microsoft Word style, default) or libreoffice; wordcraft still read as word
- Options, CLI (WORDCRAFT_ODF), docs and ROADMAP follow the two styles
- CodeRabbit: kept groups and story fingerprints follow fill_objects; padding serde default;
  list field depth; literal doubled quote; ASK/SET results not exported; dc:date offsets;
  first_pages order; unique picture names; finite raise clamp; PDF rotated-text guard;
  .ott in the password message; flat element bounded by depth; cell tile fallback;
  ROADMAP and docs reconciled
- Remove clean-room wording from the ODT tests
2026-11-04 11:55:49 +01:00
Aleksander HelgakerandClaude Opus 5.5 ba58e70cc8 Flag document fonts that aren't installed (#4770)
* 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>
2026-11-04 11:48:48 +01:00
Bryan MarkandClaude Opus 5.5 6f62716f08 Text boxes: a press inside one goes to its text, not to a picture under it (#4240)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:31:58 +01:00
alehelandClaude Opus 5.5 56c85834f1 Merge origin/main into the ODT branch (gap 19)
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
  `<p>&nbsp;</p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
  arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
  parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:51:35 +01:00
Martin BallandClaude Sonnet 5.5 bff0d1d925 #3867 follow-up: header repeats count canvas members, one budget per layout; contours and chain widths don't depend on cache history (#4385)
* #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>
2026-11-04 00:45:19 +00:00
Dylan M. Taylor 051fd875ef Proofing: look up unchecked runs by binary search, not a scan per issue (#5344)
paragraph_issues dropped issues touching a run left unchecked by testing
every run of the paragraph against every issue. A paragraph can have many
runs and many issues within the proofing size limit, so the filter was
quadratic in the paragraph's size. The unchecked ranges are now merged and
sorted once, each issue is looked up by binary search, and paragraphs
without unchecked runs skip the filter.
2026-11-03 08:55:09 -05:00
Claude 6f6559aacb ODT: widow control as a profile case; tabs measured from the indent kept
Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.

LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 06:18:46 +00:00
ljkx bcbc8619e9 #5290: Page Up/Down move by a page in Outline, Draft and Web Layout (#5300)
* #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.
2026-11-02 22:43:22 -05:00
Claude e635c8bd96 M5 #19: ODT cell background pictures
LibreOffice's elegant agenda puts a small gradient picture behind cells
(§17.3 style:background-image in a table-cell style). Cells can now
carry a background picture (CellProps::picture: media, own size,
repeat none/tile/stretch, alignment), which layout draws over the
cell's shading and under its text, clipped to the cell (Placed::Clip of
Placed::Image; tagged as an artifact in PDF). The ODT reader reads it
(style:repeat, style:position, the picture from the package) and the
writer writes it back with the picture in the package (once per
picture).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 00:10:57 +00:00
Claude 5063000781 M5 #19: ODT keeps table styles, SmartArt and section column balance
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).

- Table styles: ODF has no table styles of Word's kind, so each cell now
  states what the style gives it (fills, its borders, bold header text,
  its paragraph spacing), under the paragraph's own styles as layout
  applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
  table_char_patch). The style's border mapping moved to
  styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
  single spacing changed their text.
- Columned sections say whether their columns end balanced
  (text:dont-balance-text-columns, §20.428, read as its name says; its
  value descriptions contradict it). Unstated, the ODF profile decides
  (balanceSectionColumns: LibreOffice balances, WordCraft as Word).
  SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
  an invisible frame keeping the graphic's box), which LibreOffice draws
  as WordCraft does.

LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 23:24:24 +00:00
Claude 8d5485c8bd M5 #19: ODF interpretation profile (WordCraft / LibreOffice / custom)
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).

Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.

- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
  choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
  ROADMAP updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 20:54:05 +00:00
41d46625e0 DOCX: VML horizontal rules take their line (#3956)
* 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>
2026-11-02 10:55:20 -05:00
Fin WarmanandBrandon Thomas 4f2d0da04c Shapes: align soft-edge paint and caret opacity (#1600)
* 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>
2026-11-02 10:53:00 -05:00
Claude be2666da15 Gap 19: ODT padding without a border, unequal columns, column breaks
Found with LibreOffice's report templates and one-page résumé.

Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
  without a border; measured, 0.5 in of padding and no border moves the
  text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
  `Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
  setting, now renamed `Settings::odf_layout` to cover all of
  LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
  Word documents are unchanged.

Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
  and sections) were made equal. They now keep their widths and are
  written back as shares. The writer leaves out `fo:column-gap` beside
  them, because LibreOffice ignores a page's column widths when a gap is
  stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
  with Word's column break, written back on the paragraph.

The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document gets worse.

gaps.md row 19 and the ROADMAP log are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 15:39:32 +00:00
Claude 959770fa13 Gap 19: ODT lines of only pictures are as tall as the pictures
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.

- A new document setting, `Settings::object_lines_fit_objects`, makes
  the layout size such lines by the pictures alone. The ODT reader turns
  it on, so other documents keep Word's layout. An equation or any text
  on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
  Letter page with 1 inch margins. It now uses the region's paper with
  LibreOffice's 2 cm margins.

WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 14:51:51 +00:00
Claude 8f65b867c0 Gap 19: ODT tables fit the margins and keep their spacing; header spacing kept
Found with LibreOffice's report templates.

Tables:
- A table filling the margins (`table:align="margins"`) but wider than
  them ran off the page. It is now fitted to the text width, its columns
  in proportion. Checked in LibreOffice, which fits such a table with or
  without relative column widths; the old test's claim otherwise is
  corrected.
- A table's own space above and below (`fo:margin-top` and
  `fo:margin-bottom` on the table) was dropped. It is now read into new
  `TableProps::space_before`/`space_after`, laid out (added to the
  spacing of the paragraphs around it, as LibreOffice does) and written
  back.

Headers and footers:
- A header taller than its band lost its spacing from the body, moving
  the text up. LibreOffice keeps the spacing below the content unless it
  is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
  `SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
  past a tall header or footer, and the writer writes it back. Word keeps
  no gap, so DOCX documents are unchanged.

Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 13:38:36 +00:00
Claude 5cdf2608bf Gap 19: ODT tables fixed layout and starting a page; a page break before a table's first row moves it
- A table starting a page (`fo:break-before="page"` on the table,
  LibreOffice's agenda minutes) didn't. Its first paragraph now has a
  page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
  now moves the whole table to the next page, as Word does. It was
  ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
  LibreOffice's columns never fit their content.

LibreOffice's 6orange agenda now round-trips with the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 10:08:59 +00:00
Hamza NaveedandBrandon Thomas ee8622ddb3 Count the runs proofing text walks, instead of timing it (#5185) (#5190)
* 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>
2026-11-02 04:55:59 -05:00
Claude 078945e10c Emphasis marks: modelled, drawn, set, and kept in DOCX and ODT
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
  to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
  is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
  it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
  `w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
  disc the dot, a dot below the under-dot.

Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
  already draws nothing for SET, so its result stays as it was, as
  `ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 09:46:23 +00:00
8a191fbf11 Shapes: arrowheads keep their kind and size and are drawn at Word's sizes (#5192)
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>
2026-11-01 14:46:16 -05:00
499fc6289e Columns: the line between them is drawn for selected text, as tall as the section's text (#5178) (#5183)
* #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>
2026-11-01 14:39:35 -05:00
935aaf7147 Layout tests: compare pairs of layouts drawn with the same faces (#5167)
The numeral and automatic-space tests compare two layouts of one text. Tests
running alongside share the font database, and one loading a font between the
two layouts can change the face a character falls back to: U+06DD drew in
Segoe UI in one layout and Noto Sans Arabic in the other on Windows, failing
arabic_mode_keeps_arabic_indic_digits_as_typed and
context_mode_follows_run_direction now and then, and
spaces_punctuation_hangul_and_latin_alone_get_nothing failed the same way. The
pairs are now laid out again until both use the same faces.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:36:14 -05:00
a64d9748f2 Tables: a row isn't split to leave a picture cell empty beside its text; it moves on whole, as in Word (#4272) (#5161)
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>
2026-11-01 14:23:01 -05:00
c395760959 Tables: a middle or bottom cell of a row split across pages lines up on the first page (#5145)
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>
2026-11-01 14:21:25 -05:00
6ab1023ed6 Layout: tab leader dots spaced and placed as in Word (#5144)
* 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>
2026-11-01 14:19:41 -05:00
VoiceLessQandBrandon Thomas 4a3586c686 Paragraph side borders lie outside the text (#5036) (#5088)
* #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>
2026-11-01 14:14:59 -05:00
Fin WarmanandBrandon Thomas e5f2885ecc Use the body boundary for header margin anchors (#2485)
* 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>
2026-11-01 14:11:56 -05:00
Claude b87c80cdc7 Layout: text in a turned text box turns with it
A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.

PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.

Clicks and the caret inside a turned box still go by its upright text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:42:07 +00:00
Claude 45b852ad4a Layout: in-line drawings move with their run's raise or lowering
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
  the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.

A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:24:15 +00:00
Claude f217aafa53 Layout: a sloped line's arrowheads sit at its ends
The renderers draw a line from its frame's top left to its bottom right
(DrawingML's `line`; flips turn the frame), but the arrowhead code and
`end_direction` assumed a line from the bottom left to the top right, so
a sloped line's arrowheads were drawn at the other diagonal's corners,
apart from the line. Horizontal and vertical lines hid it. The
arrowheads now start from the line's own ends, pointing along it.

Regression test: `tests::a_lines_arrowheads_sit_at_its_ends`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 07:15:27 +00:00
Claude e15a86bd55 Gap 19: ODT text box frames keep their line, fill, padding and fit
A text box frame's line (colour and width), fill and padding are read
from its graphic style and that style's parents (LibreOffice's Frame
style included) instead of a fixed thin black line on white, and written
back. ODF's defaults (no line, fill or padding) apply where the styles
say nothing. A frame without a height, or with a `fo:min-height` on its
text box, opens as a box that resizes to fit its text and is saved with
a least height again. A LibreOffice text-box document was 1,117 px apart
after a round trip; now 736.

Layout: a floating text box that resizes to fit its text now takes its
text's height (drawn and wrapped around), as inline ones already did;
before, it kept its stored height (an ODT frame without one showed as a
1 pt line with the text spilling over the paragraph).

A formula sub-document's content.xml stays a bare `math:math` root, as
ODF 1.3 Part 3 §2.2.9 B requires (pinned by a test), and the reader now
skips a wrapping `office:math` some writers add instead of taking it for
the MathML root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 03:34:12 +00:00
a4bcf05e11 Layout: a kept heading goes over with a three-line paragraph that widow control moves whole (#4991)
Keep with next asked for two lines of the paragraph after a kept heading under widow/orphan
control. A three-line paragraph cannot split at all there (two lines and a widow, or an orphan
and two), so it moved over whole and left the heading alone at the foot of the page. Word moves
the heading with it: ask for all three lines in that case.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:49:46 -04:00
ReesandBrandon Thomas a4b1682b41 Index laid-out objects during connector rerouting (#3631) (#4622)
* #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>
2026-10-30 00:13:32 -04:00