diff --git a/docx-core/examples/header_with_page_num.rs b/docx-core/examples/header_with_page_num.rs new file mode 100644 index 0000000..ed700e8 --- /dev/null +++ b/docx-core/examples/header_with_page_num.rs @@ -0,0 +1,22 @@ +use docx_rs::*; + +pub fn main() -> Result<(), DocxError> { + let path = std::path::Path::new("./output/examples/header_with_page_num.docx"); + let file = std::fs::File::create(path).unwrap(); + let header = Header::new() + .add_page_num( + PageNum::new() + .wrap("none") + .v_anchor("text") + .h_anchor("margin") + .x_align("right") + .y(1), + ) + .add_paragraph(Paragraph::new().add_run(Run::new().add_text("Hello"))); + Docx::new() + .header(header) + .add_paragraph(Paragraph::new().add_run(Run::new().add_text("World"))) + .build() + .pack(file)?; + Ok(()) +} diff --git a/docx-core/examples/reader.rs b/docx-core/examples/reader.rs index 5162007..3410672 100644 --- a/docx-core/examples/reader.rs +++ b/docx-core/examples/reader.rs @@ -4,7 +4,7 @@ use std::fs::File; use std::io::{Read, Write}; pub fn main() { - let mut file = File::open("./header-image.docx").unwrap(); + let mut file = File::open("./hello.docx").unwrap(); let mut buf = vec![]; file.read_to_end(&mut buf).unwrap(); diff --git a/docx-core/src/documents/elements/frame_property.rs b/docx-core/src/documents/elements/frame_property.rs new file mode 100644 index 0000000..514316f --- /dev/null +++ b/docx-core/src/documents/elements/frame_property.rs @@ -0,0 +1,146 @@ +use serde::Serialize; + +use crate::documents::BuildXML; +use crate::xml_builder::*; + +// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.frameproperties?view=openxml-3.0.1 +#[derive(Debug, Clone, PartialEq, Serialize, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "wasm", derive(ts_rs::TS))] +#[cfg_attr(feature = "wasm", ts(export))] +pub struct FrameProperty { + /// Frame Height + /// Represents the following attribute in the schema: w:h + #[serde(skip_serializing_if = "Option::is_none")] + pub h: Option, + /// Frame Height Type + /// Represents the following attribute in the schema: w:hRule + #[serde(skip_serializing_if = "Option::is_none")] + pub h_rule: Option, + /// Frame Horizontal Positioning Base + /// Represents the following attribute in the schema: w:hAnchor + #[serde(skip_serializing_if = "Option::is_none")] + pub h_anchor: Option, + /// Horizontal Frame Padding + /// Represents the following attribute in the schema: w:hSpace + #[serde(skip_serializing_if = "Option::is_none")] + pub h_space: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub v_anchor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub v_space: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub w: Option, + /// Text Wrapping Around Frame + /// Represents the following attribute in the schema: w:wrap + #[serde(skip_serializing_if = "Option::is_none")] + pub wrap: Option, + /// Absolute Horizontal Position + /// Represents the following attribute in the schema: w:x + #[serde(skip_serializing_if = "Option::is_none")] + pub x: Option, + /// Relative Horizontal Position + /// Represents the following attribute in the schema: w:xAlign + #[serde(skip_serializing_if = "Option::is_none")] + pub x_align: Option, + /// Absolute Vertical Position + /// Represents the following attribute in the schema: w:y + #[serde(skip_serializing_if = "Option::is_none")] + pub y: Option, + /// Relative Vertical Position + /// Represents the following attribute in the schema: w:yAlign + #[serde(skip_serializing_if = "Option::is_none")] + pub y_align: Option, +} + +impl FrameProperty { + pub fn new() -> FrameProperty { + Default::default() + } + + pub fn wrap(mut self, wrap: impl Into) -> Self { + self.wrap = Some(wrap.into()); + self + } + + pub fn v_anchor(mut self, anchor: impl Into) -> Self { + self.v_anchor = Some(anchor.into()); + self + } + + pub fn h_anchor(mut self, anchor: impl Into) -> Self { + self.h_anchor = Some(anchor.into()); + self + } + + pub fn h_rule(mut self, r: impl Into) -> Self { + self.h_rule = Some(r.into()); + self + } + + pub fn x_align(mut self, align: impl Into) -> Self { + self.x_align = Some(align.into()); + self + } + + pub fn y_align(mut self, align: impl Into) -> Self { + self.y_align = Some(align.into()); + self + } + + pub fn h_space(mut self, x: i32) -> Self { + self.h_space = Some(x); + self + } + + pub fn v_space(mut self, x: i32) -> Self { + self.v_space = Some(x); + self + } + + pub fn x(mut self, x: i32) -> Self { + self.x = Some(x); + self + } + + pub fn y(mut self, y: i32) -> Self { + self.y = Some(y); + self + } + + pub fn width(mut self, n: u32) -> Self { + self.w = Some(n); + self + } + + pub fn height(mut self, n: u32) -> Self { + self.h = Some(n); + self + } +} + +impl BuildXML for FrameProperty { + fn build(&self) -> Vec { + let b = XMLBuilder::new(); + b.frame_property(self).build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_q_format() { + let c = FrameProperty::new().wrap("none"); + let b = c.build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#""# + ); + } +} diff --git a/docx-core/src/documents/elements/instr_page.rs b/docx-core/src/documents/elements/instr_page.rs new file mode 100644 index 0000000..1e45f6f --- /dev/null +++ b/docx-core/src/documents/elements/instr_page.rs @@ -0,0 +1,20 @@ +use serde::Serialize; + +use crate::documents::*; + +#[derive(Serialize, Debug, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct InstrPAGE {} + +impl InstrPAGE { + pub fn new() -> Self { + Self {} + } +} + +impl BuildXML for InstrPAGE { + fn build(&self) -> Vec { + let instr = "PAGE".to_owned(); + instr.into() + } +} diff --git a/docx-core/src/documents/elements/instr_text.rs b/docx-core/src/documents/elements/instr_text.rs index f2def1d..2ca84d4 100644 --- a/docx-core/src/documents/elements/instr_text.rs +++ b/docx-core/src/documents/elements/instr_text.rs @@ -8,6 +8,7 @@ use crate::xml_builder::*; pub enum InstrText { TOC(InstrToC), TC(InstrTC), + PAGE(InstrPAGE), PAGEREF(InstrPAGEREF), HYPERLINK(InstrHyperlink), Unsupported(String), @@ -19,6 +20,7 @@ impl BuildXML for Box { InstrText::TOC(toc) => toc.build(), InstrText::TC(tc) => tc.build(), InstrText::PAGEREF(page_ref) => page_ref.build(), + InstrText::PAGE(page) => page.build(), InstrText::HYPERLINK(_link) => todo!(), InstrText::Unsupported(s) => s.as_bytes().to_vec(), }; @@ -54,6 +56,12 @@ impl Serialize for InstrText { t.serialize_field("data", s)?; t.end() } + InstrText::PAGE(ref s) => { + let mut t = serializer.serialize_struct("PAGE", 2)?; + t.serialize_field("type", "page")?; + t.serialize_field("data", s)?; + t.end() + } InstrText::HYPERLINK(ref s) => { let mut t = serializer.serialize_struct("HYPERLINK", 2)?; t.serialize_field("type", "hyperlink")?; diff --git a/docx-core/src/documents/elements/mod.rs b/docx-core/src/documents/elements/mod.rs index f5b079e..7695e2f 100644 --- a/docx-core/src/documents/elements/mod.rs +++ b/docx-core/src/documents/elements/mod.rs @@ -29,6 +29,7 @@ mod fld_char; mod font; mod font_scheme; mod footer_reference; +mod frame_property; mod grid_span; mod header_reference; mod highlight; @@ -37,6 +38,7 @@ mod indent; mod indent_level; mod insert; mod instr_hyperlink; +mod instr_page; mod instr_pageref; mod instr_tc; mod instr_text; @@ -61,6 +63,7 @@ mod numbering_id; mod numbering_property; mod outline_lvl; mod page_margin; +mod page_num; mod page_num_type; mod page_size; mod paragraph; @@ -154,6 +157,7 @@ pub use fld_char::*; pub use font::*; pub use font_scheme::*; pub use footer_reference::*; +pub use frame_property::*; pub use grid_span::*; pub use header_reference::*; pub use highlight::*; @@ -162,6 +166,7 @@ pub use indent::*; pub use indent_level::*; pub use insert::*; pub use instr_hyperlink::*; +pub use instr_page::*; pub use instr_pageref::*; pub use instr_tc::*; pub use instr_text::*; @@ -186,6 +191,7 @@ pub use numbering_id::*; pub use numbering_property::*; pub use outline_lvl::*; pub use page_margin::*; +pub use page_num::*; pub use page_num_type::*; pub use page_size::*; pub use paragraph::*; diff --git a/docx-core/src/documents/elements/page_num.rs b/docx-core/src/documents/elements/page_num.rs new file mode 100644 index 0000000..8e0c22f --- /dev/null +++ b/docx-core/src/documents/elements/page_num.rs @@ -0,0 +1,197 @@ +use serde::Serialize; + +use crate::documents::*; +use crate::types::*; +use crate::xml_builder::*; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct PageNum { + pub instr: InstrPAGE, + pub frame_property: Option, +} + +impl Default for PageNum { + fn default() -> Self { + Self { + instr: InstrPAGE {}, + frame_property: Some(FrameProperty { + wrap: Some("none".to_owned()), + v_anchor: Some("text".to_owned()), + h_anchor: Some("margin".to_owned()), + x_align: Some("right".to_owned()), + y: Some(1), + ..Default::default() + }), + } + } +} + +impl PageNum { + pub fn new() -> Self { + Self::default() + } + + pub fn wrap(mut self, wrap: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + wrap: Some(wrap.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn v_anchor(mut self, anchor: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + v_anchor: Some(anchor.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn h_anchor(mut self, anchor: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + h_anchor: Some(anchor.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn h_rule(mut self, r: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + h_rule: Some(r.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn x_align(mut self, align: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + x_align: Some(align.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn y_align(mut self, align: impl Into) -> Self { + self.frame_property = Some(FrameProperty { + y_align: Some(align.into()), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn h_space(mut self, x: i32) -> Self { + self.frame_property = Some(FrameProperty { + h_space: Some(x), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn v_space(mut self, x: i32) -> Self { + self.frame_property = Some(FrameProperty { + v_space: Some(x), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn x(mut self, x: i32) -> Self { + self.frame_property = Some(FrameProperty { + x: Some(x), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn y(mut self, y: i32) -> Self { + self.frame_property = Some(FrameProperty { + y: Some(y), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn width(mut self, n: u32) -> Self { + self.frame_property = Some(FrameProperty { + w: Some(n), + ..self.frame_property.unwrap_or_default() + }); + self + } + + pub fn height(mut self, n: u32) -> Self { + self.frame_property = Some(FrameProperty { + h: Some(n), + ..self.frame_property.unwrap_or_default() + }); + self + } + + fn inner_build(&self) -> Vec { + let p = StructuredDataTagProperty::new(); + let mut b = XMLBuilder::new(); + + b = b + .open_structured_tag() + .add_child(&p) + .open_structured_tag_content(); + + let mut p = Paragraph::new().add_run( + Run::new() + .add_field_char(FieldCharType::Begin, false) + .add_instr_text(InstrText::PAGE(self.instr.clone())) + .add_field_char(FieldCharType::Separate, false) + .add_text("1") + .add_field_char(FieldCharType::End, false), + ); + + if let Some(ref f) = self.frame_property { + p.property.frame_property = Some(f.clone()); + } + + b = b.add_child(&p); + b = b.close().close(); + b.build() + } +} + +impl BuildXML for PageNum { + fn build(&self) -> Vec { + self.inner_build() + } +} + +impl BuildXML for Box { + fn build(&self) -> Vec { + self.inner_build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_page() { + let b = PageNum::new().build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#"PAGE1 +"# + ); + } + + #[test] + fn test_page_with_wrap() { + let b = PageNum::new().wrap("none").x_align("left").build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#"PAGE1 +"# + ); + } +} diff --git a/docx-core/src/documents/elements/paragraph_property.rs b/docx-core/src/documents/elements/paragraph_property.rs index 8530a4f..06ab0c5 100644 --- a/docx-core/src/documents/elements/paragraph_property.rs +++ b/docx-core/src/documents/elements/paragraph_property.rs @@ -40,6 +40,8 @@ pub struct ParagraphProperty { pub paragraph_property_change: Option, #[serde(skip_serializing_if = "Option::is_none")] pub borders: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub frame_property: Option, } // 17.3.1.26 @@ -138,6 +140,11 @@ impl ParagraphProperty { self } + pub fn frame_property(mut self, s: FrameProperty) -> Self { + self.frame_property = Some(s); + self + } + pub(crate) fn hanging_chars(mut self, chars: i32) -> Self { if let Some(indent) = self.indent { self.indent = Some(indent.hanging_chars(chars)); @@ -179,6 +186,7 @@ fn inner_build(p: &ParagraphProperty) -> Vec { .add_child(&p.run_property) .add_optional_child(&p.style) .add_optional_child(&p.numbering_property) + .add_optional_child(&p.frame_property) .add_optional_child(&p.alignment) .add_optional_child(&p.indent) .add_optional_child(&p.line_spacing) diff --git a/docx-core/src/documents/footer.rs b/docx-core/src/documents/footer.rs index c0fd603..9f728dc 100644 --- a/docx-core/src/documents/footer.rs +++ b/docx-core/src/documents/footer.rs @@ -32,12 +32,28 @@ impl Footer { self.children.push(FooterChild::Table(Box::new(t))); self } + + pub fn add_page_num(mut self, p: PageNum) -> Self { + self.children.push(FooterChild::PageNum(Box::new(p))); + self + } + + /// reader only + pub(crate) fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Self { + if t.has_numbering { + self.has_numbering = true + } + self.children + .push(FooterChild::StructuredDataTag(Box::new(t))); + self + } } #[derive(Debug, Clone, PartialEq)] pub enum FooterChild { Paragraph(Box), Table(Box), + PageNum(Box), StructuredDataTag(Box), } @@ -59,6 +75,12 @@ impl Serialize for FooterChild { t.serialize_field("data", c)?; t.end() } + FooterChild::PageNum(ref r) => { + let mut t = serializer.serialize_struct("PageNum", 2)?; + t.serialize_field("type", "pageNum")?; + t.serialize_field("data", r)?; + t.end() + } FooterChild::StructuredDataTag(ref r) => { let mut t = serializer.serialize_struct("StructuredDataTag", 2)?; t.serialize_field("type", "structuredDataTag")?; @@ -78,6 +100,7 @@ impl BuildXML for Footer { match c { FooterChild::Paragraph(p) => b = b.add_child(p), FooterChild::Table(t) => b = b.add_child(t), + FooterChild::PageNum(p) => b = b.add_child(p), FooterChild::StructuredDataTag(t) => b = b.add_child(t), } } diff --git a/docx-core/src/documents/header.rs b/docx-core/src/documents/header.rs index 9c1cc9e..92fbffc 100644 --- a/docx-core/src/documents/header.rs +++ b/docx-core/src/documents/header.rs @@ -33,7 +33,13 @@ impl Header { self } - pub fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Self { + pub fn add_page_num(mut self, p: PageNum) -> Self { + self.children.push(HeaderChild::PageNum(Box::new(p))); + self + } + + /// reader only + pub(crate) fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Self { if t.has_numbering { self.has_numbering = true } @@ -47,6 +53,7 @@ impl Header { pub enum HeaderChild { Paragraph(Box), Table(Box
), + PageNum(Box), StructuredDataTag(Box), } @@ -68,6 +75,12 @@ impl Serialize for HeaderChild { t.serialize_field("data", c)?; t.end() } + HeaderChild::PageNum(ref r) => { + let mut t = serializer.serialize_struct("PageNum", 2)?; + t.serialize_field("type", "pageNum")?; + t.serialize_field("data", r)?; + t.end() + } HeaderChild::StructuredDataTag(ref r) => { let mut t = serializer.serialize_struct("StructuredDataTag", 2)?; t.serialize_field("type", "structuredDataTag")?; @@ -87,6 +100,7 @@ impl BuildXML for Header { match c { HeaderChild::Paragraph(p) => b = b.add_child(p), HeaderChild::Table(t) => b = b.add_child(t), + HeaderChild::PageNum(p) => b = b.add_child(p), HeaderChild::StructuredDataTag(t) => b = b.add_child(t), } } diff --git a/docx-core/src/documents/mod.rs b/docx-core/src/documents/mod.rs index efb9f70..9de4b4e 100644 --- a/docx-core/src/documents/mod.rs +++ b/docx-core/src/documents/mod.rs @@ -925,6 +925,7 @@ impl Docx { Some("header"), ); } + HeaderChild::PageNum(_) => {} HeaderChild::StructuredDataTag(tag) => { for child in tag.children.iter_mut() { if let StructuredDataTagChild::Paragraph(paragraph) = child { @@ -970,6 +971,7 @@ impl Docx { Some("header"), ); } + HeaderChild::PageNum(_) => {} HeaderChild::StructuredDataTag(tag) => { for child in tag.children.iter_mut() { if let StructuredDataTagChild::Paragraph(paragraph) = child { @@ -1015,6 +1017,7 @@ impl Docx { Some("header"), ); } + HeaderChild::PageNum(_) => {} HeaderChild::StructuredDataTag(tag) => { for child in tag.children.iter_mut() { if let StructuredDataTagChild::Paragraph(paragraph) = child { @@ -1059,6 +1062,7 @@ impl Docx { Some("footer"), ); } + FooterChild::PageNum(_) => {} FooterChild::Table(table) => { collect_images_from_table( table, @@ -1104,6 +1108,7 @@ impl Docx { Some("footer"), ); } + FooterChild::PageNum(_) => {} FooterChild::Table(table) => { collect_images_from_table( table, @@ -1149,6 +1154,7 @@ impl Docx { Some("footer"), ); } + FooterChild::PageNum(_) => {} FooterChild::Table(table) => { collect_images_from_table( table, diff --git a/docx-core/src/reader/footer.rs b/docx-core/src/reader/footer.rs index 465848c..c511e33 100644 --- a/docx-core/src/reader/footer.rs +++ b/docx-core/src/reader/footer.rs @@ -30,6 +30,12 @@ impl FromXML for Footer { } continue; } + XMLElement::StructuredDataTag => { + if let Ok(tag) = StructuredDataTag::read(&mut parser, &attributes) { + footer = footer.add_structured_data_tag(tag); + } + continue; + } _ => {} } } diff --git a/docx-core/src/reader/frame_property.rs b/docx-core/src/reader/frame_property.rs new file mode 100644 index 0000000..2b0cf59 --- /dev/null +++ b/docx-core/src/reader/frame_property.rs @@ -0,0 +1,68 @@ +use std::io::Read; +use std::str::FromStr; + +use xml::attribute::OwnedAttribute; +use xml::reader::EventReader; + +use super::*; + +impl ElementReader for FrameProperty { + fn read( + _r: &mut EventReader, + attrs: &[OwnedAttribute], + ) -> Result { + let mut fp = FrameProperty::new(); + for a in attrs { + let local_name = &a.name.local_name; + let e = XMLElement::from_str(local_name).unwrap(); + match e { + XMLElement::Wrap => { + fp = fp.wrap(a.value.clone()); + } + XMLElement::HeightRule => { + fp = fp.h_rule(a.value.clone()); + } + XMLElement::HAnchor => { + fp = fp.h_anchor(a.value.clone()); + } + XMLElement::VAnchor => { + fp = fp.v_anchor(a.value.clone()); + } + XMLElement::HSpace => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.h_space(s as i32) + } + } + XMLElement::VSpace => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.v_space(s as i32) + } + } + XMLElement::XAlign => fp = fp.x_align(a.value.clone()), + XMLElement::YAlign => fp = fp.y_align(a.value.clone()), + XMLElement::W => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.width(s as u32) + } + } + XMLElement::H => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.height(s as u32) + } + } + XMLElement::X => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.x(s as i32) + } + } + XMLElement::Y => { + if let Ok(s) = f64::from_str(&a.value) { + fp = fp.y(s as i32) + } + } + _ => {} + } + } + Ok(fp) + } +} diff --git a/docx-core/src/reader/mod.rs b/docx-core/src/reader/mod.rs index 3d2b4da..69b52ab 100644 --- a/docx-core/src/reader/mod.rs +++ b/docx-core/src/reader/mod.rs @@ -19,6 +19,7 @@ mod errors; mod font_group; mod font_scheme; mod footer; +mod frame_property; mod from_xml; mod header; mod header_or_footer_rels; diff --git a/docx-core/src/reader/paragraph_property.rs b/docx-core/src/reader/paragraph_property.rs index 288696c..fbd3061 100644 --- a/docx-core/src/reader/paragraph_property.rs +++ b/docx-core/src/reader/paragraph_property.rs @@ -110,6 +110,11 @@ impl ElementReader for ParagraphProperty { p.section_property = Some(sp); } } + XMLElement::FrameProperty => { + if let Ok(pr) = FrameProperty::read(r, &attributes) { + p.frame_property = Some(pr); + } + } XMLElement::Tabs => { if let Ok(tabs) = Tabs::read(r, &attributes) { for t in tabs.tabs { diff --git a/docx-core/src/reader/xml_element.rs b/docx-core/src/reader/xml_element.rs index ae64e8c..c5536a0 100644 --- a/docx-core/src/reader/xml_element.rs +++ b/docx-core/src/reader/xml_element.rs @@ -159,6 +159,18 @@ pub enum XMLElement { StructuredDataTag, Type, PageNumType, + FrameProperty, + H, + HAnchor, + HSpace, + VAnchor, + VSpace, + W, + Wrap, + X, + XAlign, + Y, + YAlign, Unsupported, } @@ -392,6 +404,18 @@ impl FromStr for XMLElement { "evenAndOddHeaders" => Ok(XMLElement::EvenAndOddHeaders), "sdt" => Ok(XMLElement::StructuredDataTag), "pgNumType" => Ok(XMLElement::PageNumType), + "framePr" => Ok(XMLElement::FrameProperty), + "h" => Ok(XMLElement::H), + "hAnchor" => Ok(XMLElement::HAnchor), + "hSpace" => Ok(XMLElement::HSpace), + "vAnchor" => Ok(XMLElement::VAnchor), + "vSpace" => Ok(XMLElement::VSpace), + "w" => Ok(XMLElement::W), + "wrap" => Ok(XMLElement::Wrap), + "x" => Ok(XMLElement::X), + "xAlign" => Ok(XMLElement::XAlign), + "y" => Ok(XMLElement::Y), + "yAlign" => Ok(XMLElement::YAlign), "type" => Ok(XMLElement::Type), _ => Ok(XMLElement::Unsupported), } diff --git a/docx-core/src/xml_builder/elements.rs b/docx-core/src/xml_builder/elements.rs index e1100cb..8b23b97 100644 --- a/docx-core/src/xml_builder/elements.rs +++ b/docx-core/src/xml_builder/elements.rs @@ -1,6 +1,7 @@ use super::XMLBuilder; use super::XmlEvent; use crate::types::*; +use crate::FrameProperty; const EXPECT_MESSAGE: &str = "should write buf"; @@ -567,6 +568,65 @@ impl XMLBuilder { self.close() } + /** + pub h_space: Option, + pub v_space: Option, + */ + + pub(crate) fn frame_property(mut self, prop: &FrameProperty) -> Self { + let mut w = XmlEvent::start_element("w:framePr"); + let wrap: String = prop.wrap.iter().cloned().collect(); + if prop.wrap.is_some() { + w = w.attr("w:wrap", &wrap); + } + let h_rule: String = prop.h_rule.iter().cloned().collect(); + if prop.h_rule.is_some() { + w = w.attr("w:hRule", &h_rule); + } + let h_anchor: String = prop.h_anchor.iter().cloned().collect(); + if prop.h_anchor.is_some() { + w = w.attr("w:hAnchor", &h_anchor); + } + let v_anchor: String = prop.v_anchor.iter().cloned().collect(); + if prop.v_anchor.is_some() { + w = w.attr("w:vAnchor", &v_anchor); + } + let x_align: String = prop.x_align.iter().cloned().collect(); + if prop.x_align.is_some() { + w = w.attr("w:xAlign", &x_align); + } + let y_align: String = prop.y_align.iter().cloned().collect(); + if prop.y_align.is_some() { + w = w.attr("w:yAlign", &y_align); + } + let x: String = format!("{}", prop.x.unwrap_or_default()); + if prop.x.is_some() { + w = w.attr("w:x", &x); + } + let y: String = format!("{}", prop.y.unwrap_or_default()); + if prop.y.is_some() { + w = w.attr("w:y", &y); + } + let h_space: String = format!("{}", prop.h_space.unwrap_or_default()); + if prop.h_space.is_some() { + w = w.attr("w:h_space", &h_space); + } + let v_space: String = format!("{}", prop.v_space.unwrap_or_default()); + if prop.v_space.is_some() { + w = w.attr("w:v_space", &v_space); + } + let width: String = format!("{}", prop.w.unwrap_or_default()); + if prop.w.is_some() { + w = w.attr("w:w", &width); + } + let h: String = format!("{}", prop.h.unwrap_or_default()); + if prop.h.is_some() { + w = w.attr("w:h", &h); + } + self.writer.write(w).expect(EXPECT_MESSAGE); + self.close() + } + pub(crate) fn page_num_type(mut self, start: Option, chap_style: Option) -> Self { let mut w = XmlEvent::start_element("w:pgNumType"); let start_string = format!("{}", start.unwrap_or_default()); diff --git a/docx-wasm/js/footer.ts b/docx-wasm/js/footer.ts index 1c58839..a7a44df 100644 --- a/docx-wasm/js/footer.ts +++ b/docx-wasm/js/footer.ts @@ -1,9 +1,10 @@ +import { PageNum } from "./page-num"; import { Paragraph } from "./paragraph"; import { Table } from "./table"; export class Footer { hasNumberings = false; - children: (Paragraph | Table)[] = []; + children: (Paragraph | Table | PageNum)[] = []; addParagraph(p: Paragraph) { if (p.hasNumberings) { @@ -20,4 +21,9 @@ export class Footer { this.children.push(t); return this; } + + addPageNum(p: PageNum) { + this.children.push(p); + return this; + } } diff --git a/docx-wasm/js/header.ts b/docx-wasm/js/header.ts index da2d614..bd416e5 100644 --- a/docx-wasm/js/header.ts +++ b/docx-wasm/js/header.ts @@ -1,9 +1,10 @@ +import { PageNum } from "./page-num"; import { Paragraph } from "./paragraph"; import { Table } from "./table"; export class Header { hasNumberings = false; - children: (Paragraph | Table)[] = []; + children: (Paragraph | Table | PageNum)[] = []; addParagraph(p: Paragraph) { if (p.hasNumberings) { @@ -20,4 +21,9 @@ export class Header { this.children.push(t); return this; } + + addPageNum(p: PageNum) { + this.children.push(p); + return this; + } } diff --git a/docx-wasm/js/index.ts b/docx-wasm/js/index.ts index f76472d..f3fa7d6 100644 --- a/docx-wasm/js/index.ts +++ b/docx-wasm/js/index.ts @@ -422,8 +422,11 @@ export class Docx { this.sectionProperty._header.children.forEach((c) => { if (c instanceof Paragraph) { header = header.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { header = header.add_table(c.build()); + } else { + const p = c.build(); + header = header.add_page_num(p); } }); docx = docx.header(header); @@ -434,8 +437,11 @@ export class Docx { this.sectionProperty._firstHeader.children.forEach((c) => { if (c instanceof Paragraph) { header = header.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { header = header.add_table(c.build()); + } else { + const p = c.build(); + header = header.add_page_num(p); } }); docx = docx.first_header(header); @@ -446,8 +452,11 @@ export class Docx { this.sectionProperty._evenHeader.children.forEach((c) => { if (c instanceof Paragraph) { header = header.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { header = header.add_table(c.build()); + } else { + const p = c.build(); + header = header.add_page_num(p); } }); docx = docx.even_header(header); @@ -458,8 +467,11 @@ export class Docx { this.sectionProperty._footer.children.forEach((c) => { if (c instanceof Paragraph) { footer = footer.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { footer = footer.add_table(c.build()); + } else { + const p = c.build(); + footer = footer.add_page_num(p); } }); docx = docx.footer(footer); @@ -470,8 +482,11 @@ export class Docx { this.sectionProperty._firstFooter.children.forEach((c) => { if (c instanceof Paragraph) { footer = footer.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { footer = footer.add_table(c.build()); + } else { + const p = c.build(); + footer = footer.add_page_num(p); } }); docx = docx.first_footer(footer); @@ -482,8 +497,11 @@ export class Docx { this.sectionProperty._evenFooter.children.forEach((c) => { if (c instanceof Paragraph) { footer = footer.add_paragraph(build(c)); - } else { + } else if (c instanceof Table) { footer = footer.add_table(c.build()); + } else { + const p = c.build(); + footer = footer.add_page_num(p); } }); docx = docx.even_footer(footer); @@ -668,5 +686,6 @@ export * from "./tab"; export * from "./json"; export * from "./webextension"; export * from "./header"; +export * from "./page-num"; export * from "./footer"; export * from "./image"; diff --git a/docx-wasm/js/json/bindings/FrameProperty.ts b/docx-wasm/js/json/bindings/FrameProperty.ts new file mode 100644 index 0000000..5ecfe2b --- /dev/null +++ b/docx-wasm/js/json/bindings/FrameProperty.ts @@ -0,0 +1,2 @@ + +export interface FrameProperty { h?: number, hRule?: string, hAnchor?: string, hSpace?: number, vAnchor?: string, vSpace?: number, w?: number, wrap?: string, x?: number, xAlign?: string, y?: number, yAlign?: string, } \ No newline at end of file diff --git a/docx-wasm/js/json/paragraph.ts b/docx-wasm/js/json/paragraph.ts index 460bf32..38a8738 100644 --- a/docx-wasm/js/json/paragraph.ts +++ b/docx-wasm/js/json/paragraph.ts @@ -6,6 +6,9 @@ import { SectionPropertyJSON, } from ".."; import { LineSpacingJSON } from "./line_spacing"; +import { FrameProperty as FramePropertyJSON } from "./bindings/FrameProperty"; + +export { FrameProperty as FramePropertyJSON } from "./bindings/FrameProperty"; export type ParagraphChildJSON = | RunJSON @@ -76,6 +79,7 @@ export type ParagraphPropertyJSON = { }; sectionProperty?: SectionPropertyJSON; tabs: CustomTabStopJSON[]; + frameProperty?: FramePropertyJSON; }; export type ParagraphJSON = { diff --git a/docx-wasm/js/page-num.ts b/docx-wasm/js/page-num.ts new file mode 100644 index 0000000..5a23e03 --- /dev/null +++ b/docx-wasm/js/page-num.ts @@ -0,0 +1,132 @@ +import * as wasm from "./pkg/docx_wasm"; + +export type FrameProperty = { + h?: number; + hRule?: string; + hAnchor?: string; + hSpace?: number; + vAnchor?: string; + vSpace?: number; + w?: number; + wrap?: string; + x?: number; + xAlign?: string; + y?: number; + yAlign?: string; +}; + +export class PageNum { + frameProperty: FrameProperty | null = null; + + height(h: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.h = h; + return this; + } + hRule(r: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.hRule = r; + return this; + } + + hAnchor(a: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.hAnchor = a; + return this; + } + + hSpace(s: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.hSpace = s; + return this; + } + + vAnchor(a: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.vAnchor = a; + return this; + } + + vSpace(s: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.vSpace = s; + return this; + } + + width(w: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.w = w; + return this; + } + + wrap(w: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.wrap = w; + return this; + } + + x(x: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.x = x; + return this; + } + + xAlign(a: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.xAlign = a; + return this; + } + + y(y: number) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.y = y; + return this; + } + + yAlign(y: string) { + this.frameProperty = { ...this.frameProperty }; + this.frameProperty.yAlign = y; + return this; + } + + build() { + let pageNum = wasm.createPageNum(); + if (this.frameProperty?.h != null) { + pageNum = pageNum.height(this.frameProperty.h); + } + if (this.frameProperty?.hRule != null) { + pageNum = pageNum.h_rule(this.frameProperty.hRule); + } + if (this.frameProperty?.hAnchor != null) { + pageNum = pageNum.h_anchor(this.frameProperty.hAnchor); + } + if (this.frameProperty?.hSpace != null) { + pageNum = pageNum.h_space(this.frameProperty.hSpace); + } + if (this.frameProperty?.vAnchor != null) { + pageNum = pageNum.v_anchor(this.frameProperty.vAnchor); + } + if (this.frameProperty?.vSpace != null) { + pageNum = pageNum.v_space(this.frameProperty.vSpace); + } + if (this.frameProperty?.w != null) { + pageNum = pageNum.width(this.frameProperty.w); + } + if (this.frameProperty?.wrap != null) { + pageNum = pageNum.wrap(this.frameProperty.wrap); + } + if (this.frameProperty?.x != null) { + pageNum = pageNum.x(this.frameProperty.x); + } + if (this.frameProperty?.xAlign != null) { + pageNum = pageNum.x_align(this.frameProperty.xAlign); + } + if (this.frameProperty?.y != null) { + pageNum = pageNum.y(this.frameProperty.y); + } + if (this.frameProperty?.yAlign != null) { + pageNum = pageNum.y_align(this.frameProperty.yAlign); + } + return pageNum; + } +} diff --git a/docx-wasm/js/section-property.ts b/docx-wasm/js/section-property.ts index 14c0fac..ac9d257 100644 --- a/docx-wasm/js/section-property.ts +++ b/docx-wasm/js/section-property.ts @@ -75,7 +75,7 @@ export class SectionProperty { return this; } - pageTypeNum({ start, chapStyle }: { start: number; chapStyle: string }) { + pageTypeNum({ start, chapStyle }: { start?: number; chapStyle?: string }) { this._pageTypeNum = { start, chapStyle }; return this; } diff --git a/docx-wasm/package.json b/docx-wasm/package.json index ae7cdb4..0b84c29 100644 --- a/docx-wasm/package.json +++ b/docx-wasm/package.json @@ -1,6 +1,6 @@ { "name": "docx-wasm", - "version": "0.4.12-beta1", + "version": "0.4.12-beta5", "main": "dist/node/index.js", "browser": "dist/web/index.js", "author": "bokuweb ", diff --git a/docx-wasm/src/footer.rs b/docx-wasm/src/footer.rs index 5e4fef6..623265b 100644 --- a/docx-wasm/src/footer.rs +++ b/docx-wasm/src/footer.rs @@ -27,4 +27,9 @@ impl Footer { self.0 = self.0.add_table(t.take()); self } + + pub fn add_page_num(mut self, t: PageNum) -> Self { + self.0 = self.0.add_page_num(t.take()); + self + } } diff --git a/docx-wasm/src/header.rs b/docx-wasm/src/header.rs index ff5c969..ff3dda4 100644 --- a/docx-wasm/src/header.rs +++ b/docx-wasm/src/header.rs @@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*; pub struct Header(docx_rs::Header); #[wasm_bindgen(js_name = createHeader)] -pub fn create_footer() -> Header { +pub fn create_header() -> Header { Header(docx_rs::Header::new()) } @@ -27,4 +27,9 @@ impl Header { self.0 = self.0.add_table(t.take()); self } + + pub fn add_page_num(mut self, t: PageNum) -> Self { + self.0 = self.0.add_page_num(t.take()); + self + } } diff --git a/docx-wasm/src/lib.rs b/docx-wasm/src/lib.rs index 5c3f9c2..f5f3497 100644 --- a/docx-wasm/src/lib.rs +++ b/docx-wasm/src/lib.rs @@ -12,6 +12,7 @@ mod level_override; mod line_spacing; mod numbering; mod page_margin; +mod page_num; mod page_num_type; mod paragraph; mod pic; @@ -41,6 +42,7 @@ pub use level_override::*; pub use line_spacing::*; pub use numbering::*; pub use page_margin::*; +pub use page_num::*; pub use page_num_type::*; pub use paragraph::*; pub use pic::*; diff --git a/docx-wasm/src/page_num.rs b/docx-wasm/src/page_num.rs new file mode 100644 index 0000000..6752abe --- /dev/null +++ b/docx-wasm/src/page_num.rs @@ -0,0 +1,79 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Debug)] +pub struct PageNum(docx_rs::PageNum); + +#[wasm_bindgen(js_name = createPageNum)] +pub fn create_page_num() -> PageNum { + PageNum(docx_rs::PageNum::new()) +} + +impl PageNum { + pub fn take(self) -> docx_rs::PageNum { + self.0 + } +} + +#[wasm_bindgen] +impl PageNum { + pub fn wrap(mut self, wrap: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().wrap(wrap)); + self + } + + pub fn v_anchor(mut self, anchor: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().v_anchor(anchor)); + self + } + + pub fn h_anchor(mut self, anchor: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().h_anchor(anchor)); + self + } + + pub fn h_rule(mut self, r: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().h_rule(r)); + self + } + + pub fn x_align(mut self, align: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().x_align(align)); + self + } + + pub fn y_align(mut self, align: &str) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().y_align(align)); + self + } + + pub fn h_space(mut self, x: i32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().h_space(x)); + self + } + + pub fn v_space(mut self, x: i32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().v_space(x)); + self + } + + pub fn x(mut self, x: i32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().x(x)); + self + } + + pub fn y(mut self, y: i32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().y(y)); + self + } + + pub fn width(mut self, n: u32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().width(n)); + self + } + + pub fn height(mut self, n: u32) -> Self { + self.0.frame_property = Some(self.0.frame_property.unwrap_or_default().height(n)); + self + } +} diff --git a/docx-wasm/test/__snapshots__/index.test.js.snap b/docx-wasm/test/__snapshots__/index.test.js.snap index a219176..0755f92 100644 --- a/docx-wasm/test/__snapshots__/index.test.js.snap +++ b/docx-wasm/test/__snapshots__/index.test.js.snap @@ -36217,6 +36217,13 @@ Object { "hasNumbering": false, "id": "2948A9FC", "property": Object { + "frameProperty": Object { + "hAnchor": "margin", + "vAnchor": "text", + "wrap": "none", + "xAlign": "right", + "y": 1, + }, "runProperty": Object { "style": "ae", }, @@ -36358,6 +36365,13 @@ Object { "hasNumbering": false, "id": "66B15C28", "property": Object { + "frameProperty": Object { + "hAnchor": "margin", + "vAnchor": "text", + "wrap": "none", + "xAlign": "right", + "y": 1, + }, "runProperty": Object { "style": "ae", }, @@ -105236,6 +105250,16 @@ Object { "name": "envelope address", "next": null, "paragraphProperty": Object { + "frameProperty": Object { + "h": 2268, + "hAnchor": "page", + "hRule": "exact", + "hSpace": 142, + "w": 6804, + "wrap": "auto", + "xAlign": "center", + "yAlign": "bottom", + }, "indent": Object { "end": null, "firstLineChars": null, @@ -119878,6 +119902,16 @@ Object { "name": "envelope address", "next": null, "paragraphProperty": Object { + "frameProperty": Object { + "h": 1980, + "hAnchor": "page", + "hRule": "exact", + "hSpace": 180, + "w": 7920, + "wrap": "auto", + "xAlign": "center", + "yAlign": "bottom", + }, "indent": Object { "end": null, "firstLineChars": null, @@ -137972,6 +138006,13 @@ Object { "next": null, "paragraphProperty": Object { "alignment": "right", + "frameProperty": Object { + "hAnchor": "text", + "hSpace": 181, + "vAnchor": "text", + "wrap": "around", + "y": 1, + }, "runProperty": Object {}, "tabs": Array [], }, @@ -139121,6 +139162,9 @@ Object { "name": "TRILOGO", "next": null, "paragraphProperty": Object { + "frameProperty": Object { + "wrap": "around", + }, "lineSpacing": Object { "before": 5280, }, @@ -156530,6 +156574,13 @@ Object { "hasNumbering": false, "id": "638F96CF", "property": Object { + "frameProperty": Object { + "hAnchor": "margin", + "vAnchor": "text", + "wrap": "around", + "xAlign": "center", + "y": 1, + }, "runProperty": Object {}, "tabs": Array [], }, @@ -171523,6 +171574,24 @@ exports[`writer should write page size 3`] = ` " `; +exports[`writer should write pageNum in header 1`] = ` +" + + + + + + +" +`; + +exports[`writer should write pageNum in header 2`] = ` +" + + Hello world!! +" +`; + exports[`writer should write paragraph delete 1`] = ` " diff --git a/docx-wasm/test/index.test.js b/docx-wasm/test/index.test.js index a537090..5836309 100644 --- a/docx-wasm/test/index.test.js +++ b/docx-wasm/test/index.test.js @@ -1043,4 +1043,20 @@ describe("writer", () => { } } }); + + test("should write pageNum in header", () => { + const p = new w.Paragraph().addRun(new w.Run().addText("Hello world!!")); + const page = new w.PageNum(); + const header = new w.Header().addParagraph(p).addPageNum(page); + const buffer = new w.Docx().header(header).addParagraph(p).build(); + + writeFileSync("../output/js/header_in_page_num.docx", buffer); + + const z = new Zip(Buffer.from(buffer)); + for (const e of z.getEntries()) { + if (e.entryName.match(/document.xml/)) { + expect(z.readAsText(e)).toMatchSnapshot(); + } + } + }); });