1use docx_rs::{
7 AbstractNumbering, BreakType, Docx, Hyperlink, HyperlinkType, IndentLevel, Level, LevelJc,
8 LevelText, NumberFormat, Numbering, NumberingId, Pic, RunFonts, SpecialIndentType, Start,
9};
10use easydoc_core::{
11 DocumentBlock, DocumentContent, DocumentImage, DocumentList, DocumentTable, DocumentTextRun,
12 HeadingLevel, Result,
13};
14
15const BULLET_NUM_ID: usize = 10;
17const DECIMAL_NUM_ID: usize = 11;
19
20thread_local! {
25 static RENDERED_MATH: std::cell::RefCell<Vec<(String, String, bool)>> =
26 const { std::cell::RefCell::new(Vec::new()) };
27}
28
29fn render_math_marker() -> String {
31 let n = RENDERED_MATH.with(|cell| cell.borrow().len());
32 format!("@@EASYDOC_MATH_{n}@@")
33}
34
35#[must_use]
41pub fn take_rendered_math() -> Vec<(String, String, bool)> {
42 RENDERED_MATH.with(|cell| std::mem::take(&mut *cell.borrow_mut()))
43}
44
45const DYNAMIC_ABSTRACT_NUM_START: usize = 100;
48const DYNAMIC_NUM_ID_START: usize = 100;
50
51fn add_list_numberings(docx: Docx) -> Docx {
57 let mut bullet_abstract = AbstractNumbering::new(0);
59 bullet_abstract.multi_level_type = Some("hybridMultilevel".to_string());
60 let bullet_abstract = bullet_abstract
61 .add_level(
62 Level::new(
63 0,
64 Start::new(1),
65 NumberFormat::new("bullet"),
66 LevelText::new("\u{2022}"), LevelJc::new("left"),
68 )
69 .indent(Some(720), Some(SpecialIndentType::Hanging(360)), None, None),
70 )
71 .add_level(
72 Level::new(
73 1,
74 Start::new(1),
75 NumberFormat::new("bullet"),
76 LevelText::new("\u{25E6}"), LevelJc::new("left"),
78 )
79 .indent(
80 Some(1080),
81 Some(SpecialIndentType::Hanging(360)),
82 None,
83 None,
84 ),
85 )
86 .add_level(
87 Level::new(
88 2,
89 Start::new(1),
90 NumberFormat::new("bullet"),
91 LevelText::new("\u{25AA}"), LevelJc::new("left"),
93 )
94 .indent(
95 Some(1440),
96 Some(SpecialIndentType::Hanging(360)),
97 None,
98 None,
99 ),
100 );
101
102 let mut decimal_abstract = AbstractNumbering::new(1);
104 decimal_abstract.multi_level_type = Some("hybridMultilevel".to_string());
105 let decimal_abstract = decimal_abstract
106 .add_level(
107 Level::new(
108 0,
109 Start::new(1),
110 NumberFormat::new("decimal"),
111 LevelText::new("%1."),
112 LevelJc::new("left"),
113 )
114 .indent(Some(720), Some(SpecialIndentType::Hanging(360)), None, None),
115 )
116 .add_level(
117 Level::new(
118 1,
119 Start::new(1),
120 NumberFormat::new("lowerLetter"),
121 LevelText::new("%2."),
122 LevelJc::new("left"),
123 )
124 .indent(
125 Some(1080),
126 Some(SpecialIndentType::Hanging(360)),
127 None,
128 None,
129 ),
130 )
131 .add_level(
132 Level::new(
133 2,
134 Start::new(1),
135 NumberFormat::new("lowerRoman"),
136 LevelText::new("%3."),
137 LevelJc::new("left"),
138 )
139 .indent(
140 Some(1440),
141 Some(SpecialIndentType::Hanging(360)),
142 None,
143 None,
144 ),
145 );
146
147 docx.add_abstract_numbering(bullet_abstract)
148 .add_abstract_numbering(decimal_abstract)
149 .add_numbering(Numbering::new(BULLET_NUM_ID, 0))
150 .add_numbering(Numbering::new(DECIMAL_NUM_ID, 1))
151}
152
153static DYNAMIC_NUM_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
155
156fn register_custom_start_numbering(docx: &mut Docx, start: u32) -> usize {
162 use std::sync::atomic::Ordering;
163
164 let idx = DYNAMIC_NUM_COUNTER.fetch_add(1, Ordering::Relaxed);
165 let abstract_num_id = DYNAMIC_ABSTRACT_NUM_START + idx;
166 let num_id = DYNAMIC_NUM_ID_START + idx;
167
168 let mut abstract_num = AbstractNumbering::new(abstract_num_id);
169 abstract_num.multi_level_type = Some("hybridMultilevel".to_string());
170 let abstract_num = abstract_num
171 .add_level(
172 Level::new(
173 0,
174 Start::new(start as usize),
175 NumberFormat::new("decimal"),
176 LevelText::new("%1."),
177 LevelJc::new("left"),
178 )
179 .indent(Some(720), Some(SpecialIndentType::Hanging(360)), None, None),
180 )
181 .add_level(
182 Level::new(
183 1,
184 Start::new(1),
185 NumberFormat::new("lowerLetter"),
186 LevelText::new("%2."),
187 LevelJc::new("left"),
188 )
189 .indent(
190 Some(1080),
191 Some(SpecialIndentType::Hanging(360)),
192 None,
193 None,
194 ),
195 )
196 .add_level(
197 Level::new(
198 2,
199 Start::new(1),
200 NumberFormat::new("lowerRoman"),
201 LevelText::new("%3."),
202 LevelJc::new("left"),
203 )
204 .indent(
205 Some(1440),
206 Some(SpecialIndentType::Hanging(360)),
207 None,
208 None,
209 ),
210 );
211
212 let owned = std::mem::replace(docx, Docx::new());
214 *docx = owned
215 .add_abstract_numbering(abstract_num)
216 .add_numbering(Numbering::new(num_id, abstract_num_id));
217
218 num_id
219}
220
221fn add_runs_with_hyperlinks(
227 p: docx_rs::Paragraph,
228 runs: &[DocumentTextRun],
229 bold: bool,
230) -> docx_rs::Paragraph {
231 let mut p = p;
232 let mut i = 0;
233 while i < runs.len() {
234 if let Some(ref url) = runs[i].hyperlink {
235 let mut link = Hyperlink::new(url.as_str(), HyperlinkType::External);
237 while i < runs.len() && runs[i].hyperlink.as_deref() == Some(url.as_str()) {
238 link = link.add_run(text_run_to_docx_run(&runs[i], bold));
239 i += 1;
240 }
241 p = p.add_hyperlink(link);
242 } else {
243 p = p.add_run(text_run_to_docx_run(&runs[i], bold));
244 i += 1;
245 }
246 }
247 p
248}
249
250pub fn render_document_content(content: &DocumentContent) -> Result<Docx> {
258 let mut docx = Docx::new();
259
260 docx = add_list_numberings(docx);
263
264 for block in &content.blocks {
265 docx = render_block(docx, block)?;
266 }
267
268 Ok(docx)
269}
270
271fn render_block(mut docx: Docx, block: &DocumentBlock) -> Result<Docx> {
273 match block {
274 DocumentBlock::Heading { level, runs } => {
275 let _heading_level = u8_to_heading_level(*level);
276 let p = docx_rs::Paragraph::new()
277 .style(heading_style_name(*level))
278 .outline_lvl(heading_outline_level(*level));
279 let p = add_runs_with_hyperlinks(p, runs, true);
280 docx = docx.add_paragraph(p);
281 }
282 DocumentBlock::Paragraph(runs) => {
283 let p = docx_rs::Paragraph::new();
284 let p = add_runs_with_hyperlinks(p, runs, false);
285 docx = docx.add_paragraph(p);
286 }
287 DocumentBlock::Table(table) => {
288 docx = render_table(docx, table)?;
289 }
290 DocumentBlock::List(list) => {
291 docx = render_list(docx, list)?;
292 }
293 DocumentBlock::Image(image) => {
294 docx = render_image(docx, image)?;
295 }
296 DocumentBlock::ThematicBreak | DocumentBlock::PageBreak => {
297 let p =
298 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_break(BreakType::Page));
299 docx = docx.add_paragraph(p);
300 }
301 DocumentBlock::ColumnBreak => {
302 let p =
303 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_break(BreakType::Column));
304 docx = docx.add_paragraph(p);
305 }
306 DocumentBlock::CodeBlock { language: _, code } => {
307 let mut p = docx_rs::Paragraph::new();
309 let r = docx_rs::Run::new()
310 .add_text(code.as_str())
311 .fonts(RunFonts::new().ascii("Courier New"))
312 .size(20); p = p.add_run(r);
314 docx = docx.add_paragraph(p);
315 }
316 DocumentBlock::TextBox(blocks) => {
317 for inner in blocks {
319 docx = render_block(docx, inner)?;
320 }
321 }
322 DocumentBlock::Footnote { id, blocks } => {
323 let body = plain_blocks(blocks);
326 let mut p = docx_rs::Paragraph::new();
327 p = p.add_run(docx_rs::Run::new().add_text(format!("[^{id}]: {body}")));
328 docx = docx.add_paragraph(p);
329 }
330 DocumentBlock::Endnote { id, blocks } => {
331 let body = plain_blocks(blocks);
333 let mut p = docx_rs::Paragraph::new();
334 p = p.add_run(docx_rs::Run::new().add_text(format!("[^endnote-{id}]: {body}")));
335 docx = docx.add_paragraph(p);
336 }
337 DocumentBlock::Section {
338 blocks,
339 section_type: _,
340 } => {
341 for inner in blocks {
343 docx = render_block(docx, inner)?;
344 }
345 }
346 DocumentBlock::Math {
347 omml: _,
348 latex,
349 display,
350 } => {
351 let text = latex.clone().unwrap_or_default();
355 if !text.is_empty() {
356 let marker = render_math_marker();
357 let mut p = docx_rs::Paragraph::new();
358 p = p.add_run(
359 docx_rs::Run::new()
360 .add_text(&marker)
361 .fonts(RunFonts::new().ascii("Courier New")),
362 );
363 RENDERED_MATH.with(|cell| {
364 cell.borrow_mut().push((marker, text, *display));
365 });
366 docx = docx.add_paragraph(p);
367 }
368 }
369 _ => {
370 }
372 }
373 Ok(docx)
374}
375
376fn text_run_to_docx_run(run: &DocumentTextRun, bold: bool) -> docx_rs::Run {
378 let mut r = docx_rs::Run::new();
379 r = r.add_text(run.text.as_str());
380 if run.bold || bold {
381 r = r.bold();
382 }
383 if run.italic {
384 r = r.italic();
385 }
386 if run.strikethrough {
387 r = r.strike();
388 }
389 r
390}
391
392fn render_table(mut docx: Docx, table: &DocumentTable) -> Result<Docx> {
394 let mut rows: Vec<docx_rs::TableRow> = Vec::new();
395
396 for table_row in &table.rows {
397 let mut cells: Vec<docx_rs::TableCell> = Vec::new();
398 for cell in &table_row.cells {
399 let mut cell_paragraphs: Vec<docx_rs::Paragraph> = Vec::new();
400 for block in &cell.blocks {
401 let p = render_block_to_paragraph(block)?;
402 cell_paragraphs.push(p);
403 }
404 let mut tc = docx_rs::TableCell::new();
405 for p in cell_paragraphs {
406 tc = tc.add_paragraph(p);
407 }
408 if cell.column_span > 1 {
409 tc = tc.grid_span(cell.column_span as usize);
410 }
411 if cell.row_span == 0 {
414 tc = tc.vertical_merge(docx_rs::VMergeType::Continue);
415 } else if cell.row_span > 1 {
416 tc = tc.vertical_merge(docx_rs::VMergeType::Restart);
417 }
418 cells.push(tc);
419 }
420 rows.push(docx_rs::TableRow::new(cells));
421 }
422
423 docx = docx.add_table(docx_rs::Table::new(rows));
424 Ok(docx)
425}
426
427fn plain_blocks(blocks: &[DocumentBlock]) -> String {
432 let mut out = String::new();
433 for block in blocks {
434 match block {
435 DocumentBlock::Paragraph(runs) | DocumentBlock::Heading { runs, .. } => {
436 for run in runs {
437 out.push_str(&run.text);
438 }
439 }
440 DocumentBlock::Footnote { blocks, .. } | DocumentBlock::Endnote { blocks, .. } => {
441 out.push_str(&plain_blocks(blocks));
442 }
443 _ => {}
444 }
445 }
446 out
447}
448
449fn render_block_to_paragraph(block: &DocumentBlock) -> Result<docx_rs::Paragraph> {
451 match block {
452 DocumentBlock::Heading { level: _, runs } => {
453 let p = docx_rs::Paragraph::new();
454 Ok(add_runs_with_hyperlinks(p, runs, true))
455 }
456 DocumentBlock::Paragraph(runs) => {
457 let p = docx_rs::Paragraph::new();
458 Ok(add_runs_with_hyperlinks(p, runs, false))
459 }
460 DocumentBlock::List(list) => {
461 let mut p = docx_rs::Paragraph::new();
463 let num_id = if list.ordered {
464 DECIMAL_NUM_ID
465 } else {
466 BULLET_NUM_ID
467 };
468 p = p.numbering(NumberingId::new(num_id), IndentLevel::new(0));
469 for item in &list.items {
470 for inner_block in &item.blocks {
471 if let DocumentBlock::Paragraph(runs) = inner_block {
472 p = add_runs_with_hyperlinks(p, runs, false);
473 }
474 }
475 }
476 Ok(p)
477 }
478 DocumentBlock::CodeBlock { code, .. } => {
479 let mut p = docx_rs::Paragraph::new();
480 let r = docx_rs::Run::new()
481 .add_text(code.as_str())
482 .fonts(RunFonts::new().ascii("Courier New"))
483 .size(20);
484 p = p.add_run(r);
485 Ok(p)
486 }
487 _ => Ok(docx_rs::Paragraph::new()),
488 }
489}
490
491fn render_list(mut docx: Docx, list: &DocumentList) -> Result<Docx> {
496 docx = render_list_at_level(docx, list, 0)?;
497 Ok(docx)
498}
499
500fn render_list_at_level(mut docx: Docx, list: &DocumentList, level: usize) -> Result<Docx> {
505 let num_id = if list.ordered {
506 if let Some(start) = list.start_number
508 && start != 1
509 {
510 register_custom_start_numbering(&mut docx, start)
511 } else {
512 DECIMAL_NUM_ID
513 }
514 } else {
515 BULLET_NUM_ID
516 };
517 let ilvl = level.min(2);
519
520 for item in &list.items {
521 let mut p =
522 docx_rs::Paragraph::new().numbering(NumberingId::new(num_id), IndentLevel::new(ilvl));
523
524 for block in &item.blocks {
526 match block {
527 DocumentBlock::Paragraph(runs) => {
528 p = add_runs_with_hyperlinks(p, runs, false);
529 }
530 DocumentBlock::Heading { runs, .. } => {
531 p = add_runs_with_hyperlinks(p, runs, true);
532 }
533 _ => {
534 }
537 }
538 }
539
540 docx = docx.add_paragraph(p);
541
542 if let Some(nested) = &item.nested {
544 docx = render_list_at_level(docx, nested, level + 1)?;
545 }
546 }
547 Ok(docx)
548}
549
550fn render_image(mut docx: Docx, image: &DocumentImage) -> Result<Docx> {
552 if let Some(data) = &image.data {
553 let pic = Pic::new(data);
554 let p = docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_image(pic));
555 docx = docx.add_paragraph(p);
556 }
557 Ok(docx)
558}
559
560fn u8_to_heading_level(level: u8) -> HeadingLevel {
561 match level {
562 1 => HeadingLevel::H1,
563 2 => HeadingLevel::H2,
564 3 => HeadingLevel::H3,
565 4 => HeadingLevel::H4,
566 5 => HeadingLevel::H5,
567 _ => HeadingLevel::H6,
568 }
569}
570
571fn heading_style_name(level: u8) -> &'static str {
572 match level {
573 1 => "Heading1",
574 2 => "Heading2",
575 3 => "Heading3",
576 4 => "Heading4",
577 5 => "Heading5",
578 _ => "Heading6",
579 }
580}
581
582fn heading_outline_level(level: u8) -> usize {
583 (level.saturating_sub(1)) as usize
584}
585
586pub fn render_with_handler<H: easydoc_core::traits::DocWriteHandler>(
603 content: &DocumentContent,
604 handler: &mut H,
605) -> Result<Docx> {
606 let ctx = easydoc_core::traits::DocWriteContext {
607 path: String::new(),
608 };
609
610 handler.before_document(&ctx)?;
611
612 let mut docx = Docx::new();
613 docx = add_list_numberings(docx);
614 let mut para_index: usize = 0;
615 let mut table_index: usize = 0;
616
617 for block in &content.blocks {
618 match block {
619 DocumentBlock::Heading { .. } | DocumentBlock::Paragraph(_) => {
620 let p_ctx = easydoc_core::traits::ParagraphContext { index: para_index };
621 handler.before_paragraph(&p_ctx)?;
622 docx = render_block(docx, block)?;
623 handler.after_paragraph(&p_ctx)?;
624 para_index += 1;
625 }
626 DocumentBlock::Table(table) => {
627 let t_ctx = easydoc_core::traits::TableWriteContext {
628 index: table_index,
629 row_count: table.rows.len(),
630 };
631 handler.before_table(&t_ctx)?;
632 docx = render_block(docx, block)?;
633 handler.after_table(&t_ctx)?;
634 table_index += 1;
635 }
636 _ => {
637 docx = render_block(docx, block)?;
638 }
639 }
640 }
641
642 handler.after_document(&ctx)?;
643
644 Ok(docx)
645}
646
647#[cfg(test)]
648mod coverage_tests {
649 use super::*;
650 use easydoc_core::{
651 CellContext, DocWriteContext, DocWriteHandler, DocumentListItem, DocumentTableCell,
652 DocumentTableRow, ParagraphContext, TableWriteContext,
653 };
654
655 fn make_text_run(text: &str) -> DocumentTextRun {
656 DocumentTextRun {
657 text: text.into(),
658 bold: false,
659 italic: false,
660 strikethrough: false,
661 hyperlink: None,
662 }
663 }
664
665 fn make_bold_run(text: &str) -> DocumentTextRun {
666 DocumentTextRun {
667 text: text.into(),
668 bold: true,
669 italic: false,
670 strikethrough: false,
671 hyperlink: None,
672 }
673 }
674
675 #[test]
676 fn render_heading_variants() {
677 for level in 1..=7 {
678 let content = DocumentContent {
679 blocks: vec![DocumentBlock::Heading {
680 level,
681 runs: vec![make_text_run("Title")],
682 }],
683 ..Default::default()
684 };
685 let docx = render_document_content(&content).unwrap();
686 let _ = docx;
687 }
688 }
689
690 #[test]
691 fn render_paragraph_with_runs() {
692 let content = DocumentContent {
693 blocks: vec![DocumentBlock::Paragraph(vec![
694 make_text_run("Hello "),
695 make_bold_run("World"),
696 ])],
697 ..Default::default()
698 };
699 let docx = render_document_content(&content).unwrap();
700 let _ = docx;
701 }
702
703 #[test]
704 fn render_table_with_spans() {
705 let content = DocumentContent {
706 blocks: vec![DocumentBlock::Table(DocumentTable {
707 rows: vec![DocumentTableRow {
708 cells: vec![
709 DocumentTableCell {
710 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("A")])],
711 column_span: 2,
712 row_span: 1,
713 },
714 DocumentTableCell {
715 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("B")])],
716 column_span: 1,
717 row_span: 1,
718 },
719 ],
720 is_header: true,
721 }],
722 })],
723 ..Default::default()
724 };
725 let docx = render_document_content(&content).unwrap();
726 let _ = docx;
727 }
728
729 #[test]
730 fn render_table_vertical_merge_emits_vmerge_xml() {
731 let content = DocumentContent {
733 blocks: vec![DocumentBlock::Table(DocumentTable {
734 rows: vec![
735 DocumentTableRow {
736 cells: vec![DocumentTableCell {
737 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Merged")])],
738 column_span: 1,
739 row_span: 2,
740 }],
741 is_header: false,
742 },
743 DocumentTableRow {
744 cells: vec![DocumentTableCell {
745 blocks: vec![],
746 column_span: 1,
747 row_span: 0,
748 }],
749 is_header: false,
750 },
751 ],
752 })],
753 ..Default::default()
754 };
755 let docx = render_document_content(&content).unwrap();
756 let xml = String::from_utf8(docx.build().document).expect("document.xml is UTF-8");
757 assert!(
759 xml.contains("vMerge") && xml.contains("restart") && xml.contains("continue"),
760 "document.xml should contain vMerge restart+continue, got: {xml}"
761 );
762 }
763
764 #[test]
765 fn render_table_plain_cells_have_no_vmerge() {
766 let content = DocumentContent {
767 blocks: vec![DocumentBlock::Table(DocumentTable {
768 rows: vec![DocumentTableRow {
769 cells: vec![DocumentTableCell {
770 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Plain")])],
771 column_span: 1,
772 row_span: 1,
773 }],
774 is_header: false,
775 }],
776 })],
777 ..Default::default()
778 };
779 let docx = render_document_content(&content).unwrap();
780 let xml = String::from_utf8(docx.build().document).expect("document.xml is UTF-8");
781 assert!(
782 !xml.contains("vMerge"),
783 "plain cells should not emit vMerge, got: {xml}"
784 );
785 }
786
787 #[test]
788 fn render_list_ordered_and_unordered() {
789 let content = DocumentContent {
790 blocks: vec![
791 DocumentBlock::List(DocumentList {
792 ordered: false,
793 start_number: None,
794 items: vec![DocumentListItem {
795 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Item 1")])],
796 nested: None,
797 }],
798 }),
799 DocumentBlock::List(DocumentList {
800 ordered: true,
801 start_number: Some(5),
802 items: vec![DocumentListItem {
803 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Item 2")])],
804 nested: Some(Box::new(DocumentList {
805 ordered: false,
806 start_number: None,
807 items: vec![DocumentListItem {
808 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run(
809 "Nested",
810 )])],
811 nested: None,
812 }],
813 })),
814 }],
815 }),
816 ],
817 ..Default::default()
818 };
819 let docx = render_document_content(&content).unwrap();
820 let _ = docx;
821 }
822
823 #[test]
824 fn render_image_with_data() {
825 let content = DocumentContent {
826 blocks: vec![DocumentBlock::Image(DocumentImage {
827 alt_text: Some("test".into()),
828 data: Some(vec![
829 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
830 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
831 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44,
832 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00,
833 0x01, 0xE2, 0x21, 0xBC, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
834 0xAE, 0x42, 0x60, 0x82,
835 ]),
836 extension: Some("png".into()),
837 })],
838 ..Default::default()
839 };
840 let docx = render_document_content(&content).unwrap();
841 let _ = docx;
842 }
843
844 #[test]
845 fn render_image_without_data() {
846 let content = DocumentContent {
847 blocks: vec![DocumentBlock::Image(DocumentImage {
848 alt_text: Some("test".into()),
849 data: None,
850 extension: None,
851 })],
852 ..Default::default()
853 };
854 let docx = render_document_content(&content).unwrap();
855 let _ = docx;
856 }
857
858 #[test]
859 fn render_codeblock() {
860 let content = DocumentContent {
861 blocks: vec![DocumentBlock::CodeBlock {
862 language: Some("rust".into()),
863 code: "fn main() {}".into(),
864 }],
865 ..Default::default()
866 };
867 let docx = render_document_content(&content).unwrap();
868 let _ = docx;
869 }
870
871 #[test]
872 fn render_textbox() {
873 let content = DocumentContent {
874 blocks: vec![DocumentBlock::TextBox(vec![DocumentBlock::Paragraph(
875 vec![make_text_run("inside")],
876 )])],
877 ..Default::default()
878 };
879 let docx = render_document_content(&content).unwrap();
880 let _ = docx;
881 }
882
883 #[test]
884 fn render_footnote_and_endnote() {
885 let content = DocumentContent {
886 blocks: vec![
887 DocumentBlock::Footnote {
888 id: 1,
889 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("note")])],
890 },
891 DocumentBlock::Endnote {
892 id: 2,
893 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("end")])],
894 },
895 ],
896 ..Default::default()
897 };
898 let docx = render_document_content(&content).unwrap();
899 let _ = docx;
900 }
901
902 #[test]
903 fn render_section() {
904 let content = DocumentContent {
905 blocks: vec![DocumentBlock::Section {
906 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("in section")])],
907 section_type: Some("nextPage".into()),
908 }],
909 ..Default::default()
910 };
911 let docx = render_document_content(&content).unwrap();
912 let _ = docx;
913 }
914
915 #[test]
916 fn render_thematic_and_page_and_column_break() {
917 let content = DocumentContent {
918 blocks: vec![
919 DocumentBlock::ThematicBreak,
920 DocumentBlock::PageBreak,
921 DocumentBlock::ColumnBreak,
922 ],
923 ..Default::default()
924 };
925 let docx = render_document_content(&content).unwrap();
926 let _ = docx;
927 }
928
929 #[test]
930 fn render_with_handler_all_blocks() {
931 struct TestHandler;
932 impl DocWriteHandler for TestHandler {
933 fn order() -> i32 {
934 0
935 }
936 fn before_document(&mut self, _: &DocWriteContext) -> Result<()> {
937 Ok(())
938 }
939 fn after_document(&mut self, _: &DocWriteContext) -> Result<()> {
940 Ok(())
941 }
942 fn before_paragraph(&mut self, _: &ParagraphContext) -> Result<()> {
943 Ok(())
944 }
945 fn after_paragraph(&mut self, _: &ParagraphContext) -> Result<()> {
946 Ok(())
947 }
948 fn before_table(&mut self, _: &TableWriteContext) -> Result<()> {
949 Ok(())
950 }
951 fn after_table(&mut self, _: &TableWriteContext) -> Result<()> {
952 Ok(())
953 }
954 fn before_cell(&mut self, _: &CellContext) -> Result<()> {
955 Ok(())
956 }
957 fn after_cell(&mut self, _: &CellContext) -> Result<()> {
958 Ok(())
959 }
960 }
961 let content = DocumentContent {
962 blocks: vec![
963 DocumentBlock::Heading {
964 level: 1,
965 runs: vec![make_text_run("H1")],
966 },
967 DocumentBlock::Paragraph(vec![make_text_run("P")]),
968 DocumentBlock::Table(DocumentTable { rows: vec![] }),
969 DocumentBlock::CodeBlock {
970 language: None,
971 code: "x".into(),
972 },
973 DocumentBlock::ThematicBreak,
974 DocumentBlock::PageBreak,
975 DocumentBlock::ColumnBreak,
976 DocumentBlock::Section {
977 blocks: vec![],
978 section_type: None,
979 },
980 ],
981 ..Default::default()
982 };
983 let mut handler = TestHandler;
984 let docx = render_with_handler(&content, &mut handler).unwrap();
985 let _ = docx;
986 }
987
988 #[test]
989 fn heading_style_names() {
990 assert_eq!(heading_style_name(1), "Heading1");
991 assert_eq!(heading_style_name(2), "Heading2");
992 assert_eq!(heading_style_name(3), "Heading3");
993 assert_eq!(heading_style_name(4), "Heading4");
994 assert_eq!(heading_style_name(5), "Heading5");
995 assert_eq!(heading_style_name(6), "Heading6");
996 assert_eq!(heading_style_name(7), "Heading6"); }
998
999 #[test]
1000 fn heading_outline_levels() {
1001 assert_eq!(heading_outline_level(1), 0);
1002 assert_eq!(heading_outline_level(2), 1);
1003 assert_eq!(heading_outline_level(6), 5);
1004 assert_eq!(heading_outline_level(0), 0); }
1006
1007 #[test]
1008 fn u8_to_heading_level_all() {
1009 assert_eq!(u8_to_heading_level(1), HeadingLevel::H1);
1010 assert_eq!(u8_to_heading_level(2), HeadingLevel::H2);
1011 assert_eq!(u8_to_heading_level(3), HeadingLevel::H3);
1012 assert_eq!(u8_to_heading_level(4), HeadingLevel::H4);
1013 assert_eq!(u8_to_heading_level(5), HeadingLevel::H5);
1014 assert_eq!(u8_to_heading_level(6), HeadingLevel::H6);
1015 assert_eq!(u8_to_heading_level(99), HeadingLevel::H6); }
1017
1018 #[test]
1019 fn render_ordered_list_with_custom_start_number() {
1020 let content = DocumentContent {
1021 blocks: vec![DocumentBlock::List(DocumentList {
1022 ordered: true,
1023 start_number: Some(5),
1024 items: vec![
1025 DocumentListItem {
1026 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Fifth")])],
1027 nested: None,
1028 },
1029 DocumentListItem {
1030 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Sixth")])],
1031 nested: None,
1032 },
1033 ],
1034 })],
1035 ..Default::default()
1036 };
1037 let docx = render_document_content(&content).unwrap();
1038 let _ = docx;
1040 }
1041
1042 #[test]
1043 fn render_ordered_list_with_default_start_number() {
1044 let content = DocumentContent {
1045 blocks: vec![DocumentBlock::List(DocumentList {
1046 ordered: true,
1047 start_number: Some(1),
1048 items: vec![DocumentListItem {
1049 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("First")])],
1050 nested: None,
1051 }],
1052 })],
1053 ..Default::default()
1054 };
1055 let docx = render_document_content(&content).unwrap();
1056 let _ = docx;
1057 }
1058
1059 #[test]
1060 fn render_ordered_list_with_none_start_number() {
1061 let content = DocumentContent {
1062 blocks: vec![DocumentBlock::List(DocumentList {
1063 ordered: true,
1064 start_number: None,
1065 items: vec![DocumentListItem {
1066 blocks: vec![DocumentBlock::Paragraph(vec![make_text_run("Default")])],
1067 nested: None,
1068 }],
1069 })],
1070 ..Default::default()
1071 };
1072 let docx = render_document_content(&content).unwrap();
1073 let _ = docx;
1074 }
1075
1076 #[test]
1077 fn text_run_to_docx_run_styles() {
1078 let run = DocumentTextRun {
1079 text: "test".into(),
1080 bold: true,
1081 italic: true,
1082 strikethrough: true,
1083 hyperlink: None,
1084 };
1085 let r = text_run_to_docx_run(&run, false);
1086 let _ = r;
1087 let r2 = text_run_to_docx_run(&run, true);
1088 let _ = r2;
1089 }
1090
1091 #[test]
1092 fn render_block_to_paragraph_codeblock() {
1093 let block = DocumentBlock::CodeBlock {
1094 language: Some("python".into()),
1095 code: "print()".into(),
1096 };
1097 let p = render_block_to_paragraph(&block).unwrap();
1098 let _ = p;
1099 }
1100
1101 #[test]
1102 fn render_block_to_paragraph_heading() {
1103 let block = DocumentBlock::Heading {
1104 level: 2,
1105 runs: vec![make_text_run("Sub")],
1106 };
1107 let p = render_block_to_paragraph(&block).unwrap();
1108 let _ = p;
1109 }
1110
1111 #[test]
1112 fn render_block_to_paragraph_fallback() {
1113 let block = DocumentBlock::ThematicBreak;
1115 let p = render_block_to_paragraph(&block).unwrap();
1116 let _ = p;
1117 }
1118}