Improve toc writer2 (#389)

* fix: tab for toc

* fix:

* feat: Add paragraph.raw_text

* fix: improve toc item

* fix: toc item builder

* fix:

* fix

* feat: impl page ref for toc

* fix

* fix

* feat: Suppport hyperlink

* feat: Support alias

* feat: disable auto items

* feat: Add toc styles

* fix: clippy

* fix: test

* fix
main
bokuweb 2022-01-03 02:18:04 +09:00 committed by GitHub
parent 31dacfb72a
commit c89783e8d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1292 additions and 132 deletions

View File

@ -3,15 +3,29 @@ use docx_rs::*;
pub fn main() -> Result<(), DocxError> { pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/dirty_toc.docx"); let path = std::path::Path::new("./output/dirty_toc.docx");
let file = std::fs::File::create(&path).unwrap(); let file = std::fs::File::create(&path).unwrap();
let p = Paragraph::new() let p1 = Paragraph::new()
.add_run(Run::new().add_text("Hello")) .add_run(Run::new().add_text("Hello"))
.style("Heading1") .style("Heading1")
.page_break_before(true); .page_break_before(true);
let style = Style::new("Heading1", StyleType::Paragraph).name("Heading 1"); let style1 = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
let p2 = Paragraph::new()
.add_run(Run::new().add_text("World"))
.style("Heading2")
.page_break_before(true);
let style2 = Style::new("Heading2", StyleType::Paragraph).name("Heading 2");
let p4 = Paragraph::new()
.add_run(Run::new().add_text("Foo"))
.style("Heading4")
.page_break_before(true);
let style4 = Style::new("Heading4", StyleType::Paragraph).name("Heading 4");
Docx::new() Docx::new()
.add_style(style) .add_style(style1)
.add_style(style2)
.add_style(style4)
.add_table_of_contents(TableOfContents::new().heading_styles_range(1, 3)) .add_table_of_contents(TableOfContents::new().heading_styles_range(1, 3))
.add_paragraph(p) .add_paragraph(p1)
.add_paragraph(p2)
.add_paragraph(p4)
.build() .build()
.pack(file)?; .pack(file)?;
Ok(()) Ok(())

View File

@ -0,0 +1,28 @@
use docx_rs::*;
pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/toc_simple.docx");
let file = std::fs::File::create(&path).unwrap();
let p1 = Paragraph::new()
.add_run(Run::new().add_text("Hello"))
.style("Heading1")
.page_break_before(true);
let style1 = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
let p2 = Paragraph::new()
.add_run(Run::new().add_text("World"))
.style("Heading2")
.page_break_before(true);
Docx::new()
.add_style(style1)
.add_table_of_contents(
TableOfContents::new()
.heading_styles_range(1, 3)
.alias("Table of contents"),
)
.add_paragraph(p1)
.add_paragraph(p2)
.build()
.pack(file)?;
Ok(())
}

View File

@ -0,0 +1,37 @@
use docx_rs::*;
pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/toc_with_hyperlink.docx");
let file = std::fs::File::create(&path).unwrap();
let p1 = Paragraph::new()
.add_run(Run::new().add_text("Hello"))
.style("Heading1")
.page_break_before(true);
let style1 = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
let p2 = Paragraph::new()
.add_run(Run::new().add_text("World"))
.style("Heading2")
.page_break_before(true);
let style2 = Style::new("Heading2", StyleType::Paragraph).name("Heading 2");
let p4 = Paragraph::new()
.add_run(Run::new().add_text("Foo"))
.style("Heading4")
.page_break_before(true);
let style4 = Style::new("Heading4", StyleType::Paragraph).name("Heading 4");
Docx::new()
.add_style(style1)
.add_style(style2)
.add_style(style4)
.add_table_of_contents(
TableOfContents::new()
.heading_styles_range(1, 3)
.hyperlink()
.alias("table of contents"),
)
.add_paragraph(p1)
.add_paragraph(p2)
.add_paragraph(p4)
.build()
.pack(file)?;
Ok(())
}

View File

@ -0,0 +1,44 @@
use docx_rs::*;
pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/toc_with_item.docx");
let file = std::fs::File::create(&path).unwrap();
let p1 = Paragraph::new()
.add_run(Run::new().add_text("Hello"))
.style("Heading1")
.page_break_before(true);
let style1 = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
let p2 = Paragraph::new()
.add_run(Run::new().add_text("World"))
.style("Heading2")
.page_break_before(true);
let style2 = Style::new("Heading2", StyleType::Paragraph).name("Heading 2");
Docx::new()
.add_style(style1)
.add_style(style2)
.add_table_of_contents(
TableOfContents::new()
.alias("Table of contents")
.heading_styles_range(1, 3)
.add_item(
TableOfContentsItem::new()
.text("Hello")
.toc_key("_Toc00000000")
.level(1)
.page_ref("2"),
)
.add_item(
TableOfContentsItem::new()
.text("World")
.toc_key("_Toc00000001")
.level(2)
.page_ref("3"),
),
)
.add_paragraph(p1)
.add_paragraph(p2)
.build()
.pack(file)?;
Ok(())
}

View File

@ -0,0 +1,36 @@
use docx_rs::*;
pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/toc_with_tc.docx");
let file = std::fs::File::create(&path).unwrap();
let p1 = Paragraph::new()
.add_run(Run::new().add_text("Hello"))
.style("Heading1")
.page_break_before(true);
let style1 = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
let p2 = Paragraph::new()
.add_run(Run::new().add_text("World"))
.style("Heading2")
.page_break_before(true);
let tc = Paragraph::new()
.add_run(
Run::new()
.add_field_char(FieldCharType::Begin, false)
.add_instr_text(InstrText::TC(InstrTC::new("tc_test").level(4)))
.add_field_char(FieldCharType::Separate, false)
.add_field_char(FieldCharType::End, false),
)
.page_break_before(true);
Docx::new()
.add_style(style1)
.add_table_of_contents(
TableOfContents::new().heading_styles_range(1, 3), // .tc_field_level_range(3, 4),
)
.add_paragraph(p1)
.add_paragraph(p2)
.add_paragraph(tc)
.build()
.pack(file)?;
Ok(())
}

View File

@ -0,0 +1,31 @@
#[cfg(not(test))]
use std::sync::atomic::AtomicUsize;
#[cfg(not(test))]
static BOOKMARK_ID: AtomicUsize = AtomicUsize::new(1);
#[cfg(not(test))]
pub fn generate_bookmark_id() -> usize {
use std::sync::atomic::Ordering;
let id = BOOKMARK_ID.load(Ordering::Relaxed);
BOOKMARK_ID.store(id.wrapping_add(1), Ordering::Relaxed);
id
}
#[cfg(not(test))]
pub fn reset_bookmark_id() {
use std::sync::atomic::Ordering;
BOOKMARK_ID.load(Ordering::Relaxed);
BOOKMARK_ID.store(1, Ordering::Relaxed);
}
#[cfg(test)]
pub fn generate_bookmark_id() -> usize {
1
}
#[cfg(test)]
pub fn reset_bookmark_id() {
// NOP
}

View File

@ -268,9 +268,7 @@ mod tests {
str::from_utf8(&b).unwrap(), str::from_utf8(&b).unwrap(),
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 wp14"> <w:document xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 wp14">
<w:body><w:sdt> <w:body><w:sdt><w:sdtPr><w:rPr /></w:sdtPr><w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent>
<w:sdtPr />
<w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent>
</w:sdt><w:sectPr><w:pgSz w:w="11906" w:h="16838" /><w:pgMar w:top="1985" w:right="1701" w:bottom="1701" w:left="1701" w:header="851" w:footer="992" w:gutter="0" /><w:cols w:space="425" /><w:docGrid w:type="lines" w:linePitch="360" /></w:sectPr></w:body> </w:sdt><w:sectPr><w:pgSz w:w="11906" w:h="16838" /><w:pgMar w:top="1985" w:right="1701" w:bottom="1701" w:left="1701" w:header="851" w:footer="992" w:gutter="0" /><w:cols w:space="425" /><w:docGrid w:type="lines" w:linePitch="360" /></w:sectPr></w:body>
</w:document>"# </w:document>"#
); );

View File

@ -59,7 +59,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
serde_json::to_string(&graphic).unwrap(), 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":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"pattern1"}}]}}],"property":{"runProperty":{},"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}}]}}]}]}"#, r#"{"children":[{"dataType":"wpShape","children":[{"type":"shape","data":{"children":[{"type":"textbox","data":{"children":[{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"pattern1"}}]}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"tabs":[],"divId":null},"hasNumbering":false}}],"has_numbering":false}],"hasNumbering":false}}]}}]}]}"#,
); );
} }
} }

View File

@ -90,7 +90,7 @@ mod tests {
.num_style_link("style1"); .num_style_link("style1");
assert_eq!( assert_eq!(
serde_json::to_string(&c).unwrap(), 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":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"runProperty":{},"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":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"tabs":[],"divId":null},"runProperty":{},"suffix":"tab","pstyle":null,"levelRestart":null}]}"#,
); );
} }
} }

View File

@ -65,3 +65,21 @@ impl std::str::FromStr for InstrPAGEREF {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
use std::str;
#[test]
fn test_page_ref() {
let b = InstrPAGEREF::new("_Toc00000000").hyperlink().build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"PAGEREF _Toc00000000 \h"#
);
}
}

View File

@ -15,52 +15,52 @@ impl StyleWithLevel {
pub struct InstrToC { pub struct InstrToC {
// \o If no heading range is specified, all heading levels used in the document are listed. // \o If no heading range is specified, all heading levels used in the document are listed.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
heading_styles_range: Option<(usize, usize)>, pub heading_styles_range: Option<(usize, usize)>,
// \l Includes TC fields that assign entries to one of the levels specified by text in this switch's field-argument as a range having the form startLevel-endLevel, // \l Includes TC fields that assign entries to one of the levels specified by text in this switch's field-argument as a range having the form startLevel-endLevel,
// where startLevel and endLevel are integers, and startLevel has a value equal-to or less-than endLevel. // where startLevel and endLevel are integers, and startLevel has a value equal-to or less-than endLevel.
// TC fields that assign entries to lower levels are skipped. // TC fields that assign entries to lower levels are skipped.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
tc_field_level_range: Option<(usize, usize)>, pub tc_field_level_range: Option<(usize, usize)>,
// \n Without field-argument, omits page numbers from the table of contents. // \n Without field-argument, omits page numbers from the table of contents.
// .Page numbers are omitted from all levels unless a range of entry levels is specified by text in this switch's field-argument. // .Page numbers are omitted from all levels unless a range of entry levels is specified by text in this switch's field-argument.
// A range is specified as for \l. // A range is specified as for \l.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
omit_page_numbers_level_range: Option<(usize, usize)>, pub omit_page_numbers_level_range: Option<(usize, usize)>,
// \b includes entries only from the portion of the document marked by the bookmark named by text in this switch's field-argument. // \b includes entries only from the portion of the document marked by the bookmark named by text in this switch's field-argument.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
entry_bookmark_name: Option<String>, pub entry_bookmark_name: Option<String>,
// \t Uses paragraphs formatted with styles other than the built-in heading styles. // \t Uses paragraphs formatted with styles other than the built-in heading styles.
// . text in this switch's field-argument specifies those styles as a set of comma-separated doublets, // . text in this switch's field-argument specifies those styles as a set of comma-separated doublets,
// with each doublet being a comma-separated set of style name and table of content level. \t can be combined with \o. // with each doublet being a comma-separated set of style name and table of content level. \t can be combined with \o.
styles_with_levels: Vec<StyleWithLevel>, pub styles_with_levels: Vec<StyleWithLevel>,
// struct S texWin Lis switch's field-argument specifies a sequence of character // struct S texWin Lis switch's field-argument specifies a sequence of character
// . The default is a tab with leader dots. // . The default is a tab with leader dots.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
entry_and_page_number_separator: Option<String>, pub entry_and_page_number_separator: Option<String>,
// \d // \d
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
sequence_and_page_numbers_separator: Option<String>, pub sequence_and_page_numbers_separator: Option<String>,
// \a // \a
caption_label: Option<String>, pub caption_label: Option<String>,
// \c // \c
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
caption_label_including_numbers: Option<String>, pub caption_label_including_numbers: Option<String>,
// \s // \s
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
seq_field_identifier_for_prefix: Option<String>, pub seq_field_identifier_for_prefix: Option<String>,
// \f // \f
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
tc_field_identifier: Option<String>, pub tc_field_identifier: Option<String>,
// \h // \h
hyperlink: bool, pub hyperlink: bool,
// \w // \w
preserve_tab: bool, pub preserve_tab: bool,
// \x // \x
preserve_new_line: bool, pub preserve_new_line: bool,
// \u // \u
use_applied_paragraph_line_level: bool, pub use_applied_paragraph_line_level: bool,
// \z Hides tab leader and page numbers in Web layout view. // \z Hides tab leader and page numbers in Web layout view.
hide_tab_and_page_numbers_in_webview: bool, pub hide_tab_and_page_numbers_in_webview: bool,
} }
impl InstrToC { impl InstrToC {

View File

@ -87,6 +87,7 @@ mod table_grid;
mod table_indent; mod table_indent;
mod table_layout; mod table_layout;
mod table_of_contents; mod table_of_contents;
mod table_of_contents_item;
mod table_property; mod table_property;
mod table_row; mod table_row;
mod table_row_property; mod table_row_property;
@ -195,6 +196,7 @@ pub use table_grid::*;
pub use table_indent::*; pub use table_indent::*;
pub use table_layout::*; pub use table_layout::*;
pub use table_of_contents::*; pub use table_of_contents::*;
pub use table_of_contents_item::*;
pub use table_property::*; pub use table_property::*;
pub use table_row::*; pub use table_row::*;
pub use table_row_property::*; pub use table_row_property::*;

View File

@ -1,5 +1,7 @@
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::str::FromStr;
use crate::documents::BuildXML; use crate::documents::BuildXML;
use crate::xml_builder::*; use crate::xml_builder::*;
@ -12,6 +14,23 @@ impl Name {
pub fn new(name: impl Into<String>) -> Name { pub fn new(name: impl Into<String>) -> Name {
Name { name: name.into() } Name { name: name.into() }
} }
pub fn starts_with(&self, s: &str) -> bool {
self.name.starts_with(s)
}
pub fn is_heading(&self) -> bool {
self.name.to_lowercase().starts_with("heading")
}
pub fn get_heading_number(&self) -> Option<usize> {
let replaced = self.name.to_lowercase().replace("heading ", "");
if let Ok(n) = usize::from_str(&replaced) {
Some(n)
} else {
None
}
}
} }
impl BuildXML for Name { impl BuildXML for Name {

View File

@ -138,6 +138,22 @@ impl Paragraph {
self self
} }
pub(crate) fn unshift_run(mut self, run: Run) -> Paragraph {
self.children.insert(0, ParagraphChild::Run(Box::new(run)));
self
}
pub(crate) fn wrap_by_bookmark(mut self, id: usize, name: impl Into<String>) -> Paragraph {
self.children.insert(
0,
ParagraphChild::BookmarkStart(BookmarkStart::new(id, name)),
);
self.children
.push(ParagraphChild::BookmarkEnd(BookmarkEnd::new(id)));
self
}
pub fn add_hyperlink(mut self, link: Hyperlink) -> Self { pub fn add_hyperlink(mut self, link: Hyperlink) -> Self {
self.children.push(ParagraphChild::Hyperlink(link)); self.children.push(ParagraphChild::Hyperlink(link));
self self
@ -219,6 +235,11 @@ impl Paragraph {
self self
} }
pub fn add_tab(mut self, t: Tab) -> Self {
self.property = self.property.add_tab(t);
self
}
pub fn indent( pub fn indent(
mut self, mut self,
left: Option<i32>, left: Option<i32>,
@ -275,6 +296,35 @@ impl Paragraph {
self.property = self.property.line_spacing(spacing); self.property = self.property.line_spacing(spacing);
self self
} }
pub fn raw_text(&self) -> String {
let mut s = "".to_string();
// For now support only run and ins.
for c in self.children.iter() {
match c {
ParagraphChild::Insert(i) => {
for c in i.children.iter() {
if let InsertChild::Run(r) = c {
for c in r.children.iter() {
if let RunChild::Text(t) = c {
s.push_str(&t.text);
}
}
}
}
}
ParagraphChild::Run(run) => {
for c in run.children.iter() {
if let RunChild::Text(t) = c {
s.push_str(&t.text);
}
}
}
_ => {}
}
}
s
}
} }
impl BuildXML for Paragraph { impl BuildXML for Paragraph {
@ -374,7 +424,7 @@ mod tests {
let p = Paragraph::new().add_run(run); let p = Paragraph::new().add_run(run);
assert_eq!( assert_eq!(
serde_json::to_string(&p).unwrap(), serde_json::to_string(&p).unwrap(),
r#"{"id":"12345678","children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"divId":null},"hasNumbering":false}"#, r#"{"id":"12345678","children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"tabs":[],"divId":null},"hasNumbering":false}"#,
); );
} }
@ -385,7 +435,17 @@ mod tests {
let p = Paragraph::new().add_insert(ins); let p = Paragraph::new().add_insert(ins);
assert_eq!( assert_eq!(
serde_json::to_string(&p).unwrap(), serde_json::to_string(&p).unwrap(),
r#"{"id":"12345678","children":[{"type":"insert","data":{"children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"author":"unnamed","date":"1970-01-01T00:00:00Z"}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":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":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"author":"unnamed","date":"1970-01-01T00:00:00Z"}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"tabs":[],"divId":null},"hasNumbering":false}"#
); );
} }
}
#[test]
fn test_raw_text() {
let b = Paragraph::new()
.add_run(Run::new().add_text("Hello"))
.add_insert(Insert::new(Run::new().add_text("World")))
.add_delete(Delete::new().add_run(Run::new().add_delete_text("!!!!!")))
.raw_text();
assert_eq!(b, "HelloWorld".to_owned());
}}

View File

@ -5,7 +5,7 @@ use crate::documents::BuildXML;
use crate::types::{AlignmentType, SpecialIndentType}; use crate::types::{AlignmentType, SpecialIndentType};
use crate::xml_builder::*; use crate::xml_builder::*;
#[derive(Serialize, Debug, Clone, PartialEq)] #[derive(Serialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ParagraphProperty { pub struct ParagraphProperty {
pub run_property: RunProperty, pub run_property: RunProperty,
@ -19,29 +19,11 @@ pub struct ParagraphProperty {
pub page_break_before: bool, pub page_break_before: bool,
pub window_control: bool, pub window_control: bool,
pub outline_lvl: Option<OutlineLvl>, pub outline_lvl: Option<OutlineLvl>,
pub tabs: Vec<Tab>,
// read only // read only
pub(crate) div_id: Option<String>, pub(crate) div_id: Option<String>,
} }
impl Default for ParagraphProperty {
fn default() -> Self {
ParagraphProperty {
run_property: RunProperty::new(),
style: None,
numbering_property: None,
alignment: None,
indent: None,
line_spacing: None,
keep_next: false,
keep_lines: false,
page_break_before: false,
window_control: false,
outline_lvl: None,
div_id: None,
}
}
}
// 17.3.1.26 // 17.3.1.26
// pPr (Paragraph Properties) // pPr (Paragraph Properties)
// This element specifies a set of paragraph properties which shall be applied to the contents of the parent // This element specifies a set of paragraph properties which shall be applied to the contents of the parent
@ -111,6 +93,11 @@ impl ParagraphProperty {
self self
} }
pub fn add_tab(mut self, t: Tab) -> Self {
self.tabs.push(t);
self
}
pub(crate) fn hanging_chars(mut self, chars: i32) -> Self { pub(crate) fn hanging_chars(mut self, chars: i32) -> Self {
if let Some(indent) = self.indent { if let Some(indent) = self.indent {
self.indent = Some(indent.hanging_chars(chars)); self.indent = Some(indent.hanging_chars(chars));
@ -154,6 +141,14 @@ impl BuildXML for ParagraphProperty {
b = b.window_control() b = b.window_control()
} }
if !self.tabs.is_empty() {
b = b.open_tabs();
for t in self.tabs.iter() {
b = b.tab(t.val, t.leader, t.pos);
}
b = b.close();
}
b.close().build() b.close().build()
} }
} }
@ -221,7 +216,7 @@ mod tests {
let b = c.indent(Some(20), Some(SpecialIndentType::FirstLine(10)), None, None); let b = c.indent(Some(20), Some(SpecialIndentType::FirstLine(10)), None, None);
assert_eq!( assert_eq!(
serde_json::to_string(&b).unwrap(), serde_json::to_string(&b).unwrap(),
r#"{"runProperty":{},"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}"# r#"{"runProperty":{},"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,"tabs":[],"divId":null}"#
); );
} }

View File

@ -5,7 +5,7 @@ use crate::xml_builder::*;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ParagraphStyle { pub struct ParagraphStyle {
val: String, pub val: String,
} }
impl Default for ParagraphStyle { impl Default for ParagraphStyle {

View File

@ -10,6 +10,7 @@ use crate::xml_builder::*;
pub struct StructuredDataTagProperty { pub struct StructuredDataTagProperty {
pub run_property: RunProperty, pub run_property: RunProperty,
pub data_binding: Option<DataBinding>, pub data_binding: Option<DataBinding>,
pub alias: Option<String>,
} }
impl Default for StructuredDataTagProperty { impl Default for StructuredDataTagProperty {
@ -17,6 +18,7 @@ impl Default for StructuredDataTagProperty {
Self { Self {
run_property: RunProperty::new(), run_property: RunProperty::new(),
data_binding: None, data_binding: None,
alias: None,
} }
} }
} }
@ -30,16 +32,25 @@ impl StructuredDataTagProperty {
self.data_binding = Some(d); self.data_binding = Some(d);
self self
} }
pub fn alias(mut self, v: impl Into<String>) -> Self {
self.alias = Some(v.into());
self
}
} }
impl BuildXML for StructuredDataTagProperty { impl BuildXML for StructuredDataTagProperty {
fn build(&self) -> Vec<u8> { fn build(&self) -> Vec<u8> {
XMLBuilder::new() let mut b = XMLBuilder::new()
.open_structured_tag_property() .open_structured_tag_property()
.add_child(&self.run_property) .add_child(&self.run_property)
.add_optional_child(&self.data_binding) .add_optional_child(&self.data_binding);
.close()
.build() if let Some(ref alias) = self.alias {
b = b.alias(alias);
}
b.close().build()
} }
} }
@ -60,4 +71,15 @@ mod tests {
r#"<w:sdtPr><w:rPr /></w:sdtPr>"# r#"<w:sdtPr><w:rPr /></w:sdtPr>"#
); );
} }
#[test]
fn test_with_alias() {
let c = StructuredDataTagProperty::new().alias("summary");
let b = c.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:sdtPr><w:rPr /><w:alias w:val="summary" />
</w:sdtPr>"#
);
}
} }

View File

@ -1,26 +1,40 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::documents::BuildXML; use crate::documents::BuildXML;
use crate::types::*;
use crate::xml_builder::*; use crate::xml_builder::*;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
pub struct Tab {} pub struct Tab {
pub val: Option<TabValueType>,
impl Tab { pub leader: Option<TabLeaderType>,
pub fn new() -> Tab { pub pos: Option<usize>,
Default::default()
}
} }
impl Default for Tab { impl Tab {
fn default() -> Self { pub fn new() -> Self {
Tab {} Default::default()
}
pub fn val(mut self, v: TabValueType) -> Self {
self.val = Some(v);
self
}
pub fn leader(mut self, v: TabLeaderType) -> Self {
self.leader = Some(v);
self
}
pub fn pos(mut self, v: usize) -> Self {
self.pos = Some(v);
self
} }
} }
impl BuildXML for Tab { impl BuildXML for Tab {
fn build(&self) -> Vec<u8> { fn build(&self) -> Vec<u8> {
let b = XMLBuilder::new(); let b = XMLBuilder::new();
b.tab().build() b.tab(self.val, self.leader, self.pos).build()
} }
} }

View File

@ -185,7 +185,7 @@ mod tests {
.grid_span(2); .grid_span(2);
assert_eq!( assert_eq!(
serde_json::to_string(&c).unwrap(), serde_json::to_string(&c).unwrap(),
r#"{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{},"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}"#, r#"{"children":[{"type":"paragraph","data":{"id":"12345678","children":[{"type":"run","data":{"runProperty":{},"children":[{"type":"text","data":{"preserveSpace":true,"text":"Hello"}}]}}],"property":{"runProperty":{},"style":null,"numberingProperty":null,"alignment":null,"indent":null,"lineSpacing":null,"keepNext":false,"keepLines":false,"pageBreakBefore":false,"windowControl":false,"outlineLvl":null,"tabs":[],"divId":null},"hasNumbering":false}}],"property":{"width":null,"borders":null,"gridSpan":2,"verticalMerge":null,"verticalAlign":null,"textDirection":null,"shading":null},"hasNumbering":false}"#,
); );
} }
} }

View File

@ -5,8 +5,15 @@ use crate::types::*;
use crate::xml_builder::*; use crate::xml_builder::*;
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_TOCTOC_topic_ID0ELZO1.html // https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_TOCTOC_topic_ID0ELZO1.html
// This struct is only used by writers
#[derive(Serialize, Debug, Clone, PartialEq, Default)] #[derive(Serialize, Debug, Clone, PartialEq, Default)]
pub struct TableOfContents(pub InstrToC); pub struct TableOfContents {
pub instr: InstrToC,
pub items: Vec<TableOfContentsItem>,
// pub disable_auto_items: bool,
pub dirty: bool,
pub alias: Option<String>,
}
impl TableOfContents { impl TableOfContents {
pub fn new() -> Self { pub fn new() -> Self {
@ -14,31 +21,85 @@ impl TableOfContents {
} }
pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self { pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self {
self.0 = self.0.heading_styles_range(start, end); self.instr = self.instr.heading_styles_range(start, end);
self
}
pub fn hyperlink(mut self) -> Self {
self.instr = self.instr.hyperlink();
self
}
pub fn alias(mut self, a: impl Into<String>) -> Self {
self.alias = Some(a.into());
self
}
// pub fn tc_field_level_range(mut self, start: usize, end: usize) -> Self {
// self.instr = self.instr.tc_field_level_range(start, end);
// self
// }
pub fn add_item(mut self, t: TableOfContentsItem) -> Self {
self.items.push(t);
self
}
// pub fn disable_auto_items(mut self) -> Self {
// self.disable_auto_items = true;
// self
// }
pub fn dirty(mut self) -> Self {
self.dirty = true;
self self
} }
} }
impl BuildXML for TableOfContents { impl BuildXML for TableOfContents {
fn build(&self) -> Vec<u8> { fn build(&self) -> Vec<u8> {
let p1 = Paragraph::new().add_run( let mut p = StructuredDataTagProperty::new();
Run::new() if let Some(ref alias) = self.alias {
.add_field_char(FieldCharType::Begin, true) p = p.alias(alias);
.add_instr_text(InstrText::TOC(self.0.clone())) }
.add_field_char(FieldCharType::Separate, false), if self.items.is_empty() {
); let p1 = Paragraph::new().add_run(
let p2 = Paragraph::new().add_run(Run::new().add_field_char(FieldCharType::End, false)); Run::new()
.add_field_char(FieldCharType::Begin, true)
.add_instr_text(InstrText::TOC(self.instr.clone()))
.add_field_char(FieldCharType::Separate, false),
);
let p2 = Paragraph::new().add_run(Run::new().add_field_char(FieldCharType::End, false));
XMLBuilder::new() XMLBuilder::new()
.open_structured_tag() .open_structured_tag()
.open_structured_tag_property() .add_child(&p)
.close() .open_structured_tag_content()
.open_structured_tag_content() .add_child(&p1)
.add_child(&p1) .add_child(&p2)
.add_child(&p2) .close()
.close() .close()
.close() .build()
.build() } else {
let items: Vec<TableOfContentsItem> = self
.items
.iter()
.map(|item| {
let mut item = item.clone();
item.instr = self.instr.clone();
item.dirty = self.dirty;
item
})
.collect();
XMLBuilder::new()
.open_structured_tag()
.add_child(&p)
.open_structured_tag_content()
.add_child(&items)
.close()
.close()
.build()
}
} }
} }
@ -55,10 +116,25 @@ mod tests {
let b = TableOfContents::new().heading_styles_range(1, 3).build(); let b = TableOfContents::new().heading_styles_range(1, 3).build();
assert_eq!( assert_eq!(
str::from_utf8(&b).unwrap(), str::from_utf8(&b).unwrap(),
r#"<w:sdt> r#"<w:sdt><w:sdtPr><w:rPr /></w:sdtPr><w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent>
<w:sdtPr />
<w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="true" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r></w:p><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent>
</w:sdt>"# </w:sdt>"#
); );
} }
/*
#[test]
fn test_toc_with_items() {
let b = TableOfContents::new()
.heading_styles_range(1, 3)
.add_items(Paragraph::new().add_run(Run::new().add_text("Hello")))
.build();
assert_eq!(
str::from_utf8(&b).unwrap(),
r#"<w:sdt>
<w:sdtPr />
<w:sdtContent><w:p w14:paraId="12345678"><w:pPr><w:rPr /></w:pPr><w:r><w:rPr /><w:fldChar w:fldCharType="begin" w:dirty="false" /><w:instrText>TOC \o &quot;1-3&quot;</w:instrText><w:fldChar w:fldCharType="separate" w:dirty="false" /></w:r><w:r><w:rPr /><w:t xml:space="preserve">Hello</w:t></w:r><w:r><w:rPr /><w:fldChar w:fldCharType="end" w:dirty="false" /></w:r></w:p></w:sdtContent>
</w:sdt>"#
);
}
*/
} }

View File

@ -0,0 +1,146 @@
use serde::Serialize;
use crate::documents::*;
use crate::types::*;
use crate::xml_builder::*;
#[derive(Serialize, Debug, Clone, PartialEq, Default)]
pub struct TableOfContentsItem {
pub instr: InstrToC,
pub text: String,
pub toc_key: String,
pub level: usize,
pub dirty: bool,
pub page_ref: Option<String>,
}
impl TableOfContentsItem {
pub fn new() -> Self {
Self {
level: 1,
..Default::default()
}
}
pub fn instr(mut self, instr: InstrToC) -> Self {
self.instr = instr;
self
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self
}
pub fn level(mut self, level: usize) -> Self {
self.level = level;
self
}
pub fn toc_key(mut self, key: impl Into<String>) -> Self {
self.toc_key = key.into();
self
}
pub fn page_ref(mut self, r: impl Into<String>) -> Self {
self.page_ref = Some(r.into());
self
}
}
impl BuildXML for Vec<TableOfContentsItem> {
fn build(&self) -> Vec<u8> {
let mut b = XMLBuilder::new()
.open_structured_tag()
.open_structured_tag_property()
.close()
.open_structured_tag_content();
for (i, t) in self.iter().enumerate() {
let mut p = Paragraph::new().style(&format!("ToC{}", t.level));
if i == 0 {
p = p.unshift_run(
Run::new()
.add_field_char(FieldCharType::Begin, t.dirty)
.add_instr_text(InstrText::TOC(t.instr.clone()))
.add_field_char(FieldCharType::Separate, false),
);
p = p.add_tab(
Tab::new()
.val(TabValueType::Right)
.leader(TabLeaderType::Dot)
// TODO: for now set 80000
.pos(80000),
);
let run = Run::new().add_text(&t.text);
let page_ref = Run::new()
.add_field_char(FieldCharType::Begin, false)
.add_instr_text(InstrText::PAGEREF(
InstrPAGEREF::new(&t.toc_key).hyperlink(),
))
.add_field_char(FieldCharType::Separate, false)
.add_text(t.page_ref.to_owned().unwrap_or_else(|| "1".to_string()))
.add_field_char(FieldCharType::End, false);
if t.instr.hyperlink {
p = p.add_hyperlink(
Hyperlink::new()
.anchor(&t.toc_key)
.add_run(run)
.add_run(Run::new().add_tab())
.add_run(page_ref),
);
} else {
p = p
.add_run(run)
.add_run(Run::new().add_tab())
.add_run(page_ref);
}
b = b.add_child(&p);
} else {
let mut p = Paragraph::new().style(&format!("ToC{}", t.level));
p = p.add_tab(
Tab::new()
.val(TabValueType::Right)
.leader(TabLeaderType::Dot)
// TODO: for now set 80000
.pos(80000),
);
let run = Run::new().add_text(&t.text);
let page_ref = Run::new()
.add_field_char(FieldCharType::Begin, false)
.add_instr_text(InstrText::PAGEREF(
InstrPAGEREF::new(&t.toc_key).hyperlink(),
))
.add_field_char(FieldCharType::Separate, false)
.add_text(t.page_ref.to_owned().unwrap_or_else(|| "1".to_string()))
.add_field_char(FieldCharType::End, false);
if t.instr.hyperlink {
p = p.add_hyperlink(
Hyperlink::new()
.anchor(&t.toc_key)
.add_run(run)
.add_run(Run::new().add_tab())
.add_run(page_ref),
)
} else {
p = p
.add_run(run)
.add_run(Run::new().add_tab())
.add_run(page_ref);
}
b = b.add_child(&p);
}
if i == self.len() - 1 {
let mut p = Paragraph::new().style(&format!("ToC{}", t.level));
p = p.add_run(Run::new().add_field_char(FieldCharType::End, false));
b = b.add_child(&p);
}
}
b.close().close().build()
}
}

View File

@ -8,8 +8,8 @@ use crate::xml_builder::*;
#[derive(Debug, Clone, Deserialize, PartialEq)] #[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Text { pub struct Text {
text: String, pub text: String,
preserve_space: bool, pub preserve_space: bool,
} }
impl Text { impl Text {

View File

@ -1,5 +1,6 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
mod bookmark_id;
mod build_xml; mod build_xml;
mod comments; mod comments;
mod comments_extended; mod comments_extended;
@ -20,11 +21,13 @@ mod history_id;
mod numberings; mod numberings;
mod paragraph_id; mod paragraph_id;
mod pic_id; mod pic_id;
mod preset_styles;
mod rels; mod rels;
mod settings; mod settings;
mod styles; mod styles;
mod taskpanes; mod taskpanes;
mod taskpanes_rels; mod taskpanes_rels;
mod toc_key;
mod web_settings; mod web_settings;
mod webextension; mod webextension;
mod xml_docx; mod xml_docx;
@ -34,6 +37,7 @@ pub(crate) use history_id::HistoryId;
pub(crate) use paragraph_id::*; pub(crate) use paragraph_id::*;
pub(crate) use pic_id::*; pub(crate) use pic_id::*;
pub use bookmark_id::*;
pub use comments::*; pub use comments::*;
pub use comments_extended::*; pub use comments_extended::*;
pub use content_types::*; pub use content_types::*;
@ -55,6 +59,7 @@ pub use settings::*;
pub use styles::*; pub use styles::*;
pub use taskpanes::*; pub use taskpanes::*;
pub use taskpanes_rels::*; pub use taskpanes_rels::*;
pub use toc_key::*;
pub use web_settings::*; pub use web_settings::*;
pub use webextension::*; pub use webextension::*;
pub use xml_docx::*; pub use xml_docx::*;
@ -412,11 +417,40 @@ impl Docx {
self self
} }
pub fn build(&mut self) -> XMLDocx { pub fn build(mut self) -> XMLDocx {
self.reset(); self.reset();
self.update_comments(); self.update_comments();
let tocs: Vec<(usize, TableOfContents)> = self
.document
.children
.iter()
.enumerate()
.filter_map(|(i, child)| {
if let DocumentChild::TableOfContents(toc) = child {
Some((i, toc.clone()))
} else {
None
}
})
.collect();
if !tocs.is_empty() {
for i in 1..=9 {
self.styles = self
.styles
.add_style(crate::documents::preset_styles::toc(i));
}
}
for (i, toc) in tocs {
if toc.items.is_empty() {
let children = update_document_by_toc(self.document.children, &self.styles, toc, i);
self.document.children = children;
}
}
let (image_ids, images) = self.create_images(); let (image_ids, images) = self.create_images();
let web_extensions = self.web_extensions.iter().map(|ext| ext.build()).collect(); let web_extensions = self.web_extensions.iter().map(|ext| ext.build()).collect();
let custom_items = self.custom_items.iter().map(|xml| xml.build()).collect(); let custom_items = self.custom_items.iter().map(|xml| xml.build()).collect();
@ -827,3 +861,71 @@ impl Docx {
(image_ids, images) (image_ids, images)
} }
} }
fn update_document_by_toc(
document_children: Vec<DocumentChild>,
styles: &Styles,
toc: TableOfContents,
toc_index: usize,
) -> Vec<DocumentChild> {
let heading_map = styles.create_heading_style_map();
let mut items = vec![];
let mut children = vec![];
for child in document_children.into_iter() {
match child {
DocumentChild::Paragraph(mut paragraph) => {
if let Some(heading_level) = paragraph
.property
.style
.as_ref()
.map(|p| p.val.to_string())
.and_then(|sid| heading_map.get(&sid))
{
if let Some((min, max)) = toc.instr.heading_styles_range {
if min <= *heading_level && max >= *heading_level {
let toc_key = TocKey::generate();
items.push(
TableOfContentsItem::new()
.text(paragraph.raw_text())
.toc_key(&toc_key)
.level(*heading_level),
);
paragraph =
paragraph.wrap_by_bookmark(generate_bookmark_id(), &toc_key);
}
}
if let Some((_min, _max)) = toc.instr.tc_field_level_range {
// TODO: check tc field
}
}
children.push(DocumentChild::Paragraph(paragraph));
}
DocumentChild::Table(ref _table) => {
// TODO:
// for row in &table.rows {
// for cell in &row.cells {
// for content in &cell.children {
// match content {
// TableCellContent::Paragraph(paragraph) => {}
// TableCellContent::Table(_) => {
// // TODO: Support table in table
// }
// }
// }
// }
// }
children.push(child);
}
_ => {
children.push(child);
}
}
}
let mut toc = toc;
toc.items = items;
children[toc_index] = DocumentChild::TableOfContents(toc);
children
}

View File

@ -0,0 +1,3 @@
mod toc;
pub use toc::*;

View File

@ -0,0 +1,9 @@
use crate::documents::*;
use crate::types::*;
pub fn toc(level: i32) -> Style {
Style::new(format!("ToC{}", level), StyleType::Paragraph)
.name(format!("toc {}", level))
.align(AlignmentType::Both)
.indent(Some((level - 1) * 200), None, None, Some((level - 1) * 100))
}

View File

@ -41,6 +41,24 @@ impl Styles {
self.doc_defaults = doc_defaults; self.doc_defaults = doc_defaults;
self self
} }
pub fn find_style_by_id(&self, id: &str) -> Option<&Style> {
self.styles.iter().find(|s| s.style_id == id)
}
pub fn create_heading_style_map(&self) -> std::collections::HashMap<String, usize> {
self.styles
.iter()
.filter_map(|s| {
if s.name.is_heading() {
let n = s.name.get_heading_number();
n.map(|n| (s.style_id.clone(), n))
} else {
None
}
})
.collect()
}
} }
impl Default for Styles { impl Default for Styles {
@ -84,4 +102,13 @@ mod tests {
r#"<w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 w15"><w:docDefaults><w:rPrDefault><w:rPr /></w:rPrDefault></w:docDefaults><w:style w:type="paragraph" w:styleId="Normal"><w:name w:val="Normal" /><w:rPr /><w:pPr><w:rPr /></w:pPr><w:basedOn w:val="Normal" /><w:next w:val="Normal" /><w:qFormat /></w:style><w:style w:type="paragraph" w:styleId="Title"><w:name w:val="TitleName" /><w:rPr /><w:pPr><w:rPr /></w:pPr><w:basedOn w:val="Normal" /><w:next w:val="Normal" /><w:qFormat /></w:style></w:styles>"# r#"<w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 w15"><w:docDefaults><w:rPrDefault><w:rPr /></w:rPrDefault></w:docDefaults><w:style w:type="paragraph" w:styleId="Normal"><w:name w:val="Normal" /><w:rPr /><w:pPr><w:rPr /></w:pPr><w:basedOn w:val="Normal" /><w:next w:val="Normal" /><w:qFormat /></w:style><w:style w:type="paragraph" w:styleId="Title"><w:name w:val="TitleName" /><w:rPr /><w:pPr><w:rPr /></w:pPr><w:basedOn w:val="Normal" /><w:next w:val="Normal" /><w:qFormat /></w:style></w:styles>"#
); );
} }
#[test]
fn test_heading_style() {
let c = Styles::new().add_style(Style::new("ToC", StyleType::Paragraph).name("heading 3"));
let mut m = std::collections::HashMap::new();
m.insert("ToC".to_string(), 3);
let b = c.create_heading_style_map();
assert_eq!(b, m);
}
} }

View File

@ -0,0 +1,26 @@
#[cfg(not(test))]
use std::sync::atomic::AtomicUsize;
#[cfg(not(test))]
static TOC_KEY: AtomicUsize = AtomicUsize::new(0);
#[cfg(not(test))]
pub trait TocKeyGenerator {
fn generate() -> String {
use std::sync::atomic::Ordering;
let id = TOC_KEY.load(Ordering::Relaxed);
TOC_KEY.store(id + 1, Ordering::Relaxed);
format!("_Toc{:08}", id)
}
}
pub struct TocKey {}
impl TocKeyGenerator for TocKey {}
#[cfg(test)]
pub trait TocKeyGenerator {
fn generate() -> String {
"_Toc00000000".to_string()
}
}

View File

@ -198,7 +198,6 @@ pub fn read_xml(xml: &str) -> Result<Docx, ReaderError> {
// Read web settings // Read web settings
let web_settings_path = rels.find_target_path(WEB_SETTINGS_TYPE); let web_settings_path = rels.find_target_path(WEB_SETTINGS_TYPE);
dbg!(&web_settings_path);
if let Some(web_settings_path) = web_settings_path { if let Some(web_settings_path) = web_settings_path {
let data = read_zip( let data = read_zip(
&mut archive, &mut archive,

View File

@ -4,6 +4,8 @@ use thiserror::Error;
pub enum TypeError { pub enum TypeError {
#[error("Failed to convert str to enum.")] #[error("Failed to convert str to enum.")]
FromStrError, FromStrError,
#[error("Failed to convert str. This is because {0} is unsupported")]
Unsupported(String),
#[error("Unknown error.")] #[error("Unknown error.")]
Unknown, Unknown,
} }

View File

@ -16,6 +16,8 @@ pub mod section_type;
pub mod shd_type; pub mod shd_type;
pub mod special_indent_type; pub mod special_indent_type;
pub mod style_type; pub mod style_type;
pub mod tab_leader_type;
pub mod tab_value_type;
pub mod table_alignment_type; pub mod table_alignment_type;
pub mod table_layout_type; pub mod table_layout_type;
pub mod text_direction_type; pub mod text_direction_type;
@ -42,6 +44,8 @@ pub use section_type::*;
pub use shd_type::*; pub use shd_type::*;
pub use special_indent_type::*; pub use special_indent_type::*;
pub use style_type::*; pub use style_type::*;
pub use tab_leader_type::*;
pub use tab_value_type::*;
pub use table_alignment_type::*; pub use table_alignment_type::*;
pub use table_layout_type::*; pub use table_layout_type::*;
pub use text_direction_type::*; pub use text_direction_type::*;

View File

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use wasm_bindgen::prelude::*;
use super::errors;
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum TabLeaderType {
Dot,
Heavy,
Hyphen,
MiddleDot,
None,
Underscore,
}
impl fmt::Display for TabLeaderType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TabLeaderType::Dot => write!(f, "dot"),
TabLeaderType::Heavy => write!(f, "heavy"),
TabLeaderType::Hyphen => write!(f, "hyphen"),
TabLeaderType::MiddleDot => write!(f, "middleDot"),
TabLeaderType::None => write!(f, "none"),
TabLeaderType::Underscore => write!(f, "underscore"),
}
}
}
impl FromStr for TabLeaderType {
type Err = errors::TypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"dot" => Ok(TabLeaderType::Dot),
"heavy" => Ok(TabLeaderType::Heavy),
"hyphen" => Ok(TabLeaderType::Hyphen),
"middleDot" => Ok(TabLeaderType::MiddleDot),
"none" => Ok(TabLeaderType::None),
"underscore" => Ok(TabLeaderType::Underscore),
_ => Err(errors::TypeError::Unsupported(s.to_string())),
}
}
}

View File

@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use wasm_bindgen::prelude::*;
use super::errors;
#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum TabValueType {
Bar,
Center,
Clear,
Decimal,
End,
Right,
Num,
Start,
Left,
}
impl fmt::Display for TabValueType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TabValueType::Bar => write!(f, "bar"),
TabValueType::Center => write!(f, "center"),
TabValueType::Clear => write!(f, "clear"),
TabValueType::Decimal => write!(f, "decimal"),
TabValueType::End => write!(f, "end"),
TabValueType::Right => write!(f, "right"),
TabValueType::Num => write!(f, "num"),
TabValueType::Start => write!(f, "start"),
TabValueType::Left => write!(f, "left"),
}
}
}
impl FromStr for TabValueType {
type Err = errors::TypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bar" => Ok(TabValueType::Bar),
"center" => Ok(TabValueType::Center),
"clear" => Ok(TabValueType::Clear),
"decimal" => Ok(TabValueType::Decimal),
"end" => Ok(TabValueType::End),
"right" => Ok(TabValueType::Right),
"num" => Ok(TabValueType::Num),
"start" => Ok(TabValueType::Start),
"left" => Ok(TabValueType::Left),
_ => Err(errors::TypeError::Unsupported(s.to_string())),
}
}
}

View File

@ -1,6 +1,5 @@
use super::XMLBuilder; use super::XMLBuilder;
use super::XmlEvent; use super::XmlEvent;
use crate::types::line_spacing_type::LineSpacingType;
use crate::types::*; use crate::types::*;
const EXPECT_MESSAGE: &str = "should write buf"; const EXPECT_MESSAGE: &str = "should write buf";
@ -117,6 +116,7 @@ impl XMLBuilder {
open!(open_structured_tag, "w:sdt"); open!(open_structured_tag, "w:sdt");
open!(open_structured_tag_content, "w:sdtContent"); open!(open_structured_tag_content, "w:sdtContent");
open!(open_structured_tag_property, "w:sdtPr"); open!(open_structured_tag_property, "w:sdtPr");
closed_with_str!(alias, "w:alias");
// i.e. <w:outlineLvl ...> // i.e. <w:outlineLvl ...>
closed_with_usize!(outline_lvl, "w:outlineLvl"); closed_with_usize!(outline_lvl, "w:outlineLvl");
@ -313,7 +313,6 @@ impl XMLBuilder {
closed!(shd, "w:shd", "w:val", "w:color", "w:fill"); closed!(shd, "w:shd", "w:val", "w:color", "w:fill");
closed!(tab, "w:tab");
closed!(tab_with_pos, "w:tab", "w:val", "w:pos"); closed!(tab_with_pos, "w:tab", "w:val", "w:pos");
closed!(br, "w:br", "w:type"); closed!(br, "w:br", "w:type");
@ -516,6 +515,43 @@ impl XMLBuilder {
self.close() self.close()
} }
pub(crate) fn tab(
mut self,
v: Option<TabValueType>,
leader: Option<TabLeaderType>,
pos: Option<usize>,
) -> Self {
let v_string = if let Some(v) = v {
v.to_string()
} else {
"".to_string()
};
let leader_string = if let Some(leader) = leader {
leader.to_string()
} else {
"".to_string()
};
let pos_string = format!("{}", pos.unwrap_or_default());
let mut t = XmlEvent::start_element("w:tab");
if v.is_some() {
t = t.attr("w:val", &v_string);
}
if leader.is_some() {
t = t.attr("w:leader", &leader_string);
}
if pos.is_some() {
t = t.attr("w:pos", &pos_string);
}
self.writer.write(t).expect(EXPECT_MESSAGE);
self.close()
}
} }
#[cfg(test)] #[cfg(test)]

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

@ -1109,7 +1109,7 @@ export class Docx {
build() { build() {
const docx = this.createDocx(); const docx = this.createDocx();
const buf = docx.build(this.hasNumberings); const buf = docx.build(this.hasNumberings);
docx.free(); // docx.free();
return buf; return buf;
} }
} }

View File

@ -165,7 +165,7 @@ impl Docx {
self self
} }
pub fn build(&mut self, has_numberings: bool) -> Result<Vec<u8>, JsValue> { pub fn build(mut self, has_numberings: bool) -> Result<Vec<u8>, JsValue> {
let buf = Vec::new(); let buf = Vec::new();
let mut cur = std::io::Cursor::new(buf); let mut cur = std::io::Cursor::new(buf);
if has_numberings { if has_numberings {

File diff suppressed because it is too large Load Diff