diff --git a/README.md b/README.md index 936ea23..af04829 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # docx-rs [![GitHub Actions Status](https://github.com/bokuweb/docx-rs/workflows/Continuous%20Integration/badge.svg)](https://github.com/bokuweb/docx-rs/actions) + +# Features + +- [x] Paragraph diff --git a/docx-core/src/documents/document.rs b/docx-core/src/documents/document.rs index b852ef8..eb3f758 100644 --- a/docx-core/src/documents/document.rs +++ b/docx-core/src/documents/document.rs @@ -3,33 +3,33 @@ use crate::documents::BuildXML; use crate::xml_builder::*; #[derive(Debug)] -pub struct Document { - children: Vec, +pub struct Document<'a> { + children: Vec>, } #[derive(Debug, Clone)] -pub enum DocumentChild { - Paragraph(Paragraph), - Table(Table), +pub enum DocumentChild<'a> { + Paragraph(Paragraph<'a>), + Table(Table<'a>), } -impl Document { - pub fn new() -> Document { +impl<'a> Document<'a> { + pub fn new() -> Document<'a> { Default::default() } - pub fn add_paragraph(mut self, p: Paragraph) -> Self { + pub fn add_paragraph(mut self, p: Paragraph<'a>) -> Self { self.children.push(DocumentChild::Paragraph(p)); self } - pub fn add_table(mut self, t: Table) -> Self { + pub fn add_table(mut self, t: Table<'a>) -> Self { self.children.push(DocumentChild::Table(t)); self } } -impl Default for Document { +impl<'a> Default for Document<'a> { fn default() -> Self { Self { children: Vec::new(), @@ -37,7 +37,7 @@ impl Default for Document { } } -impl BuildXML for Document { +impl<'a> BuildXML for Document<'a> { fn build(&self) -> Vec { let mut b = XMLBuilder::new() .declaration(Some(true)) diff --git a/docx-core/src/documents/elements/delete.rs b/docx-core/src/documents/elements/delete.rs new file mode 100644 index 0000000..c840df2 --- /dev/null +++ b/docx-core/src/documents/elements/delete.rs @@ -0,0 +1,60 @@ +use crate::documents::{BuildXML, HistoryId, Run}; +use crate::xml_builder::*; + +#[derive(Debug, Clone)] +pub struct Delete<'a> { + author: &'a str, + date: &'a str, + run: Run, +} + +impl<'a> Default for Delete<'a> { + fn default() -> Delete<'a> { + Delete { + author: "unnamed", + date: "1970-01-01T00:00:00Z", + run: Run::new(), + } + } +} + +impl<'a> Delete<'a> { + pub fn new() -> Delete<'a> { + Default::default() + } + + pub fn run(mut self, run: Run) -> Delete<'a> { + self.run = run; + self + } +} + +impl<'a> HistoryId for Delete<'a> {} + +impl<'a> BuildXML for Delete<'a> { + fn build(&self) -> Vec { + XMLBuilder::new() + .open_delete(&self.generate(), self.author, self.date) + .add_child(&self.run) + .close() + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_delete_default() { + let b = Delete::new().build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/delete_text.rs b/docx-core/src/documents/elements/delete_text.rs new file mode 100644 index 0000000..848f3e7 --- /dev/null +++ b/docx-core/src/documents/elements/delete_text.rs @@ -0,0 +1,41 @@ +use crate::documents::BuildXML; +use crate::xml_builder::*; + +#[derive(Debug, Clone)] +pub struct DeleteText { + text: String, + preserve_space: bool, +} + +impl DeleteText { + pub fn new(text: impl Into) -> DeleteText { + DeleteText { + text: text.into(), + preserve_space: true, + } + } +} + +impl BuildXML for DeleteText { + fn build(&self) -> Vec { + XMLBuilder::new().delete_text(&self.text, true).build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_build() { + let b = DeleteText::new("Hello").build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#"Hello"# + ); + } +} diff --git a/docx-core/src/documents/elements/insert.rs b/docx-core/src/documents/elements/insert.rs new file mode 100644 index 0000000..5cba8df --- /dev/null +++ b/docx-core/src/documents/elements/insert.rs @@ -0,0 +1,60 @@ +use crate::documents::{BuildXML, HistoryId, Run}; +use crate::xml_builder::*; + +#[derive(Debug, Clone)] +pub struct Insert<'a> { + author: &'a str, + date: &'a str, + run: Run, +} + +impl<'a> Default for Insert<'a> { + fn default() -> Insert<'a> { + Insert { + author: "unnamed", + date: "1970-01-01T00:00:00Z", + run: Run::new(), + } + } +} + +impl<'a> Insert<'a> { + pub fn new() -> Insert<'a> { + Default::default() + } + + pub fn run(mut self, run: Run) -> Insert<'a> { + self.run = run; + self + } +} + +impl<'a> HistoryId for Insert<'a> {} + +impl<'a> BuildXML for Insert<'a> { + fn build(&self) -> Vec { + XMLBuilder::new() + .open_insert(&self.generate(), self.author, self.date) + .add_child(&self.run) + .close() + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_ins_default() { + let b = Insert::new().build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/mod.rs b/docx-core/src/documents/elements/mod.rs index 489ce4e..51e05cd 100644 --- a/docx-core/src/documents/elements/mod.rs +++ b/docx-core/src/documents/elements/mod.rs @@ -4,11 +4,14 @@ mod bold_cs; mod br; mod color; mod default_tab_stop; +mod delete; +mod delete_text; mod doc_defaults; mod font; mod grid_span; mod highlight; mod indent; +mod insert; mod italic; mod italic_cs; mod justification; @@ -48,11 +51,14 @@ pub use bold_cs::*; pub use br::*; pub use color::*; pub use default_tab_stop::*; +pub use delete::*; +pub use delete_text::*; pub use doc_defaults::*; pub use font::*; pub use grid_span::*; pub use highlight::*; pub use indent::*; +pub use insert::*; pub use italic::*; pub use italic_cs::*; pub use justification::*; diff --git a/docx-core/src/documents/elements/paragraph.rs b/docx-core/src/documents/elements/paragraph.rs index 5589ee5..9fcf45c 100644 --- a/docx-core/src/documents/elements/paragraph.rs +++ b/docx-core/src/documents/elements/paragraph.rs @@ -1,67 +1,98 @@ -use super::{ParagraphProperty, Run}; +use super::{Delete, Insert, ParagraphProperty, Run}; use crate::documents::BuildXML; use crate::types::*; use crate::xml_builder::*; #[derive(Debug, Clone)] -pub struct Paragraph { - runs: Vec, +pub struct Paragraph<'a> { + children: Vec>, property: ParagraphProperty, attrs: Vec<(String, String)>, } -impl Default for Paragraph { +impl<'a> Default for Paragraph<'a> { fn default() -> Self { Self { - runs: Vec::new(), + children: Vec::new(), property: ParagraphProperty::new(), attrs: Vec::new(), } } } -impl Paragraph { - pub fn new() -> Paragraph { +#[derive(Debug, Clone)] +pub enum ParagraphChild<'a> { + Run(Run), + Insert(Insert<'a>), + Delete(Delete<'a>), +} + +impl<'a> BuildXML for ParagraphChild<'a> { + fn build(&self) -> Vec { + match self { + ParagraphChild::Run(v) => v.build(), + ParagraphChild::Insert(v) => v.build(), + ParagraphChild::Delete(v) => v.build(), + } + } +} + +impl<'a> Paragraph<'a> { + pub fn new() -> Paragraph<'a> { Default::default() } - pub fn add_run(mut self, run: Run) -> Paragraph { - self.runs.push(run); + pub fn add_run(mut self, run: Run) -> Paragraph<'a> { + self.children.push(ParagraphChild::Run(run)); self } - pub fn add_attr(mut self, key: impl Into, val: impl Into) -> Paragraph { + pub fn add_insert(mut self, insert: Insert<'a>) -> Paragraph<'a> { + self.children.push(ParagraphChild::Insert(insert)); + self + } + + pub fn add_delete(mut self, delete: Delete<'a>) -> Paragraph<'a> { + self.children.push(ParagraphChild::Delete(delete)); + self + } + + pub fn add_attr(mut self, key: impl Into, val: impl Into) -> Paragraph<'a> { self.attrs.push((key.into(), val.into())); self } - pub fn align(mut self, alignment_type: AlignmentType) -> Paragraph { + pub fn align(mut self, alignment_type: AlignmentType) -> Paragraph<'a> { self.property = self.property.align(alignment_type); self } - pub fn size(mut self, size: usize) -> Paragraph { - self.runs = self.runs.into_iter().map(|r| r.size(size)).collect(); - self - } + // pub fn size(mut self, size: usize) -> Paragraph<'a> { + // self.children = self.children.into_iter().map(|r| r.size(size)).collect(); + // self + // } - pub fn style(mut self, style_id: &str) -> Paragraph { + pub fn style(mut self, style_id: &str) -> Paragraph<'a> { self.property = self.property.style(style_id); self } - pub fn indent(mut self, left: usize, special_indent: Option) -> Paragraph { + pub fn indent( + mut self, + left: usize, + special_indent: Option, + ) -> Paragraph<'a> { self.property = self.property.indent(left, special_indent); self } } -impl BuildXML for Paragraph { +impl<'a> BuildXML for Paragraph<'a> { fn build(&self) -> Vec { XMLBuilder::new() .open_paragraph(&self.attrs) .add_child(&self.property) - .add_children(&self.runs) + .add_children(&self.children) .close() .build() } @@ -86,18 +117,6 @@ mod tests { ); } - #[test] - fn test_paragraph_size() { - let b = Paragraph::new() - .add_run(Run::new().add_text("Hello")) - .size(60) - .build(); - assert_eq!( - str::from_utf8(&b).unwrap(), - r#"Hello"# - ); - } - #[test] fn test_custom_attr() { let b = Paragraph::new() diff --git a/docx-core/src/documents/elements/run.rs b/docx-core/src/documents/elements/run.rs index 08a1bad..dc7a58a 100644 --- a/docx-core/src/documents/elements/run.rs +++ b/docx-core/src/documents/elements/run.rs @@ -1,4 +1,4 @@ -use super::{Break, RunProperty, Tab, Text}; +use super::{Break, DeleteText, RunProperty, Tab, Text}; use crate::documents::BuildXML; use crate::types::BreakType; use crate::xml_builder::*; @@ -22,6 +22,7 @@ impl Default for Run { #[derive(Debug, Clone)] pub enum RunChild { Text(Text), + DeleteText(DeleteText), Tab(Tab), Break(Break), } @@ -38,6 +39,11 @@ impl Run { self } + pub fn add_delete_text(mut self, text: &str) -> Run { + self.children.push(RunChild::Text(Text::new(text))); + self + } + pub fn add_tab(mut self) -> Run { self.children.push(RunChild::Tab(Tab::new())); self @@ -81,6 +87,7 @@ impl BuildXML for Run { for c in &self.children { match c { RunChild::Text(t) => b = b.add_child(t), + RunChild::DeleteText(t) => b = b.add_child(t), RunChild::Tab(t) => b = b.add_child(t), RunChild::Break(t) => b = b.add_child(t), } diff --git a/docx-core/src/documents/elements/table.rs b/docx-core/src/documents/elements/table.rs index 2ea2123..697fd4a 100644 --- a/docx-core/src/documents/elements/table.rs +++ b/docx-core/src/documents/elements/table.rs @@ -3,14 +3,14 @@ use crate::documents::BuildXML; use crate::xml_builder::*; #[derive(Debug, Clone)] -pub struct Table { +pub struct Table<'a> { property: TableProperty, - rows: Vec, + rows: Vec>, grid: Vec, } -impl Table { - pub fn new(rows: Vec) -> Table { +impl<'a> Table<'a> { + pub fn new(rows: Vec>) -> Table<'a> { let property = TableProperty::new(); let grid = vec![]; Self { @@ -20,13 +20,13 @@ impl Table { } } - pub fn set_grid(mut self, grid: Vec) -> Table { + pub fn set_grid(mut self, grid: Vec) -> Table<'a> { self.grid = grid; self } } -impl BuildXML for Table { +impl<'a> BuildXML for Table<'a> { fn build(&self) -> Vec { let grid = TableGrid::new(self.grid.clone()); let b = XMLBuilder::new() diff --git a/docx-core/src/documents/elements/table_cell.rs b/docx-core/src/documents/elements/table_cell.rs index b580a95..2b98f13 100644 --- a/docx-core/src/documents/elements/table_cell.rs +++ b/docx-core/src/documents/elements/table_cell.rs @@ -4,40 +4,40 @@ use crate::types::*; use crate::xml_builder::*; #[derive(Debug, Clone)] -pub struct TableCell { +pub struct TableCell<'a> { property: TableCellProperty, - contents: Vec, + contents: Vec>, } #[derive(Debug, Clone)] -pub enum TableCellContent { - Paragraph(Paragraph), +pub enum TableCellContent<'a> { + Paragraph(Paragraph<'a>), } -impl TableCell { - pub fn new() -> TableCell { +impl<'a> TableCell<'a> { + pub fn new() -> TableCell<'a> { let property = TableCellProperty::new(); let contents = vec![]; Self { property, contents } } - pub fn add_paragraph(mut self, p: Paragraph) -> TableCell { + pub fn add_paragraph(mut self, p: Paragraph<'a>) -> TableCell<'a> { self.contents.push(TableCellContent::Paragraph(p)); self } - pub fn vertical_merge(mut self, t: VMergeType) -> TableCell { + pub fn vertical_merge(mut self, t: VMergeType) -> TableCell<'a> { self.property = self.property.vertical_merge(t); self } - pub fn grid_span(mut self, v: usize) -> TableCell { + pub fn grid_span(mut self, v: usize) -> TableCell<'a> { self.property = self.property.grid_span(v); self } } -impl BuildXML for TableCell { +impl<'a> BuildXML for TableCell<'a> { fn build(&self) -> Vec { let b = XMLBuilder::new(); let mut b = b.open_table_cell().add_child(&self.property); diff --git a/docx-core/src/documents/elements/table_row.rs b/docx-core/src/documents/elements/table_row.rs index a525174..bd30478 100644 --- a/docx-core/src/documents/elements/table_row.rs +++ b/docx-core/src/documents/elements/table_row.rs @@ -3,19 +3,19 @@ use crate::documents::BuildXML; use crate::xml_builder::*; #[derive(Debug, Clone)] -pub struct TableRow { +pub struct TableRow<'a> { property: TableRowProperty, - cells: Vec, + cells: Vec>, } -impl TableRow { - pub fn new(cells: Vec) -> TableRow { +impl<'a> TableRow<'a> { + pub fn new(cells: Vec>) -> TableRow<'a> { let property = TableRowProperty::new(); Self { property, cells } } } -impl BuildXML for TableRow { +impl<'a> BuildXML for TableRow<'a> { fn build(&self) -> Vec { let b = XMLBuilder::new() .open_table_row() diff --git a/docx-core/src/documents/history_id.rs b/docx-core/src/documents/history_id.rs new file mode 100644 index 0000000..a025ffb --- /dev/null +++ b/docx-core/src/documents/history_id.rs @@ -0,0 +1,21 @@ +#[allow(unused)] +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[allow(dead_code)] +static HISTORY_ID: AtomicUsize = AtomicUsize::new(0); + +#[cfg(not(test))] +pub trait HistoryId { + fn generate(&self) -> String { + let id = HISTORY_ID.load(Ordering::Relaxed); + HISTORY_ID.store(id + 1, Ordering::Relaxed); + format!("{}", id) + } +} + +#[cfg(test)] +pub trait HistoryId { + fn generate(&self) -> &str { + "123" + } +} diff --git a/docx-core/src/documents/mod.rs b/docx-core/src/documents/mod.rs index 03be5b3..421ff86 100644 --- a/docx-core/src/documents/mod.rs +++ b/docx-core/src/documents/mod.rs @@ -5,12 +5,14 @@ mod document; mod document_rels; mod elements; mod font_table; +mod history_id; mod rels; mod settings; mod styles; mod xml_docx; -pub(crate) use build_xml::*; +pub(crate) use build_xml::BuildXML; +pub(crate) use history_id::HistoryId; pub use content_types::*; pub use doc_props::*; @@ -30,7 +32,7 @@ pub struct Docx<'a> { document_rels: DocumentRels, doc_props: DocProps<'a>, styles: Styles, - document: Document, + document: Document<'a>, settings: Settings, font_table: FontTable, } @@ -63,12 +65,12 @@ impl<'a> Docx<'a> { Default::default() } - pub fn add_paragraph(mut self, p: Paragraph) -> Docx<'a> { + pub fn add_paragraph(mut self, p: Paragraph<'a>) -> Docx<'a> { self.document = self.document.add_paragraph(p); self } - pub fn add_table(mut self, t: Table) -> Docx<'a> { + pub fn add_table(mut self, t: Table<'a>) -> Docx<'a> { self.document = self.document.add_table(t); self } diff --git a/docx-core/src/xml_builder/elements.rs b/docx-core/src/xml_builder/elements.rs index 32ae686..74518a4 100644 --- a/docx-core/src/xml_builder/elements.rs +++ b/docx-core/src/xml_builder/elements.rs @@ -22,6 +22,19 @@ impl XMLBuilder { self.writer.write(text).expect(EXPECT_MESSAGE); self.close() } + // i.e. + pub(crate) fn delete_text(mut self, text: &str, preserve_space: bool) -> Self { + let space = if preserve_space { + "preserve" + } else { + "default" + }; + self.writer + .write(XmlEvent::start_element("w:delText").attr("xml:space", space)) + .expect(EXPECT_MESSAGE); + self.writer.write(text).expect(EXPECT_MESSAGE); + self.close() + } // i.e. opened_el!(open_run, "w:r"); opened_el!(open_run_property, "w:rPr"); @@ -148,6 +161,9 @@ impl XMLBuilder { "w:bottom", "w:gutter" ); + + opened_el!(open_insert, "w:ins", "w:id", "w:author", "w:data"); + opened_el!(open_delete, "w:del", "w:id", "w:author", "w:data"); } #[cfg(test)] diff --git a/docx-core/tests/lib.rs b/docx-core/tests/lib.rs index c8372b7..73eb01d 100644 --- a/docx-core/tests/lib.rs +++ b/docx-core/tests/lib.rs @@ -47,11 +47,7 @@ pub fn size() -> Result<(), DocxError> { let path = std::path::Path::new("./tests/output/size.docx"); let file = std::fs::File::create(&path).unwrap(); Docx::new() - .add_paragraph( - Paragraph::new() - .add_run(Run::new().add_text("Hello")) - .size(60), - ) + .add_paragraph(Paragraph::new().add_run(Run::new().add_text("Hello").size(60))) .add_paragraph( Paragraph::new() .add_run(Run::new().add_text(" Wor").size(50)) @@ -219,3 +215,18 @@ pub fn custom_attr_paragraph() -> Result<(), DocxError> { .pack(file)?; Ok(()) } + +#[test] +pub fn history() -> Result<(), DocxError> { + let path = std::path::Path::new("./tests/output/history.docx"); + let file = std::fs::File::create(&path).unwrap(); + Docx::new() + .add_paragraph( + Paragraph::new() + .add_insert(Insert::new().run(Run::new().add_text("Hello"))) + .add_delete(Delete::new().run(Run::new().add_delete_text("World"))), + ) + .build() + .pack(file)?; + Ok(()) +} diff --git a/fixtures/history_libre_office/[Content_Types].xml b/fixtures/history_libre_office/[Content_Types].xml new file mode 100644 index 0000000..dc111cb --- /dev/null +++ b/fixtures/history_libre_office/[Content_Types].xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/fixtures/history_libre_office/_rels/.rels b/fixtures/history_libre_office/_rels/.rels new file mode 100644 index 0000000..f0b72e7 --- /dev/null +++ b/fixtures/history_libre_office/_rels/.rels @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/fixtures/history_libre_office/docProps/app.xml b/fixtures/history_libre_office/docProps/app.xml new file mode 100644 index 0000000..d248599 --- /dev/null +++ b/fixtures/history_libre_office/docProps/app.xml @@ -0,0 +1,2 @@ + +6LibreOffice/6.0.7.3$Linux_X86_64 LibreOffice_project/00m0$Build-311551 \ No newline at end of file diff --git a/fixtures/history_libre_office/docProps/core.xml b/fixtures/history_libre_office/docProps/core.xml new file mode 100644 index 0000000..dbc6cf3 --- /dev/null +++ b/fixtures/history_libre_office/docProps/core.xml @@ -0,0 +1,2 @@ + +2019-11-15T14:17:59Zja-JP2019-11-15T14:46:47Z6 \ No newline at end of file diff --git a/fixtures/history_libre_office/history.docx b/fixtures/history_libre_office/history.docx new file mode 100644 index 0000000..106fbce Binary files /dev/null and b/fixtures/history_libre_office/history.docx differ diff --git a/fixtures/history_libre_office/word/_rels/document.xml.rels b/fixtures/history_libre_office/word/_rels/document.xml.rels new file mode 100644 index 0000000..8a2db8a --- /dev/null +++ b/fixtures/history_libre_office/word/_rels/document.xml.rels @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/fixtures/history_libre_office/word/document.xml b/fixtures/history_libre_office/word/document.xml new file mode 100644 index 0000000..8c2b701 --- /dev/null +++ b/fixtures/history_libre_office/word/document.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + W + + + + + + a + + + + + + rld + + + + + + Hello + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/history_libre_office/word/fontTable.xml b/fixtures/history_libre_office/word/fontTable.xml new file mode 100644 index 0000000..3916a0e --- /dev/null +++ b/fixtures/history_libre_office/word/fontTable.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/fixtures/history_libre_office/word/settings.xml b/fixtures/history_libre_office/word/settings.xml new file mode 100644 index 0000000..ce04db3 --- /dev/null +++ b/fixtures/history_libre_office/word/settings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/fixtures/history_libre_office/word/styles.xml b/fixtures/history_libre_office/word/styles.xml new file mode 100644 index 0000000..1f8b301 --- /dev/null +++ b/fixtures/history_libre_office/word/styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file