Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/bugfixes/issue-463/after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-463/before.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-463/gt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 3 additions & 11 deletions crates/office2pdf/src/render/typst_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1567,17 +1567,9 @@ fn generate_block(out: &mut String, block: &Block, ctx: &mut GenCtx) -> Result<(
let settings: Option<String> = first_paragraph.and_then(|paragraph| {
word_line_height_settings(&paragraph.runs, &paragraph.style, ctx.line_grid_pitch)
});
if let Some(settings) = settings {
out.push_str("#block(width: 100%)[\n");
out.push_str(&settings);
// The wrapper's line box spans Word's full line advance, so
// the item gaps are the raw `w:spacing` values.
generate_list(out, list, true)?;
out.push_str("]\n");
Ok(())
} else {
generate_list(out, list, false)
}
// `generate_list` emits the wrapper itself, so the line box and
// the list's own `w:spacing` gaps share one block (issue #463).
generate_list(out, list, settings.as_deref())
}
Block::MathEquation(math) => {
generate_math_equation(out, math);
Expand Down
62 changes: 59 additions & 3 deletions crates/office2pdf/src/render/typst_gen_list_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ fn test_generate_list_preserves_paragraph_spacing_between_items() {

assert!(output.source.contains("spacing: 19pt"), "{}", output.source);
assert!(
output.source.contains("#block(below: 19pt)"),
output.source.contains("#block(width: 100%, below: 19pt)"),
"{}",
output.source
);
Expand Down Expand Up @@ -269,7 +269,7 @@ fn test_generate_list_uses_word_line_box_and_boundary_spacing() {
assert!(source.contains("#set par(leading: 0pt)"), "{source}");
assert!(source.contains("spacing: 8pt"), "{source}");
assert!(
source.contains("#block(above: 0pt, below: 8pt)"),
source.contains("#block(width: 100%, above: 0pt, below: 8pt)"),
"{source}"
);
}
Expand Down Expand Up @@ -308,7 +308,7 @@ fn test_generate_list_combines_exact_line_height_with_paragraph_spacing() {

assert!(output.source.contains("spacing: 24pt"), "{}", output.source);
assert!(
output.source.contains("#block(below: 24pt)"),
output.source.contains("#block(width: 100%, below: 24pt)"),
"{}",
output.source
);
Expand Down Expand Up @@ -897,3 +897,59 @@ fn test_generate_list_metric_spacing_is_the_raw_paragraph_gap() {
"expected list below spacing to be the raw 4pt gap in: {source}"
);
}

#[test]
fn test_list_wrapper_block_carries_the_edge_spacing() {
// The line-height wrapper used to leave `above`/`below` unset, so
// Typst's own 1.2em default block spacing governed the gap between a
// list and its neighbours while the computed gap sat on an inner block
// where it could not reach the boundary. A numbered item followed by a
// shaded code paragraph opened ~10pt too far (issue #463).
use crate::ir::List;

let make_item = |text: &str| ListItem {
content: vec![Paragraph {
style: ParagraphStyle {
space_after: Some(4.0),
..ParagraphStyle::default()
},
runs: vec![Run {
text: text.to_string(),
style: TextStyle {
font_family: Some("Libertinus Serif".to_string()),
font_size: Some(10.0),
..TextStyle::default()
},
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
};
let list = List {
kind: ListKind::Ordered,
items: vec![
make_item("Convert a single file."),
make_item("Batch convert."),
],
level_styles: BTreeMap::new(),
};
let source = generate_typst(&make_doc(vec![make_flow_page(vec![Block::List(list)])]))
.unwrap()
.source;

assert!(
!source.contains("#block(width: 100%)["),
"the list wrapper must pin its own spacing, not inherit Typst's default: {source}"
);
let wrapper_start = source
.find("#block(width: 100%")
.expect("the list is wrapped in a full-width block");
let wrapper_params =
&source[wrapper_start..source[wrapper_start..].find(")[").unwrap() + wrapper_start];
assert!(
wrapper_params.contains("below: 4pt"),
"the outermost list block carries the item gap: {wrapper_params}"
);
}
39 changes: 27 additions & 12 deletions crates/office2pdf/src/render/typst_gen_lists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,21 @@ fn common_list_line_box(list: &List) -> Option<LineBox> {
.then_some(first)
}

/// Emit a list, wrapped in a single block that carries both the Word line
/// box (`line_height_settings`, when the paragraphs use one) and the list's
/// own `w:spacing` edge gaps.
///
/// The two used to be separate blocks, with the gaps on the inner one. Typst
/// does not apply block spacing at a container's edges, so the gaps never
/// reached the boundary and the outer wrapper fell back to Typst's own 1.2em
/// `block.spacing` — about 10pt too much after a one-item numbered list
/// (issue #463).
pub(super) fn generate_list(
out: &mut String,
list: &List,
wrapper_spans_full_line: bool,
line_height_settings: Option<&str>,
) -> Result<(), ConvertError> {
let wrapper_spans_full_line: bool = line_height_settings.is_some();
let root_level: u32 = list_root_level(list);
let style = list_style_for_level(list, root_level);
let fallback_marker_style = common_list_level_text_style(&list.items, root_level);
Expand All @@ -360,17 +370,22 @@ pub(super) fn generate_list(
let (space_before, space_after) = list_edge_spacing(list, root_level, wrapper_spans_full_line);
let line_box = common_list_line_box(list);
let start_at = list.items.first().and_then(|item| item.start_at);
if space_before.is_some() || space_after.is_some() {
out.push_str("#block(");
write_block_params(
out,
&ParagraphStyle {
space_before,
space_after,
..ParagraphStyle::default()
},
);
let needs_wrapper: bool =
space_before.is_some() || space_after.is_some() || line_height_settings.is_some();
if needs_wrapper {
// The wrapper must span the full line width: Typst blocks shrink to
// their content by default, which would strand a wide list item.
out.push_str("#block(width: 100%");
if let Some(above) = space_before {
let _ = write!(out, ", above: {}pt", format_f64(above));
}
if let Some(below) = space_after {
let _ = write!(out, ", below: {}pt", format_f64(below));
}
out.push_str(")[\n");
if let Some(settings) = line_height_settings {
out.push_str(settings);
}
write_line_box_settings(out, line_box);
}
write_list_open(
Expand All @@ -384,7 +399,7 @@ pub(super) fn generate_list(
);
generate_list_items(out, list, &list.items, root_level, wrapper_spans_full_line)?;
out.push_str(")\n");
if space_before.is_some() || space_after.is_some() {
if needs_wrapper {
out.push_str("]\n");
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/office2pdf/src/render/typst_gen_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ fn generate_cell_content(
if can_render_fixed_text_list_inline(list) {
generate_fixed_text_list(out, list, true, None)?;
} else {
generate_list(out, list, false)?;
generate_list(out, list, None)?;
}
}
Block::MathEquation(math) => generate_math_equation(out, math),
Expand Down
Loading