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");
}
#[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;