use super::*;
use crate::ir::*;
use std::collections::BTreeMap;
use std::io::Cursor;
fn build_docx_bytes(paragraphs: Vec<docx_rs::Paragraph>) -> Vec<u8> {
let mut docx = docx_rs::Docx::new();
for p in paragraphs {
docx = docx.add_paragraph(p);
}
let buf = Vec::new();
let mut cursor = Cursor::new(buf);
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
fn build_docx_bytes_with_page_setup(
paragraphs: Vec<docx_rs::Paragraph>,
width_twips: u32,
height_twips: u32,
margin_top: i32,
margin_bottom: i32,
margin_left: i32,
margin_right: i32,
) -> Vec<u8> {
let mut docx = docx_rs::Docx::new()
.page_size(width_twips, height_twips)
.page_margin(
docx_rs::PageMargin::new()
.top(margin_top)
.bottom(margin_bottom)
.left(margin_left)
.right(margin_right),
);
for p in paragraphs {
docx = docx.add_paragraph(p);
}
let buf = Vec::new();
let mut cursor = Cursor::new(buf);
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
fn first_run(doc: &Document) -> &Run {
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let para = match &page.content[0] {
Block::Paragraph(p) => p,
_ => panic!("Expected Paragraph"),
};
¶.runs[0]
}
fn first_paragraph(doc: &Document) -> &Paragraph {
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
match &page.content[0] {
Block::Paragraph(p) => p,
_ => panic!("Expected Paragraph block"),
}
}
fn all_blocks(doc: &Document) -> &[Block] {
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
&page.content
}
#[path = "docx_foundation_tests.rs"]
mod foundation_tests;
fn build_docx_with_table(table: docx_rs::Table) -> Vec<u8> {
let docx = docx_rs::Docx::new().add_table(table);
let buf = Vec::new();
let mut cursor = Cursor::new(buf);
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
fn first_table(doc: &Document) -> &crate::ir::Table {
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
for block in &page.content {
if let Block::Table(t) = block {
return t;
}
}
panic!("No Table block found");
}
#[path = "docx_table_tests.rs"]
mod table_tests;
#[path = "docx_image_tests.rs"]
mod image_tests;
fn build_docx_with_numbering(
abstract_nums: Vec<docx_rs::AbstractNumbering>,
numberings: Vec<docx_rs::Numbering>,
paragraphs: Vec<docx_rs::Paragraph>,
) -> Vec<u8> {
let mut nums = docx_rs::Numberings::new();
for an in abstract_nums {
nums = nums.add_abstract_numbering(an);
}
for n in numberings {
nums = nums.add_numbering(n);
}
let mut docx = docx_rs::Docx::new().numberings(nums);
for p in paragraphs {
docx = docx.add_paragraph(p);
}
let mut cursor = Cursor::new(Vec::new());
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
#[test]
fn test_parse_simple_bulleted_list() {
let abstract_num = docx_rs::AbstractNumbering::new(0).add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("bullet"),
docx_rs::LevelText::new("•"),
docx_rs::LevelJc::new("left"),
));
let numbering = docx_rs::Numbering::new(1, 0);
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Item A"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Item B"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Item C"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let lists: Vec<&List> = page
.content
.iter()
.filter_map(|b| match b {
Block::List(l) => Some(l),
_ => None,
})
.collect();
assert_eq!(lists.len(), 1, "Expected 1 list block");
assert_eq!(lists[0].kind, ListKind::Unordered);
assert_eq!(lists[0].items.len(), 3);
assert_eq!(lists[0].items[0].level, 0);
assert_eq!(
lists[0].level_styles.get(&0),
Some(&ListLevelStyle {
kind: ListKind::Unordered,
numbering_pattern: None,
full_numbering: false,
marker_text: None,
marker_style: None,
})
);
let text0: String = lists[0].items[0]
.content
.iter()
.flat_map(|p| p.runs.iter().map(|r| r.text.as_str()))
.collect();
assert_eq!(text0, "Item A");
}
#[test]
fn test_parse_simple_numbered_list() {
let abstract_num = docx_rs::AbstractNumbering::new(0).add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("decimal"),
docx_rs::LevelText::new("%1."),
docx_rs::LevelJc::new("left"),
));
let numbering = docx_rs::Numbering::new(1, 0);
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("First"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Second"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let lists: Vec<&List> = page
.content
.iter()
.filter_map(|b| match b {
Block::List(l) => Some(l),
_ => None,
})
.collect();
assert_eq!(lists.len(), 1, "Expected 1 list block");
assert_eq!(lists[0].kind, ListKind::Ordered);
assert_eq!(lists[0].items.len(), 2);
assert_eq!(lists[0].items[0].start_at, Some(1));
assert_eq!(
lists[0].level_styles.get(&0),
Some(&ListLevelStyle {
kind: ListKind::Ordered,
numbering_pattern: Some("1.".to_string()),
full_numbering: false,
marker_text: None,
marker_style: None,
})
);
}
#[test]
fn test_parse_nested_multi_level_list() {
let abstract_num = docx_rs::AbstractNumbering::new(0)
.add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("bullet"),
docx_rs::LevelText::new("•"),
docx_rs::LevelJc::new("left"),
))
.add_level(docx_rs::Level::new(
1,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("bullet"),
docx_rs::LevelText::new("◦"),
docx_rs::LevelJc::new("left"),
));
let numbering = docx_rs::Numbering::new(1, 0);
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Top level"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Nested item"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(1)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Back to top"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let lists: Vec<&List> = page
.content
.iter()
.filter_map(|b| match b {
Block::List(l) => Some(l),
_ => None,
})
.collect();
assert_eq!(lists.len(), 1, "Expected 1 list block");
assert_eq!(lists[0].items.len(), 3);
assert_eq!(lists[0].items[0].level, 0);
assert_eq!(lists[0].items[1].level, 1);
assert_eq!(lists[0].items[2].level, 0);
assert_eq!(
lists[0].level_styles.get(&1),
Some(&ListLevelStyle {
kind: ListKind::Unordered,
numbering_pattern: None,
full_numbering: false,
marker_text: None,
marker_style: None,
})
);
}
#[test]
fn test_parse_numbered_list_start_override() {
let abstract_num = docx_rs::AbstractNumbering::new(0).add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("decimal"),
docx_rs::LevelText::new("%1."),
docx_rs::LevelJc::new("left"),
));
let numbering =
docx_rs::Numbering::new(1, 0).add_override(docx_rs::LevelOverride::new(0).start(3));
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Third"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Fourth"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let list = page
.content
.iter()
.find_map(|block| match block {
Block::List(list) => Some(list),
_ => None,
})
.expect("Expected list block");
assert_eq!(list.items[0].start_at, Some(3));
assert_eq!(list.items[1].start_at, None);
assert_eq!(
list.level_styles.get(&0),
Some(&ListLevelStyle {
kind: ListKind::Ordered,
numbering_pattern: Some("1.".to_string()),
full_numbering: false,
marker_text: None,
marker_style: None,
})
);
}
#[test]
fn test_parse_mixed_ordered_and_bulleted_levels() {
let abstract_num = docx_rs::AbstractNumbering::new(0)
.add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("decimal"),
docx_rs::LevelText::new("%1."),
docx_rs::LevelJc::new("left"),
))
.add_level(docx_rs::Level::new(
1,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("bullet"),
docx_rs::LevelText::new("•"),
docx_rs::LevelJc::new("left"),
));
let numbering = docx_rs::Numbering::new(1, 0);
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Step"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Bullet child"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(1)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let list = page
.content
.iter()
.find_map(|block| match block {
Block::List(list) => Some(list),
_ => None,
})
.expect("Expected list block");
assert_eq!(list.kind, ListKind::Ordered);
assert_eq!(
list.level_styles,
BTreeMap::from([
(
0,
ListLevelStyle {
kind: ListKind::Ordered,
numbering_pattern: Some("1.".to_string()),
full_numbering: false,
marker_text: None,
marker_style: None,
},
),
(
1,
ListLevelStyle {
kind: ListKind::Unordered,
numbering_pattern: None,
full_numbering: false,
marker_text: None,
marker_style: None,
},
),
])
);
}
#[test]
fn test_parse_mixed_list_and_paragraphs() {
let abstract_num = docx_rs::AbstractNumbering::new(0).add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("decimal"),
docx_rs::LevelText::new("%1."),
docx_rs::LevelJc::new("left"),
));
let numbering = docx_rs::Numbering::new(1, 0);
let data = build_docx_with_numbering(
vec![abstract_num],
vec![numbering],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Item 1"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Item 2"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Regular paragraph")),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let list_count = page
.content
.iter()
.filter(|b| matches!(b, Block::List(_)))
.count();
let para_count = page
.content
.iter()
.filter(|b| matches!(b, Block::Paragraph(_)))
.count();
assert!(list_count >= 1, "Expected at least 1 list block");
assert!(para_count >= 1, "Expected at least 1 paragraph block");
}
#[test]
fn test_merges_adjacent_lists_with_different_num_ids() {
let abstract_num = docx_rs::AbstractNumbering::new(0)
.add_level(docx_rs::Level::new(
0,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("decimal"),
docx_rs::LevelText::new("%1."),
docx_rs::LevelJc::new("left"),
))
.add_level(docx_rs::Level::new(
1,
docx_rs::Start::new(1),
docx_rs::NumberFormat::new("bullet"),
docx_rs::LevelText::new("\u{2022}"),
docx_rs::LevelJc::new("left"),
));
let data = build_docx_with_numbering(
vec![abstract_num],
vec![docx_rs::Numbering::new(1, 0), docx_rs::Numbering::new(2, 0)],
vec![
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("First"))
.numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Second"))
.numbering(docx_rs::NumberingId::new(2), docx_rs::IndentLevel::new(0)),
docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Sub"))
.numbering(docx_rs::NumberingId::new(2), docx_rs::IndentLevel::new(1)),
],
);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let lists: Vec<&List> = page
.content
.iter()
.filter_map(|block| match block {
Block::List(list) => Some(list),
_ => None,
})
.collect();
assert_eq!(
lists.len(),
1,
"adjacent list paragraphs must merge into a single list"
);
let list = lists[0];
assert_eq!(list.kind, ListKind::Ordered);
assert_eq!(list.items.len(), 3);
assert_eq!(list.items[0].level, 0);
assert_eq!(list.items[0].start_at, Some(1));
assert_eq!(list.items[1].level, 0);
assert_eq!(
list.items[1].start_at, None,
"the second ordered item continues counting (-> 2.), it must not restart"
);
assert_eq!(
list.items[2].level, 1,
"the sub-item stays nested at level 1"
);
assert_eq!(
list.level_styles.get(&0).map(|style| style.kind),
Some(ListKind::Ordered)
);
assert_eq!(
list.level_styles.get(&1).map(|style| style.kind),
Some(ListKind::Unordered)
);
}
#[test]
fn test_preserves_empty_paragraph_after_drawing_only_anchor() {
let document_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<w:body>
<w:p><w:r><mc:AlternateContent><mc:Choice Requires="wps"><w:drawing>
<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="1" behindDoc="0" locked="0" layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="column"><wp:posOffset>366395</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="paragraph"><wp:posOffset>141605</wp:posOffset></wp:positionV>
<wp:extent cx="1590675" cy="733425"/>
<wp:wrapNone/>
<wp:docPr id="1" name="Shape 1"/>
<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<wps:wsp><wps:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="1590840" cy="733320"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
<a:solidFill><a:srgbClr val="729fcf"/></a:solidFill>
<a:ln w="0"><a:solidFill><a:srgbClr val="3465a4"/></a:solidFill></a:ln>
</wps:spPr></wps:wsp>
</a:graphicData></a:graphic>
</wp:anchor></w:drawing></mc:Choice></mc:AlternateContent></w:r></w:p>
<w:p><w:r><w:t>After drawing</w:t></w:r></w:p>
<w:sectPr/>
</w:body></w:document>"#;
let parser = DocxParser;
let (doc, _warnings) = parser
.parse(
&build_docx_with_math(document_xml),
&ConvertOptions::default(),
)
.unwrap();
let blocks = all_blocks(&doc);
assert!(
matches!(blocks[0], Block::FloatingShape(_)),
"drawing-only paragraph should emit the floating shape first"
);
assert!(
matches!(&blocks[1], Block::Paragraph(paragraph) if paragraph.runs.is_empty()),
"drawing-only paragraph mark must remain as an empty paragraph spacer"
);
assert!(
matches!(&blocks[2], Block::Paragraph(paragraph) if paragraph.runs[0].text == "After drawing"),
"following content should stay after the preserved paragraph mark"
);
}
#[path = "docx_page_feature_tests.rs"]
mod page_feature_tests;
fn build_docx_bytes_with_styles(
paragraphs: Vec<docx_rs::Paragraph>,
styles: Vec<docx_rs::Style>,
) -> Vec<u8> {
let mut docx = docx_rs::Docx::new();
for s in styles {
docx = docx.add_style(s);
}
for p in paragraphs {
docx = docx.add_paragraph(p);
}
let buf = Vec::new();
let mut cursor = Cursor::new(buf);
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
fn build_docx_bytes_with_stylesheet(
paragraphs: Vec<docx_rs::Paragraph>,
styles: docx_rs::Styles,
) -> Vec<u8> {
let mut docx = docx_rs::Docx::new().styles(styles);
for p in paragraphs {
docx = docx.add_paragraph(p);
}
let buf = Vec::new();
let mut cursor = Cursor::new(buf);
docx.build().pack(&mut cursor).unwrap();
cursor.into_inner()
}
#[path = "docx_style_tests.rs"]
mod style_tests;
#[test]
fn test_hyperlink_single_link_in_paragraph() {
let link = docx_rs::Hyperlink::new("https://example.com", docx_rs::HyperlinkType::External)
.add_run(docx_rs::Run::new().add_text("Click here"));
let para = docx_rs::Paragraph::new().add_hyperlink(link);
let data = build_docx_bytes(vec![para]);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let para = match &page.content[0] {
Block::Paragraph(p) => p,
_ => panic!("Expected Paragraph"),
};
assert_eq!(para.runs.len(), 1);
assert_eq!(para.runs[0].text, "Click here");
assert_eq!(para.runs[0].href, Some("https://example.com".to_string()));
}
#[test]
fn test_hyperlink_mixed_text_and_link() {
let link = docx_rs::Hyperlink::new("https://rust-lang.org", docx_rs::HyperlinkType::External)
.add_run(docx_rs::Run::new().add_text("Rust"));
let para = docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("Visit "))
.add_hyperlink(link)
.add_run(docx_rs::Run::new().add_text(" for more."));
let data = build_docx_bytes(vec![para]);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let para = match &page.content[0] {
Block::Paragraph(p) => p,
_ => panic!("Expected Paragraph"),
};
assert_eq!(para.runs.len(), 3);
assert_eq!(para.runs[0].text, "Visit ");
assert_eq!(para.runs[0].href, None);
assert_eq!(para.runs[1].text, "Rust");
assert_eq!(para.runs[1].href, Some("https://rust-lang.org".to_string()));
assert_eq!(para.runs[2].text, " for more.");
assert_eq!(para.runs[2].href, None);
}
#[test]
fn test_hyperlink_multiple_links_in_paragraph() {
let link1 = docx_rs::Hyperlink::new("https://first.com", docx_rs::HyperlinkType::External)
.add_run(docx_rs::Run::new().add_text("First"));
let link2 = docx_rs::Hyperlink::new("https://second.com", docx_rs::HyperlinkType::External)
.add_run(docx_rs::Run::new().add_text("Second"));
let para = docx_rs::Paragraph::new()
.add_hyperlink(link1)
.add_run(docx_rs::Run::new().add_text(" and "))
.add_hyperlink(link2);
let data = build_docx_bytes(vec![para]);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let page = match &doc.pages[0] {
Page::Flow(p) => p,
_ => panic!("Expected FlowPage"),
};
let para = match &page.content[0] {
Block::Paragraph(p) => p,
_ => panic!("Expected Paragraph"),
};
assert_eq!(para.runs.len(), 3);
assert_eq!(para.runs[0].text, "First");
assert_eq!(para.runs[0].href, Some("https://first.com".to_string()));
assert_eq!(para.runs[1].text, " and ");
assert_eq!(para.runs[1].href, None);
assert_eq!(para.runs[2].text, "Second");
assert_eq!(para.runs[2].href, Some("https://second.com".to_string()));
}
#[path = "docx_notes_textbox_tests.rs"]
mod notes_textbox_tests;
fn build_docx_with_math(document_xml: &str) -> Vec<u8> {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let options = zip::write::FileOptions::default();
zip.start_file("[Content_Types].xml", options).unwrap();
std::io::Write::write_all(
&mut zip,
br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#,
)
.unwrap();
zip.start_file("_rels/.rels", options).unwrap();
std::io::Write::write_all(
&mut zip,
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/_rels/document.xml.rels", options)
.unwrap();
std::io::Write::write_all(
&mut zip,
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#,
)
.unwrap();
zip.start_file("word/document.xml", options).unwrap();
std::io::Write::write_all(&mut zip, document_xml.as_bytes()).unwrap();
zip.finish().unwrap().into_inner()
}
fn build_docx_with_columns(document_xml: &str) -> Vec<u8> {
build_docx_with_math(document_xml)
}
#[path = "docx_layout_rtl_tests.rs"]
mod layout_rtl_tests;
#[path = "docx_math_chart_metadata_tests.rs"]
mod math_chart_metadata_tests;
#[test]
fn issue_189_footer_preserves_inline_image_and_rtl_text() {
let data = include_bytes!("../../../../tests/fixtures/docx/issue_189_footer_image_rtl.docx");
let parser = DocxParser;
let (document, warnings) = parser
.parse(data, &ConvertOptions::default())
.expect("issue #189 fixture should parse");
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
let Page::Flow(page) = &document.pages[0] else {
panic!("expected flow page");
};
let footer = page.footer.as_ref().expect("default footer");
assert_eq!(footer.paragraphs.len(), 3);
assert_eq!(footer.paragraphs[0].elements.len(), 1, "footer image");
assert_eq!(footer.paragraphs[1].elements.len(), 1, "French footer text");
assert_eq!(
footer.paragraphs[2].style.direction,
Some(TextDirection::Rtl),
"Arabic footer paragraph direction"
);
let footer_text: String = footer
.paragraphs
.iter()
.flat_map(|paragraph| ¶graph.elements)
.filter_map(|element| match element {
HFInline::Run(run) => Some(run.text.as_str()),
_ => None,
})
.collect();
assert!(footer_text.contains("Généré par m3llm.cafe"));
assert!(footer_text.contains("صنع بواسطة m3llm.cafe"));
let typst = crate::render::typst_gen::generate_typst(&document)
.expect("issue #189 fixture should generate Typst");
assert_eq!(typst.images.len(), 1, "footer image asset");
assert!(typst.source.contains("#image(\"img-0.png\""));
assert!(typst.source.contains("#text(dir: rtl)["));
assert!(typst.source.contains("footer_content = block(width: 100%)"));
assert!(typst.source.contains("-measure(footer_content).height / 2"));
assert!(typst.source.contains("Généré par m3llm.cafe"));
assert!(typst.source.contains("صنع بواسطة m3llm.cafe"));
let result = crate::convert_bytes(data, crate::Format::Docx, &ConvertOptions::default())
.expect("issue #189 fixture should convert to PDF");
let pdf_text = pdf_extract::extract_text_from_mem(&result.pdf)
.expect("issue #189 PDF text should extract");
assert!(pdf_text.contains("Généré par m3llm.cafe"));
assert!(pdf_text.contains("عنص"), "extracted PDF text: {pdf_text:?}");
assert!(
pdf_text.contains("ةطساوب"),
"extracted PDF text: {pdf_text:?}"
);
}