mirror of
https://github.com/aphrody-labs/wordcraft.git
synced 2026-10-14 03:29:04 +00:00
R19: keep with next splits a paragraph that doesn't fit anyway (#933)
Word moves a "keep with next" paragraph to the next page only when it would fit on this one by itself; one that doesn't fit anyway breaks as usual, its last lines going over with the next paragraph (with keep lines together it moves whole). A chain of kept paragraphs moves together. Measured in Word for Windows: 20 synthetic cases. NIH's five-line kept paragraph no longer leaves a third of a page empty. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c9079196db
commit
fc52079f58
4 files changed
+62
-5
No files matched your search
@@ -151,6 +151,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-12 | Hidden end-of-cell marks (`w:hideMark`) read, written and laid out like Word (9 cases measured in Word for Windows): a cell's empty last paragraph takes no room, so rows of such empty cells shrink to their borders |
|
||||
| 2026-10-12 | Text frames (`w:framePr`) in headers, footers, cells and text boxes: placed apart with text wrapping around them, and kept on save (a framed footer page number took a line of its own and was lost on save) |
|
||||
| 2026-10-12 | List indents like Word's when a numbered style has indents of its own: the style giving the list (and styles based on it) beat the level's indents, and a paragraph with its list switched off drops the styles' indents (27 cases measured in Word for Windows); NIH's Heading 3s now sit at their 0.9" |
|
||||
| 2026-10-12 | Keep with next like Word (20 cases measured in Word for Windows): a kept paragraph moves to the next page only when it would fit on this one by itself, otherwise it breaks as usual; chains of kept paragraphs move together |
|
||||
| 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) |
|
||||
|---|---|
|
||||
|
||||
@@ -2615,9 +2615,11 @@ fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize,
|
||||
}
|
||||
}
|
||||
pb.y += before;
|
||||
// Keep lines together: if it doesn't fit but would on an empty column, move it.
|
||||
if pl.rp.keep_lines || pl.rp.keep_next {
|
||||
let need = pl.height + if pl.rp.keep_next { next_first_line(ctx, block, width) } else { 0.0 };
|
||||
// Keep lines together: if it doesn't fit but would on an empty column, move it. Keep with
|
||||
// next alone moves a paragraph only when it would fit here by itself (Word for Windows): one
|
||||
// that doesn't breaks as usual, its last lines going over with the next paragraph.
|
||||
if pl.rp.keep_lines || (pl.rp.keep_next && pb.y + pl.height <= pb.bottom + 0.01) {
|
||||
let need = pl.height + if pl.rp.keep_next { next_first_line(ctx, block, width, 0) } else { 0.0 };
|
||||
// (Against the whole column: balanced columns grow to hold it.)
|
||||
if pb.y + need > pb.bottom && !pb.at_top() && need <= pb.uncapped_bottom() - pb.top {
|
||||
pb.advance(block, body_top);
|
||||
@@ -2883,7 +2885,10 @@ fn mirror_shift(rp: &wordcraft_doc::resolve::ResolvedPara, number: u32, web: boo
|
||||
}
|
||||
|
||||
/// Height of the first line of the block after `block` (for keep-with-next).
|
||||
fn next_first_line(ctx: &mut Ctx, block: usize, width: f32) -> f32 {
|
||||
/// The room the paragraphs after `block` need on its page for keep with next: the next one's
|
||||
/// first line, or all of it and what it keeps with in turn when it is kept with the next too
|
||||
/// (a chain moves together in Word).
|
||||
fn next_first_line(ctx: &mut Ctx, block: usize, width: f32, depth: usize) -> f32 {
|
||||
match ctx.doc.body.get(block + 1).map(|b| &**b) {
|
||||
Some(Block::Para(p)) => {
|
||||
// Don't advance list counters for a lookahead: lay out without the label.
|
||||
@@ -2907,7 +2912,11 @@ fn next_first_line(ctx: &mut Ctx, block: usize, width: f32) -> f32 {
|
||||
};
|
||||
let _ = rp;
|
||||
let pl = para::layout_para(p, &env);
|
||||
pl.rp.space_before + pl.lines.first().map(|l| l.height).unwrap_or(0.0)
|
||||
if pl.rp.keep_next && depth < 8 {
|
||||
pl.rp.space_before + pl.height + pl.rp.space_after + next_first_line(ctx, block + 1, width, depth + 1)
|
||||
} else {
|
||||
pl.rp.space_before + pl.lines.first().map(|l| l.height).unwrap_or(0.0)
|
||||
}
|
||||
}
|
||||
Some(Block::Table(_)) => 24.0,
|
||||
None => 0.0,
|
||||
|
||||
@@ -4341,3 +4341,49 @@ fn list_indents_and_style_indents_combine_like_word() {
|
||||
assert!((got - want).abs() < 0.5, "{what}: {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep with next at a page bottom, measured in Word for Windows (`plan/word` R19, 20 cases): a
|
||||
/// paragraph that would fit on the page by itself moves over with the next one, but one that
|
||||
/// doesn't fit anyway breaks as usual, its last lines going over with the next paragraph (with
|
||||
/// Keep lines together it moves whole); a chain of kept paragraphs moves together. NIH's
|
||||
/// five-line kept paragraph left a third of a page empty.
|
||||
#[test]
|
||||
fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() {
|
||||
// Page of each of the kept paragraph's 5 lines and of the first paragraph after it.
|
||||
let pages = |fillers: usize, kept: ParaProps, next: ParaProps| {
|
||||
let mut d = Document::new();
|
||||
let plain = ParaProps {
|
||||
space_before: Some(0.0),
|
||||
space_after: Some(0.0),
|
||||
line_spacing: Some(wordcraft_doc::props::LineSpacing::Multiple(1.0)),
|
||||
..Default::default()
|
||||
};
|
||||
let para = |text: &str, p: &ParaProps| {
|
||||
let mut x = wordcraft_doc::Paragraph::with_text(text, Default::default());
|
||||
x.props = ParaProps { keep_next: p.keep_next, keep_lines: p.keep_lines, ..plain.clone() };
|
||||
wordcraft_doc::para_block(x)
|
||||
};
|
||||
d.body = (0..fillers).map(|i| para(&format!("F{i}"), &plain)).collect();
|
||||
d.body.push(para("Ka\nKb\nKc\nKd\nKe", &kept));
|
||||
d.body.push(para("NX", &next));
|
||||
d.body.push(para("NY", &plain));
|
||||
let l = lay(&d);
|
||||
let k = fillers;
|
||||
[0, 3, 6, 9, 12]
|
||||
.map(|off| l.caret(&Pos::body(k, off)).unwrap().page)
|
||||
.into_iter()
|
||||
.chain([l.caret(&Pos::body(k + 1, 0)).unwrap().page])
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let none = ParaProps::default();
|
||||
let kn = ParaProps { keep_next: Some(true), ..Default::default() };
|
||||
let kl = ParaProps { keep_lines: Some(true), ..kn.clone() };
|
||||
// Find where the kept paragraph just fits: its last line on page 1, the next one's on page 2.
|
||||
let fit = (20..80).find(|n| pages(*n, none.clone(), none.clone()) == [0, 0, 0, 0, 0, 1]).expect("a filler count that fills the page");
|
||||
assert_eq!(pages(fit, kn.clone(), none.clone()), [1, 1, 1, 1, 1, 1], "fits by itself: moves with the next");
|
||||
let split = pages(fit + 1, none.clone(), none.clone());
|
||||
assert!(split[0] == 0 && split[4] == 1, "one more line splits it: {split:?}");
|
||||
assert_eq!(pages(fit + 1, kn.clone(), none.clone()), split, "doesn't fit anyway: breaks as usual");
|
||||
assert_eq!(pages(fit + 1, kl.clone(), none.clone()), [1, 1, 1, 1, 1, 1], "with keep lines together it moves whole");
|
||||
assert_eq!(pages(fit - 1, kn.clone(), kn.clone()), [1, 1, 1, 1, 1, 1], "a kept chain moves together");
|
||||
}
|
||||
@@ -80,6 +80,7 @@ font can't match Word's metrics.
|
||||
| 2026-10-12 | trivial | Hidden end-of-cell mark (`w:hideMark`): a cell's empty last paragraph takes no room, spacing and all; a row of only such empty cells is just its borders |
|
||||
| 2026-10-12 | trivial | Text frames in headers, footers, cells and text boxes are placed like floating objects with the text wrapping around them; body frames still lay out in line |
|
||||
| 2026-10-12 | trivial | List indents and style indents combine like Word's: a numbered style's own indents (and those of styles based on it) beat the list level's, its base styles' don't, the paragraph's own list beats every style; a paragraph that switches its list off (`numId` 0) drops the styles' indents |
|
||||
| 2026-10-12 | trivial | Keep with next: a paragraph that doesn't fit on the page anyway breaks as usual (its last lines go over with the next paragraph) instead of moving whole; a chain of kept paragraphs moves together |
|
||||
| 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