use super::*;
#[test]
fn test_text_box_auto_numbered_paragraphs_group_into_list() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr indent="-216000"><a:buAutoNum type="arabicPeriod"/></a:pPr><a:r><a:t>First</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr indent="-216000"><a:buAutoNum type="arabicPeriod"/></a:pPr><a:r><a:t>Second</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
assert_eq!(blocks.len(), 1, "Expected a single grouped list block");
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
assert_eq!(list.kind, crate::ir::ListKind::Ordered);
assert_eq!(list.items.len(), 2);
assert_eq!(
list.level_styles
.get(&0)
.and_then(|style| style.numbering_pattern.as_deref()),
Some("1.")
);
assert_eq!(list.items[0].content[0].runs[0].text, "First");
assert_eq!(list.items[1].content[0].runs[0].text, "Second");
}
#[test]
fn test_text_box_bulleted_paragraphs_group_into_list() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr indent="-216000"><a:buChar char="•"/></a:pPr><a:r><a:t>First bullet</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr indent="-216000"><a:buChar char="•"/></a:pPr><a:r><a:t>Second bullet</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
assert_eq!(blocks.len(), 1, "Expected a single grouped list block");
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
assert_eq!(list.kind, crate::ir::ListKind::Unordered);
assert_eq!(list.items.len(), 2);
assert_eq!(list.items[0].content[0].runs[0].text, "First bullet");
assert_eq!(list.items[1].content[0].runs[0].text, "Second bullet");
}
#[test]
fn test_text_box_bulleted_paragraph_preserves_char_marker_and_uses_run_style() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr indent="-216000"><a:buFontTx/><a:buChar char="-"/></a:pPr>"#,
r#"<a:r><a:rPr lang="en-US" sz="1400"><a:solidFill><a:srgbClr val="112233"/></a:solidFill><a:latin typeface="Pretendard"/></a:rPr><a:t>First bullet</a:t></a:r>"#,
r#"</a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
let style = list.level_styles.get(&0).expect("Expected level 0 style");
assert_eq!(style.marker_text.as_deref(), Some("-"));
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_family.as_deref()),
Some("Pretendard")
);
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_size),
Some(14.0)
);
assert_eq!(
style.marker_style.as_ref().and_then(|style| style.color),
Some(Color::new(0x11, 0x22, 0x33))
);
}
#[test]
fn test_text_box_bulleted_paragraph_preserves_explicit_marker_font() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr indent="-216000"><a:buFont typeface="Wingdings"/><a:buChar char="è"/></a:pPr>"#,
r#"<a:r><a:rPr lang="en-US" sz="1400"><a:latin typeface="Pretendard"/></a:rPr><a:t>Symbol bullet</a:t></a:r>"#,
r#"</a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
let style = list.level_styles.get(&0).expect("Expected level 0 style");
assert_eq!(style.marker_text.as_deref(), Some("è"));
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_family.as_deref()),
Some("Wingdings")
);
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_size),
Some(14.0)
);
}
#[test]
fn test_text_box_paragraph_line_spacing_pct_extracted() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr><a:lnSpc><a:spcPct val="150000"/></a:lnSpc></a:pPr><a:r><a:t>First</a:t></a:r></a:p>"#,
r#"<a:p><a:r><a:t>Second</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
match paragraph.style.line_spacing {
Some(crate::ir::LineSpacing::Proportional(factor)) => {
assert!((factor - 1.5).abs() < f64::EPSILON);
}
other => panic!("Expected proportional line spacing, got {other:?}"),
}
}
#[test]
fn test_text_box_body_pr_defaults_and_center_anchor_extracted() {
let shape = make_text_box_with_body_pr(
0,
0,
1_000_000,
500_000,
r#"<a:bodyPr anchor="ctr"><a:spAutoFit/></a:bodyPr>"#,
"Centered",
);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let text_box = match &page.elements[0].kind {
FixedElementKind::TextBox(text_box) => text_box,
other => panic!("Expected TextBox, got {other:?}"),
};
assert!((text_box.padding.left - 7.2).abs() < 0.001);
assert!((text_box.padding.right - 7.2).abs() < 0.001);
assert!((text_box.padding.top - 3.6).abs() < 0.001);
assert!((text_box.padding.bottom - 3.6).abs() < 0.001);
assert_eq!(
text_box.vertical_align,
crate::ir::TextBoxVerticalAlign::Center
);
assert!(
text_box.auto_fit,
"spAutoFit text boxes should preserve the autofit hint in the IR"
);
assert!(!text_box.no_wrap);
}
#[test]
fn test_text_box_auto_numbered_paragraph_start_override_sets_list_start() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr indent="-216000"><a:buAutoNum type="alphaUcPeriod" startAt="3"/></a:pPr><a:r><a:t>Gamma</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr indent="-216000"><a:buAutoNum type="alphaUcPeriod"/></a:pPr><a:r><a:t>Delta</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
assert_eq!(list.kind, crate::ir::ListKind::Ordered);
assert_eq!(list.items[0].start_at, Some(3));
assert_eq!(
list.level_styles
.get(&0)
.and_then(|style| style.numbering_pattern.as_deref()),
Some("A.")
);
}
#[test]
fn test_text_box_auto_numbered_paragraph_extracts_hanging_indent() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr marL="457200" indent="-457200"><a:buAutoNum type="arabicParenR"/></a:pPr><a:r><a:t>First</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr marL="457200" indent="-457200"><a:buAutoNum type="arabicParenR"/></a:pPr><a:r><a:t>Second</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
let paragraph = &list.items[0].content[0];
assert_eq!(paragraph.style.indent_left, Some(36.0));
assert_eq!(paragraph.style.indent_first_line, Some(-36.0));
assert_eq!(
list.level_styles
.get(&0)
.and_then(|style| style.numbering_pattern.as_deref()),
Some("1)")
);
}
#[test]
fn test_text_box_auto_numbered_paragraph_resolves_marker_style_from_text() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr marL="457200" indent="-457200">"#,
r#"<a:buClrTx/><a:buSzTx/><a:buFontTx/><a:buAutoNum type="arabicParenR"/>"#,
r#"</a:pPr>"#,
r#"<a:r><a:rPr lang="ko-KR" sz="2000"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:latin typeface="Pretendard Medium"/></a:rPr><a:t>First</a:t></a:r>"#,
r#"</a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
let style = list.level_styles.get(&0).expect("Expected level 0 style");
assert_eq!(style.numbering_pattern.as_deref(), Some("1)"));
assert_eq!(style.marker_text, None);
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_family.as_deref()),
Some("Pretendard Medium")
);
assert_eq!(
style
.marker_style
.as_ref()
.and_then(|style| style.font_size),
Some(20.0)
);
assert_eq!(
style.marker_style.as_ref().and_then(|style| style.color),
Some(Color::black())
);
}
#[test]
fn test_text_box_paragraph_preserves_soft_line_breaks() {
let paragraphs_xml = concat!(
r#"<a:p>"#,
r#"<a:r><a:t>Line 1</a:t></a:r>"#,
r#"<a:br/>"#,
r#"<a:r><a:t>Line 2</a:t></a:r>"#,
r#"</a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
let text: String = paragraph.runs.iter().map(|run| run.text.as_str()).collect();
assert_eq!(text, "Line 1\u{000B}Line 2");
}
#[test]
fn test_text_box_plain_paragraph_between_bullets_breaks_list_sequence() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr marL="742950" lvl="1" indent="-285750"><a:buFontTx/><a:buChar char="-"/></a:pPr><a:r><a:t>1) First bullet</a:t></a:r></a:p>"#,
r#"<a:p><a:r><a:t>-> Continuation paragraph</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr marL="742950" lvl="1" indent="-285750"><a:buFontTx/><a:buChar char="-"/></a:pPr><a:r><a:t>2) Second bullet</a:t></a:r></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
assert_eq!(blocks.len(), 3, "Expected list / paragraph / list split");
match &blocks[0] {
Block::List(list) => {
assert_eq!(list.items.len(), 1);
assert_eq!(
list.level_styles
.get(&1)
.and_then(|style| style.marker_text.as_deref()),
Some("-")
);
}
other => panic!("Expected first block to be a list, got {other:?}"),
}
match &blocks[1] {
Block::Paragraph(paragraph) => {
let text: String = paragraph.runs.iter().map(|run| run.text.as_str()).collect();
assert_eq!(text, "-> Continuation paragraph");
}
other => panic!("Expected middle block to be a paragraph, got {other:?}"),
}
match &blocks[2] {
Block::List(list) => {
assert_eq!(list.items.len(), 1);
assert_eq!(
list.level_styles
.get(&1)
.and_then(|style| style.marker_text.as_deref()),
Some("-")
);
}
other => panic!("Expected last block to be a list, got {other:?}"),
}
}
#[test]
fn test_text_box_plain_paragraph_preserves_leading_arrow_text() {
let paragraphs_xml = r#"<a:p><a:r><a:t>-> Continuation paragraph</a:t></a:r></a:p>"#;
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected paragraph block, got {other:?}"),
};
let text: String = paragraph.runs.iter().map(|run| run.text.as_str()).collect();
assert_eq!(text, "-> Continuation paragraph");
}
#[test]
fn test_text_box_plain_paragraph_preserves_escaped_gt_entity() {
let paragraphs_xml = r#"<a:p><a:r><a:t>-> Continuation paragraph</a:t></a:r></a:p>"#;
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected paragraph block, got {other:?}"),
};
let text: String = paragraph.runs.iter().map(|run| run.text.as_str()).collect();
assert_eq!(text, "-> Continuation paragraph");
}
#[test]
fn test_text_box_trailing_empty_bullets_do_not_override_nested_marker_style() {
let paragraphs_xml = concat!(
r#"<a:p><a:pPr marL="742950" lvl="1" indent="-285750"><a:buFont typeface="Wingdings"/><a:buChar char="è"/></a:pPr><a:r><a:rPr lang="en-US" sz="1400"><a:latin typeface="Pretendard"/></a:rPr><a:t>Arrow bullet</a:t></a:r></a:p>"#,
r#"<a:p><a:pPr marL="285750" indent="-285750"><a:buFontTx/><a:buChar char="-"/></a:pPr></a:p>"#,
r#"<a:p><a:pPr marL="285750" indent="-285750"><a:buFontTx/><a:buChar char="-"/></a:pPr></a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 1_000_000, 500_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
assert_eq!(list.items.len(), 1);
assert_eq!(list.items[0].level, 1);
assert_eq!(
list.level_styles
.get(&1)
.and_then(|style| style.marker_text.as_deref()),
Some("è")
);
assert!(
!list.level_styles.contains_key(&0),
"Trailing empty dash bullets should not create a level-0 marker style"
);
}
#[test]
fn test_text_box_lst_style_default_run_props_are_applied_to_runs() {
let shape = String::from(
r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr><a:defRPr sz="1400" b="1"><a:solidFill><a:srgbClr val="032543"/></a:solidFill><a:latin typeface="Pretendard SemiBold"/><a:ea typeface="Pretendard SemiBold"/><a:cs typeface="Pretendard SemiBold"/></a:defRPr></a:lvl1pPr></a:lstStyle><a:p><a:r><a:rPr lang="ko-KR"/><a:t>경력</a:t></a:r></a:p></p:txBody></p:sp>"#,
);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
let run = ¶graph.runs[0];
assert_eq!(
run.style.font_family.as_deref(),
Some("Pretendard SemiBold")
);
assert_eq!(run.style.font_size, Some(14.0));
assert_eq!(run.style.bold, Some(true));
assert_eq!(run.style.color, Some(Color::new(0x03, 0x25, 0x43)));
}
#[test]
fn test_text_box_font_ref_default_color_is_not_overridden_by_run_line_fill() {
let slide_shape = concat!(
r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="OrgBox"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>"#,
r#"<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="2000000" cy="900000"/></a:xfrm>"#,
r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:ln><a:noFill/></a:ln></p:spPr>"#,
r#"<p:style>"#,
r#"<a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef>"#,
r#"<a:fillRef idx="1"><a:schemeClr val="lt1"/></a:fillRef>"#,
r#"<a:effectRef idx="0"><a:schemeClr val="accent1"/></a:effectRef>"#,
r#"<a:fontRef idx="minor"><a:schemeClr val="dk1"/></a:fontRef>"#,
r#"</p:style>"#,
r#"<p:txBody><a:bodyPr/><a:lstStyle/>"#,
r#"<a:p><a:pPr algn="ctr"/>"#,
r#"<a:r><a:rPr lang="ko-KR">"#,
r#"<a:ln><a:solidFill><a:sysClr val="window" lastClr="FFFFFF"><a:alpha val="0"/></a:sysClr></a:solidFill></a:ln>"#,
r#"<a:latin typeface="Pretendard"/><a:ea typeface="Pretendard"/></a:rPr><a:t>이동욱 이사</a:t></a:r>"#,
r#"</a:p></p:txBody></p:sp>"#,
);
let slide_xml = make_slide_xml(&[slide_shape.to_string()]);
let theme_xml = make_theme_xml(&standard_theme_colors(), "Pretendard", "Pretendard");
let data = build_test_pptx_with_theme(SLIDE_CX, SLIDE_CY, &[slide_xml], &theme_xml);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
assert_eq!(paragraph.runs.len(), 1);
assert_eq!(paragraph.runs[0].text, "이동욱 이사");
assert_eq!(
paragraph.runs[0].style.color,
Some(Color::new(0x00, 0x00, 0x00)),
"Run line fill should not overwrite the fontRef default text color"
);
}
#[test]
fn test_non_placeholder_shape_inherits_master_other_style_run_defaults() {
let slide_shape = concat!(
r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="Caption"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>"#,
r#"<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm></p:spPr>"#,
r#"<p:txBody><a:bodyPr/><a:lstStyle/>"#,
r#"<a:p><a:r><a:rPr lang="ko-KR"/><a:t>신</a:t></a:r><a:r><a:rPr lang="ko-KR" sz="1800"/><a:t>형</a:t></a:r></a:p>"#,
r#"</p:txBody></p:sp>"#,
);
let slide_xml = make_slide_xml(&[slide_shape.to_string()]);
let layout_xml = r#"<?xml version="1.0" encoding="UTF-8"?><p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>"#;
let master_xml = r#"<?xml version="1.0" encoding="UTF-8"?><p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:txStyles><p:otherStyle><a:defPPr><a:defRPr lang="ko-KR"/></a:defPPr><a:lvl1pPr marL="0"><a:defRPr sz="1800"><a:solidFill><a:srgbClr val="224466"/></a:solidFill><a:latin typeface="Pretendard"/><a:ea typeface="Pretendard"/><a:cs typeface="Pretendard"/></a:defRPr></a:lvl1pPr></p:otherStyle></p:txStyles><p:clrMap bg1="lt1" tx1="dk1" bg2="lt1" tx2="dk1" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:sldMaster>"#;
let data =
build_test_pptx_with_layout_master(SLIDE_CX, SLIDE_CY, &slide_xml, layout_xml, master_xml);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
let text: String = paragraph.runs.iter().map(|run| run.text.as_str()).collect();
assert_eq!(text, "신형");
assert!(
paragraph
.runs
.iter()
.all(|run| run.style.font_size == Some(18.0))
);
assert!(
paragraph
.runs
.iter()
.all(|run| run.style.font_family.as_deref() == Some("Pretendard"))
);
assert!(
paragraph
.runs
.iter()
.all(|run| run.style.color == Some(Color::new(0x22, 0x44, 0x66)))
);
}
#[test]
fn test_text_box_lst_style_overrides_master_other_style_run_defaults() {
let slide_shape = concat!(
r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="TextBox"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>"#,
r#"<p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="500000"/></a:xfrm></p:spPr>"#,
r#"<p:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr><a:defRPr sz="2400"><a:latin typeface="Pretendard SemiBold"/><a:ea typeface="Pretendard SemiBold"/><a:cs typeface="Pretendard SemiBold"/></a:defRPr></a:lvl1pPr></a:lstStyle>"#,
r#"<a:p><a:r><a:rPr lang="ko-KR"/><a:t>경력</a:t></a:r></a:p>"#,
r#"</p:txBody></p:sp>"#,
);
let slide_xml = make_slide_xml(&[slide_shape.to_string()]);
let layout_xml = r#"<?xml version="1.0" encoding="UTF-8"?><p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>"#;
let master_xml = r#"<?xml version="1.0" encoding="UTF-8"?><p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:txStyles><p:otherStyle><a:lvl1pPr marL="0"><a:defRPr sz="1800"><a:latin typeface="Pretendard"/></a:defRPr></a:lvl1pPr></p:otherStyle></p:txStyles><p:clrMap bg1="lt1" tx1="dk1" bg2="lt1" tx2="dk1" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:sldMaster>"#;
let data =
build_test_pptx_with_layout_master(SLIDE_CX, SLIDE_CY, &slide_xml, layout_xml, master_xml);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
let paragraph = match &blocks[0] {
Block::Paragraph(paragraph) => paragraph,
other => panic!("Expected Paragraph block, got {other:?}"),
};
assert_eq!(paragraph.runs[0].style.font_size, Some(24.0));
assert_eq!(
paragraph.runs[0].style.font_family.as_deref(),
Some("Pretendard SemiBold")
);
}
#[test]
fn test_text_box_auto_numbered_list_continues_when_run_has_explicit_default_attrs() {
// Reproduces a bug where three consecutive `arabicParenR` paragraphs
// were split into two lists (1,2 then 1) because the third paragraph's
// text runs had explicit default attributes (b="0", i="0", solidFill
// black, etc.) that made the resolved marker style differ structurally
// from the first two paragraphs even though the visual appearance was
// identical.
let paragraphs_xml = concat!(
// Paragraph 1: simple run properties
r#"<a:p><a:pPr marL="457200" indent="-457200">"#,
r#"<a:buAutoNum type="arabicParenR"/>"#,
r#"</a:pPr>"#,
r#"<a:r><a:rPr lang="ko-KR" sz="2000"><a:latin typeface="Pretendard Medium"/></a:rPr><a:t>First</a:t></a:r>"#,
r#"</a:p>"#,
// Paragraph 2: simple run properties
r#"<a:p><a:pPr marL="457200" indent="-457200">"#,
r#"<a:buAutoNum type="arabicParenR"/>"#,
r#"</a:pPr>"#,
r#"<a:r><a:rPr lang="ko-KR" sz="2000"><a:latin typeface="Pretendard Medium"/></a:rPr><a:t>Second</a:t></a:r>"#,
r#"</a:p>"#,
// Paragraph 3: explicit default attributes (b="0", i="0", solidFill black, etc.)
r#"<a:p><a:pPr marL="457200" indent="-457200">"#,
r#"<a:buAutoNum type="arabicParenR"/>"#,
r#"</a:pPr>"#,
r#"<a:r><a:rPr lang="en-US" sz="2000" b="0" i="0" u="none" strike="noStrike">"#,
r#"<a:solidFill><a:prstClr val="black"/></a:solidFill>"#,
r#"<a:latin typeface="Pretendard Medium"/>"#,
r#"</a:rPr><a:t>Third</a:t></a:r>"#,
r#"</a:p>"#,
);
let shape = make_multi_para_text_box(0, 0, 5_000_000, 3_000_000, paragraphs_xml);
let slide = make_slide_xml(&[shape]);
let data = build_test_pptx(SLIDE_CX, SLIDE_CY, &[slide]);
let parser = PptxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = first_fixed_page(&doc);
let blocks = text_box_blocks(&page.elements[0]);
assert_eq!(
blocks.len(),
1,
"All three paragraphs should be grouped into a single list block, got {blocks:?}"
);
let list = match &blocks[0] {
Block::List(list) => list,
other => panic!("Expected List block, got {other:?}"),
};
assert_eq!(list.kind, crate::ir::ListKind::Ordered);
assert_eq!(list.items.len(), 3, "List should have 3 items");
assert_eq!(list.items[0].content[0].runs[0].text, "First");
assert_eq!(list.items[1].content[0].runs[0].text, "Second");
assert_eq!(list.items[2].content[0].runs[0].text, "Third");
}