extern crate text_document_io as document_io;
use common::long_operation::{LongOperationManager, OperationStatus};
use document_io::docx_rs::{DocumentChild, Docx, Paragraph, ParagraphChild, RunChild};
use document_io::{ExportDocxDto, ImportDjotDto, document_io_controller};
use proptest::prelude::*;
use test_harness::setup;
#[derive(Debug, Clone)]
enum GenBlock {
Plain(String),
Heading(u8, String),
Aligned(&'static str, String),
Bullet(String),
Ordered(String),
Code(String),
Task(bool, String),
}
impl GenBlock {
fn words(&self) -> Vec<&str> {
match self {
GenBlock::Plain(w)
| GenBlock::Heading(_, w)
| GenBlock::Aligned(_, w)
| GenBlock::Bullet(w)
| GenBlock::Ordered(w)
| GenBlock::Code(w)
| GenBlock::Task(_, w) => vec![w.as_str()],
}
}
fn emit(&self) -> String {
match self {
GenBlock::Plain(w) => w.clone(),
GenBlock::Heading(level, w) => format!("{} {}", "#".repeat(*level as usize), w),
GenBlock::Aligned(a, w) => format!("{{alignment={a}}}\n{w}"),
GenBlock::Bullet(w) => format!("- {w}"),
GenBlock::Ordered(w) => format!("1. {w}"),
GenBlock::Code(w) => format!("```\n{w}\n```"),
GenBlock::Task(true, w) => format!("- [x] {w}"),
GenBlock::Task(false, w) => format!("- [ ] {w}"),
}
}
}
fn word() -> impl Strategy<Value = String> {
"[a-z]{3,8}"
}
fn gen_block() -> impl Strategy<Value = GenBlock> {
prop_oneof![
word().prop_map(GenBlock::Plain),
(1u8..=6, word()).prop_map(|(l, w)| GenBlock::Heading(l, w)),
(
prop_oneof![Just("left"), Just("right"), Just("center"), Just("justify")],
word()
)
.prop_map(|(a, w)| GenBlock::Aligned(a, w)),
word().prop_map(GenBlock::Bullet),
word().prop_map(GenBlock::Ordered),
word().prop_map(GenBlock::Code),
(any::<bool>(), word()).prop_map(|(c, w)| GenBlock::Task(c, w)),
]
}
fn emit_doc(blocks: &[GenBlock]) -> String {
blocks
.iter()
.map(GenBlock::emit)
.collect::<Vec<_>>()
.join("\n\n")
}
fn build_docx(djot: &str) -> Docx {
let (db, ev, _) = setup().expect("setup");
let mut mgr = LongOperationManager::new();
let op = document_io_controller::import_djot(
&db,
&ev,
&mut mgr,
&ImportDjotDto {
djot_text: djot.to_string(),
options: Default::default(),
},
)
.expect("import_djot");
while let Some(OperationStatus::Running) = mgr.get_operation_status(&op) {
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_eq!(
mgr.get_operation_status(&op),
Some(OperationStatus::Completed),
"import did not complete for {djot:?}"
);
document_io_controller::build_docx_document(&db, &ExportDocxDto::default())
.expect("build_docx_document must not fail")
}
fn paragraphs(docx: &Docx) -> Vec<&Paragraph> {
docx.document
.children
.iter()
.filter_map(|c| match c {
DocumentChild::Paragraph(p) => Some(&**p),
_ => None,
})
.collect()
}
fn collect_text(children: &[ParagraphChild], out: &mut String) {
for child in children {
match child {
ParagraphChild::Run(run) => {
for rc in &run.children {
if let RunChild::Text(t) = rc {
out.push_str(&t.text);
}
}
}
ParagraphChild::Hyperlink(h) => collect_text(&h.children, out),
_ => {}
}
}
}
fn all_text(docx: &Docx) -> String {
let mut s = String::new();
for p in paragraphs(docx) {
collect_text(&p.children, &mut s);
s.push('\n');
}
s
}
proptest! {
#![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
#[test]
fn export_is_total_and_lossless(blocks in prop::collection::vec(gen_block(), 1..8)) {
let djot = emit_doc(&blocks);
let docx = build_docx(&djot);
let paras = paragraphs(&docx);
prop_assert!(
paras.len() >= blocks.len(),
"expected >= {} paragraphs, got {}\n--- djot ---\n{}",
blocks.len(),
paras.len(),
djot
);
let text = all_text(&docx);
for block in &blocks {
for w in block.words() {
prop_assert!(
text.contains(w),
"word {:?} missing from export\n--- djot ---\n{}\n--- text ---\n{}",
w, djot, text
);
}
}
for p in ¶s {
if let Some(np) = &p.property.numbering_property
&& let Some(id) = &np.id
{
let num = docx.numberings.numberings.iter().find(|n| n.id == id.id);
prop_assert!(
num.is_some(),
"paragraph references numbering id {} with no definition",
id.id
);
let abstract_id = num.unwrap().abstract_num_id;
prop_assert!(
docx.numberings.abstract_nums.iter().any(|a| a.id == abstract_id),
"numbering {} references missing abstract numbering {}",
id.id, abstract_id
);
}
}
}
}