mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 05:17:25 -06:00
Layout: paragraph spacing adds up when HTML auto spacing is off (#1612)
space after and the second's space before, as Word does by default. Word adds them instead when the document's compatibility options include w:doNotUseHTMLParagraphAutoSpacing. Measured in Word with five paragraphs of 12 pt before and after: 24 pt between them with the option, 12 pt without, the same in compatibility modes 14 and 15. A 60-page report with the option lost 6 pt per paragraph, about 60 pt on its cover. The option is read, laid out in the body and in boxes, and saved back. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
f3f110b442
commit
fe9d560b49
@@ -494,6 +494,10 @@ pub struct Settings {
|
||||
/// 2013 and later, which places a table's border at the margin rather than its text and lets
|
||||
/// justified lines shrink their spaces to fit more text.
|
||||
pub compat_mode: u32,
|
||||
/// `w:doNotUseHTMLParagraphAutoSpacing`: the space between two paragraphs is the first's space
|
||||
/// after plus the second's space before, not the larger of the two.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub sum_paragraph_spacing: bool,
|
||||
/// Keep contextual paragraph spacing inside legacy table cells (`allowSpaceOfSameStyleInTable`).
|
||||
/// Word ignores the layout override in compatibility mode 15, but the flag still round-trips.
|
||||
pub allow_space_of_same_style_in_table: bool,
|
||||
@@ -628,6 +632,7 @@ impl Default for Settings {
|
||||
protect_formatting: false,
|
||||
protect_hash: Vec::new(),
|
||||
compat_mode: COMPAT_MODE_CURRENT,
|
||||
sum_paragraph_spacing: false,
|
||||
allow_space_of_same_style_in_table: false,
|
||||
grid_h: DEFAULT_GRID,
|
||||
grid_v: DEFAULT_GRID,
|
||||
|
||||
@@ -1069,6 +1069,7 @@ impl Reader<'_> {
|
||||
if let Some(m) = mode.and_then(u32_of) {
|
||||
s.compat_mode = m.clamp(11, 99);
|
||||
}
|
||||
s.sum_paragraph_spacing = k.children("w:doNotUseHTMLParagraphAutoSpacing").any(on_off);
|
||||
}
|
||||
"w:trackRevisions" => s.track_changes = on_off(k),
|
||||
"w:defaultTabStop" => {
|
||||
|
||||
@@ -1049,6 +1049,10 @@ fn settings_xml(doc: &Document, footnotes: bool, endnotes: bool, template_rel: O
|
||||
w.close(tag);
|
||||
}
|
||||
w.open("w:compat", &[]);
|
||||
// CT_Compat: the older switches come before the `w:compatSetting`s.
|
||||
if doc.settings.sum_paragraph_spacing {
|
||||
w.empty("w:doNotUseHTMLParagraphAutoSpacing", &[]);
|
||||
}
|
||||
if s.allow_space_of_same_style_in_table {
|
||||
w.empty("w:allowSpaceOfSameStyleInTable", &[]);
|
||||
}
|
||||
|
||||
@@ -2567,6 +2567,20 @@ fn text_frames_read_and_round_trip() {
|
||||
assert!(q.iter().any(|x| x.props.drop_cap == Some(2) && x.props.frame.is_none()));
|
||||
}
|
||||
|
||||
/// `w:doNotUseHTMLParagraphAutoSpacing` in the settings' `w:compat`: paragraph spacing adds up.
|
||||
#[test]
|
||||
fn html_auto_spacing_off_is_read_and_kept() {
|
||||
let settings = |compat: &str| {
|
||||
let s = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings {W_NS}><w:compat>{compat}</w:compat></w:settings>"#);
|
||||
let rels = [("rId9", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings", "settings.xml")];
|
||||
let d = wordcraft_docx::read(&docx("<w:p/>", &rels, &[("word/settings.xml", &s)])).unwrap();
|
||||
let again = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
|
||||
(d.settings.sum_paragraph_spacing, again.settings.sum_paragraph_spacing)
|
||||
};
|
||||
assert_eq!(settings("<w:doNotUseHTMLParagraphAutoSpacing/>"), (true, true));
|
||||
assert_eq!(settings(""), (false, false));
|
||||
}
|
||||
|
||||
/// A right triangle (`rtTriangle`) has its right angle at the bottom left: a cover page's white
|
||||
/// corner cut-outs drawn as isosceles triangles pointed the wrong way.
|
||||
#[test]
|
||||
|
||||
@@ -1083,7 +1083,7 @@ fn layout_box(
|
||||
// space after the paragraph above (the larger one counts).
|
||||
if pl.rp.before_auto && i == 0 {
|
||||
before = 0.0;
|
||||
} else if i > 0 && before > 0.0 && prev_after > 0.0 {
|
||||
} else if i > 0 && before > 0.0 && prev_after > 0.0 && !ctx.doc.settings.sum_paragraph_spacing {
|
||||
before = (before - prev_after).max(0.0);
|
||||
}
|
||||
y += if i == 0 { before } else { before.max(0.0) };
|
||||
@@ -3139,7 +3139,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
}
|
||||
// The space between two paragraphs is the larger of the first's space after and the
|
||||
// second's space before, not their sum.
|
||||
if before > 0.0 && after > 0.0 {
|
||||
if before > 0.0 && after > 0.0 && !ctx.doc.settings.sum_paragraph_spacing {
|
||||
before = (before - after).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1392,6 +1392,39 @@ fn space_between_paragraphs_is_the_larger_of_after_and_before() {
|
||||
assert!(gap(6.0, 12.0, true).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_between_paragraphs_is_their_sum_when_html_auto_spacing_is_off() {
|
||||
// Measured in Word (compatibility modes 14 and 15): with `w:doNotUseHTMLParagraphAutoSpacing`,
|
||||
// 12 pt after and 12 pt before leave 24 pt; without it, 12 pt.
|
||||
let gaps = |sum: bool| -> (f32, f32) {
|
||||
let mut d = Document::from_text("One\nTwo");
|
||||
d.settings.sum_paragraph_spacing = sum;
|
||||
for i in 0..2 {
|
||||
let p = d.para_mut(StoryRef::Body, &Path::top(i)).unwrap();
|
||||
(p.props.space_after, p.props.space_before) = (Some(12.0), Some(12.0));
|
||||
}
|
||||
let mut t = Table::new(1, 1, 468.0);
|
||||
t.rows[0].cells[0].blocks = d.body.clone();
|
||||
d.insert_block(StoryRef::Body, &Path::top(2), wordcraft_doc::Block::Table(t)).unwrap();
|
||||
let l = lay(&d);
|
||||
let tops = |cell: bool| -> Vec<(f32, f32)> {
|
||||
l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Placed::Lines { path, y, para, .. } if (path.0.len() == 4) == cell => Some((*y, para.height)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let gap = |ys: Vec<(f32, f32)>| ys[1].0 - (ys[0].0 + ys[0].1);
|
||||
(gap(tops(false)), gap(tops(true)))
|
||||
};
|
||||
let near = |(a, b): (f32, f32), want: f32| (a - want).abs() < 0.01 && (b - want).abs() < 0.01;
|
||||
assert!(near(gaps(true), 24.0), "{:?}", gaps(true));
|
||||
assert!(near(gaps(false), 12.0), "{:?}", gaps(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_between_paragraphs_in_a_cell_follows_the_body_rule() {
|
||||
// Three paragraphs in a cell, each 6pt before and 6pt after -> the gaps between them.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -159,6 +159,7 @@ No overall parity estimate changes from these probes.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | trivial | With `w:doNotUseHTMLParagraphAutoSpacing` the space between two paragraphs is the first's space after plus the second's space before, as measured in Word; without it, still the larger (#733) |
|
||||
| 2026-10-14 | trivial | An inherited header or footer sizes its percentage-width tables by the section that defines it, as measured in Word (a portrait footer table keeps its width on inheriting landscape pages) |
|
||||
| 2026-10-14 | trivial | An empty paragraph closing a section before a new page stays on a full page instead of starting a page of its own, as measured in Word |
|
||||
| 2026-10-14 | trivial | A table row with an at-least height doesn't split where less than that height is left on the page; it starts the next page whole, as measured in Word |
|
||||
|
||||
Reference in New Issue
Block a user