mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-13 19:26:53 +00:00
Space before at the top of a page like Word (#932)
* Layout: space before at the top of a page after page breaks, as in Word Found comparing Microsoft's specification documents with Word for Windows, then measured with 32 synthetic documents in compatibility modes 12 (Word 2007) and 15 (Word 2013+): - A paragraph that ends with a page break keeps its mark with the break: WordCraft put an empty line at the top of the next page. - The paragraph after it, first on the new page, gets no space before. - A paragraph that starts with a page break starts on the new page with its space before there (WordCraft put the space on the old page). - A page break before keeps the space before in modes up to 14 and drops it in mode 15. - Natural breaks, section breaks and the first page were already right. All 32 cases now land where Word puts them (within 0.3 pt), and the caret still reaches a paragraph mark kept with its break. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Layout: after a page break before or a section break, the space after above counts Measured in Word for Windows (10 more cases): at the top of a page after a page break before (modes up to 14) or a next-page section break, the previous paragraph's space after comes off the space before (24 pt before after 10 pt after leaves 14 pt; after 30 pt leaves none). This put the numbered Heading 1s of Microsoft's specification documents 9 pt too low. All 42 page-top cases now match Word. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
97ade895d7
commit
3ede32e80e
4 files changed
+100
-1
No files matched your search
@@ -157,6 +157,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-12 | An empty paragraph that only carries a continuous section break takes no room, as in Word (10 cases measured in Word for Windows): text after it follows right on and balanced columns don't count it |
|
||||
| 2026-10-12 | Floating objects positioned relative to their paragraph start above its space before, as in Word (10 cases measured in Word for Windows) |
|
||||
| 2026-10-12 | Unequal columns: text after a column break is broken for the column it lands in (NIH's SARP form had a paragraph a word or two wide) |
|
||||
| 2026-10-12 | Space before at the top of a page like Word after page breaks (32 cases measured): a paragraph ending with a page break keeps its mark with it, the next paragraph gets no space before; page break before drops it in Word 2013 mode |
|
||||
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
|
||||
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|
||||
|---|---|
|
||||
|
||||
@@ -1218,6 +1218,9 @@ struct PageBuilder<'a> {
|
||||
orig_bottom: f32,
|
||||
/// The previous paragraph: (style, contextual spacing, space after) for contextual spacing.
|
||||
prev: Option<(String, bool, f32)>,
|
||||
/// The previous paragraph ended with a page break (its mark stayed with the break): the next
|
||||
/// paragraph, first on the new page, gets no space before (Word).
|
||||
after_break_mark: bool,
|
||||
/// Wrap areas of floating objects on this page (see [`wrap_area`]).
|
||||
excl: Vec<WrapArea>,
|
||||
/// Wrap areas of the floating tables on this page, which others may not overlap.
|
||||
@@ -1811,6 +1814,7 @@ fn layout_pass(
|
||||
carry: Vec::new(),
|
||||
orig_bottom: 0.0,
|
||||
prev: None,
|
||||
after_break_mark: false,
|
||||
excl: Vec::new(),
|
||||
float_tables: Vec::new(),
|
||||
line_no: 0,
|
||||
@@ -2628,11 +2632,27 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
let mut anchor_y = pb.y;
|
||||
let spot = |pb: &PageBuilder| (pb.pages.len(), pb.col);
|
||||
let placed_at = spot(pb);
|
||||
if pl.rp.page_break_before && !pb.at_top() && !pb.web {
|
||||
let broke_before = pl.rp.page_break_before && !pb.at_top() && !pb.web;
|
||||
if broke_before {
|
||||
pb.new_page(block, body_top);
|
||||
}
|
||||
// Contextual spacing: no space between paragraphs of the same style when either asks for it.
|
||||
let mut before = pl.rp.space_before;
|
||||
let prev_para = pb.prev.clone();
|
||||
// At the top of a page, measured in Word for Windows: no space before after a paragraph that
|
||||
// ended with a page break, nor (from Word 2013's layout, compatibility mode 15) for a page
|
||||
// break before; a section break or, in older modes, a page break before keeps it.
|
||||
let after_break_mark = std::mem::take(&mut pb.after_break_mark);
|
||||
if pb.at_top() && (after_break_mark || (broke_before && ctx.doc.settings.compat_mode >= 15)) {
|
||||
before = 0.0;
|
||||
} else if pb.at_top()
|
||||
&& let Some((_, _, after)) = &prev_para
|
||||
{
|
||||
// After a page break before or a section break the space between the paragraphs is
|
||||
// still counted as one: the previous paragraph's space after (left on its page) comes
|
||||
// off the space before here (Word: 24 pt before after 10 pt after leaves 14 pt).
|
||||
before = (before - after.max(0.0)).max(0.0);
|
||||
}
|
||||
if let Some((style, ctxl, after)) = pb.prev.take()
|
||||
&& style == pl.rp.style
|
||||
&& !pb.at_top()
|
||||
@@ -2673,6 +2693,9 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
let lead = if l0 == 0 { first.top.max(0.0) } else { 0.0 };
|
||||
let mut l1 = l0;
|
||||
let mut forced_break = None;
|
||||
// A page break followed only by the paragraph mark: the mark's (empty) line stays on this
|
||||
// page with the break, so nothing of the paragraph goes to the next page (Word).
|
||||
let mut mark_with_break = false;
|
||||
let mut new_notes: Vec<NoteBox> = Vec::new();
|
||||
let notes = para_notes(ctx.doc, p, &pl);
|
||||
while l1 < n {
|
||||
@@ -2726,6 +2749,10 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
l1 += 1;
|
||||
if matches!(l.end, LineEnd::PageBreak | LineEnd::ColumnBreak) && !pb.web {
|
||||
forced_break = Some(l.end);
|
||||
if l.end == LineEnd::PageBreak && l1 + 1 == n && pl.lines.get(l1).is_some_and(|m| m.start >= m.stop) {
|
||||
l1 += 1;
|
||||
mark_with_break = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if split {
|
||||
@@ -2836,7 +2863,21 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
pb.content_bottom = pb.content_bottom.max(pb.y);
|
||||
}
|
||||
l0 = l1;
|
||||
// A paragraph that starts with a page break starts on the new page, its space before
|
||||
// there (Word).
|
||||
let starts_with_break = l0 == 1
|
||||
&& pl.lines.first().is_some_and(|l| {
|
||||
p.text.get(l.start..l.stop).is_some_and(|t| !t.is_empty() && t.chars().all(|c| c == wordcraft_doc::para::PAGE_BREAK))
|
||||
});
|
||||
match forced_break {
|
||||
Some(LineEnd::PageBreak) if mark_with_break => {
|
||||
pb.new_page(block, body_top);
|
||||
pb.after_break_mark = true;
|
||||
}
|
||||
Some(LineEnd::PageBreak) if starts_with_break && !pb.web => {
|
||||
pb.new_page(block, body_top);
|
||||
pb.y += pl.rp.space_before.max(0.0);
|
||||
}
|
||||
Some(LineEnd::PageBreak) => pb.new_page(block, body_top),
|
||||
Some(LineEnd::ColumnBreak) => {
|
||||
pb.col_break_page = Some(pb.pages.len().saturating_sub(1));
|
||||
|
||||
@@ -4501,3 +4501,59 @@ fn text_after_a_column_break_takes_the_new_column_s_width() {
|
||||
assert!(wide_x > 72.0 + 67.0, "the text is in the second column: {lines:?}");
|
||||
assert!(wide_lines <= 3, "broken for the 348 pt column, not the 67 pt one: {lines:?}");
|
||||
}
|
||||
|
||||
/// Space before at the top of a page, measured in Word for Windows (32 cases): a paragraph that
|
||||
/// ends with a page break keeps its mark with the break (nothing of it goes to the new page) and the
|
||||
/// next paragraph gets no space before there; a paragraph that starts with a page break starts on
|
||||
/// the new page with its space before; a page break before keeps the space before in compatibility
|
||||
/// modes up to 14 (less the previous paragraph's space after) and drops it in mode 15.
|
||||
#[test]
|
||||
fn space_before_at_the_top_of_a_page_like_word() {
|
||||
let para = |text: &str, before: f32, pbb: bool| {
|
||||
let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default());
|
||||
p.props = ParaProps { space_before: Some(before), space_after: Some(0.0), page_break_before: Some(pbb), ..Default::default() };
|
||||
wordcraft_doc::para_block(p)
|
||||
};
|
||||
let probe_top = |blocks: Vec<std::sync::Arc<wordcraft_doc::Block>>, mode: u32| {
|
||||
let mut d = Document::new();
|
||||
d.settings.compat_mode = mode;
|
||||
let n = blocks.len();
|
||||
d.body = blocks;
|
||||
let l = lay(&d);
|
||||
let c = l.caret(&Pos::body(n - 1, 0)).unwrap();
|
||||
(c.page, c.top)
|
||||
};
|
||||
let plain_top = probe_top(vec![para("first", 0.0, false), para("PROBE", 0.0, true)], 15).1;
|
||||
let brk = wordcraft_doc::para::PAGE_BREAK;
|
||||
// A paragraph ending with a page break, then one with 24 pt before: no space, in both modes.
|
||||
for mode in [12, 15] {
|
||||
let (page, top) = probe_top(vec![para(&format!("first{brk}"), 0.0, false), para("PROBE", 24.0, false)], mode);
|
||||
assert_eq!(page, 1, "mode {mode}");
|
||||
assert!((top - plain_top).abs() < 0.05, "mode {mode}: the mark stays with the break and the space goes: {top} vs {plain_top}");
|
||||
}
|
||||
// A paragraph starting with a page break: its space before on the new page.
|
||||
let mut d = Document::new();
|
||||
d.body = vec![para("first", 0.0, false), para(&format!("{brk}PROBE"), 24.0, false)];
|
||||
let l = lay(&d);
|
||||
let c = l.caret(&Pos::body(1, brk.len_utf8())).unwrap();
|
||||
assert_eq!(c.page, 1);
|
||||
assert!((c.top - plain_top - 24.0).abs() < 0.05, "{} vs {}", c.top, plain_top + 24.0);
|
||||
// Page break before: kept up to mode 14, dropped in 15.
|
||||
let (_, kept) = probe_top(vec![para("first", 0.0, false), para("PROBE", 24.0, true)], 14);
|
||||
let (_, dropped) = probe_top(vec![para("first", 0.0, false), para("PROBE", 24.0, true)], 15);
|
||||
assert!((kept - plain_top - 24.0).abs() < 0.05 && (dropped - plain_top).abs() < 0.05, "{kept} {dropped} {plain_top}");
|
||||
// After a page break before (mode 14) the previous paragraph's space after comes off: 24 - 10.
|
||||
let mut first = wordcraft_doc::Paragraph::with_text("first", Default::default());
|
||||
first.props = ParaProps { space_before: Some(0.0), space_after: Some(10.0), ..Default::default() };
|
||||
let (_, top) = probe_top(vec![wordcraft_doc::para_block(first.clone()), para("PROBE", 24.0, true)], 14);
|
||||
assert!((top - plain_top - 14.0).abs() < 0.05, "{top} vs {}", plain_top + 14.0);
|
||||
// More after than before: nothing left.
|
||||
first.props.space_after = Some(30.0);
|
||||
let (_, top) = probe_top(vec![wordcraft_doc::para_block(first), para("PROBE", 24.0, true)], 14);
|
||||
assert!((top - plain_top).abs() < 0.05, "{top} vs {plain_top}");
|
||||
// The caret can still reach the end of a paragraph whose mark stayed with its page break.
|
||||
let mut d = Document::new();
|
||||
d.body = vec![para(&format!("first{brk}"), 0.0, false), para("next", 0.0, false)];
|
||||
let l = lay(&d);
|
||||
assert!(l.caret(&Pos::body(0, "first".len() + brk.len_utf8())).is_some(), "caret after the break");
|
||||
}
|
||||
@@ -84,6 +84,7 @@ font can't match Word's metrics.
|
||||
| 2026-10-12 | trivial | Continuous section breaks: an empty paragraph carrying only the break takes no room (text after it follows on; balanced columns ignore it) |
|
||||
| 2026-10-12 | trivial | Floats relative to a paragraph (or its line) are measured from the paragraph's start above its space before |
|
||||
| 2026-10-12 | trivial | Text after a column break in unequal columns is broken for its new column's width |
|
||||
| 2026-10-12 | trivial | Space before at the top of a page after page breaks, measured in Word for Windows in compatibility modes 12 and 15 (32 cases) |
|
||||
| 2026-10-11 | trivial | Chinese documents (#534): indents in characters, a page-anchored floating table after a full page moves to the next page, no blank page from a page break at the bottom of a full page |
|
||||
| 2026-10-11 | trivial | East Asian list number formats (一、二、三, 第一章, ①); list labels whose characters come from two fonts draw each part with its own font (the digits of "1、" no longer show as other letters); a list level without indents leaves the label at the paragraph edge (#534) |
|
||||
| 2026-10-11 | trivial | Vertical sections: `w:textDirection` in `w:sectPr`, upright East Asian glyphs (#491) |
|
||||
|
||||
Reference in new issue
Block a user