Word drops the space before of an empty paragraph that follows a document's
leading floating table: its PDFs of docxide-pdf's leading-empty-before-float-0
and -12 put the text after the empty anchor (beside the table) at the same
place for 0 pt and 12 pt before.
Flow::gap_before (crates/layout/src/flow.rs) kept it: a floating table
takes no room in the flow and clears `pb.prev`, so the paragraph after it
was laid out like a document's first paragraph, with its full space before.
It now drops it for body block 1 when block 0 was placed floating on this
page (the page's float_tables), the page is fresh and the paragraph holds
nothing but markers (bookmarks, comment and permission ranges). It
doesn't check that the paragraph sits beside the table, so below a
full-width leading float an empty paragraph loses its space before too;
that case is untested. Later pages and columns, and paragraphs with text,
keep their space before.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A row is split across pages from its flattened items, so a row of a nested
table marked w:cantSplit was cut between its lines like any other text in
the outer cell. Word leaves such a row whole: the outer row moves to the
next page whole when that row comes first in it (docxide-pdf
nested-first-row-prefix300/330-cant1), and otherwise splits above it
(inferred).
table_flow now moves the cut up to just below the top of the highest nested
Can't Split row it runs through (nested_keep_cut): the outer row's bottom
band below that top, so the text and nested rows ending there stay on the
page. A row runs through the cut when it ends below the room left for text,
as in the split itself; one walk over the rows from the lowest top up moves
the cut until none does. A vertically merged cell doesn't stretch its row's
band, and a cell aligned to the middle or bottom is measured from its top, as
the split runs it. This applies to the first part of a row and to the kept-chain and
header-row checks. On a fresh page, where moving on gains nothing, a nested
row taller than the room still splits at the room rather than running into
the bottom margin.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Layout: look at the text after a decimal tab once, not once per cluster
Text after a decimal tab is shifted after each of its clusters, and each
shift walked the text again from the tab to find its first '.'. A long run
without a '.' (soft hyphens take no room, so it stays on one line) made a
single paragraph's line breaking quadratic. The scan now carries on from the
cluster it reached.
* address review: layout progress doc for decimal-tab scan
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Arabic letters join across a change of formatting (#4093)
Each run of a paragraph was shaped on its own, so a letter at a run's edge
took the form of a word's start or end. A coloured letter stood apart from
its word, and a word edited with Track Changes on read with a space in it
(ال ذكاء).
Runs are now shaped with the text shown on either side of them, through the
shaper's pre- and post-context (fonts::ShapeContext). Runs left out of the
layout (hidden text, deletions in a final view) are skipped, so letters join
what is on the page. Only paragraphs with a joining script do the work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
* #4093 review: no letter joins a drop cap formatted on its own
A drop cap letter in a run of its own (coloured, say) was still passed to
the next run as the text before it, so that run's first letter joined the
drop cap, which is drawn apart from the word. The shown text now has a floor
past the drop cap, and no run takes context from before it. The drop cap
test also covers the letter in a run of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2QwHfTULQtWYgVEXk
---------
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #4985: a numFmt none list level shows its literal text
Word labels a paragraph whose level has numFmt none and lvlText RESPONSE:
with RESPONSE:. Counters::next_label returned no label for any none level;
now only a none level of placeholders shows nothing. Its own %n stays
empty, and it still doesn't count. Numbering::shows_label agrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: a "none" list level counts, and only its own number is empty
Taken from MeHail-02's numbering-literal-none branch (46405e66), checked in Word
for Windows: a none level is an ordinary level whose own number reads as
nothing. It counts and restarts the levels below it, stays empty in legal
numbering, and an empty label still tabs the text to the indent unless it
already ends there (the Chapter 1 list's Heading 2 stays at the margin).
Co-Authored-By: MeHail <mihailsmetanin15@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF in full context keeps a none-level heading's parent
Review: a none level now has a label, so its headings reach the style index,
which leaves the none level out of the levels it keeps; \w and \r took the last
kept level for the heading's own and dropped the parent. Levels now carry their
index, and the levels above are picked by it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF number switches on a none-level heading
Review: in full context an empty label of its own added a trailing period (3.),
and the suppress switch gave a none level's label (Note), which holds no number,
back whole. An empty own part is left out, and a none level's label with no
number is suppressed to nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF \s and \t on none-level labels
Review: \s took a none level's label (Note) for a chapter number; the heading's
count stands in, as for an unnumbered heading. \t matched any ancestor's number
inside a word (the a of Chapter); only the numbers the label's template shows
count now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF takes a label's numbers by their spans
Review: \s took the last word of a label (Response of "3 Response") and fell
back on the heading count for a none level showing its parent's number (%1);
\t kept text attached to a number (Section3). A label's numbers are now the
spans of the values its template shows, with their delimiters (a letter number
only standing apart from other letters); \s takes the last of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: tests edit the numbering through Arc::make_mut after merging main
Main shares the numbering between undo steps (#2694), so `Document::numbering` is an
`Arc<Numbering>`; the tests this PR adds wrote to it directly and no longer built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: MeHail <mihailsmetanin15@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* fix(fields): quoted-argument backslash escapes and body PAGE in a cardinal-text section
A backslash inside a quoted field argument makes the next character literal, so TITLE "x \* Upper" shows x * Upper as Word does. A body PAGE field now uses the section's page number format (cardinal text shows words), as headers and footers already did.
* Fields: PAGE in a body table follows the section number format; parity docs
* fmt
* docs: clarify quoted field escape rule
* Fix page fields in fitted text boxes
* Move fitted cache test after layout helpers
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
- 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)
Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
`<p> </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>
* #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>
* 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>
* 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>
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>
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
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