From 7f2dace81cefee310de387a1f1dd2ff3579afe1f Mon Sep 17 00:00:00 2001 From: bokuweb Date: Tue, 13 Dec 2022 13:33:48 +0900 Subject: [PATCH] fix: add before/after contents and with_instr_text in toc (#582) * fix * refactor js * fix: add before/after contents in toc * fix * fix --- CHANGELOG.md | 5 + docx-core/src/documents/elements/instr_toc.rs | 18 + .../documents/elements/structured_data_tag.rs | 5 + .../documents/elements/table_of_contents.rs | 134 +++++- docx-core/src/errors/mod.rs | 2 + docx-wasm/js/comment.ts | 23 + docx-wasm/js/delete.ts | 14 + docx-wasm/js/hyperlink.ts | 28 ++ docx-wasm/js/index.ts | 423 +----------------- docx-wasm/js/insert.ts | 14 + docx-wasm/js/paragraph.ts | 69 +++ docx-wasm/js/run.ts | 43 ++ docx-wasm/js/table-cell.ts | 163 ++++++- docx-wasm/js/table-of-contents.ts | 49 +- docx-wasm/js/table.ts | 51 +++ docx-wasm/package.json | 2 +- docx-wasm/src/table_of_contents.rs | 25 ++ .../test/__snapshots__/index.test.js.snap | 23 +- docx-wasm/test/index.test.js | 39 ++ 19 files changed, 703 insertions(+), 427 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4bc16..e101ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## docx-wasm@0.0.276-rc39 (13. Dec, 2022) + +- Support before/after contents in ToC. +- Support Toc from instrText. + ## docx-wasm@0.0.276-rc38 (7. Dec, 2022) - fix #584 Remove `%` from width. diff --git a/docx-core/src/documents/elements/instr_toc.rs b/docx-core/src/documents/elements/instr_toc.rs index 78f18fc..63fbe75 100644 --- a/docx-core/src/documents/elements/instr_toc.rs +++ b/docx-core/src/documents/elements/instr_toc.rs @@ -71,6 +71,10 @@ impl InstrToC { Self::default() } + pub fn with_instr_text(s: &str) -> Self { + Self::from_str(s).expect("should convert to InstrToC") + } + pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self { self.heading_styles_range = Some((start, end)); self @@ -429,4 +433,18 @@ mod tests { .add_style_with_level(StyleWithLevel::new("MySpectacularStyle2", 4)) ); } + + #[test] + fn with_instr_text() { + let s = r#"TOC \o "1-3" \h \z \u"#; + let i = InstrToC::with_instr_text(s); + assert_eq!( + i, + InstrToC::new() + .heading_styles_range(1, 3) + .use_applied_paragraph_line_level() + .hide_tab_and_page_numbers_in_webview() + .hyperlink() + ); + } } diff --git a/docx-core/src/documents/elements/structured_data_tag.rs b/docx-core/src/documents/elements/structured_data_tag.rs index 4381215..dc8e5bb 100644 --- a/docx-core/src/documents/elements/structured_data_tag.rs +++ b/docx-core/src/documents/elements/structured_data_tag.rs @@ -142,6 +142,11 @@ impl StructuredDataTag { self.property = self.property.data_binding(d); self } + + pub fn alias(mut self, v: impl Into) -> Self { + self.property = self.property.alias(v); + self + } } impl BuildXML for StructuredDataTag { diff --git a/docx-core/src/documents/elements/table_of_contents.rs b/docx-core/src/documents/elements/table_of_contents.rs index 2bba4b8..ac4b4d8 100644 --- a/docx-core/src/documents/elements/table_of_contents.rs +++ b/docx-core/src/documents/elements/table_of_contents.rs @@ -1,9 +1,38 @@ +use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; use crate::documents::*; use crate::types::*; use crate::xml_builder::*; +#[derive(Debug, Clone, PartialEq)] +pub enum TocContent { + Paragraph(Box), + Table(Box), +} + +impl Serialize for TocContent { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + TocContent::Paragraph(ref p) => { + let mut t = serializer.serialize_struct("Paragraph", 2)?; + t.serialize_field("type", "paragraph")?; + t.serialize_field("data", p)?; + t.end() + } + TocContent::Table(ref c) => { + let mut t = serializer.serialize_struct("Table", 2)?; + t.serialize_field("type", "table")?; + t.serialize_field("data", c)?; + t.end() + } + } + } +} + // 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)] @@ -14,6 +43,12 @@ pub struct TableOfContents { pub dirty: bool, pub alias: Option, pub page_ref_placeholder: Option, + // it is inserted in before toc. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub before_contents: Vec, + // it is inserted in after toc. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub after_contents: Vec, } impl TableOfContents { @@ -21,6 +56,14 @@ impl TableOfContents { Self::default() } + pub fn with_instr_text(s: &str) -> Self { + let instr = InstrToC::with_instr_text(s); + Self { + instr, + ..Self::default() + } + } + pub fn heading_styles_range(mut self, start: usize, end: usize) -> Self { self.instr = self.instr.heading_styles_range(start, end); self @@ -60,6 +103,27 @@ impl TableOfContents { self.dirty = true; self } + + pub fn add_before_paragraph(mut self, p: Paragraph) -> Self { + self.before_contents + .push(TocContent::Paragraph(Box::new(p))); + self + } + + pub fn add_after_paragraph(mut self, p: Paragraph) -> Self { + self.after_contents.push(TocContent::Paragraph(Box::new(p))); + self + } + + pub fn add_before_table(mut self, t: Table) -> Self { + self.before_contents.push(TocContent::Table(Box::new(t))); + self + } + + pub fn add_after_table(mut self, t: Table) -> Self { + self.after_contents.push(TocContent::Table(Box::new(t))); + self + } } impl BuildXML for TableOfContents { @@ -77,15 +141,36 @@ impl BuildXML for TableOfContents { ); let p2 = Paragraph::new().add_run(Run::new().add_field_char(FieldCharType::End, false)); - XMLBuilder::new() + let mut b = XMLBuilder::new() .open_structured_tag() .add_child(&p) - .open_structured_tag_content() - .add_child(&p1) - .add_child(&p2) - .close() - .close() - .build() + .open_structured_tag_content(); + + for c in self.before_contents.iter() { + match c { + TocContent::Paragraph(p) => { + b = b.add_child(p); + } + TocContent::Table(t) => { + b = b.add_child(t); + } + } + } + + b = b.add_child(&p1).add_child(&p2); + + for c in self.after_contents.iter() { + match c { + TocContent::Paragraph(p) => { + b = b.add_child(p); + } + TocContent::Table(t) => { + b = b.add_child(t); + } + } + } + + b.close().close().build() } else { let items: Vec = self .items @@ -100,14 +185,37 @@ impl BuildXML for TableOfContents { item }) .collect(); - XMLBuilder::new() + + let mut b = XMLBuilder::new() .open_structured_tag() .add_child(&p) - .open_structured_tag_content() - .add_child(&items) - .close() - .close() - .build() + .open_structured_tag_content(); + + for c in self.before_contents.iter() { + match c { + TocContent::Paragraph(p) => { + b = b.add_child(p); + } + TocContent::Table(t) => { + b = b.add_child(t); + } + } + } + + b = b.add_child(&items); + + for c in self.after_contents.iter() { + match c { + TocContent::Paragraph(p) => { + b = b.add_child(p); + } + TocContent::Table(t) => { + b = b.add_child(t); + } + } + } + + b.close().close().build() } } } diff --git a/docx-core/src/errors/mod.rs b/docx-core/src/errors/mod.rs index 9c0e5ea..391da3c 100644 --- a/docx-core/src/errors/mod.rs +++ b/docx-core/src/errors/mod.rs @@ -2,6 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum DocxError { + #[error("FromStr error.{0}")] + ConvertError(String), #[error("Failed to write XML to buffer.")] EmitterError(#[from] xml::writer::Error), #[error("Failed to zip XML documents.")] diff --git a/docx-wasm/js/comment.ts b/docx-wasm/js/comment.ts index ffc2886..66c0da7 100644 --- a/docx-wasm/js/comment.ts +++ b/docx-wasm/js/comment.ts @@ -1,6 +1,8 @@ import { Paragraph } from "./paragraph"; import { Table } from "./table"; +import * as wasm from "./pkg"; + export class Comment { id: number; _author: string; @@ -31,4 +33,25 @@ export class Comment { this._parentCommentId = id; return this; } + + build() { + let comment = wasm.createComment(this.id); + this.children.forEach((child) => { + if (child instanceof Paragraph) { + comment = comment.add_paragraph(child.build()); + } else if (child instanceof Table) { + // TODO: Support later + } + }); + if (this._author) { + comment = comment.author(this._author); + } + if (this._date) { + comment = comment.date(this._date); + } + if (this._parentCommentId) { + comment = comment.parent_comment_id(this._parentCommentId); + } + return comment; + } } diff --git a/docx-wasm/js/delete.ts b/docx-wasm/js/delete.ts index 14392ff..237911e 100644 --- a/docx-wasm/js/delete.ts +++ b/docx-wasm/js/delete.ts @@ -1,5 +1,7 @@ import { Run } from "./run"; +import * as wasm from "./pkg"; + export class Delete { run: Run; @@ -19,4 +21,16 @@ export class Delete { this._date = date; return this; } + + build() { + const run = this.run.build(); + let del = wasm.createDelete(run); + if (this._author) { + del = del.author(this._author); + } + if (this._date) { + del = del.date(this._date); + } + return del; + } } diff --git a/docx-wasm/js/hyperlink.ts b/docx-wasm/js/hyperlink.ts index 94a8604..a920a74 100644 --- a/docx-wasm/js/hyperlink.ts +++ b/docx-wasm/js/hyperlink.ts @@ -55,6 +55,34 @@ export class Hyperlink { this.children.push(end); return this; } + + build() { + let hyperlink = wasm.createHyperlink(this.v, convertHyperlinkType(this)); + + this.children.forEach((child) => { + if (child instanceof Run) { + const run = child.build(); + hyperlink = hyperlink.add_run(run); + } else if (child instanceof Insert) { + const insert = child.build(); + hyperlink = hyperlink.add_insert(insert); + } else if (child instanceof Delete) { + const del = child.build(); + hyperlink = hyperlink.add_delete(del); + } else if (child instanceof BookmarkStart) { + hyperlink = hyperlink.add_bookmark_start(child.id, child.name); + } else if (child instanceof BookmarkEnd) { + hyperlink = hyperlink.add_bookmark_end(child.id); + } else if (child instanceof Comment) { + const comment = child.build(); + hyperlink = hyperlink.add_comment_start(comment); + } else if (child instanceof CommentEnd) { + hyperlink = hyperlink.add_comment_end(child.id); + } + }); + + return hyperlink; + } } export const convertHyperlinkType = (link: Hyperlink): wasm.HyperlinkType => { diff --git a/docx-wasm/js/index.ts b/docx-wasm/js/index.ts index 9fa9a67..43bfe14 100644 --- a/docx-wasm/js/index.ts +++ b/docx-wasm/js/index.ts @@ -1,18 +1,8 @@ import { Paragraph } from "./paragraph"; -import { ParagraphProperty, setParagraphProperty } from "./paragraph-property"; -import { Insert } from "./insert"; -import { Delete } from "./delete"; -import { convertHyperlinkType, Hyperlink } from "./hyperlink"; -import { DeleteText } from "./delete-text"; -import { setTableProperty, Table } from "./table"; +import { ParagraphProperty } from "./paragraph-property"; +import { Table } from "./table"; import { TableOfContents } from "./table-of-contents"; -import { TableCell, toTextDirectionWasmType } from "./table-cell"; -import { convertBorderType, Run, RunFonts, setRunProperty } from "./run"; -import { Text } from "./text"; -import { Tab } from "./tab"; -import { Break } from "./break"; -import { Comment } from "./comment"; -import { CommentEnd } from "./comment-end"; +import { RunFonts } from "./run"; import { AbstractNumbering } from "./abstract-numbering"; import { Numbering } from "./numbering"; import { BookmarkStart } from "./bookmark-start"; @@ -24,7 +14,6 @@ import { Styles } from "./styles"; import { WebExtension } from "./webextension"; import { Footer } from "./footer"; import { Header } from "./header"; -import { Image } from "./image"; import { SectionProperty, @@ -44,6 +33,7 @@ export class Docx { | BookmarkEnd | TableOfContents )[] = []; + hasNumberings = false; abstractNumberings: AbstractNumbering[] = []; numberings: Numbering[] = []; @@ -233,122 +223,6 @@ export class Docx { return f; }; - buildRun(r: Run) { - let run = wasm.createRun(); - r.children.forEach((child) => { - if (child instanceof Text) { - run = run.add_text(child.text); - } else if (child instanceof DeleteText) { - run = run.add_delete_text(child.text); - } else if (child instanceof Tab) { - run = run.add_tab(); - } else if (child instanceof Break) { - if (child.type === "column") { - run = run.add_break(wasm.BreakType.Column); - } else if (child.type === "page") { - run = run.add_break(wasm.BreakType.Page); - } else if (child.type === "textWrapping") { - run = run.add_break(wasm.BreakType.TextWrapping); - } - } else if (child instanceof Image) { - let pic = wasm.createPic(child.data); - if (child.w != null && child.h != null) { - pic = pic.size(child.w, child.h); - } - if (child._floating) { - pic = pic.floating(); - } - if (child._offsetX != null) { - pic = pic.offset_x(child._offsetX); - } - if (child._offsetY != null) { - pic = pic.offset_x(child._offsetY); - } - if (child.rot != null) { - pic = pic.rotate(child.rot); - } - run = run.add_image(pic); - } - }); - - run = setRunProperty(run, r.property) as wasm.Run; - - return run; - } - - buildHyperlink(link: Hyperlink) { - let hyperlink = wasm.createHyperlink(link.v, convertHyperlinkType(link)); - - link.children.forEach((child) => { - if (child instanceof Run) { - const run = this.buildRun(child); - hyperlink = hyperlink.add_run(run); - } else if (child instanceof Insert) { - const insert = this.buildInsert(child); - hyperlink = hyperlink.add_insert(insert); - } else if (child instanceof Delete) { - const del = this.buildDelete(child); - hyperlink = hyperlink.add_delete(del); - } else if (child instanceof BookmarkStart) { - hyperlink = hyperlink.add_bookmark_start(child.id, child.name); - } else if (child instanceof BookmarkEnd) { - hyperlink = hyperlink.add_bookmark_end(child.id); - } else if (child instanceof Comment) { - const comment = this.buildComment(child); - hyperlink = hyperlink.add_comment_start(comment); - } else if (child instanceof CommentEnd) { - hyperlink = hyperlink.add_comment_end(child.id); - } - }); - - return hyperlink; - } - - buildInsert(i: Insert) { - const run = this.buildRun(i.run); - let insert = wasm.createInsert(run); - if (i._author) { - insert = insert.author(i._author); - } - if (i._date) { - insert = insert.date(i._date); - } - return insert; - } - - buildDelete(d: Delete) { - const run = this.buildRun(d.run); - let del = wasm.createDelete(run); - if (d._author) { - del = del.author(d._author); - } - if (d._date) { - del = del.date(d._date); - } - return del; - } - - buildComment(c: Comment) { - let comment = wasm.createComment(c.id); - c.children.forEach((child) => { - if (child instanceof Paragraph) { - comment = comment.add_paragraph(this.buildParagraph(child)); - } else if (child instanceof Table) { - // TODO: - } - }); - if (c._author) { - comment = comment.author(c._author); - } - if (c._date) { - comment = comment.date(c._date); - } - if (c._parentCommentId) { - comment = comment.parent_comment_id(c._parentCommentId); - } - return comment; - } - buildLineSpacing(p: ParagraphProperty): wasm.LineSpacing | null { const { lineSpacing } = p; if (lineSpacing == null) return null; @@ -394,267 +268,6 @@ export class Docx { return spacing; } - buildParagraph(p: Paragraph) { - let paragraph = wasm.createParagraph(); - p.children.forEach((child) => { - if (child instanceof Run) { - const run = this.buildRun(child); - paragraph = paragraph.add_run(run); - } else if (child instanceof Insert) { - const insert = this.buildInsert(child); - paragraph = paragraph.add_insert(insert); - } else if (child instanceof Delete) { - const del = this.buildDelete(child); - paragraph = paragraph.add_delete(del); - } else if (child instanceof Hyperlink) { - const hyperlink = this.buildHyperlink(child); - paragraph = paragraph.add_hyperlink(hyperlink); - } else if (child instanceof BookmarkStart) { - paragraph = paragraph.add_bookmark_start(child.id, child.name); - } else if (child instanceof BookmarkEnd) { - paragraph = paragraph.add_bookmark_end(child.id); - } else if (child instanceof Comment) { - const comment = this.buildComment(child); - paragraph = paragraph.add_comment_start(comment); - } else if (child instanceof CommentEnd) { - paragraph = paragraph.add_comment_end(child.id); - } - }); - - paragraph = setParagraphProperty(paragraph, p.property); - - if (typeof p.property.styleId !== "undefined") { - paragraph = paragraph.style(p.property.styleId); - } - - if (p.property.runProperty.del) { - paragraph = paragraph.delete( - p.property.runProperty.del.author, - p.property.runProperty.del.date - ); - } - - if (p.property.runProperty.ins) { - paragraph = paragraph.insert( - p.property.runProperty.ins.author, - p.property.runProperty.ins.date - ); - } - - if (p.property.paragraphPropertyChange) { - let change = wasm.createParagraphPropertyChange(); - change = change - .author(p.property.paragraphPropertyChange._author) - .date(p.property.paragraphPropertyChange._date); - - if (p.property.paragraphPropertyChange._property.numbering) { - change = change.numbering( - p.property.paragraphPropertyChange._property.numbering.id, - p.property.paragraphPropertyChange._property.numbering.level - ); - } - // TODO: add style, indent, alignment - paragraph = paragraph.paragraph_property_change(change); - } - - return paragraph; - } - - buildTable(t: Table) { - let table = wasm.createTable(); - t.rows.forEach((r) => { - let row = wasm.createTableRow(); - r.cells.forEach((c) => { - const cell = this.buildCell(c); - row = row.add_cell(cell); - }); - - if (r.height) { - row = row.row_height(r.height); - } - - if (r.del) { - row = row.delete(r.del.author, r.del.date); - } - - if (r.ins) { - row = row.insert(r.ins.author, r.ins.date); - } - - if (r.hRule) { - switch (r.hRule) { - case "auto": { - row = row.height_rule(wasm.HeightRule.Auto); - break; - } - case "atLeast": { - row = row.height_rule(wasm.HeightRule.AtLeast); - break; - } - case "exact": { - row = row.height_rule(wasm.HeightRule.Exact); - break; - } - } - } - table = table.add_row(row); - }); - - table = table.set_grid(new Uint32Array(t.grid)); - - if (t.property.styleId) { - table = table.style(t.property.styleId); - } - - table = setTableProperty(table, t.property); - - return table; - } - - buildCell(c: TableCell) { - let cell = wasm.createTableCell(); - c.children.forEach((c) => { - if (c instanceof Paragraph) { - const paragraph = this.buildParagraph(c); - cell = cell.add_paragraph(paragraph); - } else if (c instanceof Table) { - const table = this.buildTable(c); - cell = cell.add_table(table); - } - }); - - if (c.property.verticalMerge === "continue") { - cell = cell.vertical_merge(wasm.VMergeType.Continue); - } else if (c.property.verticalMerge === "restart") { - cell = cell.vertical_merge(wasm.VMergeType.Restart); - } - - switch (c.property.verticalAlign) { - case "top": { - cell = cell.vertical_align(wasm.VAlignType.Top); - break; - } - case "center": { - cell = cell.vertical_align(wasm.VAlignType.Center); - break; - } - case "bottom": { - cell = cell.vertical_align(wasm.VAlignType.Bottom); - break; - } - } - - if (typeof c.property.gridSpan !== "undefined") { - cell = cell.grid_span(c.property.gridSpan); - } - - if (typeof c.property.width !== "undefined") { - cell = cell.width(c.property.width); - } - - if (typeof c.property.textDirection !== "undefined") { - cell = cell.text_direction( - toTextDirectionWasmType(c.property.textDirection) - ); - } - - if (typeof c.property.borders !== "undefined") { - cell = this.buildCellBorders(c, cell); - } - - if (typeof c.property.shading !== "undefined") { - cell = cell.shading( - c.property.shading._type, - c.property.shading._color, - c.property.shading._fill - ); - } - - return cell; - } - - buildCellBorders(js: TableCell, cell: wasm.TableCell): wasm.TableCell { - if (js.property.borders.top) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Top) - .size(js.property.borders.top._size) - .color(js.property.borders.top._color) - .border_type(convertBorderType(js.property.borders.top._border_type)); - cell = cell.set_border(border); - } - - if (js.property.borders.right) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Right) - .size(js.property.borders.right._size) - .color(js.property.borders.right._color) - .border_type(convertBorderType(js.property.borders.right._border_type)); - cell = cell.set_border(border); - } - - if (js.property.borders.bottom) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Bottom) - .size(js.property.borders.bottom._size) - .color(js.property.borders.bottom._color) - .border_type( - convertBorderType(js.property.borders.bottom._border_type) - ); - cell = cell.set_border(border); - } - - if (js.property.borders.left) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Left) - .size(js.property.borders.left._size) - .color(js.property.borders.left._color) - .border_type(convertBorderType(js.property.borders.left._border_type)); - cell = cell.set_border(border); - } - - if (js.property.borders.insideH) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.InsideH) - .size(js.property.borders.insideH._size) - .color(js.property.borders.insideH._color) - .border_type( - convertBorderType(js.property.borders.insideH._border_type) - ); - cell = cell.set_border(border); - } - - if (js.property.borders.insideV) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.InsideV) - .size(js.property.borders.insideV._size) - .color(js.property.borders.insideV._color) - .border_type( - convertBorderType(js.property.borders.insideV._border_type) - ); - cell = cell.set_border(border); - } - - if (js.property.borders.tl2br) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Tl2br) - .size(js.property.borders.tl2br._size) - .color(js.property.borders.tl2br._color) - .border_type(convertBorderType(js.property.borders.tl2br._border_type)); - cell = cell.set_border(border); - } - - if (js.property.borders.tr2bl) { - const border = wasm - .createTableCellBorder(wasm.TableCellBorderPosition.Tr2bl) - .size(js.property.borders.tr2bl._size) - .color(js.property.borders.tr2bl._color) - .border_type(convertBorderType(js.property.borders.tr2bl._border_type)); - cell = cell.set_border(border); - } - - return cell; - } - buildLevel(l: Level) { let level = wasm.createLevel(l.id, l.start, l.format, l.text, l.jc); @@ -716,10 +329,10 @@ export class Docx { this.children.forEach((child) => { if (child instanceof Paragraph) { - let p = this.buildParagraph(child); + let p = child.build(); docx = docx.add_paragraph(p); } else if (child instanceof Table) { - let t = this.buildTable(child); + let t = child.build(); docx = docx.add_table(t); } else if (child instanceof BookmarkStart) { docx = docx.add_bookmark_start(child.id, child.name); @@ -779,9 +392,9 @@ export class Docx { let header = wasm.createHeader(); this.sectionProperty._header.children.forEach((c) => { if (c instanceof Paragraph) { - header = header.add_paragraph(this.buildParagraph(c)); + header = header.add_paragraph(c.build()); } else { - header = header.add_table(this.buildTable(c)); + header = header.add_table(c.build()); } }); docx = docx.header(header); @@ -791,9 +404,9 @@ export class Docx { let header = wasm.createHeader(); this.sectionProperty._firstHeader.children.forEach((c) => { if (c instanceof Paragraph) { - header = header.add_paragraph(this.buildParagraph(c)); + header = header.add_paragraph(c.build()); } else { - header = header.add_table(this.buildTable(c)); + header = header.add_table(c.build()); } }); docx = docx.first_header(header); @@ -803,9 +416,9 @@ export class Docx { let header = wasm.createHeader(); this.sectionProperty._evenHeader.children.forEach((c) => { if (c instanceof Paragraph) { - header = header.add_paragraph(this.buildParagraph(c)); + header = header.add_paragraph(c.build()); } else { - header = header.add_table(this.buildTable(c)); + header = header.add_table(c.build()); } }); docx = docx.even_header(header); @@ -815,9 +428,9 @@ export class Docx { let footer = wasm.createFooter(); this.sectionProperty._footer.children.forEach((c) => { if (c instanceof Paragraph) { - footer = footer.add_paragraph(this.buildParagraph(c)); + footer = footer.add_paragraph(c.build()); } else { - footer = footer.add_table(this.buildTable(c)); + footer = footer.add_table(c.build()); } }); docx = docx.footer(footer); @@ -827,9 +440,9 @@ export class Docx { let footer = wasm.createFooter(); this.sectionProperty._firstFooter.children.forEach((c) => { if (c instanceof Paragraph) { - footer = footer.add_paragraph(this.buildParagraph(c)); + footer = footer.add_paragraph(c.build()); } else { - footer = footer.add_table(this.buildTable(c)); + footer = footer.add_table(c.build()); } }); docx = docx.first_footer(footer); @@ -839,9 +452,9 @@ export class Docx { let footer = wasm.createFooter(); this.sectionProperty._evenFooter.children.forEach((c) => { if (c instanceof Paragraph) { - footer = footer.add_paragraph(this.buildParagraph(c)); + footer = footer.add_paragraph(c.build()); } else { - footer = footer.add_table(this.buildTable(c)); + footer = footer.add_table(c.build()); } }); docx = docx.even_footer(footer); diff --git a/docx-wasm/js/insert.ts b/docx-wasm/js/insert.ts index 546c9d8..effdbc6 100644 --- a/docx-wasm/js/insert.ts +++ b/docx-wasm/js/insert.ts @@ -1,5 +1,7 @@ import { Run } from "./run"; +import * as wasm from "./pkg"; + export class Insert { run: Run; _author: string | null = null; @@ -17,4 +19,16 @@ export class Insert { this._date = date; return this; } + + build() { + const run = this.run.build(); + let insert = wasm.createInsert(run); + if (this._author) { + insert = insert.author(this._author); + } + if (this._date) { + insert = insert.date(this._date); + } + return insert; + } } diff --git a/docx-wasm/js/paragraph.ts b/docx-wasm/js/paragraph.ts index ae22696..d75f3b0 100644 --- a/docx-wasm/js/paragraph.ts +++ b/docx-wasm/js/paragraph.ts @@ -6,6 +6,7 @@ import { AlignmentType, SpecialIndentKind, ParagraphPropertyChange, + setParagraphProperty, } from "./paragraph-property"; import { Insert } from "./insert"; import { Delete } from "./delete"; @@ -15,6 +16,8 @@ import { Comment } from "./comment"; import { CommentEnd } from "./comment-end"; import { Hyperlink } from "./hyperlink"; +import * as wasm from "./pkg"; + export type ParagraphChild = | Run | Insert @@ -155,4 +158,70 @@ export class Paragraph { this.property.paragraphPropertyChange = propertyChange; return this; } + + build() { + let paragraph = wasm.createParagraph(); + this.children.forEach((child) => { + if (child instanceof Run) { + const run = child.build(); + paragraph = paragraph.add_run(run); + } else if (child instanceof Insert) { + const insert = child.build(); + paragraph = paragraph.add_insert(insert); + } else if (child instanceof Delete) { + const del = child.build(); + paragraph = paragraph.add_delete(del); + } else if (child instanceof Hyperlink) { + const hyperlink = child.build(); + paragraph = paragraph.add_hyperlink(hyperlink); + } else if (child instanceof BookmarkStart) { + paragraph = paragraph.add_bookmark_start(child.id, child.name); + } else if (child instanceof BookmarkEnd) { + paragraph = paragraph.add_bookmark_end(child.id); + } else if (child instanceof Comment) { + const comment = child.build(); + paragraph = paragraph.add_comment_start(comment); + } else if (child instanceof CommentEnd) { + paragraph = paragraph.add_comment_end(child.id); + } + }); + + paragraph = setParagraphProperty(paragraph, this.property); + + if (typeof this.property.styleId !== "undefined") { + paragraph = paragraph.style(this.property.styleId); + } + + if (this.property.runProperty.del) { + paragraph = paragraph.delete( + this.property.runProperty.del.author, + this.property.runProperty.del.date + ); + } + + if (this.property.runProperty.ins) { + paragraph = paragraph.insert( + this.property.runProperty.ins.author, + this.property.runProperty.ins.date + ); + } + + if (this.property.paragraphPropertyChange) { + let change = wasm.createParagraphPropertyChange(); + change = change + .author(this.property.paragraphPropertyChange._author) + .date(this.property.paragraphPropertyChange._date); + + if (this.property.paragraphPropertyChange._property.numbering) { + change = change.numbering( + this.property.paragraphPropertyChange._property.numbering.id, + this.property.paragraphPropertyChange._property.numbering.level + ); + } + // TODO: add style, indent, alignment + paragraph = paragraph.paragraph_property_change(change); + } + + return paragraph; + } } diff --git a/docx-wasm/js/run.ts b/docx-wasm/js/run.ts index 88df856..8be6160 100644 --- a/docx-wasm/js/run.ts +++ b/docx-wasm/js/run.ts @@ -280,6 +280,49 @@ export class Run { }; return this; } + + build() { + let run = wasm.createRun(); + this.children.forEach((child) => { + if (child instanceof Text) { + run = run.add_text(child.text); + } else if (child instanceof DeleteText) { + run = run.add_delete_text(child.text); + } else if (child instanceof Tab) { + run = run.add_tab(); + } else if (child instanceof Break) { + if (child.type === "column") { + run = run.add_break(wasm.BreakType.Column); + } else if (child.type === "page") { + run = run.add_break(wasm.BreakType.Page); + } else if (child.type === "textWrapping") { + run = run.add_break(wasm.BreakType.TextWrapping); + } + } else if (child instanceof Image) { + let pic = wasm.createPic(child.data); + if (child.w != null && child.h != null) { + pic = pic.size(child.w, child.h); + } + if (child._floating) { + pic = pic.floating(); + } + if (child._offsetX != null) { + pic = pic.offset_x(child._offsetX); + } + if (child._offsetY != null) { + pic = pic.offset_x(child._offsetY); + } + if (child.rot != null) { + pic = pic.rotate(child.rot); + } + run = run.add_image(pic); + } + }); + + run = setRunProperty(run, this.property) as wasm.Run; + + return run; + } } export const setRunProperty = ( diff --git a/docx-wasm/js/table-cell.ts b/docx-wasm/js/table-cell.ts index 467b782..0712a62 100644 --- a/docx-wasm/js/table-cell.ts +++ b/docx-wasm/js/table-cell.ts @@ -4,6 +4,7 @@ import { Shading } from "./shading"; import { TableCellBorders, PositionKeys } from "./table-cell-borders"; import { TableCellBorderPosition, TableCellBorder } from "./table-cell-border"; import * as wasm from "./pkg"; +import { convertBorderType } from "./run"; export type VMergeType = "restart" | "continue"; @@ -118,16 +119,166 @@ export class TableCell { } setBorder(border: TableCellBorder) { - this.property.borders[ - border.position.toLowerCase() as PositionKeys - ] = border; + this.property.borders[border.position.toLowerCase() as PositionKeys] = + border; return this; } clearBorder(position: TableCellBorderPosition) { - this.property.borders[ - position.toLowerCase() as PositionKeys - ] = new TableCellBorder(position).border_type("nil"); + this.property.borders[position.toLowerCase() as PositionKeys] = + new TableCellBorder(position).border_type("nil"); return this; } + + buildCellBorders(cell: wasm.TableCell): wasm.TableCell { + if (this.property.borders.top) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Top) + .size(this.property.borders.top._size) + .color(this.property.borders.top._color) + .border_type(convertBorderType(this.property.borders.top._border_type)); + cell = cell.set_border(border); + } + + if (this.property.borders.right) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Right) + .size(this.property.borders.right._size) + .color(this.property.borders.right._color) + .border_type( + convertBorderType(this.property.borders.right._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.bottom) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Bottom) + .size(this.property.borders.bottom._size) + .color(this.property.borders.bottom._color) + .border_type( + convertBorderType(this.property.borders.bottom._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.left) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Left) + .size(this.property.borders.left._size) + .color(this.property.borders.left._color) + .border_type( + convertBorderType(this.property.borders.left._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.insideH) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.InsideH) + .size(this.property.borders.insideH._size) + .color(this.property.borders.insideH._color) + .border_type( + convertBorderType(this.property.borders.insideH._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.insideV) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.InsideV) + .size(this.property.borders.insideV._size) + .color(this.property.borders.insideV._color) + .border_type( + convertBorderType(this.property.borders.insideV._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.tl2br) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Tl2br) + .size(this.property.borders.tl2br._size) + .color(this.property.borders.tl2br._color) + .border_type( + convertBorderType(this.property.borders.tl2br._border_type) + ); + cell = cell.set_border(border); + } + + if (this.property.borders.tr2bl) { + const border = wasm + .createTableCellBorder(wasm.TableCellBorderPosition.Tr2bl) + .size(this.property.borders.tr2bl._size) + .color(this.property.borders.tr2bl._color) + .border_type( + convertBorderType(this.property.borders.tr2bl._border_type) + ); + cell = cell.set_border(border); + } + + return cell; + } + + build() { + let cell = wasm.createTableCell(); + this.children.forEach((c) => { + if (c instanceof Paragraph) { + const paragraph = c.build(); + cell = cell.add_paragraph(paragraph); + } else if (c instanceof Table) { + const table = c.build(); + cell = cell.add_table(table); + } + }); + + if (this.property.verticalMerge === "continue") { + cell = cell.vertical_merge(wasm.VMergeType.Continue); + } else if (this.property.verticalMerge === "restart") { + cell = cell.vertical_merge(wasm.VMergeType.Restart); + } + + switch (this.property.verticalAlign) { + case "top": { + cell = cell.vertical_align(wasm.VAlignType.Top); + break; + } + case "center": { + cell = cell.vertical_align(wasm.VAlignType.Center); + break; + } + case "bottom": { + cell = cell.vertical_align(wasm.VAlignType.Bottom); + break; + } + } + + if (typeof this.property.gridSpan !== "undefined") { + cell = cell.grid_span(this.property.gridSpan); + } + + if (typeof this.property.width !== "undefined") { + cell = cell.width(this.property.width); + } + + if (typeof this.property.textDirection !== "undefined") { + cell = cell.text_direction( + toTextDirectionWasmType(this.property.textDirection) + ); + } + + if (typeof this.property.borders !== "undefined") { + cell = this.buildCellBorders(cell); + } + + if (typeof this.property.shading !== "undefined") { + cell = cell.shading( + this.property.shading._type, + this.property.shading._color, + this.property.shading._fill + ); + } + + return cell; + } } diff --git a/docx-wasm/js/table-of-contents.ts b/docx-wasm/js/table-of-contents.ts index 4b5f23f..51ef4a9 100644 --- a/docx-wasm/js/table-of-contents.ts +++ b/docx-wasm/js/table-of-contents.ts @@ -1,8 +1,11 @@ +import { Paragraph } from "./paragraph"; import * as wasm from "./pkg"; +import { Table } from "./table"; import { TableOfContentsItem } from "./table-of-contents-item"; export class TableOfContents { + _instrText?: string; _headingStylesRange: [number, number] | null = null; _styleWithLevels: { styleId: string; level: number }[] = []; _hyperlink = false; @@ -11,6 +14,32 @@ export class TableOfContents { _dirty = false; _items: TableOfContentsItem[] = []; _pageRefPlaceholder = ""; + _beforeContents: (Paragraph | Table)[] = []; + _afterContents: (Paragraph | Table)[] = []; + + constructor(instrText?: string) { + this._instrText = instrText; + } + + addBeforeParagraph(p: Paragraph) { + this._beforeContents.push(p); + return this; + } + + addBeforeTable(t: Table) { + this._beforeContents.push(t); + return this; + } + + addAfterParagraph(p: Paragraph) { + this._afterContents.push(p); + return this; + } + + addAfterTable(t: Table) { + this._afterContents.push(t); + return this; + } headingStylesRange = (r: [number, number]) => { this._headingStylesRange = r; @@ -53,7 +82,9 @@ export class TableOfContents { }; buildWasmObject = () => { - let toc = wasm.createTableOfContents(); + let toc = this._instrText + ? wasm.createTableOfContentsWithInstrText(this._instrText) + : wasm.createTableOfContents(); if (this._headingStylesRange) { toc = toc.heading_styles_range( this._headingStylesRange[0], @@ -89,6 +120,22 @@ export class TableOfContents { toc = toc.add_item(item.buildWasmObject()); } + for (const c of this._beforeContents) { + if (c instanceof Paragraph) { + toc = toc.add_before_paragraph(c.build()); + } else if (c instanceof Table) { + toc = toc.add_before_table(c.build()); + } + } + + for (const c of this._afterContents) { + if (c instanceof Paragraph) { + toc = toc.add_after_paragraph(c.build()); + } else if (c instanceof Table) { + toc = toc.add_after_table(c.build()); + } + } + return toc; }; } diff --git a/docx-wasm/js/table.ts b/docx-wasm/js/table.ts index 09808cb..70df26f 100644 --- a/docx-wasm/js/table.ts +++ b/docx-wasm/js/table.ts @@ -104,6 +104,57 @@ export class Table { this.property.cellMargins.bottom = { val: v, type: t }; return this; } + + build() { + let table = wasm.createTable(); + this.rows.forEach((r) => { + let row = wasm.createTableRow(); + r.cells.forEach((c) => { + const cell = c.build(); + row = row.add_cell(cell); + }); + + if (r.height) { + row = row.row_height(r.height); + } + + if (r.del) { + row = row.delete(r.del.author, r.del.date); + } + + if (r.ins) { + row = row.insert(r.ins.author, r.ins.date); + } + + if (r.hRule) { + switch (r.hRule) { + case "auto": { + row = row.height_rule(wasm.HeightRule.Auto); + break; + } + case "atLeast": { + row = row.height_rule(wasm.HeightRule.AtLeast); + break; + } + case "exact": { + row = row.height_rule(wasm.HeightRule.Exact); + break; + } + } + } + table = table.add_row(row); + }); + + table = table.set_grid(new Uint32Array(this.grid)); + + if (this.property.styleId) { + table = table.style(this.property.styleId); + } + + table = setTableProperty(table, this.property); + + return table; + } } export const convertWidthType = (t: string) => { diff --git a/docx-wasm/package.json b/docx-wasm/package.json index 5ad239c..621c99c 100644 --- a/docx-wasm/package.json +++ b/docx-wasm/package.json @@ -1,6 +1,6 @@ { "name": "docx-wasm", - "version": "0.0.276-rc38", + "version": "0.0.276-rc39", "main": "dist/node/index.js", "browser": "dist/web/index.js", "author": "bokuweb ", diff --git a/docx-wasm/src/table_of_contents.rs b/docx-wasm/src/table_of_contents.rs index 882eec3..d19e3b8 100644 --- a/docx-wasm/src/table_of_contents.rs +++ b/docx-wasm/src/table_of_contents.rs @@ -11,6 +11,11 @@ pub fn create_table_of_contents() -> TableOfContents { TableOfContents(docx_rs::TableOfContents::new()) } +#[wasm_bindgen(js_name = createTableOfContentsWithInstrText)] +pub fn create_table_of_contents_with_instr_text(s: &str) -> TableOfContents { + TableOfContents(docx_rs::TableOfContents::with_instr_text(s)) +} + impl TableOfContents { pub fn take(self) -> docx_rs::TableOfContents { self.0 @@ -61,4 +66,24 @@ impl TableOfContents { self.0.dirty = true; self } + + pub fn add_before_paragraph(mut self, p: Paragraph) -> Self { + self.0 = self.0.add_before_paragraph(p.take()); + self + } + + pub fn add_after_paragraph(mut self, p: Paragraph) -> Self { + self.0 = self.0.add_after_paragraph(p.take()); + self + } + + pub fn add_before_table(mut self, t: Table) -> Self { + self.0 = self.0.add_before_table(t.take()); + self + } + + pub fn add_after_table(mut self, t: Table) -> Self { + self.0 = self.0.add_after_table(t.take()); + self + } } diff --git a/docx-wasm/test/__snapshots__/index.test.js.snap b/docx-wasm/test/__snapshots__/index.test.js.snap index 4aab990..31d86c6 100644 --- a/docx-wasm/test/__snapshots__/index.test.js.snap +++ b/docx-wasm/test/__snapshots__/index.test.js.snap @@ -151916,6 +151916,27 @@ Object { } `; +exports[`writer should write ToC with instrText 1`] = ` +" + + + + + +" +`; + +exports[`writer should write ToC with instrText 2`] = ` +" + + +Before contentsTOC \\\\uAfter contents + +Hello!! +World +" +`; + exports[`writer should write ToC with items 1`] = ` " @@ -152780,7 +152801,7 @@ exports[`writer should write paragraph delete 1`] = ` exports[`writer should write paragraph delete 2`] = ` " - Hello world!!Foo + Hello world!!Foo " `; diff --git a/docx-wasm/test/index.test.js b/docx-wasm/test/index.test.js index 0c27969..7556a28 100644 --- a/docx-wasm/test/index.test.js +++ b/docx-wasm/test/index.test.js @@ -756,6 +756,45 @@ describe("writer", () => { } }); + test("should write ToC with instrText", () => { + const before = new w.Paragraph().addRun( + new w.Run().addText("Before contents") + ); + const after = new w.Paragraph().addRun( + new w.Run().addText("After contents") + ); + const p1 = new w.Paragraph() + .addRun(new w.Run().addText("Hello!!")) + .pageBreakBefore(true) + .style("Heading1"); + const style1 = new w.Style("Heading1", "paragraph").name("Heading 1"); + const p2 = new w.Paragraph() + .addRun(new w.Run().addText("World")) + .pageBreakBefore(true) + .style("Heading2"); + const style2 = new w.Style("Heading2", "paragraph").name("Heading 2"); + const buffer = new w.Docx() + .addTableOfContents( + new w.TableOfContents(`TOC \o "1-3" \h \z \\u`) + .alias("Table of contents") + .addBeforeParagraph(before) + .addAfterParagraph(after) + .dirty() + ) + .addParagraph(p1) + .addParagraph(p2) + .addStyle(style1) + .addStyle(style2) + .build(); + writeFileSync("../output/js/toc_with_instr_text.docx", buffer); + const z = new Zip(Buffer.from(buffer)); + for (const e of z.getEntries()) { + if (e.entryName.match(/document.xml/)) { + expect(z.readAsText(e)).toMatchSnapshot(); + } + } + }); + test("should write paragraph delete", () => { const p1 = new w.Paragraph() .addRun(new w.Run().addText("Hello world!!"))