diff --git a/docx-core/examples/dirty_toc.rs b/docx-core/examples/dirty_toc.rs new file mode 100644 index 0000000..67f9a2a --- /dev/null +++ b/docx-core/examples/dirty_toc.rs @@ -0,0 +1,18 @@ +use docx_rs::*; + +pub fn main() -> Result<(), DocxError> { + let path = std::path::Path::new("./output/dirty_toc.docx"); + let file = std::fs::File::create(&path).unwrap(); + let p = Paragraph::new() + .add_run(Run::new().add_text("Hello")) + .style("Heading1") + .page_break_before(true); + let style = Style::new("Heading1", StyleType::Paragraph).name("Heading 1"); + Docx::new() + .add_style(style) + .add_table_of_contents(TableOfContents::new().heading_styles_range(1, 3)) + .add_paragraph(p) + .build() + .pack(file)?; + Ok(()) +} diff --git a/docx-core/src/documents/document.rs b/docx-core/src/documents/document.rs index 0a4c468..1950052 100644 --- a/docx-core/src/documents/document.rs +++ b/docx-core/src/documents/document.rs @@ -22,6 +22,7 @@ pub enum DocumentChild { CommentStart(Box), CommentEnd(CommentRangeEnd), StructuredDataTag(StructuredDataTag), + TableOfContents(TableOfContents), } impl Serialize for DocumentChild { @@ -72,6 +73,12 @@ impl Serialize for DocumentChild { t.serialize_field("data", r)?; t.end() } + DocumentChild::TableOfContents(ref r) => { + let mut t = serializer.serialize_struct("TableOfContents", 2)?; + t.serialize_field("type", "tableOfContents")?; + t.serialize_field("data", r)?; + t.end() + } } } } @@ -194,6 +201,11 @@ impl Document { self.children.push(DocumentChild::StructuredDataTag(t)); self } + + pub fn add_table_of_contents(mut self, t: TableOfContents) -> Self { + self.children.push(DocumentChild::TableOfContents(t)); + self + } } impl BuildXML for DocumentChild { @@ -206,6 +218,7 @@ impl BuildXML for DocumentChild { DocumentChild::CommentStart(v) => v.build(), DocumentChild::CommentEnd(v) => v.build(), DocumentChild::StructuredDataTag(v) => v.build(), + DocumentChild::TableOfContents(v) => v.build(), } } } @@ -243,6 +256,22 @@ mod tests { r#" Hello +"# + ); + } + + #[test] + fn test_document_with_toc() { + let toc = TableOfContents::new().heading_styles_range(1, 3); + let b = Document::new().add_table_of_contents(toc).build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#" + + + + TOC \o "1-3" + "# ); } diff --git a/docx-core/src/documents/elements/fld_char.rs b/docx-core/src/documents/elements/fld_char.rs new file mode 100644 index 0000000..96fb354 --- /dev/null +++ b/docx-core/src/documents/elements/fld_char.rs @@ -0,0 +1,54 @@ +use serde::Serialize; + +use crate::documents::*; +use crate::types::*; +use crate::xml_builder::*; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct FieldChar { + pub field_char_type: FieldCharType, + pub dirty: bool, +} + +impl FieldChar { + pub fn new(t: FieldCharType) -> Self { + Self { + field_char_type: t, + dirty: false, + } + } + + pub fn dirty(mut self) -> Self { + self.dirty = true; + self + } +} + +impl BuildXML for FieldChar { + fn build(&self) -> Vec { + XMLBuilder::new() + .field_character( + &format!("{}", self.field_char_type), + &format!("{}", &self.dirty), + ) + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_field_character() { + let b = FieldChar::new(FieldCharType::Begin).dirty().build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/instr_text.rs b/docx-core/src/documents/elements/instr_text.rs new file mode 100644 index 0000000..2476a5d --- /dev/null +++ b/docx-core/src/documents/elements/instr_text.rs @@ -0,0 +1,43 @@ +use serde::Serialize; + +use crate::documents::*; +use crate::xml_builder::*; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct InstrText { + pub val: String, +} + +impl InstrText { + pub fn new(i: impl Into) -> Self { + Self { val: i.into() } + } +} + +impl BuildXML for InstrText { + fn build(&self) -> Vec { + XMLBuilder::new() + .open_instr_text() + .plain_text(&self.val) + .close() + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_toc_instr() { + let b = InstrText::new(r#"ToC \o "1-3""#).build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#"ToC \o "1-3""# + ); + } +} diff --git a/docx-core/src/documents/elements/mod.rs b/docx-core/src/documents/elements/mod.rs index 73eb3e6..df9cb1f 100644 --- a/docx-core/src/documents/elements/mod.rs +++ b/docx-core/src/documents/elements/mod.rs @@ -22,6 +22,7 @@ mod doc_grid; mod doc_id; mod doc_var; mod drawing; +mod fld_char; mod font; mod footer_reference; mod grid_span; @@ -30,6 +31,7 @@ mod highlight; mod indent; mod indent_level; mod insert; +mod instr_text; mod italic; mod italic_cs; mod justification; @@ -77,6 +79,7 @@ mod table_cell_width; mod table_grid; mod table_indent; mod table_layout; +mod table_of_contents; mod table_property; mod table_row; mod table_row_property; @@ -120,6 +123,7 @@ pub use doc_grid::*; pub use doc_id::*; pub use doc_var::*; pub use drawing::*; +pub use fld_char::*; pub use font::*; pub use footer_reference::*; pub use grid_span::*; @@ -128,6 +132,7 @@ pub use highlight::*; pub use indent::*; pub use indent_level::*; pub use insert::*; +pub use instr_text::*; pub use italic::*; pub use italic_cs::*; pub use justification::*; @@ -175,6 +180,7 @@ pub use table_cell_width::*; pub use table_grid::*; pub use table_indent::*; pub use table_layout::*; +pub use table_of_contents::*; pub use table_property::*; pub use table_row::*; pub use table_row_property::*; diff --git a/docx-core/src/documents/elements/run.rs b/docx-core/src/documents/elements/run.rs index d82aa36..143a08d 100644 --- a/docx-core/src/documents/elements/run.rs +++ b/docx-core/src/documents/elements/run.rs @@ -32,6 +32,8 @@ pub enum RunChild { Drawing(Box), CommentStart(Box), CommentEnd(CommentRangeEnd), + FieldChar(FieldChar), + InstrText(InstrText), } impl Serialize for RunChild { @@ -81,6 +83,18 @@ impl Serialize for RunChild { t.serialize_field("data", r)?; t.end() } + RunChild::FieldChar(ref f) => { + let mut t = serializer.serialize_struct("FieldChar", 2)?; + t.serialize_field("type", "fieldChar")?; + t.serialize_field("data", f)?; + t.end() + } + RunChild::InstrText(ref i) => { + let mut t = serializer.serialize_struct("InstrText", 2)?; + t.serialize_field("type", "instrText")?; + t.serialize_field("data", i)?; + t.end() + } } } } @@ -103,6 +117,20 @@ impl Run { self } + pub fn add_field_char(mut self, t: crate::types::FieldCharType, dirty: bool) -> Run { + let mut f = FieldChar::new(t); + if dirty { + f = f.dirty(); + }; + self.children.push(RunChild::FieldChar(f)); + self + } + + pub fn add_instr_text(mut self, i: impl Into) -> Run { + self.children.push(RunChild::InstrText(InstrText::new(i))); + self + } + pub fn add_tab(mut self) -> Run { self.children.push(RunChild::Tab(Tab::new())); self @@ -216,6 +244,8 @@ impl BuildXML for Run { RunChild::Drawing(t) => b = b.add_child(t), RunChild::CommentStart(c) => b = b.add_child(c), RunChild::CommentEnd(c) => b = b.add_child(c), + RunChild::FieldChar(c) => b = b.add_child(c), + RunChild::InstrText(c) => b = b.add_child(c), } } b.close().build() diff --git a/docx-core/src/documents/elements/table_of_contents.rs b/docx-core/src/documents/elements/table_of_contents.rs new file mode 100644 index 0000000..6552a03 --- /dev/null +++ b/docx-core/src/documents/elements/table_of_contents.rs @@ -0,0 +1,79 @@ +use serde::Serialize; + +use crate::documents::*; +use crate::types::*; +use crate::xml_builder::*; + +// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_TOCTOC_topic_ID0ELZO1.html +#[derive(Serialize, Debug, Clone, PartialEq, Default)] +pub struct TableOfContents { + // If no heading range is specified, all heading levels used in the document are listed. + heading_styles_range: Option<(usize, usize)>, +} + +impl TableOfContents { + pub fn new() -> Self { + Self::default() + } + + pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self { + self.heading_styles_range = Some((start, end)); + self + } + + fn build_instr_text(&self) -> String { + let mut instr = "TOC".to_string(); + + if let Some(heading_styles_range) = self.heading_styles_range { + instr = format!( + "{} \\o "{}-{}"", + instr, heading_styles_range.0, heading_styles_range.1 + ); + } + instr + } +} + +impl BuildXML for TableOfContents { + fn build(&self) -> Vec { + let p1 = Paragraph::new().add_run( + Run::new() + .add_field_char(FieldCharType::Begin, true) + .add_instr_text(self.build_instr_text()) + .add_field_char(FieldCharType::Separate, false), + ); + let p2 = Paragraph::new().add_run(Run::new().add_field_char(FieldCharType::End, false)); + + XMLBuilder::new() + .open_structured_tag() + .open_structured_tag_property() + .close() + .open_structured_tag_content() + .add_child(&p1) + .add_child(&p2) + .close() + .close() + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_toc() { + let b = TableOfContents::new().heading_styles_range(1, 3).build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#" + + TOC \o "1-3" +"# + ); + } +} diff --git a/docx-core/src/documents/mod.rs b/docx-core/src/documents/mod.rs index 217dd5f..2bb4c9d 100644 --- a/docx-core/src/documents/mod.rs +++ b/docx-core/src/documents/mod.rs @@ -155,6 +155,11 @@ impl Docx { self } + pub fn add_style(mut self, s: Style) -> Self { + self.styles = self.styles.add_style(s); + self + } + pub fn numberings(mut self, n: Numberings) -> Self { self.numberings = n; self @@ -201,6 +206,11 @@ impl Docx { self } + pub fn add_table_of_contents(mut self, t: TableOfContents) -> Docx { + self.document = self.document.add_table_of_contents(t); + self + } + pub fn add_bookmark_start(mut self, id: usize, name: impl Into) -> Docx { self.document = self.document.add_bookmark_start(id, name); self diff --git a/docx-core/src/types/field_char_type.rs b/docx-core/src/types/field_char_type.rs new file mode 100644 index 0000000..09ca88c --- /dev/null +++ b/docx-core/src/types/field_char_type.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +// +// Please see https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_ST_FldCharType_topic_ID0E6TU2.html#topic_ID0E6TU2 +// +use std::fmt; +use std::str::FromStr; +use wasm_bindgen::prelude::*; + +use super::errors; + +#[wasm_bindgen] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum FieldCharType { + Begin, + Separate, + End, + Unsupported, +} + +impl fmt::Display for FieldCharType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + FieldCharType::Begin => write!(f, "begin"), + FieldCharType::Separate => write!(f, "separate"), + FieldCharType::End => write!(f, "end"), + FieldCharType::Unsupported => write!(f, "unsupported"), + } + } +} + +impl FromStr for FieldCharType { + type Err = errors::TypeError; + fn from_str(s: &str) -> Result { + match s { + "begin" => Ok(FieldCharType::Begin), + "separate" => Ok(FieldCharType::Separate), + "end" => Ok(FieldCharType::End), + _ => Ok(FieldCharType::Unsupported), + } + } +} diff --git a/docx-core/src/types/mod.rs b/docx-core/src/types/mod.rs index 3387481..c939f63 100644 --- a/docx-core/src/types/mod.rs +++ b/docx-core/src/types/mod.rs @@ -5,6 +5,7 @@ pub mod break_type; pub mod doc_grid_type; pub mod emu; pub mod errors; +pub mod field_char_type; pub mod font_pitch_type; pub mod height_rule; pub mod level_suffix_type; @@ -30,6 +31,7 @@ pub use break_type::*; pub use doc_grid_type::*; pub use emu::*; pub use errors::*; +pub use field_char_type::*; pub use font_pitch_type::*; pub use height_rule::*; pub use level_suffix_type::*; diff --git a/docx-core/src/xml_builder/elements.rs b/docx-core/src/xml_builder/elements.rs index c7389da..882b97d 100644 --- a/docx-core/src/xml_builder/elements.rs +++ b/docx-core/src/xml_builder/elements.rs @@ -92,6 +92,10 @@ impl XMLBuilder { // i.e. closed_with_usize!(sz_cs, "w:szCs"); + closed!(field_character, "w:fldChar", "w:fldCharType", "w:dirty"); + + open!(open_instr_text, "w:instrText"); + closed_with_str!(text_direction, "w:textDirection"); closed!(b, "w:b");