diff --git a/ROADMAP.md b/ROADMAP.md index b892e00e8..477dfb3cd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -172,6 +172,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-10-24: AutoCorrect: a word typed after a no-break space or an ideographic space is corrected as after a plain space (replacements, sentence capitals, ordinals and links). AutoCorrect left such a word as typed. - 2026-10-23: Table of contents: Update Table on a TOC set in columns (here Word's own two-column TOC, written by Word in Portuguese) keeps it in its columns (#3889). The page numbers stand at the column's right edge rather than the page's, so entries no longer run over the next column and push its page numbers off the page; the TOC styles Word stores under translated ids (`Sumrio1` for `toc 1`) are read as the TOC styles, so the entries keep their font, indents and spacing; and no blank line tops the first column. Update, save and reopen keep it so. The same keeps a Portuguese Word's TOC formatted as its TOC styles say through Update Table (#3891). - 2026-10-22: Markdown inline code replaces each line ending with a space before export, including code in lists and table cells. A following heading, list marker or HTML tag stays literal code (#3738). +- 2026-10-23: Layout: paragraphs that each start a continuous section now go onto new pages. A run of them ran off the bottom of the first page, because each new section counted as the top of a page. Empty section-break paragraphs after the first in a row take a line each until the page is full and then stay at its bottom, so the text after them starts the next page, as measured in Word (#3907). - 2026-10-22: Headers and footers: while editing the header, a single click in the footer edits the footer with the caret where clicked (and the reverse, and another page's own header or footer). The click went to the header being edited and the view scrolled back up to it, so a double-click on the footer rarely landed unless both were on screen. - 2026-10-24: MCP `type_text` with `paragraphs: true` is one command (#3973): it was a `text.newParagraph` and a `text.insert` per line, each snapshotting the whole document for Undo, so 10,000 lines took 2.8 s (doubling the lines cost 2.4–3.2×) and left an Undo step per line; now they take ~10 ms (linear) and one Undo takes them all back. A CRLF is one break, not two, and the breaks no longer run AutoCorrect (the per-line way capitalised every line but the last). Typed line breaks are the Enter key, so in a one-line content control, where Enter does nothing, they no longer split it into paragraphs. - 2026-10-21: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459). @@ -678,6 +679,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps | 2026-10-20 | minor | Restrict Editing: a file command that changes the open document is checked like any other — the privacy options, the properties and the inspector's removals are refused on a protected document, and restoring a stored version no longer lifts the protection or drops its password | | 2026-10-17 | trivial | Progress log: disabled-control reasons translated (#2678) | | 2026-10-22 | trivial | Markdown code spans normalize line endings before export, keeping block markers literal (#3738); format readiness unchanged | +| 2026-10-23 | trivial | Progress log: continuous sections paginate, #3907 | | 2026-10-22 | trivial | Progress log: a click in the footer while editing the header moves there | | 2026-10-18 | trivial | Explicit New/Open is not interrupted by startup recovery discovery; older copies stay available and Recover follows the interface theme. | | 2026-10-17 | trivial | Progress log: Home › Underline drop-down (#2738) | diff --git a/crates/layout/src/ctx.rs b/crates/layout/src/ctx.rs index b17d68277..6505b6985 100644 --- a/crates/layout/src/ctx.rs +++ b/crates/layout/src/ctx.rs @@ -43,6 +43,9 @@ pub(crate) struct Ctx<'a> { /// Empty body paragraphs with hidden marks or continuous section breaks, in order: /// they take no room (see [`place_para`]). pub(crate) quiet_marks: Vec, + /// Empty paragraphs closing a continuous section right after another section's mark, in + /// order: each takes its line while it fits, then stays at the page's bottom (#3907). + pub(crate) stacked_marks: Vec, /// Whether even pages are mirrored (mirrored margins, on pages): see [`mirror_even_pages`]. pub(crate) mirror: bool, /// Cache-key hashes of the table-style text formatting cells were laid out with diff --git a/crates/layout/src/flow.rs b/crates/layout/src/flow.rs index f52cd5ccd..5eeab77fc 100644 --- a/crates/layout/src/flow.rs +++ b/crates/layout/src/flow.rs @@ -36,6 +36,19 @@ pub(crate) fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, blo } return; } + if !pb.web && ctx.stacked_marks.binary_search(&block).is_ok() { + // Measured in Word (#3907): each takes its line while it fits; past the bottom it stays + // there instead of starting a page, so the text after a long run starts the next page. + let (y, prev, bottom, content_bottom, at) = (pb.y, pb.prev.clone(), pb.bottom, pb.content_bottom, (pb.pages.len(), pb.col)); + pb.bottom = f32::MAX / 4.0; + place_para_in_flow(ctx, pb, p, block, body_top); + pb.bottom = bottom; + if (pb.pages.len(), pb.col) == at && pb.y > bottom + 0.01 { + // At the bottom: nothing more fits on the page, even a line smaller than the mark's. + (pb.y, pb.prev, pb.content_bottom) = (bottom.max(y), prev, content_bottom); + } + return; + } place_para_in_flow(ctx, pb, p, block, body_top); } @@ -164,7 +177,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: let need = height + if pl.rp.keep_next { next_first_line(ctx, block, width, 0, &mut ctx.counters.clone()) } else { 0.0 }; // (Against the whole column: balanced columns grow to hold it.) // With the 0.01 pt of slack the other fit checks take. - if pb.y + need > pb.bottom + 0.01 && !pb.at_top() && need <= pb.uncapped_bottom() - pb.top { + if pb.y + need > pb.bottom + 0.01 && !pb.fresh() && need <= pb.uncapped_bottom() - pb.top { pb.advance(block, body_top); pb.y += pl.rp.space_before.min(0.0); applied_before = pl.rp.space_before.min(0.0); @@ -324,7 +337,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: // An empty paragraph closing a section before a new page: measured in Word, it stays // on a full page instead of starting one of its own. let closing_mark = pb.next_starts_page && p.section.is_some() && p.text.is_empty(); - if over && l1 == l0 && !pb.at_top() && !split && !only_break && !closing_mark { + if over && l1 == l0 && !pb.fresh() && !split && !only_break && !closing_mark { break; } new_notes.extend(line_notes); @@ -343,11 +356,11 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: } // Widow/orphan control (2-line minimum at either end). if forced_break.is_none() && l1 < n && pl.rp.widow_control && n >= 2 { - if l1 - l0 == 1 && l0 == 0 && !pb.at_top() { + if l1 - l0 == 1 && l0 == 0 && !pb.fresh() { l1 = l0; // orphan: move the first line to the next page } else if n - l1 == 1 && (l1 - l0 >= 3 || (l0 > 0 && l1 - l0 >= 2)) { l1 -= 1; // widow: take one more line along - } else if n - l1 == 1 && l0 == 0 && l1 - l0 == 2 && !pb.at_top() { + } else if n - l1 == 1 && l0 == 0 && l1 - l0 == 2 && !pb.fresh() { // Neither split of a three-line paragraph is allowed (two and a widow, or an // orphan and two): it moves whole, as in Word. l1 = l0; @@ -358,7 +371,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: l1 -= 1; } if l1 == l0 { - if pb.at_top() { + if pb.fresh() { l1 = l0 + 1; // can't fit even one line on an empty page: overflow } else { pb.advance(block, body_top); diff --git a/crates/layout/src/page.rs b/crates/layout/src/page.rs index 6ff5984a5..80d50487c 100644 --- a/crates/layout/src/page.rs +++ b/crates/layout/src/page.rs @@ -14,6 +14,9 @@ pub(crate) struct PageBuilder<'a> { pub(crate) cols: Vec<(f32, f32)>, pub(crate) y: f32, pub(crate) top: f32, + /// The body's top on this page: `top` moves down below the text when a continuous section + /// starts mid-page. + pub(crate) page_top: f32, pub(crate) bottom: f32, pub(crate) number: u32, pub(crate) web: bool, @@ -317,6 +320,7 @@ impl PageBuilder<'_> { self.col = 0; self.cols = s.column_boxes(); self.top = body_top; + self.page_top = body_top; self.bottom = if self.web { f32::MAX / 8.0 } else { bottom }; self.orig_bottom = self.bottom; self.y = self.top; @@ -356,6 +360,12 @@ impl PageBuilder<'_> { pub(crate) fn at_top(&self) -> bool { self.y <= self.top + 0.01 } + /// At the top with nothing above on this page, so moving on gains no room. A continuous + /// section starting mid-page is at its top but not fresh: what doesn't fit there goes to the + /// next page (#3907). + pub(crate) fn fresh(&self) -> bool { + self.at_top() && self.top <= self.page_top + 0.01 + } /// Move to the next column or page. pub(crate) fn advance(&mut self, block: usize, body_top: f32) { if self.col + 1 < self.cols.len() { diff --git a/crates/layout/src/pass.rs b/crates/layout/src/pass.rs index 68fa1ab04..b4cb7b880 100644 --- a/crates/layout/src/pass.rs +++ b/crates/layout/src/pass.rs @@ -64,6 +64,7 @@ pub(crate) fn layout_pass( track_totals: true, pct_base: None, quiet_marks: Vec::new(), + stacked_marks: Vec::new(), mirror: false, table_keys: Vec::new(), fitted: HashMap::new(), @@ -80,12 +81,13 @@ pub(crate) fn layout_pass( let read = opts.view == ViewMode::Read; let reflow = web || read; ctx.mirror = doc.settings.mirror_margins && !reflow; - ctx.quiet_marks = sections + // Only the first of a run of such marks is quiet; the rest are stacked (Word, #3907). + (ctx.stacked_marks, ctx.quiet_marks) = sections .windows(2) .filter(|w| w[1].1.start == SectionStart::Continuous) .map(|w| w[0].0) .filter(|end| doc.body.get(*end).and_then(|b| b.as_para()).is_some_and(|p| p.text.is_empty())) - .collect(); + .partition(|end| end.checked_sub(1).and_then(|i| doc.body.get(i)).and_then(|b| b.as_para()).is_some_and(|p| p.section.is_some())); if !opts.show_hidden { ctx.quiet_marks.extend( doc.body @@ -130,6 +132,7 @@ pub(crate) fn layout_pass( cols: Vec::new(), y: 0.0, top: 0.0, + page_top: 0.0, bottom: 0.0, number: 0, web, diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index e38133a41..4a5c188f5 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -8667,6 +8667,40 @@ fn an_empty_continuous_section_mark_takes_no_room() { assert_eq!(new_page[1].0, 1, "a next-page section starts a page: {new_page:?}"); } +/// Empty marks closing continuous sections one after another each keep their line while it +/// fits; only one right after text is quiet. Past the page's bottom they stay there, so the text +/// after the run starts the next page. Measured in Word (#3907): "Start", N such marks, "End": +/// 10 put "End" about 9 cm down page 1; 50, 100 and 200 all put it on page 2's first line. +#[test] +fn a_run_of_empty_continuous_section_marks_keeps_a_line_each() { + use wordcraft_doc::section::{SectionProps, SectionStart}; + let tops_sized = |marks: usize, size: Option| { + let mut d = Document::from_text(&format!("Start\n{}End", "\n".repeat(marks))); + for i in 1..=marks { + let p = d.para_mut(StoryRef::Body, &Path::top(i)).unwrap(); + p.section = Some(Box::new(SectionProps { start: SectionStart::Continuous, ..Default::default() })); + p.mark.size = size; + } + d.last_section.start = SectionStart::Continuous; + let l = lay(&d); + let (start, end) = (l.caret(&Pos::body(0, 0)).unwrap(), l.caret(&Pos::body(marks + 1, 0)).unwrap()); + (end.page, end.top - start.top) + }; + let tops = |marks: usize| tops_sized(marks, None); + let (_, one) = tops(1); + let (_, three) = tops(3); + let line = three - one; + assert!(line > 0.0, "marks after the first take room: one {one}, three {three}"); + let (_, five) = tops(5); + assert!((five - three - line).abs() < 0.15, "a line per extra mark: three {three}, five {five}"); + let (fifty, two_hundred) = (tops(50), tops(200)); + assert_eq!(fifty.0, 1, "50 marks fill page 1, End starts page 2: {fifty:?}, line {line}"); + assert!((fifty.1 - two_hundred.1).abs() < 0.01 && two_hundred.0 == 1, "more marks stay on page 1: {fifty:?} {two_hundred:?}"); + // Large marks too: the run ends at the bottom, so a smaller line after it still starts page 2. + let big = tops_sized(100, Some(24.0)); + assert!(big.0 == 1 && (big.1 - fifty.1).abs() < 0.01, "24 pt marks: {big:?}, small: {fifty:?}"); +} + /// A floating object positioned relative to its paragraph is measured from the paragraph's start /// above its space before, measured in Word for Windows (`plan/word` R22, 10 cases: 0, 10 and 20 /// pt before, offsets 0 and −7.2 pt; the previous paragraph's space after still counts). The SARP @@ -11646,3 +11680,23 @@ fn a_row_leaves_room_for_the_border_below_it() { } } } + +/// Sections starting continuously one after another still go onto new pages: a paragraph that +/// doesn't fit below the text above it moves on rather than running off the page (#3907). A +/// hundred one-line sections lay out like a hundred plain paragraphs. +#[test] +fn continuous_sections_running_off_a_page_go_onto_the_next() { + use wordcraft_doc::section::{SectionProps, SectionStart}; + let marks = 100; + let text = format!("Start\n{}End", "x\n".repeat(marks)); + let mut d = Document::from_text(&text); + for i in 1..=marks { + d.para_mut(StoryRef::Body, &Path::top(i)).unwrap().section = + Some(Box::new(SectionProps { start: SectionStart::Continuous, ..Default::default() })); + } + d.last_section.start = SectionStart::Continuous; + let end = lay(&d).caret(&Pos::body(marks + 1, 0)).unwrap(); + let plain = lay(&Document::from_text(&text)).caret(&Pos::body(marks + 1, 0)).unwrap(); + assert!(end.page > 0, "End past the first page: {end:?}"); + assert_eq!((end.page, (end.top * 10.0).round()), (plain.page, (plain.top * 10.0).round()), "sections {end:?}, plain {plain:?}"); +} diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 436c39fb8..d92e392e7 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -1,6 +1,6 @@ # Layout and pagination parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** trivial (a justified line ending in a manual line break is stretched to the margin unless `w:doNotExpandShiftReturn` is set, #3924; previously trivial: a no-break space is as wide as a space, #4192; previously trivial: a bookmark or other zero-width marker is no line-break opportunity, #4193; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: Preserve grapheme edit/wrap boundaries across independently drawn font/style pieces; Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: cu20we: outline caps; previously trivial: cu20we: box float paragraph origin; previously trivial: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: an empty paragraph whose mark is hidden shows nothing, not even its list number, which is still counted, #3982; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: symbol fonts keep their line height without the font; previously trivial: an equation-array row without `&` is centred, #2617; previously trivial: a superscript or subscript sizes its line at the full font size, without its raise or drop, as Word does; previously trivial: an object dragged near the bottom of a page lands where it is dropped, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: justified lines shrink their spaces by Word's measured limits; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: a click below a floating table goes to the text beside or after it, not to a cell of its last row, #2038; previously trivial: shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next page, #2048; previously trivial: triple borders draw three lines, #1740; previously trivial: the caret, clicks and selection edges fall between the letters of a ligature, #1974; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: VML header-cell floats always on the page, user-checked in Word for Mac, #1775; previously trivial: an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** trivial (continuous sections one after another go onto new pages instead of running off the page; empty marks after another section's mark take a line each until the page is full, then stay at its bottom, #3907; previously trivial: a justified line ending in a manual line break is stretched to the margin unless `w:doNotExpandShiftReturn` is set, #3924; previously trivial: a no-break space is as wide as a space, #4192; previously trivial: a bookmark or other zero-width marker is no line-break opportunity, #4193; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: Preserve grapheme edit/wrap boundaries across independently drawn font/style pieces; Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: cu20we: outline caps; previously trivial: cu20we: box float paragraph origin; previously trivial: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: an empty paragraph whose mark is hidden shows nothing, not even its list number, which is still counted, #3982; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: symbol fonts keep their line height without the font; previously trivial: an equation-array row without `&` is centred, #2617; previously trivial: a superscript or subscript sizes its line at the full font size, without its raise or drop, as Word does; previously trivial: an object dragged near the bottom of a page lands where it is dropped, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: justified lines shrink their spaces by Word's measured limits; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: a click below a floating table goes to the text beside or after it, not to a cell of its last row, #2038; previously trivial: shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next page, #2048; previously trivial: triple borders draw three lines, #1740; previously trivial: the caret, clicks and selection edges fall between the letters of a ligature, #1974; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: VML header-cell floats always on the page, user-checked in Word for Mac, #1775; previously trivial: an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 A document only "opens correctly" if its pages break where Word breaks them: the same lines, the same page count, footnotes and pictures on the same pages. This checklist covers line breaking, @@ -52,7 +52,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic | Caret, clicks and selection inside a ligature (fi, ffi, lam-alef) | ✅ | The glyph's advance is shared evenly among the letters' grapheme clusters, mirrored in right-to-left text; a grapheme cluster (a letter with a combining mark, an emoji ZWJ sequence, an Indic conjunct) is never split. `layout::tests::ligature`, `engine::tests::typing::right_arrow_moves_through_a_ligature_one_letter_at_a_time` (#1974); observed in Word for Mac 16.113.4 | — | | All Caps and Small Caps: the full capital of each letter, so ß draws as SS (ff as FF); the capitals drawn for one character are one cluster, so the caret, selection, hit testing and line breaking treat ß as one character | ✅ | `layout::tests::caps_draw_sharp_s_as_two_capitals_in_one_cluster` (#1581). The PDF text layer still reads the document's lowercase letters until #1473 | — | | Columns, column breaks, separators, balancing | ✅ | Columns balance on a section's last page when the next section starts continuous (also at the document end, which Word only balances when the document ends with a continuous break); not after next-page/even/odd breaks or on a page with a column break; keep-together, keep-with-next, widow/orphan control and unsplittable rows can leave them slightly uneven (#350) | — | -| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852). An otherwise empty section at page start retains its line (`empty_page_section_keeps_line`); a closing mark after text stays quiet. | 1–2 | +| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852). An otherwise empty section at page start retains its line (`empty_page_section_keeps_line`); a closing mark after text stays quiet, but one after another section's mark takes its line while it fits and then stays at the page's bottom, so the text after a long run starts the next page; a run of continuous sections with text goes onto new pages like plain text (#3907). | 1–2 | | Labels on page-break-only list paragraphs | ✅ | `layout::tests::break_only_list_has_no_label`: no bullet or number on the break, but the next number is still 3 after item 1 and a break. Plain empty items keep their labels. Synthetic DOCX observed through Adobe PDF conversion | — | | List labels after a leading page break | ✅ | `layout::tests::bidi::list_label_after_leading_page_break_uses_its_text_lines_alignment`: the number/bullet and text retain the no-break horizontal positions for start, centre, end and justified alignment, LTR/RTL, including text with a tab. The issue's synthetic DOCX exports with the number before the heading, matching the reporter's Word/LibreOffice reference (#2964); reference applications were not rerun locally | — | | List labels: number formats (decimal, letters, Russian letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `doc::section::tests::russian_letters_follow_word`, `layout::tests::chinese_counting_list_labels_and_their_font_runs`, `layout::tests::russian_letter_list_labels_draw_cyrillic`; a label mixing faces (digit + CJK comma) is drawn run by run (#534); Russian letters skip ё, й, ъ and ь and then double (аа, бб), as observed in Word. Hebrew, Hindi, Thai and Vietnamese formats show decimal; `arabicAlpha` and `arabicAbjad` draw Arabic letters, then double (أأ, بب) (`doc::section::tests::arabic_letters_count_in_both_orders`). The label stays attached to the start of the text when the paragraph is centred or aligned to the end, as in Word (#1159). | — | @@ -284,6 +284,7 @@ layout suite are pending; this is not a new Word/native-IME acceptance claim. | 2026-10-23 | trivial | A justified line ending in a manual line break is stretched to the right margin like a wrapped line, as Word does by default; with `w:doNotExpandShiftReturn` it keeps its natural spacing (#3924). `layout::tests::justified_line_ending_in_line_break_is_stretched` | | 2026-10-16 | trivial | Exact PostScript names resolve installed static faces without changing family lookup | | 2026-10-21 | trivial | Pin the font input of four geometric layout controls; assertions and renderer font policy unchanged. | +| 2026-10-23 | trivial | Continuous sections running off a page go onto the next; empty marks after a section mark take a line until the page is full, measured in Word (#3907) | | 2026-10-20 | trivial | A click below a floating table, outside its cells, goes to the paragraph beside or after it instead of a cell of its last row, as checked in Word (#2038) | | 2026-10-19 | trivial | A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, starts the next line with its text instead of ending the line (a TOC entry's page number keeps its leader); a left tab still past the right indent there takes no room (#1840) | | 2026-10-19 | trivial | Justified lines (compatibility mode 15) shrink their spaces to fit one more word by at most a quarter of the spaces' width (was a fifth) and by at most 0.355 of that word's own width with the space before it. Measured in Word for Mac with synthetic justified paragraphs: words of 2.7 to 80 pt, spaces of 3.3 and 6.7 pt, only justified text, last lines included, never in modes 12 and 14. A real paper's justified abstract now breaks like Word on all 23 lines (3 before); on 180 real-world papers, page counts matching Word went from 139 to 142, none further |