use crate::converter::ConverterRegistry;
use crate::error::Result;
use crate::metadata::TableColumn;
use crate::types::{CellData, DocValue, ErrorAction, RowData, TableData};
pub trait DocxRow {
fn schema() -> &'static [TableColumn]
where
Self: Sized;
fn from_row(row: &RowData) -> Result<Self>
where
Self: Sized;
fn from_row_with_converters(row: &RowData, registry: &ConverterRegistry) -> Result<Self>
where
Self: Sized;
fn to_row(&self) -> Result<Vec<CellData>>;
fn to_row_with_converters(&self, registry: &ConverterRegistry) -> Result<Vec<CellData>>;
}
pub trait DocConverter<T> {
fn support_type() -> std::any::TypeId
where
Self: Sized;
fn to_doc_value(&self, value: &T, column: &TableColumn) -> Result<DocValue>;
fn from_doc_value(&self, value: &DocValue, column: &TableColumn) -> Result<T>;
}
#[derive(Debug, Clone)]
pub struct DocReadContext {
pub path: String,
pub index: usize,
}
pub trait DocReadListener<T> {
fn invoke(&mut self, data: T, context: &DocReadContext) -> Result<()>;
fn invoke_table(&mut self, table: &TableData, context: &DocReadContext) -> Result<()> {
let _ = (table, context);
Ok(())
}
fn on_complete(&mut self, _context: &DocReadContext) {}
fn on_error(
&mut self,
_error: &crate::error::DocError,
_context: &DocReadContext,
) -> ErrorAction {
ErrorAction::Stop
}
fn has_next(&self, _context: &DocReadContext) -> bool {
true
}
}
#[derive(Debug, Clone)]
pub struct DocWriteContext {
pub path: String,
}
#[derive(Debug, Clone)]
pub struct ParagraphContext {
pub index: usize,
}
#[derive(Debug, Clone)]
pub struct TableWriteContext {
pub index: usize,
pub row_count: usize,
}
#[derive(Debug, Clone)]
pub struct CellContext {
pub row: usize,
pub column: usize,
pub value: DocValue,
}
pub trait DocWriteHandler {
#[must_use]
fn order() -> i32 {
0
}
fn before_document(&mut self, _ctx: &DocWriteContext) -> Result<()> {
Ok(())
}
fn after_document(&mut self, _ctx: &DocWriteContext) -> Result<()> {
Ok(())
}
fn before_paragraph(&mut self, _ctx: &ParagraphContext) -> Result<()> {
Ok(())
}
fn after_paragraph(&mut self, _ctx: &ParagraphContext) -> Result<()> {
Ok(())
}
fn before_table(&mut self, _ctx: &TableWriteContext) -> Result<()> {
Ok(())
}
fn after_table(&mut self, _ctx: &TableWriteContext) -> Result<()> {
Ok(())
}
fn before_cell(&mut self, _ctx: &CellContext) -> Result<()> {
Ok(())
}
fn after_cell(&mut self, _ctx: &CellContext) -> Result<()> {
Ok(())
}
}
pub trait DocumentReader {
fn read_model(&self, path: &std::path::Path) -> crate::Result<crate::DocumentContent>;
fn read_events(&self, path: &std::path::Path, sink: &mut dyn EventSink) -> crate::Result<()>;
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum DocumentEvent {
Heading {
level: u8,
runs: Vec<crate::DocumentTextRun>,
},
Paragraph(Vec<crate::DocumentTextRun>),
Table(crate::DocumentTable),
List(crate::DocumentList),
Image(crate::DocumentImage),
PageBreak,
ColumnBreak,
CodeBlock {
language: Option<String>,
code: String,
},
Section {
section_type: Option<String>,
},
DocumentStart,
DocumentEnd,
}
pub trait EventSink {
fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()>;
fn on_complete(&mut self) {}
}
pub struct ContentCollector {
blocks: Vec<crate::DocumentBlock>,
}
impl ContentCollector {
#[must_use]
pub fn new() -> Self {
Self { blocks: Vec::new() }
}
#[must_use]
pub fn into_content(self) -> crate::DocumentContent {
crate::DocumentContent {
metadata: crate::DocumentMeta::default(),
blocks: self.blocks,
}
}
}
impl Default for ContentCollector {
fn default() -> Self {
Self::new()
}
}
impl EventSink for ContentCollector {
fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()> {
match event {
DocumentEvent::Heading { level, runs } => {
self.blocks.push(crate::DocumentBlock::Heading {
level: *level,
runs: runs.clone(),
});
}
DocumentEvent::Paragraph(runs) => {
self.blocks
.push(crate::DocumentBlock::Paragraph(runs.clone()));
}
DocumentEvent::Table(table) => {
self.blocks.push(crate::DocumentBlock::Table(table.clone()));
}
DocumentEvent::List(list) => {
self.blocks.push(crate::DocumentBlock::List(list.clone()));
}
DocumentEvent::Image(image) => {
self.blocks.push(crate::DocumentBlock::Image(image.clone()));
}
DocumentEvent::PageBreak => {
self.blocks.push(crate::DocumentBlock::PageBreak);
}
DocumentEvent::ColumnBreak => {
self.blocks.push(crate::DocumentBlock::ColumnBreak);
}
DocumentEvent::CodeBlock { language, code } => {
self.blocks.push(crate::DocumentBlock::CodeBlock {
language: language.clone(),
code: code.clone(),
});
}
DocumentEvent::Section { section_type } => {
self.blocks.push(crate::DocumentBlock::Section {
blocks: Vec::new(),
section_type: section_type.clone(),
});
}
DocumentEvent::DocumentStart | DocumentEvent::DocumentEnd => {}
}
Ok(())
}
}
#[cfg(test)]
mod event_tests {
use super::*;
#[test]
fn document_event_debug() {
let event = DocumentEvent::DocumentStart;
assert_eq!(format!("{event:?}"), "DocumentStart");
}
#[test]
fn document_event_heading() {
let event = DocumentEvent::Heading {
level: 1,
runs: vec![crate::DocumentTextRun {
text: "Title".into(),
..crate::DocumentTextRun::default()
}],
};
match &event {
DocumentEvent::Heading { level, runs } => {
assert_eq!(*level, 1);
assert_eq!(runs[0].text, "Title");
}
_ => panic!("expected Heading"),
}
}
#[test]
fn content_collector_roundtrip() {
let mut collector = ContentCollector::new();
collector.on_event(&DocumentEvent::DocumentStart).unwrap();
collector
.on_event(&DocumentEvent::Paragraph(vec![crate::DocumentTextRun {
text: "Hello".into(),
..crate::DocumentTextRun::default()
}]))
.unwrap();
collector.on_event(&DocumentEvent::PageBreak).unwrap();
collector.on_event(&DocumentEvent::DocumentEnd).unwrap();
let content = collector.into_content();
assert_eq!(content.blocks.len(), 2);
assert!(matches!(
content.blocks[0],
crate::DocumentBlock::Paragraph(_)
));
assert!(matches!(content.blocks[1], crate::DocumentBlock::PageBreak));
}
#[test]
fn content_collector_table_and_list() {
let mut collector = ContentCollector::new();
collector
.on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
.unwrap();
collector
.on_event(&DocumentEvent::List(crate::DocumentList {
ordered: false,
start_number: None,
items: vec![],
}))
.unwrap();
let content = collector.into_content();
assert_eq!(content.blocks.len(), 2);
}
#[test]
fn content_collector_codeblock() {
let mut collector = ContentCollector::new();
collector
.on_event(&DocumentEvent::CodeBlock {
language: Some("rust".into()),
code: "fn main() {}".into(),
})
.unwrap();
let content = collector.into_content();
match &content.blocks[0] {
crate::DocumentBlock::CodeBlock { language, code } => {
assert_eq!(language.as_deref(), Some("rust"));
assert_eq!(code, "fn main() {}");
}
_ => panic!("expected CodeBlock"),
}
}
#[test]
fn content_collector_section() {
let mut collector = ContentCollector::new();
collector
.on_event(&DocumentEvent::Section {
section_type: Some("continuous".into()),
})
.unwrap();
let content = collector.into_content();
match &content.blocks[0] {
crate::DocumentBlock::Section {
blocks,
section_type,
} => {
assert!(blocks.is_empty());
assert_eq!(section_type.as_deref(), Some("continuous"));
}
_ => panic!("expected Section"),
}
}
}
#[cfg(test)]
mod trait_coverage_tests {
use super::*;
struct NoopHandler;
impl DocWriteHandler for NoopHandler {}
#[test]
fn noop_handler_all_defaults() {
let mut h = NoopHandler;
assert_eq!(NoopHandler::order(), 0);
let ctx = DocWriteContext {
path: "test".into(),
};
h.before_document(&ctx).unwrap();
h.after_document(&ctx).unwrap();
let pctx = ParagraphContext { index: 0 };
h.before_paragraph(&pctx).unwrap();
h.after_paragraph(&pctx).unwrap();
let tctx = TableWriteContext {
index: 0,
row_count: 1,
};
h.before_table(&tctx).unwrap();
h.after_table(&tctx).unwrap();
let cctx = CellContext {
row: 0,
column: 0,
value: DocValue::Empty,
};
h.before_cell(&cctx).unwrap();
h.after_cell(&cctx).unwrap();
}
#[test]
fn read_listener_defaults() {
struct TestListener;
impl DocReadListener<String> for TestListener {
fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
Ok(())
}
}
let mut listener = TestListener;
let ctx = DocReadContext {
path: "test".into(),
index: 0,
};
assert!(listener.has_next(&ctx));
assert!(matches!(
listener.on_error(&crate::DocError::Document("x".into()), &ctx),
ErrorAction::Stop
));
listener.on_complete(&ctx);
}
#[test]
fn read_listener_invoke_table_default() {
struct TestListener;
impl DocReadListener<String> for TestListener {
fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
Ok(())
}
}
let mut listener = TestListener;
let ctx = DocReadContext {
path: "test".into(),
index: 0,
};
let table = TableData {
headers: None,
rows: vec![],
};
listener.invoke_table(&table, &ctx).unwrap();
}
#[test]
fn content_collector_all_event_types() {
let mut c = ContentCollector::new();
c.on_event(&DocumentEvent::DocumentStart).unwrap();
c.on_event(&DocumentEvent::Heading {
level: 1,
runs: vec![],
})
.unwrap();
c.on_event(&DocumentEvent::Paragraph(vec![])).unwrap();
c.on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
.unwrap();
c.on_event(&DocumentEvent::List(crate::DocumentList {
ordered: false,
start_number: None,
items: vec![],
}))
.unwrap();
c.on_event(&DocumentEvent::Image(crate::DocumentImage {
alt_text: None,
data: None,
extension: None,
}))
.unwrap();
c.on_event(&DocumentEvent::PageBreak).unwrap();
c.on_event(&DocumentEvent::ColumnBreak).unwrap();
c.on_event(&DocumentEvent::CodeBlock {
language: None,
code: String::new(),
})
.unwrap();
c.on_event(&DocumentEvent::Section { section_type: None })
.unwrap();
c.on_event(&DocumentEvent::DocumentEnd).unwrap();
c.on_complete();
let content = c.into_content();
assert_eq!(content.blocks.len(), 9); }
#[test]
fn content_collector_default() {
let c = ContentCollector::default();
let content = c.into_content();
assert!(content.blocks.is_empty());
}
#[test]
fn doc_read_context_clone_debug() {
let ctx = DocReadContext {
path: "test".into(),
index: 5,
};
let ctx2 = ctx.clone();
assert_eq!(ctx2.index, 5);
assert!(format!("{ctx:?}").contains("test"));
}
#[test]
fn doc_write_context_clone_debug() {
let ctx = DocWriteContext {
path: "out.docx".into(),
};
let ctx2 = ctx.clone();
assert_eq!(ctx2.path, "out.docx");
assert!(format!("{ctx:?}").contains("out.docx"));
}
#[test]
fn paragraph_context_clone_debug() {
let ctx = ParagraphContext { index: 3 };
let ctx2 = ctx.clone();
assert_eq!(ctx2.index, 3);
assert!(format!("{ctx:?}").contains('3'));
}
#[test]
fn table_write_context_clone_debug() {
let ctx = TableWriteContext {
index: 1,
row_count: 10,
};
let ctx2 = ctx.clone();
assert_eq!(ctx2.index, 1);
assert_eq!(ctx2.row_count, 10);
assert!(format!("{ctx:?}").contains("10"));
}
#[test]
fn cell_context_clone_debug() {
let ctx = CellContext {
row: 2,
column: 3,
value: DocValue::Int(42),
};
let ctx2 = ctx.clone();
assert_eq!(ctx2.row, 2);
assert!(format!("{ctx:?}").contains("42"));
}
#[test]
fn document_event_clone_debug() {
let events = vec![
DocumentEvent::DocumentStart,
DocumentEvent::DocumentEnd,
DocumentEvent::PageBreak,
DocumentEvent::ColumnBreak,
DocumentEvent::Heading {
level: 1,
runs: vec![],
},
DocumentEvent::Paragraph(vec![]),
DocumentEvent::Section { section_type: None },
DocumentEvent::CodeBlock {
language: None,
code: String::new(),
},
];
for event in &events {
let _clone = event.clone();
let _debug = format!("{event:?}");
}
}
}