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 * fixmain
parent
31dacfb72a
commit
c89783e8d8
|
@ -3,15 +3,29 @@ use docx_rs::*;
|
|||
pub fn main() -> Result<(), DocxError> {
|
||||
let path = std::path::Path::new("./output/dirty_toc.docx");
|
||||
let file = std::fs::File::create(&path).unwrap();
|
||||
let p = Paragraph::new()
|
||||
let p1 = Paragraph::new()
|
||||
.add_run(Run::new().add_text("Hello"))
|
||||
.style("Heading1")
|
||||
.page_break_before(true);
|
||||
let style = Style::new("Heading1", StyleType::Paragraph).name("Heading 1");
|
||||
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(style)
|
||||
.add_style(style1)
|
||||
.add_style(style2)
|
||||
.add_style(style4)
|
||||
.add_table_of_contents(TableOfContents::new().heading_styles_range(1, 3))
|
||||
.add_paragraph(p)
|
||||
.add_paragraph(p1)
|
||||
.add_paragraph(p2)
|
||||
.add_paragraph(p4)
|
||||
.build()
|
||||
.pack(file)?;
|
||||
Ok(())
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -268,9 +268,7 @@ mod tests {
|
|||
str::from_utf8(&b).unwrap(),
|
||||
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:body><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="true" /><w:instrText>TOC \o "1-3"</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: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 "1-3"</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:document>"#
|
||||
);
|
||||
|
|
|
@ -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":{},"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}}]}}]}]}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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":{},"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}]}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,52 +15,52 @@ impl StyleWithLevel {
|
|||
pub struct InstrToC {
|
||||
// \o If no heading range is specified, all heading levels used in the document are listed.
|
||||
#[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,
|
||||
// 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.
|
||||
#[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.
|
||||
// .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.
|
||||
#[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.
|
||||
#[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.
|
||||
// . 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.
|
||||
styles_with_levels: Vec<StyleWithLevel>,
|
||||
pub styles_with_levels: Vec<StyleWithLevel>,
|
||||
// struct S texWin Lis switch's field-argument specifies a sequence of character
|
||||
// . The default is a tab with leader dots.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
entry_and_page_number_separator: Option<String>,
|
||||
pub entry_and_page_number_separator: Option<String>,
|
||||
// \d
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sequence_and_page_numbers_separator: Option<String>,
|
||||
pub sequence_and_page_numbers_separator: Option<String>,
|
||||
// \a
|
||||
caption_label: Option<String>,
|
||||
pub caption_label: Option<String>,
|
||||
// \c
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
caption_label_including_numbers: Option<String>,
|
||||
pub caption_label_including_numbers: Option<String>,
|
||||
// \s
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
seq_field_identifier_for_prefix: Option<String>,
|
||||
pub seq_field_identifier_for_prefix: Option<String>,
|
||||
// \f
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tc_field_identifier: Option<String>,
|
||||
pub tc_field_identifier: Option<String>,
|
||||
// \h
|
||||
hyperlink: bool,
|
||||
pub hyperlink: bool,
|
||||
// \w
|
||||
preserve_tab: bool,
|
||||
pub preserve_tab: bool,
|
||||
// \x
|
||||
preserve_new_line: bool,
|
||||
pub preserve_new_line: bool,
|
||||
// \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.
|
||||
hide_tab_and_page_numbers_in_webview: bool,
|
||||
pub hide_tab_and_page_numbers_in_webview: bool,
|
||||
}
|
||||
|
||||
impl InstrToC {
|
||||
|
|
|
@ -87,6 +87,7 @@ mod table_grid;
|
|||
mod table_indent;
|
||||
mod table_layout;
|
||||
mod table_of_contents;
|
||||
mod table_of_contents_item;
|
||||
mod table_property;
|
||||
mod table_row;
|
||||
mod table_row_property;
|
||||
|
@ -195,6 +196,7 @@ pub use table_grid::*;
|
|||
pub use table_indent::*;
|
||||
pub use table_layout::*;
|
||||
pub use table_of_contents::*;
|
||||
pub use table_of_contents_item::*;
|
||||
pub use table_property::*;
|
||||
pub use table_row::*;
|
||||
pub use table_row_property::*;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use serde::{Serialize, Serializer};
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::documents::BuildXML;
|
||||
use crate::xml_builder::*;
|
||||
|
||||
|
@ -12,6 +14,23 @@ impl Name {
|
|||
pub fn new(name: impl Into<String>) -> Name {
|
||||
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 {
|
||||
|
|
|
@ -138,6 +138,22 @@ impl Paragraph {
|
|||
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 {
|
||||
self.children.push(ParagraphChild::Hyperlink(link));
|
||||
self
|
||||
|
@ -219,6 +235,11 @@ impl Paragraph {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn add_tab(mut self, t: Tab) -> Self {
|
||||
self.property = self.property.add_tab(t);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn indent(
|
||||
mut self,
|
||||
left: Option<i32>,
|
||||
|
@ -275,6 +296,35 @@ impl Paragraph {
|
|||
self.property = self.property.line_spacing(spacing);
|
||||
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 {
|
||||
|
@ -374,7 +424,7 @@ mod tests {
|
|||
let p = Paragraph::new().add_run(run);
|
||||
assert_eq!(
|
||||
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);
|
||||
assert_eq!(
|
||||
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());
|
||||
}}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::documents::BuildXML;
|
|||
use crate::types::{AlignmentType, SpecialIndentType};
|
||||
use crate::xml_builder::*;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ParagraphProperty {
|
||||
pub run_property: RunProperty,
|
||||
|
@ -19,29 +19,11 @@ pub struct ParagraphProperty {
|
|||
pub page_break_before: bool,
|
||||
pub window_control: bool,
|
||||
pub outline_lvl: Option<OutlineLvl>,
|
||||
pub tabs: Vec<Tab>,
|
||||
// read only
|
||||
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
|
||||
// pPr (Paragraph Properties)
|
||||
// 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
|
||||
}
|
||||
|
||||
pub fn add_tab(mut self, t: Tab) -> Self {
|
||||
self.tabs.push(t);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn hanging_chars(mut self, chars: i32) -> Self {
|
||||
if let Some(indent) = self.indent {
|
||||
self.indent = Some(indent.hanging_chars(chars));
|
||||
|
@ -154,6 +141,14 @@ impl BuildXML for ParagraphProperty {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +216,7 @@ mod tests {
|
|||
let b = c.indent(Some(20), Some(SpecialIndentType::FirstLine(10)), None, None);
|
||||
assert_eq!(
|
||||
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}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::xml_builder::*;
|
|||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ParagraphStyle {
|
||||
val: String,
|
||||
pub val: String,
|
||||
}
|
||||
|
||||
impl Default for ParagraphStyle {
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::xml_builder::*;
|
|||
pub struct StructuredDataTagProperty {
|
||||
pub run_property: RunProperty,
|
||||
pub data_binding: Option<DataBinding>,
|
||||
pub alias: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for StructuredDataTagProperty {
|
||||
|
@ -17,6 +18,7 @@ impl Default for StructuredDataTagProperty {
|
|||
Self {
|
||||
run_property: RunProperty::new(),
|
||||
data_binding: None,
|
||||
alias: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,16 +32,25 @@ impl StructuredDataTagProperty {
|
|||
self.data_binding = Some(d);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alias(mut self, v: impl Into<String>) -> Self {
|
||||
self.alias = Some(v.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildXML for StructuredDataTagProperty {
|
||||
fn build(&self) -> Vec<u8> {
|
||||
XMLBuilder::new()
|
||||
let mut b = XMLBuilder::new()
|
||||
.open_structured_tag_property()
|
||||
.add_child(&self.run_property)
|
||||
.add_optional_child(&self.data_binding)
|
||||
.close()
|
||||
.build()
|
||||
.add_optional_child(&self.data_binding);
|
||||
|
||||
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>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,40 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::documents::BuildXML;
|
||||
use crate::types::*;
|
||||
use crate::xml_builder::*;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Tab {}
|
||||
|
||||
impl Tab {
|
||||
pub fn new() -> Tab {
|
||||
Default::default()
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
||||
pub struct Tab {
|
||||
pub val: Option<TabValueType>,
|
||||
pub leader: Option<TabLeaderType>,
|
||||
pub pos: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Tab {}
|
||||
impl Tab {
|
||||
pub fn new() -> Self {
|
||||
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 {
|
||||
fn build(&self) -> Vec<u8> {
|
||||
let b = XMLBuilder::new();
|
||||
b.tab().build()
|
||||
b.tab(self.val, self.leader, self.pos).build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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":{},"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}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,15 @@ use crate::types::*;
|
|||
use crate::xml_builder::*;
|
||||
|
||||
// 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)]
|
||||
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 {
|
||||
pub fn new() -> Self {
|
||||
|
@ -14,31 +21,85 @@ impl TableOfContents {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildXML for TableOfContents {
|
||||
fn build(&self) -> Vec<u8> {
|
||||
let mut p = StructuredDataTagProperty::new();
|
||||
if let Some(ref alias) = self.alias {
|
||||
p = p.alias(alias);
|
||||
}
|
||||
if self.items.is_empty() {
|
||||
let p1 = Paragraph::new().add_run(
|
||||
Run::new()
|
||||
.add_field_char(FieldCharType::Begin, true)
|
||||
.add_instr_text(InstrText::TOC(self.0.clone()))
|
||||
.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()
|
||||
.open_structured_tag()
|
||||
.open_structured_tag_property()
|
||||
.close()
|
||||
.add_child(&p)
|
||||
.open_structured_tag_content()
|
||||
.add_child(&p1)
|
||||
.add_child(&p2)
|
||||
.close()
|
||||
.close()
|
||||
.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();
|
||||
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="true" /><w:instrText>TOC \o "1-3"</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>
|
||||
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 "1-3"</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>"#
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
#[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 "1-3"</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>"#
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -8,8 +8,8 @@ use crate::xml_builder::*;
|
|||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Text {
|
||||
text: String,
|
||||
preserve_space: bool,
|
||||
pub text: String,
|
||||
pub preserve_space: bool,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
mod bookmark_id;
|
||||
mod build_xml;
|
||||
mod comments;
|
||||
mod comments_extended;
|
||||
|
@ -20,11 +21,13 @@ mod history_id;
|
|||
mod numberings;
|
||||
mod paragraph_id;
|
||||
mod pic_id;
|
||||
mod preset_styles;
|
||||
mod rels;
|
||||
mod settings;
|
||||
mod styles;
|
||||
mod taskpanes;
|
||||
mod taskpanes_rels;
|
||||
mod toc_key;
|
||||
mod web_settings;
|
||||
mod webextension;
|
||||
mod xml_docx;
|
||||
|
@ -34,6 +37,7 @@ pub(crate) use history_id::HistoryId;
|
|||
pub(crate) use paragraph_id::*;
|
||||
pub(crate) use pic_id::*;
|
||||
|
||||
pub use bookmark_id::*;
|
||||
pub use comments::*;
|
||||
pub use comments_extended::*;
|
||||
pub use content_types::*;
|
||||
|
@ -55,6 +59,7 @@ pub use settings::*;
|
|||
pub use styles::*;
|
||||
pub use taskpanes::*;
|
||||
pub use taskpanes_rels::*;
|
||||
pub use toc_key::*;
|
||||
pub use web_settings::*;
|
||||
pub use webextension::*;
|
||||
pub use xml_docx::*;
|
||||
|
@ -412,11 +417,40 @@ impl Docx {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> XMLDocx {
|
||||
pub fn build(mut self) -> XMLDocx {
|
||||
self.reset();
|
||||
|
||||
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 web_extensions = self.web_extensions.iter().map(|ext| ext.build()).collect();
|
||||
let custom_items = self.custom_items.iter().map(|xml| xml.build()).collect();
|
||||
|
@ -827,3 +861,71 @@ impl Docx {
|
|||
(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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mod toc;
|
||||
|
||||
pub use toc::*;
|
|
@ -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))
|
||||
}
|
|
@ -41,6 +41,24 @@ impl Styles {
|
|||
self.doc_defaults = doc_defaults;
|
||||
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 {
|
||||
|
@ -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>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -198,7 +198,6 @@ pub fn read_xml(xml: &str) -> Result<Docx, ReaderError> {
|
|||
|
||||
// Read web settings
|
||||
let web_settings_path = rels.find_target_path(WEB_SETTINGS_TYPE);
|
||||
dbg!(&web_settings_path);
|
||||
if let Some(web_settings_path) = web_settings_path {
|
||||
let data = read_zip(
|
||||
&mut archive,
|
||||
|
|
|
@ -4,6 +4,8 @@ use thiserror::Error;
|
|||
pub enum TypeError {
|
||||
#[error("Failed to convert str to enum.")]
|
||||
FromStrError,
|
||||
#[error("Failed to convert str. This is because {0} is unsupported")]
|
||||
Unsupported(String),
|
||||
#[error("Unknown error.")]
|
||||
Unknown,
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ pub mod section_type;
|
|||
pub mod shd_type;
|
||||
pub mod special_indent_type;
|
||||
pub mod style_type;
|
||||
pub mod tab_leader_type;
|
||||
pub mod tab_value_type;
|
||||
pub mod table_alignment_type;
|
||||
pub mod table_layout_type;
|
||||
pub mod text_direction_type;
|
||||
|
@ -42,6 +44,8 @@ pub use section_type::*;
|
|||
pub use shd_type::*;
|
||||
pub use special_indent_type::*;
|
||||
pub use style_type::*;
|
||||
pub use tab_leader_type::*;
|
||||
pub use tab_value_type::*;
|
||||
pub use table_alignment_type::*;
|
||||
pub use table_layout_type::*;
|
||||
pub use text_direction_type::*;
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
use super::XMLBuilder;
|
||||
use super::XmlEvent;
|
||||
use crate::types::line_spacing_type::LineSpacingType;
|
||||
use crate::types::*;
|
||||
|
||||
const EXPECT_MESSAGE: &str = "should write buf";
|
||||
|
@ -117,6 +116,7 @@ impl XMLBuilder {
|
|||
open!(open_structured_tag, "w:sdt");
|
||||
open!(open_structured_tag_content, "w:sdtContent");
|
||||
open!(open_structured_tag_property, "w:sdtPr");
|
||||
closed_with_str!(alias, "w:alias");
|
||||
|
||||
// i.e. <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!(tab, "w:tab");
|
||||
closed!(tab_with_pos, "w:tab", "w:val", "w:pos");
|
||||
|
||||
closed!(br, "w:br", "w:type");
|
||||
|
@ -516,6 +515,43 @@ impl XMLBuilder {
|
|||
|
||||
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)]
|
||||
|
|
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
|
@ -1109,7 +1109,7 @@ export class Docx {
|
|||
build() {
|
||||
const docx = this.createDocx();
|
||||
const buf = docx.build(this.hasNumberings);
|
||||
docx.free();
|
||||
// docx.free();
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,7 +165,7 @@ impl Docx {
|
|||
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 mut cur = std::io::Cursor::new(buf);
|
||||
if has_numberings {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue