Support for Paragraph's Line Spacing (#346)

* Support for Line Spacing

Signed-off-by: lkadalski <kadalski.lukasz@gmail.com>

* Fixing clippy and yarn test

Signed-off-by: lkadalski <kadalski.lukasz@gmail.com>
main
Łukasz Kądalski 2021-09-29 02:03:39 +02:00 committed by GitHub
parent 7fa473e9df
commit 241a08dd42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 2569 additions and 1724 deletions

View File

@ -97,39 +97,39 @@ impl BuildXML for CoreProps {
self.config
.created
.as_ref()
.map_or_else(|| "1970-01-01T00:00:00Z", |v| &v),
.map_or_else(|| "1970-01-01T00:00:00Z", |v| v),
)
.dc_creator(
self.config
.creator
.as_ref()
.map_or_else(|| "unknown", |v| &v),
.map_or_else(|| "unknown", |v| v),
)
.cp_last_modified_by(
self.config
.last_modified_by
.as_ref()
.map_or_else(|| "unknown", |v| &v),
.map_or_else(|| "unknown", |v| v),
)
.dcterms_modified(
"dcterms:W3CDTF",
self.config
.modified
.as_ref()
.map_or_else(|| "1970-01-01T00:00:00Z", |v| &v),
.map_or_else(|| "1970-01-01T00:00:00Z", |v| v),
)
.cp_revision(&self.config.revision.map_or_else(|| "1".to_owned(), convert));
if let Some(v) = self.config.description.as_ref() {
base = base.dc_description(&v);
base = base.dc_description(v);
}
if let Some(v) = self.config.language.as_ref() {
base = base.dc_language(&v);
base = base.dc_language(v);
}
if let Some(v) = self.config.subject.as_ref() {
base = base.dc_subject(&v);
base = base.dc_subject(v);
}
if let Some(v) = self.config.title.as_ref() {
base = base.dc_title(&v);
base = base.dc_title(v);
}
base.close().build()
}

View File

@ -59,7 +59,7 @@ mod tests {
);
assert_eq!(
serde_json::to_string(&graphic).unwrap(),
r#"{"children":[{"dataType":"wpShape","children":[{"type":"shape","data":{"children":[{"type":"textbox","data":{"children":[{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"pattern1"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}}],"has_numbering":false}],"hasNumbering":false}}]}}]}]}"#,
r#"{"children":[{"dataType":"wpShape","children":[{"type":"shape","data":{"children":[{"type":"textbox","data":{"children":[{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"pattern1"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}}],"has_numbering":false}],"hasNumbering":false}}]}}]}]}"#,
);
}
}

View File

@ -90,7 +90,7 @@ mod tests {
.num_style_link("style1");
assert_eq!(
serde_json::to_string(&c).unwrap(),
r#"{"id":0,"styleLink":null,"numStyleLink":"style1","levels":[{"level":1,"start":1,"format":"decimal","text":"%4.","jc":"left","paragraphProperty":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"suffix":"tab","pstyle":null,"levelRestart":null}]}"#,
r#"{"id":0,"styleLink":null,"numStyleLink":"style1","levels":[{"level":1,"start":1,"format":"decimal","text":"%4.","jc":"left","paragraphProperty":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"suffix":"tab","pstyle":null,"levelRestart":null}]}"#,
);
}
}

View File

@ -0,0 +1,53 @@
use crate::documents::BuildXML;
use crate::xml_builder::*;
use serde::*;
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CharacterSpacing {
value: i32,
}
impl CharacterSpacing {
pub fn new(s: i32) -> CharacterSpacing {
Self { value: s }
}
}
impl BuildXML for CharacterSpacing {
fn build(&self) -> Vec<u8> {
let b = XMLBuilder::new();
b.spacing(self.value).build()
}
}
impl Serialize for CharacterSpacing {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i32(self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_spacing() {
let b = CharacterSpacing::new(200).build();
assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:spacing w:val="200" />"#);
}
#[test]
fn test_spacing_json() {
let s = CharacterSpacing { value: 100 };
assert_eq!(serde_json::to_string(&s).unwrap(), r#"100"#);
}
}

View File

@ -0,0 +1,83 @@
use crate::documents::BuildXML;
use crate::xml_builder::*;
use crate::line_spacing_type::LineSpacingType;
use serde::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LineSpacing {
#[serde(skip_serializing_if = "Option::is_none")]
line_rule: Option<LineSpacingType>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
line: Option<u32>,
}
impl LineSpacing {
pub fn new(spacing: Option<LineSpacingType>) -> Self {
Self {
line_rule: spacing,
before: None,
after: None,
line: None,
}
}
pub fn before(mut self, before: Option<u32>) -> Self {
self.before = before;
self
}
pub fn after(mut self, after: Option<u32>) -> Self {
self.after = after;
self
}
pub fn line(mut self, line: Option<u32>) -> Self {
self.line = line;
self
}
}
impl BuildXML for LineSpacing {
fn build(&self) -> Vec<u8> {
let b = XMLBuilder::new();
b.line_spacing(self.before, self.after, self.line, self.line_rule)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_spacing() {
let b = LineSpacing::new(Some(LineSpacingType::Auto))
.line(Some(100))
.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:spacing w:line="100" w:lineRule="auto" />"#
);
}
#[test]
fn test_spacing_json() {
let s = LineSpacing {
line_rule: Some(LineSpacingType::Auto),
before: None,
after: None,
line: Some(100),
};
assert_eq!(
serde_json::to_string(&s).unwrap(),
r#"{"lineRule":"Auto","line":100}"#
);
}
}

View File

@ -7,6 +7,7 @@ mod bold_cs;
mod bookmark_end;
mod bookmark_start;
mod br;
mod character_spacing;
mod color;
mod comment;
mod comment_extended;
@ -36,6 +37,7 @@ mod level_jc;
mod level_override;
mod level_restart;
mod level_text;
mod line_spacing;
mod mc_fallback;
mod name;
mod next;
@ -58,7 +60,6 @@ mod run_property_default;
mod section;
mod section_property;
mod shading;
mod spacing;
mod start;
mod style;
mod sz;
@ -102,6 +103,7 @@ pub use bold_cs::*;
pub use bookmark_end::*;
pub use bookmark_start::*;
pub use br::*;
pub use character_spacing::*;
pub use color::*;
pub use comment::*;
pub use comment_extended::*;
@ -131,6 +133,7 @@ pub use level_jc::*;
pub use level_override::*;
pub use level_restart::*;
pub use level_text::*;
pub use line_spacing::*;
pub use mc_fallback::*;
pub use name::*;
pub use next::*;
@ -153,7 +156,6 @@ pub use run_property_default::*;
pub use section::*;
pub use section_property::*;
pub use shading::*;
pub use spacing::*;
pub use start::*;
pub use style::*;
pub use sz::*;

View File

@ -244,8 +244,16 @@ impl Paragraph {
self
}
pub fn line_height(mut self, h: u32) -> Self {
self.property = self.property.line_height(h);
pub fn line_spacing(
mut self,
before: Option<u32>,
after: Option<u32>,
line: Option<u32>,
spacing_type: Option<LineSpacingType>,
) -> Self {
self.property = self
.property
.line_spacing(before, after, line, spacing_type);
self
}
}
@ -324,13 +332,25 @@ mod tests {
);
}
#[test]
fn test_line_spacing_and_character_spacing() {
let b = Paragraph::new()
.line_spacing(Some(20), Some(30), Some(200), Some(LineSpacingType::Auto))
.add_run(Run::new().add_text("Hello"))
.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:p w14:paraId="12345678"><w:pPr><w:rPr /><w:spacing w:before="20" w:after="30" w:line="200" w:lineRule="auto" /></w:pPr><w:r><w:rPr /><w:t xml:space="preserve">Hello</w:t></w:r></w:p>"#
);
}
#[test]
fn test_paragraph_run_json() {
let run = Run::new().add_text("Hello");
let p = Paragraph::new().add_run(run);
assert_eq!(
serde_json::to_string(&p).unwrap(),
r#"{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}"#,
r#"{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}"#,
);
}
@ -341,7 +361,7 @@ mod tests {
let p = Paragraph::new().add_insert(ins);
assert_eq!(
serde_json::to_string(&p).unwrap(),
r#"{"id":"12345678","children":[{"type":"insert","data":{"children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"author":"unnamed","date":"1970-01-01T00:00:00Z"}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}"#
r#"{"id":"12345678","children":[{"type":"insert","data":{"children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"author":"unnamed","date":"1970-01-01T00:00:00Z"}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}"#
);
}
}

View File

@ -2,7 +2,7 @@ use serde::Serialize;
use super::*;
use crate::documents::BuildXML;
use crate::types::{AlignmentType, SpecialIndentType};
use crate::types::{AlignmentType, LineSpacingType, SpecialIndentType};
use crate::xml_builder::*;
#[derive(Serialize, Debug, Clone, PartialEq)]
@ -13,7 +13,7 @@ pub struct ParagraphProperty {
pub numbering_property: Option<NumberingProperty>,
pub alignment: Option<Justification>,
pub indent: Option<Indent>,
pub line_height: Option<u32>,
pub line_spacing: Option<LineSpacing>,
pub keep_next: bool,
pub keep_lines: bool,
pub page_break_before: bool,
@ -31,7 +31,7 @@ impl Default for ParagraphProperty {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
@ -78,8 +78,19 @@ impl ParagraphProperty {
self
}
pub fn line_height(mut self, h: u32) -> Self {
self.line_height = Some(h);
pub fn line_spacing(
mut self,
before: Option<u32>,
after: Option<u32>,
line: Option<u32>,
spacing_type: Option<LineSpacingType>,
) -> Self {
self.line_spacing = Some(
LineSpacing::new(spacing_type)
.after(after)
.before(before)
.line(line),
);
self
}
@ -125,11 +136,6 @@ impl ParagraphProperty {
impl BuildXML for ParagraphProperty {
fn build(&self) -> Vec<u8> {
let spacing = if let Some(s) = self.line_height {
Some(Spacing::new(crate::SpacingType::Line(s)))
} else {
None
};
let mut b = XMLBuilder::new()
.open_paragraph_property()
.add_child(&self.run_property)
@ -137,7 +143,7 @@ impl BuildXML for ParagraphProperty {
.add_optional_child(&self.numbering_property)
.add_optional_child(&self.alignment)
.add_optional_child(&self.indent)
.add_optional_child(&spacing)
.add_optional_child(&self.line_spacing)
.add_optional_child(&self.outline_lvl);
if self.keep_next {
@ -222,7 +228,19 @@ mod tests {
let b = c.indent(Some(20), Some(SpecialIndentType::FirstLine(10)), None, None);
assert_eq!(
serde_json::to_string(&b).unwrap(),
r#"{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":{"start":20,"startChars":null,"end":null,"specialIndent":{"type":"firstLine","val":10},"hangingChars":null,"firstLineChars":null},"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null}"#
r#"{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":{"start":20,"startChars":null,"end":null,"specialIndent":{"type":"firstLine","val":10},"hangingChars":null,"firstLineChars":null},"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null}"#
);
}
#[test]
fn test_line_spacing() {
let props = ParagraphProperty::new();
let bytes = props
.line_spacing(None, None, Some(100), Some(LineSpacingType::AtLeast))
.build();
assert_eq!(
str::from_utf8(&bytes).unwrap(),
r#"<w:pPr><w:rPr /><w:spacing w:line="100" w:lineRule="atLeast" /></w:pPr>"#
)
}
}

View File

@ -142,7 +142,7 @@ impl Run {
self
}
pub fn spacing(mut self, v: i32) -> Run {
pub fn character_spacing(mut self, v: i32) -> Run {
self.run_property = self.run_property.spacing(v);
self
}
@ -277,13 +277,13 @@ mod tests {
italic: Some(Italic::new()),
italic_cs: Some(ItalicCs::new()),
vanish: Some(Vanish::new()),
spacing: Some(100),
character_spacing: Some(CharacterSpacing::new(100)),
..RunProperty::default()
},
};
assert_eq!(
serde_json::to_string(&run).unwrap(),
r#"{"runProperty":{"sz":30,"szCs":30,"color":"C9211E","highlight":"yellow","vertAlign":null,"underline":"single","bold":true,"boldCs":true,"italic":true,"italicCs":true,"vanish":true,"spacing":100,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"tab"},{"type":"text","data":{"preserveSpace":true,"text":"Hello"}},{"type":"break","data":{"breakType":"page"}},{"type":"deleteText","data":{"text":"deleted","preserveSpace":true}}]}"#,
r#"{"runProperty":{"sz":30,"szCs":30,"color":"C9211E","highlight":"yellow","vertAlign":null,"underline":"single","bold":true,"boldCs":true,"italic":true,"italicCs":true,"vanish":true,"characterSpacing":100,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"tab"},{"type":"text","data":{"preserveSpace":true,"text":"Hello"}},{"type":"break","data":{"breakType":"page"}},{"type":"deleteText","data":{"text":"deleted","preserveSpace":true}}]}"#,
);
}
}

View File

@ -19,7 +19,7 @@ pub struct RunProperty {
pub italic: Option<Italic>,
pub italic_cs: Option<ItalicCs>,
pub vanish: Option<Vanish>,
pub spacing: Option<i32>,
pub character_spacing: Option<CharacterSpacing>,
pub fonts: Option<RunFonts>,
pub text_border: Option<TextBorder>,
pub del: Option<Delete>,
@ -38,7 +38,7 @@ impl RunProperty {
}
pub fn spacing(mut self, spacing: i32) -> RunProperty {
self.spacing = Some(spacing);
self.character_spacing = Some(CharacterSpacing::new(spacing));
self
}
@ -127,7 +127,7 @@ impl Default for RunProperty {
italic_cs: None,
vanish: None,
fonts: None,
spacing: None,
character_spacing: None,
text_border: None,
del: None,
ins: None,
@ -138,9 +138,6 @@ impl Default for RunProperty {
impl BuildXML for RunProperty {
fn build(&self) -> Vec<u8> {
let b = XMLBuilder::new();
let spacing = self
.spacing
.map(|s| Spacing::new(crate::SpacingType::Value(s)));
b.open_run_property()
.add_optional_child(&self.sz)
.add_optional_child(&self.sz_cs)
@ -157,7 +154,7 @@ impl BuildXML for RunProperty {
.add_optional_child(&self.ins)
.add_optional_child(&self.del)
.add_optional_child(&self.vert_align)
.add_optional_child(&spacing)
.add_optional_child(&self.character_spacing)
.close()
.build()
}
@ -230,4 +227,13 @@ mod tests {
r#"<w:rPr><w:rFonts w:eastAsia="Hiragino" /></w:rPr>"#
);
}
#[test]
fn test_character_spacing() {
let c = RunProperty::new().spacing(20);
let b = c.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:rPr><w:spacing w:val="20" /></w:rPr>"#
);
}
}

View File

@ -1,50 +0,0 @@
use crate::documents::BuildXML;
use crate::types::*;
use crate::xml_builder::*;
use serde::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Spacing {
spacing_type: SpacingType,
}
impl Spacing {
pub fn new(s: SpacingType) -> Spacing {
Self { spacing_type: s }
}
}
impl BuildXML for Spacing {
fn build(&self) -> Vec<u8> {
let b = XMLBuilder::new();
b.spacing(self.spacing_type).build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_spacing() {
let b = Spacing::new(SpacingType::Value(200)).build();
assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:spacing w:val="200" />"#);
}
#[test]
fn test_spacing_json() {
let s = Spacing {
spacing_type: SpacingType::Value(100),
};
assert_eq!(
serde_json::to_string(&s).unwrap(),
r#"{"spacingType":{"type":"value","data":100}}"#
);
}
}

View File

@ -185,7 +185,7 @@ mod tests {
.grid_span(2);
assert_eq!(
serde_json::to_string(&c).unwrap(),
r#"{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"spacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineHeight":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}}],"property":{"width":null,"borders":null,"gridSpan":2,"verticalMerge":null,"verticalAlign":null,"textDirection":null,"shading":null},"hasNumbering":false}"#,
r#"{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{"sz":null,"szCs":null,"color":null,"highlight":null,"vertAlign":null,"underline":null,"bold":null,"boldCs":null,"italic":null,"italicCs":null,"vanish":null,"characterSpacing":null,"fonts":null,"textBorder":null,"del":null,"ins":null},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}}],"property":{"width":null,"borders":null,"gridSpan":2,"verticalMerge":null,"verticalAlign":null,"textDirection":null,"shading":null},"hasNumbering":false}"#,
);
}
}

View File

@ -637,7 +637,7 @@ impl Docx {
if let RunChild::Drawing(d) = child {
if let Some(DrawingData::Pic(pic)) = &mut d.data {
image_ids.push(pic.id);
let b = std::mem::replace(&mut pic.image, vec![]);
let b = std::mem::take(&mut pic.image);
images.push((pic.id, b));
}
}
@ -659,10 +659,7 @@ impl Docx {
&mut d.data
{
image_ids.push(pic.id);
let b = std::mem::replace(
&mut pic.image,
vec![],
);
let b = std::mem::take(&mut pic.image);
images.push((pic.id, b));
}
}

View File

@ -0,0 +1,34 @@
use crate::line_spacing_type::LineSpacingType;
use crate::ReaderError;
use std::str::FromStr;
use xml::attribute::OwnedAttribute;
pub type LineSpacingResult = Result<
(
Option<u32>,
Option<u32>,
Option<u32>,
Option<LineSpacingType>,
),
ReaderError,
>;
pub fn read_line_spacing(attributes: &[OwnedAttribute]) -> LineSpacingResult {
let mut before: Option<u32> = None;
let mut after: Option<u32> = None;
let mut line: Option<u32> = None;
let mut spacing_type: Option<LineSpacingType> = None;
for a in attributes {
let local_name = &a.name.local_name;
if local_name == "before" {
before = Some(u32::from_str(&a.value)?);
} else if local_name == "after" {
after = Some(u32::from_str(&a.value)?);
} else if local_name == "line" {
line = Some(u32::from_str(&a.value)?);
} else if local_name == "lineRule" {
spacing_type = Some(LineSpacingType::from_str(&a.value)?);
}
}
Ok((before, after, line, spacing_type))
}

View File

@ -3,6 +3,7 @@ mod border;
mod id;
mod indent;
mod indent_level;
pub(crate) mod line_spacing;
mod name;
mod val;
mod width;

View File

@ -11,7 +11,7 @@ impl ElementReader for Div {
r: &mut EventReader<R>,
attrs: &[OwnedAttribute],
) -> Result<Self, ReaderError> {
let id = read_id(&attrs).unwrap_or_default();
let id = read_id(attrs).unwrap_or_default();
let mut div = Div::new(id);
loop {
let e = r.next();

View File

@ -29,7 +29,7 @@ pub fn read_document_rels(
.ok_or(ReaderError::DocumentRelsNotFoundError)?;
let p = find_rels_filename(&main_path)?;
let p = p.to_str().ok_or(ReaderError::DocumentRelsNotFoundError)?;
let data = read_zip(archive, &p)?;
let data = read_zip(archive, p)?;
let rels = read_rels_xml(&data[..], dir)?;
Ok(rels)
}

View File

@ -9,16 +9,6 @@ use super::*;
use super::attributes::*;
use crate::types::*;
fn read_lineheight(attributes: &[OwnedAttribute]) -> Option<u32> {
for a in attributes {
let local_name = &a.name.local_name;
if let "line" = local_name.as_str() {
return value_to_dax(&a.value).ok().map(|l| l as u32);
}
}
None
}
impl ElementReader for Paragraph {
fn read<R: Read>(
r: &mut EventReader<R>,
@ -95,9 +85,9 @@ impl ElementReader for Paragraph {
continue;
}
XMLElement::Spacing => {
if let Some(line) = read_lineheight(&attributes) {
p = p.line_height(line);
}
let (before, after, line, spacing_type) =
attributes::line_spacing::read_line_spacing(&attributes)?;
p = p.line_spacing(before, after, line, spacing_type);
continue;
}
XMLElement::Justification => {
@ -194,7 +184,7 @@ mod tests {
Some(1270),
None,
)),
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -230,7 +220,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: Some(Indent::new(None, None, None, Some(100))),
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -261,7 +251,7 @@ mod tests {
numbering_property: None,
alignment: Some(Justification::new(AlignmentType::Left.to_string())),
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -296,7 +286,7 @@ mod tests {
),
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: true,
@ -333,7 +323,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -372,7 +362,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -410,7 +400,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -460,7 +450,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,
@ -502,7 +492,7 @@ mod tests {
numbering_property: None,
alignment: None,
indent: None,
line_height: None,
line_spacing: None,
..Default::default()
},
has_numbering: false,

View File

@ -1,10 +1,10 @@
// One inch equates to 914400 EMUs and a centimeter is 360000 one pixel equates to 9525
type EMU = u32;
type Emu = u32;
pub fn to_px(v: EMU) -> u32 {
pub fn to_px(v: Emu) -> u32 {
v / 9525
}
pub fn from_px(v: EMU) -> u32 {
pub fn from_px(v: Emu) -> u32 {
v * 9525
}

View File

@ -0,0 +1,25 @@
use crate::types::errors;
use crate::TypeError;
use serde::*;
use std::str::FromStr;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum LineSpacingType {
Auto,
AtLeast,
Exact,
}
impl FromStr for LineSpacingType {
type Err = errors::TypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(LineSpacingType::Auto),
"atLeast" => Ok(LineSpacingType::AtLeast),
"exact" => Ok(LineSpacingType::Exact),
_ => Err(TypeError::FromStrError),
}
}
}

View File

@ -8,11 +8,11 @@ pub mod errors;
pub mod font_pitch_type;
pub mod height_rule;
pub mod level_suffix_type;
pub mod line_spacing_type;
pub mod page_margin;
pub mod page_orientation_type;
pub mod section_type;
pub mod shd_type;
pub mod spacing;
pub mod special_indent_type;
pub mod style_type;
pub mod table_alignment_type;
@ -33,11 +33,11 @@ pub use errors::*;
pub use font_pitch_type::*;
pub use height_rule::*;
pub use level_suffix_type::*;
pub use line_spacing_type::*;
pub use page_margin::*;
pub use page_orientation_type::*;
pub use section_type::*;
pub use shd_type::*;
pub use spacing::*;
pub use special_indent_type::*;
pub use style_type::*;
pub use table_alignment_type::*;

View File

@ -1,30 +0,0 @@
use serde::ser::{SerializeStruct, Serializer};
use serde::*;
#[derive(Copy, Clone, Debug, PartialEq, Deserialize)]
pub enum SpacingType {
Value(i32),
Line(u32),
}
impl Serialize for SpacingType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
SpacingType::Value(ref s) => {
let mut t = serializer.serialize_struct("SpacingType", 2)?;
t.serialize_field("type", "value")?;
t.serialize_field("data", s)?;
t.end()
}
SpacingType::Line(ref s) => {
let mut t = serializer.serialize_struct("SpacingType", 2)?;
t.serialize_field("type", "line")?;
t.serialize_field("data", s)?;
t.end()
}
}
}
}

View File

@ -1,5 +1,6 @@
use super::XMLBuilder;
use super::XmlEvent;
use crate::types::line_spacing_type::LineSpacingType;
use crate::types::*;
const EXPECT_MESSAGE: &str = "should write buf";
@ -153,26 +154,54 @@ impl XMLBuilder {
}
// i.e. <w:spacing ... >
pub(crate) fn spacing(mut self, s: crate::types::SpacingType) -> Self {
match s {
SpacingType::Value(v) => {
pub(crate) fn spacing(mut self, s: i32) -> Self {
self.writer
.write(XmlEvent::start_element("w:spacing").attr("w:val", &format!("{}", v)))
.write(XmlEvent::start_element("w:spacing").attr("w:val", &format!("{}", s)))
.expect(EXPECT_MESSAGE);
self.close()
}
SpacingType::Line(v) => {
self.writer
.write(
XmlEvent::start_element("w:spacing")
.attr("w:line", &format!("{}", v))
.attr("w:lineRule", "auto"),
)
.expect(EXPECT_MESSAGE);
// i.e. <w:spacing ... >
pub(crate) fn line_spacing(
mut self,
before: Option<u32>,
after: Option<u32>,
line: Option<u32>,
spacing: Option<LineSpacingType>,
) -> Self {
let mut xml_event = XmlEvent::start_element("w:spacing");
let before_val: String;
let after_val: String;
let line_val: String;
if let Some(before) = before {
before_val = format!("{}", before);
xml_event = xml_event.attr("w:before", &before_val)
}
if let Some(after) = after {
after_val = format!("{}", after);
xml_event = xml_event.attr("w:after", &after_val)
}
if let Some(line) = line {
line_val = format!("{}", line);
xml_event = xml_event.attr("w:line", &line_val)
}
if let Some(spacing_type) = spacing {
match spacing_type {
LineSpacingType::Auto => {
xml_event = xml_event.attr("w:lineRule", "auto");
}
LineSpacingType::AtLeast => {
xml_event = xml_event.attr("w:lineRule", "atLeast");
}
LineSpacingType::Exact => {
xml_event = xml_event.attr("w:lineRule", "exact");
}
}
}
self.writer.write(xml_event).expect(EXPECT_MESSAGE);
self.close()
}
}
}
//
// Table elements
@ -413,7 +442,7 @@ impl XMLBuilder {
w = w.attr("w:linePitch", &line_pitch_string);
}
if char_space.is_some() {
w = w.attr("w:charSpace", &&char_space_string);
w = w.attr("w:charSpace", &char_space_string);
}
self.writer.write(w).expect(EXPECT_MESSAGE);

View File

@ -57,8 +57,8 @@ impl XMLBuilder {
self.writer
.write(
XmlEvent::start_element("Override")
.attr("PartName", &name)
.attr("ContentType", &content_type),
.attr("PartName", name)
.attr("ContentType", content_type),
)
.expect("should write to buf");
self.close()
@ -68,8 +68,8 @@ impl XMLBuilder {
self.writer
.write(
XmlEvent::start_element("Default")
.attr("ContentType", &extension)
.attr("Extension", &name),
.attr("ContentType", extension)
.attr("Extension", name),
)
.expect("should write to buf");
self.close()

View File

@ -71,7 +71,7 @@ where
format!("word/webextensions/webextension{}.xml", i + 1),
options,
)?;
zip.write_all(&ext)?;
zip.write_all(ext)?;
}
}

View File

@ -415,3 +415,28 @@ pub fn date() -> Result<(), DocxError> {
.pack(file)?;
Ok(())
}
#[test]
pub fn line_spacing() -> Result<(), DocxError> {
let path = std::path::Path::new("./tests/output/line_spacing.docx");
let file = std::fs::File::create(&path).unwrap();
Docx::new()
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(DUMMY))
.line_spacing(Some(300), None, Some(300), Some(LineSpacingType::Auto)),
)
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(DUMMY))
.line_spacing(None, None, Some(300), Some(LineSpacingType::AtLeast)),
)
.add_paragraph(
Paragraph::new()
.add_run(Run::new().add_text(DUMMY).character_spacing(100))
.line_spacing(None, Some(300), Some(300), Some(LineSpacingType::Exact)),
)
.build()
.pack(file)?;
Ok(())
}

View File

@ -259,3 +259,18 @@ pub fn read_extended_comment() {
file.write_all(json.as_bytes()).unwrap();
file.flush().unwrap();
}
#[test]
pub fn read_line_spacing() {
let mut file = File::open("../fixtures/line_spacing/line_spacing.docx").unwrap();
let mut buf = vec![];
file.read_to_end(&mut buf).unwrap();
let json = read_docx(&buf).unwrap().json();
assert_json_snapshot!("line_spacing", &json);
let path = std::path::Path::new("./tests/output/line_spacing.json");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(json.as_bytes()).unwrap();
file.flush().unwrap();
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -417,8 +417,24 @@ export class Docx {
paragraph = paragraph.bold();
}
if (p.property.lineHeight) {
paragraph = paragraph.line_height(p.property.lineHeight);
if (typeof p.property.lineSpacing !== "undefined") {
const {lineSpacing} = p.property;
let kind;
switch (p.property.lineSpacing.lineRule) {
case "atLeast": {
kind = wasm.LineSpacingType.AtLeast;
break;
}
case "auto": {
kind = wasm.LineSpacingType.Auto;
break;
}
case "exact": {
kind = wasm.LineSpacingType.Exact;
break;
}
}
paragraph = paragraph.line_spacing(lineSpacing.before, lineSpacing.after, lineSpacing.line, kind);
}
if (p.property.runProperty.italic) {

View File

@ -0,0 +1,9 @@
export type LineSpacingJSON = {
before: number | null;
after: number | null;
line: number | null;
lineRule: {
type: "atLeast" | "auto" | "exact";
val: number;
} | null;
};

View File

@ -1,6 +1,7 @@
import { RunJSON, RunPropertyJSON } from "./run";
import { IndentJSON } from "./indent";
import { CommentRangeStartJSON, CommentRangeEndJSON } from "..";
import {LineSpacingJSON} from "./line_spacing";
export type ParagraphChildJSON =
| RunJSON
@ -22,7 +23,7 @@ export type ParagraphPropertyJSON = {
numberingProperty: NumberingPropertyJSON | null;
alignment: "left" | "center" | "right" | "justified" | "both";
indent: IndentJSON | null;
lineHeight: number | null;
lineSpacing: LineSpacingJSON | null;
divId: string | null;
keepNext: boolean;
keepLines: boolean;

View File

@ -26,6 +26,8 @@ export type AlignmentType =
export type SpecialIndentKind = "firstLine" | "hanging";
export type LineSpacingType = "atLeast" | "auto" | "exact"
export type ParagraphProperty = {
align?: AlignmentType;
styleId?: string;
@ -38,7 +40,12 @@ export type ParagraphProperty = {
id: number;
level: number;
};
lineHeight?: number;
lineSpacing?: {
before?: number;
after?: number;
line?: number;
lineRule?: LineSpacingType;
};
runProperty: RunProperty;
keepNext: boolean;
keepLines: boolean;
@ -121,8 +128,13 @@ export class Paragraph {
return this;
}
lineHeight(lineHeight: number) {
this.property = { ...this.property, lineHeight };
lineSpacing(before?: number, after?: number, line?: number, lineRule?: LineSpacingType) {
this.property.lineSpacing = {
before,
after,
line,
lineRule
}
return this;
}

View File

@ -112,8 +112,14 @@ impl Paragraph {
self
}
pub fn line_height(mut self, line_height: u32) -> Self {
self.0 = self.0.line_height(line_height);
pub fn line_spacing(
mut self,
before: Option<u32>,
after: Option<u32>,
line: Option<u32>,
spacing_type: Option<docx_rs::LineSpacingType>,
) -> Self {
self.0 = self.0.line_spacing(before, after, line, spacing_type);
self
}

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,14 @@ describe("reader", () => {
const json = w.readDocx(buffer);
expect(json).toMatchSnapshot();
});
test("should read line spacing docx", () => {
const buffer = readFileSync(
"../fixtures/line_spacing/line_spacing.docx"
);
const json = w.readDocx(buffer);
expect(json).toMatchSnapshot();
});
});
describe("writer", () => {
@ -336,4 +344,18 @@ describe("writer", () => {
}
}
});
test("should write line spacing", () => {
const p = new w.Paragraph()
.addRun(new w.Run().addText("Hello "))
.lineSpacing(100, "", 100, 1);
const buffer = new w.Docx().addParagraph(p).build();
writeFileSync("../output/line_spacing.docx", buffer);
const z = new Zip(Buffer.from(buffer));
for (const e of z.getEntries()) {
if (e.entryName.match(/document.xml|numbering.xml/)) {
expect(z.readAsText(e)).toMatchSnapshot();
}
}
});
});

Binary file not shown.