use super::*;
#[test]
fn test_table_cell_with_multiple_paragraphs() {
let multi_para_cell = TableCell {
content: vec![
Block::Paragraph(Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "First para".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}),
Block::Paragraph(Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "Second para".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}),
],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![multi_para_cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let doc = make_doc(vec![make_flow_page(vec![Block::Table(table)])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains("First para"),
"Expected First para in: {result}"
);
assert!(
result.contains("Second para"),
"Expected Second para in: {result}"
);
}
#[test]
fn test_table_cell_simple_list_uses_compact_fixed_text_layout() {
let list = List {
kind: ListKind::Unordered,
items: vec![
ListItem {
content: vec![Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "First item".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
ListItem {
content: vec![Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "Second item".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
],
level_styles: BTreeMap::new(),
};
let cell = TableCell {
content: vec![Block::List(list)],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let doc = make_doc(vec![make_flow_page(vec![Block::Table(table)])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains("#stack(dir: ttb"),
"Expected compact stack-based list layout in: {result}"
);
assert!(
!result.contains("#list("),
"Compact table-cell lists should not use Typst list layout in: {result}"
);
assert!(result.contains("First item"));
assert!(result.contains("Second item"));
}
#[test]
fn test_table_cell_simple_list_treats_default_and_explicit_left_as_same_style() {
let list = List {
kind: ListKind::Unordered,
items: vec![
ListItem {
content: vec![Paragraph {
style: ParagraphStyle {
alignment: Some(Alignment::Left),
..ParagraphStyle::default()
},
runs: vec![Run {
text: "First item".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
ListItem {
content: vec![Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "Second item".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
],
level_styles: BTreeMap::new(),
};
let cell = TableCell {
content: vec![Block::List(list)],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let doc = make_doc(vec![make_flow_page(vec![Block::Table(table)])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains("#stack(dir: ttb"),
"Expected compact stack-based list layout when only left-alignment explicitness differs: {result}"
);
assert!(
!result.contains("#list("),
"Equivalent left-alignment styles should not force Typst list layout in: {result}"
);
}
#[test]
fn test_table_cell_compact_list_adds_inter_item_spacing_from_line_spacing() {
let list = List {
kind: ListKind::Unordered,
items: vec![
ListItem {
content: vec![Paragraph {
style: ParagraphStyle {
line_spacing: Some(LineSpacing::Proportional(1.5)),
..ParagraphStyle::default()
},
runs: vec![Run {
text: "First item".to_string(),
style: TextStyle {
font_size: Some(24.0),
..TextStyle::default()
},
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
ListItem {
content: vec![Paragraph {
style: ParagraphStyle {
line_spacing: Some(LineSpacing::Proportional(1.5)),
..ParagraphStyle::default()
},
runs: vec![Run {
text: "Second item".to_string(),
style: TextStyle {
font_size: Some(24.0),
..TextStyle::default()
},
href: None,
footnote: None,
}],
}],
level: 0,
start_at: None,
},
],
level_styles: BTreeMap::new(),
};
let cell = TableCell {
content: vec![Block::List(list)],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let doc = make_doc(vec![make_flow_page(vec![Block::Table(table)])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains("#set par(leading: 12pt)"),
"Expected paragraph leading derived from PPT line spacing in: {result}"
);
assert!(
result.contains("#stack(dir: ttb, spacing: 12pt"),
"Compact table-cell lists should add inter-item spacing derived from PPT line spacing in: {result}"
);
}
#[test]
fn test_east_asian_table_cell_uses_natural_line_height_not_grid() {
let Some((ascender, descender, word_pitch_em)) =
crate::render::pdf::font_line_metrics_em("Libertinus Serif")
else {
return; };
let font_size: f64 = 10.0;
let metric_em: f64 = ascender + descender;
let top_em: f64 = word_pitch_em * ascender / metric_em;
let bottom_em: f64 = word_pitch_em * descender / metric_em;
let grid_leading = 18.0 - metric_em * font_size;
let cell = TableCell {
content: vec![Block::Paragraph(Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "회의 안건".to_string(),
style: TextStyle {
font_family: Some("Libertinus Serif".to_string()),
font_size: Some(font_size),
..TextStyle::default()
},
href: None,
footnote: None,
}],
})],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let mut page = match make_flow_page(vec![Block::Table(table)]) {
Page::Flow(flow) => flow,
_ => unreachable!(),
};
page.line_grid_pitch = Some(18.0);
let doc = make_doc(vec![Page::Flow(page)]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains(&format!(
"top-edge: {}em, bottom-edge: -{}em",
format_f64(top_em),
format_f64(bottom_em)
)),
"Korean cell must fill the full hhea line box: {result}"
);
assert!(
result.contains("#set par(leading: 0pt)"),
"cell line box uses zero leading (box already equals the full line): {result}"
);
assert!(
!result.contains(&format!("leading: {}pt", format_f64(grid_leading))),
"Korean cell must NOT snap to the grid pitch: {result}"
);
}
#[test]
fn test_latin_table_cell_uses_natural_line_height() {
let Some((ascender, descender, word_pitch_em)) =
crate::render::pdf::font_line_metrics_em("Libertinus Serif")
else {
return;
};
let font_size: f64 = 10.0;
let metric_em: f64 = ascender + descender;
let top_em: f64 = word_pitch_em * ascender / metric_em;
let bottom_em: f64 = word_pitch_em * descender / metric_em;
let cell = TableCell {
content: vec![Block::Paragraph(Paragraph {
style: ParagraphStyle::default(),
runs: vec![Run {
text: "Agenda".to_string(),
style: TextStyle {
font_family: Some("Libertinus Serif".to_string()),
font_size: Some(font_size),
..TextStyle::default()
},
href: None,
footnote: None,
}],
})],
..TableCell::default()
};
let table = Table {
rows: vec![TableRow {
cells: vec![cell],
height: None,
}],
column_widths: vec![200.0],
..Table::default()
};
let doc = make_doc(vec![make_flow_page(vec![Block::Table(table)])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains(&format!(
"top-edge: {}em, bottom-edge: -{}em",
format_f64(top_em),
format_f64(bottom_em)
)),
"Latin cell must fill the full hhea line box: {result}"
);
}