An empty continuous section mark takes no room (#940)

* R21: an empty continuous section mark takes no room

An empty paragraph that only carries a section break before a continuous
section takes no room in Word: the text after it follows right on, and
columns balanced above it don't count it. A break on a paragraph with text
keeps its line. Measured in Word for Windows: 10 synthetic cases. The SARP
form's fields drifted a line lower at each of its continuous breaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ROADMAP: keep the progress log separator under its header

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:
Brandon ThomasandClaude Opus 5.5 authored and GitHub committed 2026-10-12 10:02:07 -04:00
1 parent 8c67ef7c28
commit 5383d80f75
4 files changed
+70 -1

No files matched your search

+2 -1
View File
@@ -132,8 +132,8 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
## Progress log
| Date | What landed |
| 2026-10-12 | AutoCorrect keys its built-in English replacement list by language (#835): Polish, Croatian, Bosnian and Serbian `i` ("and") is no longer turned into `I`; English and untagged text unchanged |
|---|---|
| 2026-10-12 | AutoCorrect keys its built-in English replacement list by language (#835): Polish, Croatian, Bosnian and Serbian `i` ("and") is no longer turned into `I`; English and untagged text unchanged |
| 2026-10-12 | Ribbon groups fit above their labels: View › Show splits its four check boxes into two columns (Hide White Space, #645, had pushed the fourth onto the "Show" label), Developer › Controls loses the gaps that ran into its label, and Layout › Paragraph (#654) no longer trips a debug assertion and sits 2.5 pt higher; a test draws every tab in every language and fails on any group taller than the ribbon |
| 2026-10-12 | Window caption: a non-Latin file name is drawn with a document fallback face (or the CJK UI font), instead of `?` |
| 2026-10-12 | 32-bit Windows starts on OpenGL. DirectX 12 graphics pipeline creation returns "The parameter is incorrect" and the app exited before the window appeared |
@@ -153,6 +153,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 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-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-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) |
|---|---|
+28
View File
@@ -600,6 +600,9 @@ struct Ctx<'a> {
uses_totals: bool,
/// Whether to note `uses_totals` now (off while laying out headers and footers).
track_totals: bool,
/// Body blocks that are empty paragraphs carrying only a section break before a continuous
/// section, in order: they take no room (see [`place_para`]).
quiet_marks: Vec<usize>,
}
impl Ctx<'_> {
@@ -1755,11 +1758,18 @@ fn layout_pass(
grid: para::Grid::default(),
uses_totals: false,
track_totals: true,
quiet_marks: Vec::new(),
};
let sections = doc.sections();
let web = matches!(opts.view, ViewMode::Web | ViewMode::Draft | ViewMode::Outline);
let read = opts.view == ViewMode::Read;
let reflow = web || read;
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();
let outline = (opts.view == ViewMode::Outline).then(|| OutlinePass::new(doc, opts.outline.clone().unwrap_or_default()));
let default_sect = SectionProps::default();
let mut web_sect;
@@ -2584,7 +2594,25 @@ fn para_notes(doc: &Document, p: &Paragraph, pl: &ParaLayout) -> Vec<(usize, u32
notes
}
/// Place a body paragraph. An empty paragraph that only carries a section break before a
/// continuous section takes no room (Word for Windows: the text after it follows right on, and
/// columns balanced above it don't count it).
fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, body_top: f32) {
if !pb.web && ctx.quiet_marks.binary_search(&block).is_ok() {
// Its (empty) line stays where the text ended, even at the bottom of a full column.
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, pb.prev, pb.content_bottom) = (y, prev, content_bottom);
}
return;
}
place_para_in_flow(ctx, pb, p, block, body_top);
}
fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: usize, body_top: f32) {
let width = pb.col_w();
// Floating objects anchored here join the page's wrap areas before the text is laid out; if
// the paragraph then starts somewhere else (another column or page, or contextual spacing
+39
View File
@@ -4387,3 +4387,42 @@ fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() {
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");
}
/// An empty paragraph that only carries a section break before a continuous section takes no
/// room, measured in Word for Windows (`plan/word` R21, 10 cases): the text after it follows right
/// on, and columns balanced above it don't count it (two one-line paragraphs in two columns sit
/// side by side). A section break on a paragraph with text keeps its line, and so does any break
/// before a section on a new page. The SARP form's fields drifted a line lower at each of its
/// many continuous breaks.
#[test]
fn an_empty_continuous_section_mark_takes_no_room() {
use wordcraft_doc::section::{SectionProps, SectionStart};
// "Pa", the section's last paragraph (`mark`), "Pb", "Pc" in the next section of `cols`
// columns, then "Pd" after a second break.
let tops = |mark: &str, cols: u32, start: SectionStart| {
let mut d = Document::from_text(&format!("Pa\n{mark}\nPb\nPc\n\nPd"));
d.para_mut(StoryRef::Body, &Path::top(1)).unwrap().section =
Some(Box::new(SectionProps { start: SectionStart::Continuous, ..Default::default() }));
let mut s = SectionProps { start, ..Default::default() };
s.columns.count = cols;
d.para_mut(StoryRef::Body, &Path::top(4)).unwrap().section = Some(Box::new(s));
d.last_section.start = SectionStart::Continuous;
let l = lay(&d);
[0, 2, 3, 5].map(|i| {
let c = l.caret(&Pos::body(i, 0)).unwrap();
(c.page, (c.top * 10.0).round() / 10.0, c.x)
})
};
let one = tops("", 1, SectionStart::Continuous);
let line = one[2].1 - one[1].1;
assert!((one[1].1 - one[0].1 - line).abs() < 0.15, "Pb right under Pa: {one:?}");
assert!((one[3].1 - one[2].1 - line).abs() < 0.15, "Pd right under Pc: {one:?}");
let text = tops("Mark", 1, SectionStart::Continuous);
assert!((text[1].1 - text[0].1 - 2.0 * line).abs() < 0.15, "a mark with text keeps its line: {text:?}");
let two = tops("", 2, SectionStart::Continuous);
assert_eq!(two[1].1, two[2].1, "balanced: Pb and Pc side by side: {two:?}");
assert!(two[2].2 > two[1].2 + 100.0, "Pc in the second column: {two:?}");
assert!((two[3].1 - two[1].1 - line).abs() < 0.15, "Pd a line below them: {two:?}");
let new_page = tops("", 1, SectionStart::NextPage);
assert_eq!(new_page[1].0, 1, "a next-page section starts a page: {new_page:?}");
}
+1
View File
@@ -81,6 +81,7 @@ font can't match Word's metrics.
| 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-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-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) |