diff --git a/ROADMAP.md b/ROADMAP.md index 047b50e138..5e09190eea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -240,6 +240,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-11-03: Layout: a row of a nested table marked Can't Split isn't split with its outer row any more. The outer row moves to the next page whole when that row comes first in it, as Word does (docxide-pdf `nested-first-row-*-cant1`), and otherwise splits above it (inferred, not yet checked in Word); on a fresh page a nested row taller than the room still splits rather than running into the margin. Readiness unchanged. - 2026-11-03: Layout: no line break between a full stop and an opening parenthesis. UAX #14 allows one there, Word doesn't: `29.05.2023г.(оформление ЗУ)` and `2023.(Example detail)` now wrap whole at the space before, as in the docxide-pdf `dot-open-token*` Word references. Readiness unchanged. - 2026-11-03: Layout: before Word 2013 layout a floating table that may not overlap another and is moved below it keeps its own left edge, as Word places it, also when its first cell has no left margin; the layout took a zero margin for Word 2013 layout and stepped the table in by its distance from the text. Readiness unchanged. +- 2026-11-03: Layout: an empty paragraph after a document's leading floating table takes no space before, as in Word's PDFs of docxide-pdf's `leading-empty-before-float` cases (beside the table); below a full-width one it loses it too, which is untested. Readiness unchanged. - 2026-11-03: Security: pasting text boxes charges each copied story for its formatting too (run, mark, paragraph and section properties), not only its text and objects, so a story with huge style or font names can't copy past the paste's 32 MiB text box budget. Readiness unchanged. - 2026-11-03: Security: `.doc` import keeps one width, shading, alignment and merge override per table cell, the last one winning as before, instead of a copy every time a paragraph's properties repeat the sprm, so a shared PAPX can't pile up copies on every paragraph mark. Readiness unchanged. - 2026-11-03: Security: `.doc` import attaches each section to the paragraph that ends it through an index of paragraph ends, instead of scanning every paragraph for every section, so a file with a thousand sections that end nowhere can't multiply the work over its paragraphs. Readiness unchanged. @@ -920,6 +921,7 @@ MATH glyphs that reference the same construction now share its immutable variant | 2026-11-03 | trivial | Progress log: a picture in a paragraph whose hidden mark closes a section keeps its room (#5288) | | 2026-11-03 | trivial | Progress log: a nested table's Can't Split row moves on with its outer row | | 2026-11-03 | trivial | Progress log: no line break between a full stop and an opening parenthesis | +| 2026-11-03 | trivial | Progress log: an empty paragraph after a document's leading floating table takes no space before | | 2026-11-03 | trivial | Progress log: Replace and Remove Table of Contents with Word's title above a section break (#4041) | | 2026-11-03 | trivial | Progress log: language-packs-2 published and pinned, 75 Language packs | | 2026-11-02 | trivial | Progress log: language-packs-1 published and pinned, 61 Language packs (CP4.4) | diff --git a/crates/layout/src/flow.rs b/crates/layout/src/flow.rs index 5fd30ff997..233add4ef0 100644 --- a/crates/layout/src/flow.rs +++ b/crates/layout/src/flow.rs @@ -281,6 +281,17 @@ impl<'a> Flow<'a> { if self.pl.rp.before_auto && self.block == 0 { before = 0.0; } + // An empty paragraph after a document's leading table, placed floating on this page, has no + // space before (Word's PDFs: 0 pt and 12 pt before put the text after it at the same place). + // Only that shape is observed: later pages and columns keep theirs. + if self.block == 1 + && pb.fresh() + && !pb.web + && pb.pages.last().is_some_and(|pg| pg.float_tables.contains(&Path::top(0))) + && only_markers(self.p, 0..self.p.text.len()) + { + before = 0.0; + } before } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 784c4a7a69..cc5df29f05 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -9,6 +9,7 @@ mod bidi; mod budget; mod cell_floats; mod complex; +mod float_top; mod grid; mod ligature; pub(crate) mod linear; diff --git a/crates/layout/src/tests/float_top.rs b/crates/layout/src/tests/float_top.rs new file mode 100644 index 0000000000..fb555de219 --- /dev/null +++ b/crates/layout/src/tests/float_top.rs @@ -0,0 +1,53 @@ +//! An empty paragraph after a document's leading floating table takes no space before. Word's PDFs +//! of docxide-pdf's `leading-empty-before-float-0` and `-12` put the text after the empty anchor at +//! the same place for 0 pt and 12 pt before. + +use super::lay; +use crate::{Arc, Placed, StoryRef}; +use wordcraft_doc::para::{Anchor, InlineObject}; +use wordcraft_doc::props::TableFloat; +use wordcraft_doc::{Block, Document, Paragraph, Table, para_block}; + +/// Where the lines of body block `block` start on page one. +fn line_y(d: &Document, block: u32) -> f32 { + lay(d).pages[0] + .items + .iter() + .find_map(|i| match i { + Placed::Lines { story: StoryRef::Body, path, y, .. } if path.0 == [block] => Some(*y), + _ => None, + }) + .unwrap() +} + +/// `table` (a floating one) or nothing, then a paragraph of `text` with `before`, then "After". +fn document(table: bool, text: &str, before: f32) -> Document { + let mut p = Paragraph::with_text(text, Default::default()); + // "#" stands for a bookmark, a zero-width marker. + if text == "#" { + p = Paragraph::new(); + p.insert_object(0, InlineObject::BookmarkStart { name: "_GoBack".into() }, &Default::default()).unwrap(); + } + p.props.space_before = Some(before); + let mut d = Document::new(); + d.body = vec![para_block(p), para_block(Paragraph::with_text("After", Default::default()))]; + if table { + let mut t = Table::new(1, 1, 200.0); + t.rows[0].cells[0].blocks = vec![para_block(Paragraph::with_text("Table", Default::default()))]; + t.props.float = Some(TableFloat { h_rel: Anchor::Margin, v_rel: Anchor::Paragraph, ..Default::default() }); + d.body.insert(0, Arc::new(Block::Table(t))); + } + d +} + +#[test] +fn an_empty_paragraph_after_a_floating_table_at_the_page_top_takes_no_space_before() { + let empty = line_y(&document(true, "", 12.0), 2); + let plain = line_y(&document(false, "", 0.0), 1); + assert!((empty - plain).abs() < 0.01, "after the empty anchor at {empty}, with no table nor space before at {plain}"); + // A bookmark alone doesn't make it a paragraph with text. + assert!((line_y(&document(true, "#", 12.0), 2) - plain).abs() < 0.01); + // One with text keeps it, and so does an empty first paragraph with no table before it. + assert!(line_y(&document(true, "Text", 12.0), 2) > empty + 11.99); + assert!(line_y(&document(false, "", 12.0), 1) > plain + 11.99); +} diff --git a/docs/layout-parity.md b/docs/layout-parity.md index bb08fdadee..942c87e8af 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -70,6 +70,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic | Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; a click below one, outside its cells, goes to the text beside or after it, not to its last row, as checked in Word (#2038); exact line-bottom contact counts for tables only (#3977), both in row-span selection and word skipping; exact-spacing table contact uses the specified height rather than paragraph-mark metrics, with original Source Sans 3 controls in modes 11/12/14/15; expected Word rule from [cyril265's Word for Mac 16.114.1 measurements](https://github.com/storytold/wordcraft/issues/3977#issuecomment-6374217574), not a new Word run of these originals; anchor-order replay remains #3790's scope; other overlap rules untested | 2–3 | | Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; a click below one, outside its cells, goes to the text beside or after it, not to its last row, as checked in Word (#2038); the same for a click within 2pt of a cell whose text is turned comes from the issue and synthetic tests, not a check by hand (#4181); other overlap rules untested | 2–3 | | Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; a click below one, outside its cells, goes to the text beside or after it, not to its last row, as checked in Word (#2038); exact line-bottom contact counts for tables only (#3977), both in row-span selection and word skipping; exact-spacing table contact uses the specified height rather than paragraph-mark metrics, with original Source Sans 3 controls in modes 11/12/14/15; expected Word rule from [cyril265's Word for Mac 16.114.1 measurements](https://github.com/storytold/wordcraft/issues/3977#issuecomment-6374217574), not a new Word run of these originals; anchor-order replay remains #3790's scope; one with `tblOverlap=never` moved below another keeps its distance from the text's left edge only in Word 2013 layout (mode 15), before it its own left edge, also when its first cell has no left margin (docxide `displaced-anchor-*`); other overlap rules untested | 2–3 | +| Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; a click below one, outside its cells, goes to the text beside or after it, not to its last row, as checked in Word (#2038); exact line-bottom contact counts for tables only (#3977), both in row-span selection and word skipping; exact-spacing table contact uses the specified height rather than paragraph-mark metrics, with original Source Sans 3 controls in modes 11/12/14/15; expected Word rule from [cyril265's Word for Mac 16.114.1 measurements](https://github.com/storytold/wordcraft/issues/3977#issuecomment-6374217574), not a new Word run of these originals; anchor-order replay remains #3790's scope; other overlap rules untested; an empty paragraph (or one of bookmarks and other markers only) after a document's leading floating table takes no space before (docxide-pdf's `leading-empty-before-float` Word PDFs, the paragraph beside the table); below a full-width one it loses it too, untested; a paragraph with text below a full-width one keeps it (`connector-paragraph-anchor-0`/`-240`); one with text beside a narrow one, and later pages and columns, are untested and keep it | 2–3 | | Nested tables | 🟡 | render; polish missing | 2–4 | | Floating pictures/shapes placement (relative to page/margin/column/paragraph/line/character) | ✅ | #136; in a header or footer table cell, a float not laid out in its cell (`layoutInCell="0"`, VML `o:allowincell="f"`, or any VML shape in a header or footer: user-checked in Word for Mac) and placed relative to the page or margins is placed on the page, drawn with the header behind or in front of its text, on every page: Word's picture watermark in a header that starts with a table is centred behind the body text (#1775, `layout::tests::header_cell_float_placed_on_the_page`). Other cell floats are still placed relative to their cell. A float anchored in a line of a long paragraph that goes onto a later page is drawn on that page (observed in Word for Mac: a picture dragged onto a page that only continues a paragraph stays there, saved with its anchor on that page and its offset from the paragraph's top there), and that page's lines are broken again around it (the #1374 relay, so not past its limit); relative to the paragraph, it is measured from where the paragraph continues there; text added on the pages before leaves it where it is. One anchored right after a page break stays on the page before (a cover page, #1632). The page is decided before the lines are placed; a float whose anchor only goes to the next page because the float itself narrows the lines stays on the first page, with the text wrapping around it; on a later page, a float anchored on a line moved to a later page by footnotes or widow control is drawn on that page, and one whose own wrap pushes its line on stays with the text wrapping around it. Not yet: a float anchored in a later column of the same page is still placed with the paragraph's first piece, as before (#4250, `layout::tests::a_float_anchored_on_a_later_page_of_its_paragraph_is_drawn_on_that_page` and the tests after it) | — | | Floating pictures/shapes placement (relative to page/margin/column/paragraph/line/character) | ✅ | #136; in a header or footer table cell, a float not laid out in its cell (`layoutInCell="0"`, VML `o:allowincell="f"`, or any VML shape in a header or footer: user-checked in Word for Mac) and placed relative to the page or margins is placed on the page, drawn with the header behind or in front of its text, on every page: Word's picture watermark in a header that starts with a table is centred behind the body text (#1775, `layout::tests::header_cell_float_placed_on_the_page`). Other cell floats are still placed relative to their cell. A float anchored in a line of a long paragraph that goes onto a later page is drawn on that page (observed in Word for Mac: a picture dragged onto a page that only continues a paragraph stays there, saved with its anchor on that page and its offset from the paragraph's top there), and that page's lines are broken again around it (the #1374 relay, so not past its limit); relative to the paragraph, it is measured from where the paragraph continues there; text added on the pages before leaves it where it is. One anchored right after a page break, with nothing after it that takes room (other anchors, bookmarks, comment or field marks), stays on the page before (a cover page, #1632); with anything after it that takes room (text, a space or tab, an inline picture, another break), it is on the new page with it (observed in Word for Mac: a shape dragged beside the first line after a page break inside a paragraph is saved anchored right after the break, positioned from the paragraph's top on the new page, and stays there, #4262, `layout::tests::a_float_anchored_before_text_after_a_page_break_is_drawn_on_the_next_page`). The page is decided before the lines are placed; a float whose anchor only goes to the next page because the float itself narrows the lines stays on the first page, with the text wrapping around it; on a later page, a float anchored on a line moved to a later page by footnotes or widow control is drawn on that page, and one whose own wrap pushes its line on stays with the text wrapping around it. Not yet: a float anchored in a later column of the same page is still placed with the paragraph's first piece, as before (#4250, `layout::tests::a_float_anchored_on_a_later_page_of_its_paragraph_is_drawn_on_that_page` and the tests after it) | — | @@ -329,6 +330,7 @@ The front-of-text opt-out is retained from the earlier implementation; this patc | 2026-11-03 | trivial | A row of a nested table marked Can't Split isn't split with its outer row: the outer row moves on whole when that row comes first in it, and otherwise splits above it (inferred); on a fresh page a nested row taller than the room still splits (docxide-pdf `nested-first-row-*-cant1`) | | 2026-11-03 | trivial | No line break between a full stop and an opening parenthesis: `2023.(Example` wraps whole at the space before, as in the docxide-pdf `dot-open-token*` Word references | | 2026-11-03 | trivial | Before Word 2013 layout a floating table moved below another keeps its own left edge, also when its first cell has no left margin (the mode was read from that margin being zero); readiness estimates unchanged | +| 2026-11-03 | trivial | An empty paragraph after a document's leading floating table takes no space before, as Word's PDFs of docxide-pdf's `leading-empty-before-float` cases show (the paragraph beside the table); below a full-width one it loses it too, untested; paragraphs with text, later pages and columns keep it (`layout::tests::float_top`) | | 2026-11-01 | trivial | Columns: the line between them runs from the section's top on the page to the bottom of its tallest column; a continuous section mid-page gets its line (#5178) | | 2026-10-31 | trivial | A table row that would leave a cell empty on a page that already holds something (its inline picture or first line runs past the room) moves on whole instead of leaving the other cells' text beside the empty cell; an at-least row grown by its picture no longer splits off the picture (#4272) | | 2026-11-01 | trivial | A cell aligned to the middle or bottom of a row split across pages is aligned within the part left on the page when its text fits there, and starts at the top of the cell otherwise, as measured in Word |