345: ignore explicit page breaks in table cells (#2574)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Fin Warman
2026-10-18 11:42:39 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 6dd7c3bc3a
commit 5b211d6dc5
4 changed files with 119 additions and 1 deletions
+2 -1
View File
@@ -1055,9 +1055,10 @@ fn layout_box(
// Deletions in balloons leave the body's text only: a header, note or text box keeps its own
// inline (restored for the story around this box when it's done).
let outer_hide = std::mem::replace(&mut ctx.hide_deleted, ctx.opts.hides_deleted(story));
let empty_cell = para::CellText::default();
let (table, in_cell) = match context {
BoxContext::Other => (None, false),
BoxContext::Cell(table) => (table, true),
BoxContext::Cell(table) => (Some(table.unwrap_or(&empty_cell)), true),
};
// Word 16.113.3: the override applies in modes 11/12/14, but not mode 15.
let allow_same_style_space =
+2
View File
@@ -1517,6 +1517,8 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
match c {
'\t' => push(&mut b, ClKind::Tab, 0.0, 0.0),
LINE_BREAK | '\r' => push(&mut b, ClKind::LineBreak, 0.0, 0.0),
// Word ignores explicit page breaks in cells, preserving their source offsets.
PAGE_BREAK if env.table.is_some() => push(&mut b, ClKind::Marker, 0.0, 0.0),
PAGE_BREAK => push(&mut b, ClKind::PageBreak, 0.0, 0.0),
COLUMN_BREAK => push(&mut b, ClKind::ColumnBreak, 0.0, 0.0),
_ => {
+112
View File
@@ -8416,3 +8416,115 @@ fn the_word_being_typed_has_no_spelling_squiggle() {
assert_eq!(marks(&left, false), ["helo", "wrld"]);
assert_eq!(c.misses, misses);
}
fn cell_break_doc(text: &str) -> Document {
use wordcraft_doc::props::{HeightRule, LineSpacing, VAlign};
let mut d = Document::new();
d.last_section.page_w = 344.0;
d.last_section.margin_left = 72.0;
d.last_section.margin_right = 72.0;
let mut p = Paragraph::with_text(text, CharProps { size: Some(11.0), ..Default::default() });
p.props.line_spacing = Some(LineSpacing::Exactly(12.0));
p.props.space_before = Some(3.0);
p.props.space_after = Some(3.0);
let prefix = text.bytes().take_while(|&c| c == wordcraft_doc::para::PAGE_BREAK as u8 || c == b'\n').count();
p.format(0, prefix, &|props| props.size = Some(3.0)).unwrap();
let mut t = Table::new(1, 1, 200.0);
t.props.cell_margins = Some([0.0; 4]);
t.rows[0].props.height = Some(20.0);
t.rows[0].props.height_rule = HeightRule::AtLeast;
t.rows[0].cells[0].props.valign = Some(VAlign::Center);
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p)];
d.body = vec![Arc::new(Block::Table(t))];
d
}
#[test]
fn cell_breaks_take_no_height() {
let height = |d: &Document| {
lay(d).pages[0]
.items
.iter()
.find_map(|it| match it {
Placed::Cell { rect, .. } => Some(rect.h),
_ => None,
})
.unwrap()
};
let plain = height(&cell_break_doc("Heading"));
for text in ["\u{000c}Heading", "\u{000c}\u{000c}\u{000c}Heading", "Head\u{000c}ing", "Heading\u{000c}\u{000c}\u{000c}", "é\u{000c}界"] {
let d = cell_break_doc(text);
assert_eq!(height(&d), plain, "cell page breaks take no height: {text:?}");
let l = lay(&d);
let para = l.pages[0]
.items
.iter()
.find_map(|it| match it {
Placed::Lines { para, .. } => Some(para),
_ => None,
})
.unwrap();
assert_eq!(para.lines.len(), 1);
assert!(!para.clusters.iter().any(|c| c.kind == para::ClKind::PageBreak));
for c in &para.clusters {
assert!(text.is_char_boundary(c.start) && text.is_char_boundary(c.end));
if text.get(c.start..c.end) == Some("\u{000c}") {
assert_eq!(c.kind, para::ClKind::Marker);
}
}
let starts: Vec<_> = para.clusters.iter().filter(|c| c.kind == para::ClKind::Text).map(|c| c.start).collect();
let expected: Vec<_> = text.char_indices().filter_map(|(i, c)| (c != '\u{000c}').then_some(i)).collect();
assert_eq!(starts, expected, "visible clusters retain source offsets");
}
assert!(height(&cell_break_doc("\n\n\nHeading")) > plain, "ordinary line breaks retain their lines");
}
#[test]
fn cell_break_cache_is_scoped() {
let mut d = cell_break_doc("\u{000c}Heading");
let Block::Table(t) = &*d.body[0] else { panic!("table") };
let p = t.rows[0].cells[0].blocks[0].clone();
d.body.push(p);
assert_eq!(lay(&d).pages.len(), 2, "a reused paragraph's body page break still starts a page");
}
#[test]
fn cell_breaks_with_bookmarks() {
for offset in [0, 1, 3] {
let mut d = cell_break_doc("\u{000c}\u{000c}\u{000c}Heading");
let Block::Table(t) = Arc::make_mut(&mut d.body[0]) else { panic!("table") };
let Block::Para(p) = Arc::make_mut(&mut t.rows[0].cells[0].blocks[0]) else { panic!("paragraph") };
p.insert_object(offset, InlineObject::BookmarkStart { name: "control".into() }, &Default::default()).unwrap();
let original = p.clone();
let l = lay(&d);
let para = l.pages[0]
.items
.iter()
.find_map(|it| match it {
Placed::Lines { para, .. } => Some(para),
_ => None,
})
.unwrap();
assert!(!para.clusters.iter().any(|c| c.kind == para::ClKind::PageBreak));
assert_eq!(para.lines.len(), 1);
assert_eq!(para.clusters.iter().find(|c| c.kind == para::ClKind::Text).unwrap().start, 6);
let Block::Table(t) = &*d.body[0] else { panic!("table") };
let Block::Para(p) = &*t.rows[0].cells[0].blocks[0] else { panic!("paragraph") };
assert_eq!(p, &original, "layout preserves bookmark objects and source bytes");
}
}
#[test]
fn cell_break_only_is_marker() {
let d = cell_break_doc("\u{000c}");
let l = lay(&d);
let para = l.pages[0]
.items
.iter()
.find_map(|it| match it {
Placed::Lines { para, .. } => Some(para),
_ => None,
})
.unwrap();
assert!(para.clusters.iter().all(|c| c.kind == para::ClKind::Marker));
}
+3
View File
@@ -193,6 +193,8 @@ Behind-text pictures in table cells participate in centre and bottom vertical al
TOC refresh reuses direct tab formatting from the first cached entry with a number-separator stop at each level for numbered entries, including imported range fields. Page-number tabs still use the TOC section width. No new default number-tab position is inferred.
Explicit cell page breaks (Word for Mac 16.113.2, compatibility modes 14 and 15): leading, interior and trailing breaks take no line height. Ordinary line breaks retain their lines; body page breaks still paginate. Synthetic regressions preserve UTF-8 cluster boundaries, bookmark objects and source offsets, including body/cell cache reuse without table-style formatting. Width, border strokes and residual row metrics remain outside this change.
## Revision history
| Date | Change | Summary |
@@ -221,6 +223,7 @@ TOC refresh reuses direct tab formatting from the first cached entry with a numb
| 2026-10-15 | trivial | Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. |
| 2026-10-16 | trivial | Zero-extent drawing anchors contribute no inline font height. Synthetic regressions. |
| 2026-10-16 | trivial | Preserve direct TOC number tabs during regeneration |
| 2026-10-16 | trivial | Explicit cell page breaks take no line height; original native Word controls in modes 14 and 15 and synthetic cluster/cache regressions. |
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |