422 Commits
Author SHA1 Message Date
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
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
2f78615291 #4720: Chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes (#4726)
Chart and diagram text was shaped in one face, the document's body font,
so Chinese legends and axis labels showed boxes while body text was fine.
Characters the face lacks now go to a fallback face, one face per run
when one covers it all, as in body text; a bold fallback face without a
bold style is emboldened.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 00:12:55 -04:00
Fin WarmanandBrandon Thomas f658c872a2 Tables: share spanning minima by preferred widths (#1883)
* Tables: share span minima by preferred widths (#1883)

* 1222: Match nested preferred span weights

* docs: word the #1883 layout-parity entries

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:40:41 -04:00
Dylan M. Taylor 937c7e532d Refactor layout for strict Clippy checks 2026-10-28 01:06:48 +00:00
aiongg 4b8b06fe03 Merge pull request #4602 from Silvertree2010/perf/export-speed
Speed up PDF export by about 24 %
2026-10-27 13:41:24 -04:00
aiongg f8e327b068 Merge pull request #3936 from benandez/fix/3907-section-marks
#3907: continuous sections running off a page go onto the next; empty section marks in a row stack to the page bottom
2026-10-27 12:37:46 -04:00
NetterSilverandClaude Opus 5.5 ffa127c615 Skip per-character table lookups for plain text in layout and PDF export
Shortcuts that give the same answers as the lookups they skip:

- is_rtl: nothing before U+0590 is right to left, so Latin, Greek and
  Cyrillic text skips the bidi class search.
- strong_rtl and is_mark: ASCII letters are the only strong ASCII
  characters, and no ASCII character is a mark.
- cluster_starts: in ASCII text every character starts a grapheme except LF
  after CR, and no ASCII character joins a cluster.
- display lines: a glyph run's vectors and text are sized up front, and the
  symbol-font check is made once per run instead of once per cluster.
- PDF glyph runs: the baseline (the y most glyphs share) is only worked
  out for italic runs, the only ones that use it, and a run on one baseline
  returns it without counting.

Tests check each shortcut against the full lookup (every code point for the
bidi classes, every ASCII pair for the clusters).

PDF export of the export_bench document, interleaved with main: median
293 ms -> 241 ms (-18 % with the previous commit). The PDF bytes are
identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:39:09 +01:00
NetterSilverandClaude Opus 5.5 8cdc172171 Lay out the paragraph after a kept heading once
A paragraph with keep with next measures the paragraph after it before it is
placed (next_first_line). That measurement called layout_para directly, so
every paragraph following a heading was laid out twice in each layout pass:
once to measure it and once, through the cache, when it was placed.

The measurement now goes through the layout cache under the key
para_labelled uses (Ctx::para_ahead) when its inputs are the same: no
proofing, no typed word, not inside a text box, no inline text boxes that
resize, the body's tracked-deletion view. Otherwise it is laid out on its
own as before. It counts nothing (list labels, equation numbers and the
page-total flag are restored), so pagination is unchanged.

Adds crates/engine/examples/export_bench.rs, which saves a long generated
document (the sample document 200 times, 259 pages) through io::save_bytes
and prints wall times:

  cargo run --release -p wordcraft-engine --example export_bench -- pdf 200 9

PDF export on that document, interleaved with main (Ryzen, 16 threads):
median 286 ms -> 256 ms (-10.6 %). The PDF bytes are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:39:09 +01:00
Martin BallandClaude Opus 5.5 1b485e1f90 #3907: CodeRabbit — a kept paragraph is measured against the next page's body (a different first-page header)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:24:50 +00:00
Martin BallandClaude Opus 5.5 751eb4b204 #3907: CodeRabbit — a stacked mark past the page bottom draws no line number either
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:39:26 +00:00
Martin BallandClaude Opus 5.5 5225569d44 #3907: review — an empty mark taking no room, or stacked past the page bottom, draws no list label; an empty mark after a text paragraph closing a section stays quiet
dpkruse's Genuine Steps Certificate: a bulleted text paragraph closes a
two-column continuous section, then an empty bulleted one closes another.
Word draws no bullet for the empty one; it is now quiet (takes no room) and
draws no label, still counted. A stacked mark that doesn't fit keeps its
line for the caret but draws nothing below the margin (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:04:05 +00:00
Martin BallandClaude Opus 5.5 f2fe7654e9 #3907: review — a kept paragraph low in a continuous section moves to the next page; a page break before a stacked mark keeps the new page's bottom
CodeRabbit (local): keep-lines compared its height with the room below the
mid-page section start, so a paragraph that fits on a new page split instead.
It now compares with the next column, or the next page's body when this is the
last column. A stacked mark whose page break before starts a page no longer
gets the previous page's bottom back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:56:44 +00:00
Martin BallandClaude Opus 5.5 7757828295 #3907: continuous sections running off a page go onto the next; empty marks in a row stack to the page bottom
A continuous section moves the column top down below the text above. at_top()
then held at every new section start, so a paragraph that did not fit was
placed anyway, as on an empty page. A run of one-line continuous sections ran
off the bottom of page 1 (100 of them reached 2,480 pt down a 792 pt page).
The overflow checks for paragraphs now use fresh(): at the top with nothing
above on this page. at_top() still drives the spacing rules, unchanged.

Empty paragraphs that close a continuous section right after another
section's mark took no room. Measured in Word on synthetic files ("Start", N
such marks, "End"): 10 put "End" about 9 cm down page 1; 50, 100 and 200 all
put it on the first line of page 2, the same with 24 pt marks or no paragraph
spacing. So each takes its line while it fits, then stays at the page's
bottom. A mark right after text stays quiet, as measured before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:56:35 +00:00
Martin BallandClaude Opus 5.5 04ebc4ed42 #3995: Thai one-face fallback tests skip unless a Thai font with Latin is installed
Noto Sans Thai and its siblings have no Latin letters or digits, so on a
machine whose only Thai fonts are Noto no face covers the test run and
fallback_for_run rightly returns None. The guard now asks whether the Thai
substitute covers the whole run, which doesn't go through fallback_for_run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:52:32 +00:00
aiongg 5113be5569 refactor(layout): split math.rs into math/ 2026-10-26 21:16:49 +00:00
aiongg 5a84dd2f4f refactor(layout): split display.rs into display/ 2026-10-26 21:16:49 +00:00
aiongg 4f74183576 refactor(layout): split para.rs into para/ 2026-10-26 21:16:49 +00:00
aiongg 15f50c96df refactor(layout): split lib.rs into modules 2026-10-26 21:16:49 +00:00
aiongg fe73d141fe refactor(layout): move tests_*.rs files to tests/ 2026-10-26 20:54:02 +00:00
aiongg 3b84c637ea refactor(layout): move math tests to math/tests.rs 2026-10-26 20:54:02 +00:00
Dylan M. Taylor d859c673c1 Sonar: reduce cognitive complexity in highlight, review and outline commands 2026-10-26 16:01:36 -04:00
Martin BallandClaude Opus 5.5 49c2f61373 #3866: tests_linear imports Page and Rgb (main's layout tests didn't compile after #4392)
#4392's tests used Page and Rgb, which other changes merged around it no longer
brought into scope, so main's CI gate fails with E0425/E0422 in tests_linear.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
2026-10-26 08:08:46 +00:00
Dylan M. Taylor 2c99a42780 Merge pull request #4392 from benandez/fix/3866-followup
#3866 follow-up: PDF export draws items with authors and cell clips built once per page; endnote selection linear
2026-10-26 01:31:58 -04:00
Martin BallandClaude Opus 5.5 b94821ea16 #3866: review — count a step per page walked in the first-page index, so the hinted-page test sees an eager build
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 04:05:47 +00:00
Martin BallandClaude Opus 5.5 e71c9ce31d #3866: review — the first-page index is built only when the hinted page doesn't show the paragraph
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 04:01:47 +00:00
Martin BallandClaude Opus 5.5 ecdc90cdc4 #3866: review — port the clipped-cell lookup test (nesting, first in page order, other story/row) to the layout crate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 03:15:35 +00:00
Martin BallandClaude Opus 5.5 e9dd252096 #3866: review — PDF export builds the revision authors and clipped cells once per page; part_pieces finds a paragraph's first page from one index; grid_cols once; balloon colour test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 02:57:08 +00:00
Dylan M. Taylor a95a570edc Merge branch 'main' into sonar/newcode-lints-1 2026-10-25 22:47:05 -04:00
Dylan M. Taylor 66601038df tests(layout): build the nbsp test document without struct-update syntax (#4380) 2026-10-25 21:52:01 -04:00
Dylan M. Taylor 4bb3679e41 Sonar: use PoisonError::into_inner, explicit test imports, [[ in import-cert
- proof: pass PoisonError::into_inner instead of an identity closure (S1612).
- layout tests: replace glob super::* imports with explicit names (S2208).
- packaging/macos/import-cert.sh: use [[ for the status test (S7688).
2026-10-25 21:28:59 -04:00
Martin BallandClaude Sonnet 5.5 3ee1978910 #3692: rebase onto main — rustfmt after the merge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 3cffe22c00 #3692: review — picture outline cache keys stay unique: the per-pass one documented, the lasting one holds its bytes' allocation
Alphas keys decoded pictures by their bytes' address and length, which is
sound only while the document is borrowed for the layout pass; that is now
said where the key is. LayoutCache's outlines outlive the documents they were
made for and use the same key, so each entry now keeps a Weak reference to
the bytes: their allocation can't be freed and the address reused by another
picture of the same length while the entry is kept. MAX_FLOW_WIDTHS spells
out which layout a box past the 16-width cap reuses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 1ae38ec2be #3692: review — stacked linked boxes share one layout; past the cap a box takes one in its own direction
Stacked text is laid out at STACK_WIDTH whatever the frame, so its flow key now is too.
The nearest-layout fallback only picks layouts of the box's TextVert; the first box in a
direction is laid out (at most a few more layouts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 0f9b926497 #3692: review — a picture's transparency is kept for the whole layout, not just the last one decoded
Crops alternating between pictures decoded each again and again, spending the budget on
pictures that fit in it. Every picture charged to the budget keeps its alpha (bounded by
the same 2^28-pixel budget).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 32e4900bc5 #3692: a linked chain past 16 widths reuses the nearest; opaque pictures and budget refusals don't cost a later picture its outline
Ultrareview finding 6: boxes past a chain's 16th distinct width (compared
by exact float equality) showed no text, which resized boxes hit easily.
Widths are now told apart to a twip, and past the cap a box takes the
nearest layout already made (same direction, widest that fits, else the
narrowest). Work stays bounded at 16 story layouts.

Ultrareview finding 7 (also raised by CodeRabbit): opaque non-JPEG
pictures were decoded and charged to the per-layout pixel budget though
they give no outline, and a budget refusal was cached in LayoutCache as a
permanent "no outline". The header (read via into_decoder, PNG tRNS
included) now says whether a picture can be transparent before anything is
charged; Alphas::outline reports OverBudget distinctly from Ok(None), and
Ctx::wrap_outline (via picture_outline) doesn't keep a refusal in
LayoutCache::outlines. Within one layout a refused picture is remembered
so its header isn't re-read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 b578be0459 Audit finding 38: the outline cache keys on media and crop, but working
out any crop's outline decoded the whole shared picture again, so many
small picture references with distinct crops multiplied full-image
decodes without repeating the encoded image. Layout now keeps the last
picture's transparency (one alpha byte per pixel) and reuses it for
every crop of it, and charges each decode's pixels against
MAX_DECODED_PIXELS (2^28) per layout; past it pictures wrap as their
rectangles. Tests: tests_budget::a_picture_cropped_many_ways_is_decoded_once,
tests_budget::decoding_pictures_stops_at_the_layouts_budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:44 -04:00
Martin BallandClaude Opus 5.5 96579e6b4e #3692: repeated table header rows are capped by items, not rows
Audit finding 11: place_table cloned every item of the header rows onto
each page a table spills onto, but its cap (200 000) counted rows. An
exact-height header row keeps all its overflowing paragraphs without
growing, so one header row of thousands of items over hundreds of
pages placed millions of copies. The cap now counts placed items
(MAX_HEADER_ITEMS = 500 000, a row costing at least one) and the
header set repeats whole or not at all; past it later pages carry no
header rows and pagination still finishes. Test:
tests_budget::repeated_header_items_are_capped_across_the_table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Opus 5.5 8efba59b35 #3692: a linked text-box chain lays its story out at most 16 widths
Audit finding 7: flow() laid the head box's whole story out again at
every distinct box line length in the chain and kept each layout while
splitting, so a chain of many one-line boxes of different widths
multiplied the story's layout work and memory by the chain length.
Past MAX_FLOW_WIDTHS (16) distinct lengths the text stops: the box that
would need another layout and the rest of the chain show nothing, as
overflow past the last box is hidden. Work is now at most 16x the
story. Test: tests_budget::a_linked_chain_lays_its_story_out_at_a_bounded_number_of_widths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Opus 5.5 4963e30eed #3692: a row's gridAfter is bounded like the DOCX reader's
Audit finding 35: Table::cols() added a deserialized row's grid_after
(u32) unchecked, and layout_table sized its column vectors from it, so a
small native JSON document could ask for a 16 GB grid (and overflow the
sum on 32-bit wasm). Row::grid_after() now clamps to MAX_COLS as
first_grid() does and the importer does; cols() sums with saturating
adds; layout reads the bounded value. Test:
tests_budget::a_huge_grid_after_is_bounded_like_the_docx_readers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Sonnet 5.5 1fa7383264 #3692: rebase onto main — drop the Ctx sections list the linked header/footer index replaces
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 00:19:17 +00:00
Martin BallandClaude Sonnet 5.5 17e648bde7 #3692: rebase onto main — sort the merged test module declarations (rustfmt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 00:19:17 +00:00