diff --git a/docx-core/examples/hyperlink.rs b/docx-core/examples/hyperlink.rs new file mode 100644 index 0000000..804074c --- /dev/null +++ b/docx-core/examples/hyperlink.rs @@ -0,0 +1,24 @@ +use docx_rs::*; + +pub fn main() -> Result<(), DocxError> { + let path = std::path::Path::new("./output/hyperlink.docx"); + let file = std::fs::File::create(&path).unwrap(); + Docx::new() + .add_paragraph( + Paragraph::new().add_hyperlink( + Hyperlink::new() + .anchor("anchor") + .add_run(Run::new().add_text("Hello")), + ), + ) + .add_bookmark_start(1, "anchor") + .add_paragraph( + Paragraph::new() + .add_run(Run::new().add_text("World")) + .page_break_before(true), + ) + .add_bookmark_end(1) + .build() + .pack(file)?; + Ok(()) +} diff --git a/docx-core/src/documents/elements/hyperlink.rs b/docx-core/src/documents/elements/hyperlink.rs new file mode 100644 index 0000000..82c2445 --- /dev/null +++ b/docx-core/src/documents/elements/hyperlink.rs @@ -0,0 +1,111 @@ +use serde::Serialize; + +use super::*; +use crate::documents::BuildXML; +use crate::xml_builder::*; + +#[derive(Serialize, Debug, Clone, PartialEq, Default)] +pub struct Hyperlink { + pub rid: Option, + pub anchor: Option, + pub history: bool, + pub children: Vec, +} + +impl Hyperlink { + pub fn new() -> Self { + Hyperlink::default() + } + + pub fn rid(mut self, rid: impl Into) -> Self { + self.rid = Some(rid.into()); + self + } + + pub fn anchor(mut self, anchor: impl Into) -> Self { + self.anchor = Some(anchor.into()); + self + } + + pub fn history(mut self) -> Self { + self.history = true; + self + } + + pub fn add_run(mut self, run: Run) -> Self { + self.children.push(ParagraphChild::Run(Box::new(run))); + self + } + + pub fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Self { + self.children + .push(ParagraphChild::StructuredDataTag(Box::new(t))); + self + } + + pub fn add_insert(mut self, insert: Insert) -> Self { + self.children.push(ParagraphChild::Insert(insert)); + self + } + + pub fn add_delete(mut self, delete: Delete) -> Self { + self.children.push(ParagraphChild::Delete(delete)); + self + } + + pub fn add_bookmark_start(mut self, id: usize, name: impl Into) -> Self { + self.children + .push(ParagraphChild::BookmarkStart(BookmarkStart::new(id, name))); + self + } + + pub fn add_bookmark_end(mut self, id: usize) -> Self { + self.children + .push(ParagraphChild::BookmarkEnd(BookmarkEnd::new(id))); + self + } + + pub fn add_comment_start(mut self, comment: Comment) -> Self { + self.children.push(ParagraphChild::CommentStart(Box::new( + CommentRangeStart::new(comment), + ))); + self + } + + pub fn add_comment_end(mut self, id: usize) -> Self { + self.children + .push(ParagraphChild::CommentEnd(CommentRangeEnd::new(id))); + self + } +} + +impl BuildXML for Hyperlink { + fn build(&self) -> Vec { + let b = XMLBuilder::new(); + b.open_hyperlink(self.rid.as_ref(), self.anchor.as_ref(), self.history) + .add_children(&self.children) + .close() + .build() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[cfg(test)] + use pretty_assertions::assert_eq; + use std::str; + + #[test] + fn test_hyperlink() { + let l = Hyperlink::new() + .anchor("ToC1") + .add_run(Run::new().add_text("hello")); + let b = l.build(); + assert_eq!( + str::from_utf8(&b).unwrap(), + r#"hello"# + ); + } +} diff --git a/docx-core/src/documents/elements/mod.rs b/docx-core/src/documents/elements/mod.rs index a885b7c..c3883c1 100644 --- a/docx-core/src/documents/elements/mod.rs +++ b/docx-core/src/documents/elements/mod.rs @@ -29,6 +29,7 @@ mod footer_reference; mod grid_span; mod header_reference; mod highlight; +mod hyperlink; mod indent; mod indent_level; mod insert; @@ -135,6 +136,7 @@ pub use footer_reference::*; pub use grid_span::*; pub use header_reference::*; pub use highlight::*; +pub use hyperlink::*; pub use indent::*; pub use indent_level::*; pub use insert::*; diff --git a/docx-core/src/documents/elements/paragraph.rs b/docx-core/src/documents/elements/paragraph.rs index 41137e3..222035e 100644 --- a/docx-core/src/documents/elements/paragraph.rs +++ b/docx-core/src/documents/elements/paragraph.rs @@ -32,6 +32,7 @@ pub enum ParagraphChild { Insert(Insert), Delete(Delete), BookmarkStart(BookmarkStart), + Hyperlink(Hyperlink), BookmarkEnd(BookmarkEnd), CommentStart(Box), CommentEnd(CommentRangeEnd), @@ -44,6 +45,7 @@ impl BuildXML for ParagraphChild { ParagraphChild::Run(v) => v.build(), ParagraphChild::Insert(v) => v.build(), ParagraphChild::Delete(v) => v.build(), + ParagraphChild::Hyperlink(v) => v.build(), ParagraphChild::BookmarkStart(v) => v.build(), ParagraphChild::BookmarkEnd(v) => v.build(), ParagraphChild::CommentStart(v) => v.build(), @@ -77,6 +79,12 @@ impl Serialize for ParagraphChild { t.serialize_field("data", r)?; t.end() } + ParagraphChild::Hyperlink(ref r) => { + let mut t = serializer.serialize_struct("hyperlink", 2)?; + t.serialize_field("type", "hyperlink")?; + t.serialize_field("data", r)?; + t.end() + } ParagraphChild::BookmarkStart(ref r) => { let mut t = serializer.serialize_struct("BookmarkStart", 2)?; t.serialize_field("type", "bookmarkStart")?; @@ -130,6 +138,11 @@ impl Paragraph { self } + pub fn add_hyperlink(mut self, link: Hyperlink) -> Self { + self.children.push(ParagraphChild::Hyperlink(link)); + self + } + pub fn add_structured_data_tag(mut self, t: StructuredDataTag) -> Self { self.children .push(ParagraphChild::StructuredDataTag(Box::new(t))); diff --git a/docx-core/src/documents/mod.rs b/docx-core/src/documents/mod.rs index 2bb4c9d..ddef04b 100644 --- a/docx-core/src/documents/mod.rs +++ b/docx-core/src/documents/mod.rs @@ -487,6 +487,46 @@ impl Docx { crate::reset_para_id(); } + fn insert_comment_to_map( + &self, + comment_map: &mut HashMap, + c: &CommentRangeStart, + ) { + let comment = c.get_comment(); + let comment_id = comment.id(); + for child in comment.children { + if let CommentChild::Paragraph(child) = child { + let para_id = child.id.clone(); + comment_map.insert(comment_id, para_id.clone()); + } + // TODO: Support table + } + } + + fn push_comment_and_comment_extended( + &self, + comments: &mut Vec, + comments_extended: &mut Vec, + comment_map: &HashMap, + c: &CommentRangeStart, + ) { + let comment = c.get_comment(); + for child in comment.children { + if let CommentChild::Paragraph(child) = child { + let para_id = child.id.clone(); + comments.push(c.get_comment()); + let comment_extended = CommentExtended::new(para_id); + if let Some(parent_comment_id) = comment.parent_comment_id { + let parent_para_id = comment_map.get(&parent_comment_id).unwrap().clone(); + comments_extended.push(comment_extended.parent_paragraph_id(parent_para_id)); + } else { + comments_extended.push(comment_extended); + } + } + // TODO: Support table + } + } + // Traverse and clone comments from document and add to comments node. fn update_comments(&mut self) { let mut comments: Vec = vec![]; @@ -498,14 +538,13 @@ impl Docx { DocumentChild::Paragraph(paragraph) => { for child in ¶graph.children { if let ParagraphChild::CommentStart(c) = child { - let comment = c.get_comment(); - let comment_id = comment.id(); - for child in comment.children { - if let CommentChild::Paragraph(child) = child { - let para_id = child.id.clone(); - comment_map.insert(comment_id, para_id.clone()); + self.insert_comment_to_map(&mut comment_map, c); + } + if let ParagraphChild::Hyperlink(h) = child { + for child in &h.children { + if let ParagraphChild::CommentStart(c) = child { + self.insert_comment_to_map(&mut comment_map, c); } - // TODO: Support table } } } @@ -518,15 +557,16 @@ impl Docx { TableCellContent::Paragraph(paragraph) => { for child in ¶graph.children { if let ParagraphChild::CommentStart(c) = child { - let comment = c.get_comment(); - let comment_id = comment.id(); - for child in comment.children { - if let CommentChild::Paragraph(child) = child { - let para_id = child.id.clone(); - comment_map - .insert(comment_id, para_id.clone()); + self.insert_comment_to_map(&mut comment_map, c); + } + if let ParagraphChild::Hyperlink(h) = child { + for child in &h.children { + if let ParagraphChild::CommentStart(c) = child { + self.insert_comment_to_map( + &mut comment_map, + c, + ); } - // TODO: Support table } } } @@ -548,23 +588,23 @@ impl Docx { DocumentChild::Paragraph(paragraph) => { for child in ¶graph.children { if let ParagraphChild::CommentStart(c) = child { - let comment = c.get_comment(); - for child in comment.children { - if let CommentChild::Paragraph(child) = child { - let para_id = child.id.clone(); - comments.push(c.get_comment()); - let comment_extended = CommentExtended::new(para_id); - if let Some(parent_comment_id) = comment.parent_comment_id { - let parent_para_id = - comment_map.get(&parent_comment_id).unwrap().clone(); - comments_extended.push( - comment_extended.parent_paragraph_id(parent_para_id), - ); - } else { - comments_extended.push(comment_extended); - } + self.push_comment_and_comment_extended( + &mut comments, + &mut comments_extended, + &comment_map, + c, + ); + } + if let ParagraphChild::Hyperlink(h) = child { + for child in &h.children { + if let ParagraphChild::CommentStart(c) = child { + self.push_comment_and_comment_extended( + &mut comments, + &mut comments_extended, + &comment_map, + c, + ); } - // TODO: Support table } } } @@ -577,30 +617,22 @@ impl Docx { TableCellContent::Paragraph(paragraph) => { for child in ¶graph.children { if let ParagraphChild::CommentStart(c) = child { - let comment = c.get_comment(); - for child in comment.children { - if let CommentChild::Paragraph(child) = child { - let para_id = child.id.clone(); - comments.push(c.get_comment()); - let comment_extended = - CommentExtended::new(para_id); - if let Some(parent_comment_id) = - comment.parent_comment_id - { - let parent_para_id = comment_map - .get(&parent_comment_id) - .unwrap() - .clone(); - comments_extended.push( - comment_extended - .parent_paragraph_id( - parent_para_id, - ), - ); - } else { - comments_extended - .push(comment_extended); - } + self.push_comment_and_comment_extended( + &mut comments, + &mut comments_extended, + &comment_map, + c, + ); + } + if let ParagraphChild::Hyperlink(h) = child { + for child in &h.children { + if let ParagraphChild::CommentStart(c) = child { + self.push_comment_and_comment_extended( + &mut comments, + &mut comments_extended, + &comment_map, + c, + ); } } } diff --git a/docx-core/src/xml_builder/elements.rs b/docx-core/src/xml_builder/elements.rs index 83c4a83..a061bc4 100644 --- a/docx-core/src/xml_builder/elements.rs +++ b/docx-core/src/xml_builder/elements.rs @@ -82,6 +82,26 @@ impl XMLBuilder { self.close() } + pub(crate) fn open_hyperlink( + mut self, + rid: Option<&String>, + anchor: Option<&String>, + history: bool, + ) -> Self { + let mut e = XmlEvent::start_element("w:hyperlink"); + if let Some(rid) = rid { + e = e.attr("w:rid", rid); + } + if let Some(anchor) = anchor { + e = e.attr("w:anchor", anchor); + } + if history { + e = e.attr("w:history", "true"); + } + self.writer.write(e).expect(EXPECT_MESSAGE); + self + } + // i.e. open!(open_run, "w:r"); open!(open_run_property, "w:rPr"); diff --git a/docx-wasm/js/hyperlink.ts b/docx-wasm/js/hyperlink.ts new file mode 100644 index 0000000..b3344da --- /dev/null +++ b/docx-wasm/js/hyperlink.ts @@ -0,0 +1,65 @@ +import { Run } from "./run"; +import { Insert } from "./insert"; +import { Delete } from "./delete"; +import { BookmarkStart } from "./bookmark-start"; +import { BookmarkEnd } from "./bookmark-end"; +import { Comment } from "./comment"; +import { CommentEnd } from "./comment-end"; +import { ParagraphChild } from "./paragraph"; + +export class Hyperlink { + _rid?: string; + _anchor?: string; + _history: boolean = false; + children: ParagraphChild[] = []; + + addRun(run: Run) { + this.children.push(run); + return this; + } + + addInsert(ins: Insert) { + this.children.push(ins); + return this; + } + + addDelete(del: Delete) { + this.children.push(del); + return this; + } + + addBookmarkStart(id: number, name: string) { + this.children.push(new BookmarkStart(id, name)); + return this; + } + + addBookmarkEnd(id: number) { + this.children.push(new BookmarkEnd(id)); + return this; + } + + addCommentStart(comment: Comment) { + this.children.push(comment); + return this; + } + + addCommentEnd(end: CommentEnd) { + this.children.push(end); + return this; + } + + rid(rid: string) { + this._rid = rid; + return this; + } + + anchor(anchor: string) { + this._anchor = anchor; + return this; + } + + history() { + this._history = true; + return this; + } +} diff --git a/docx-wasm/js/index.ts b/docx-wasm/js/index.ts index 7cd1b8a..e32cf58 100644 --- a/docx-wasm/js/index.ts +++ b/docx-wasm/js/index.ts @@ -1,6 +1,7 @@ import { Paragraph, ParagraphProperty } from "./paragraph"; import { Insert } from "./insert"; import { Delete } from "./delete"; +import { Hyperlink } from "./hyperlink"; import { DeleteText } from "./delete-text"; import { Table } from "./table"; import { TableCell, toTextDirectionWasmType } from "./table-cell"; @@ -320,6 +321,43 @@ export class Docx { return run; } + buildHyperlink(link: Hyperlink) { + let hyperlink = wasm.createHyperlink(); + if (link._history) { + hyperlink = hyperlink.history(); + } + if (link._anchor) { + hyperlink = hyperlink.anchor(link._anchor); + } + if (link._rid) { + hyperlink = hyperlink.rid(link._rid); + } + + 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); @@ -422,6 +460,9 @@ export class Docx { } 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) { @@ -1086,6 +1127,7 @@ export * from "./run"; export * from "./text"; export * from "./style"; export * from "./styles"; +export * from "./hyperlink"; export * from "./comment"; export * from "./comment-end"; export * from "./numbering"; diff --git a/docx-wasm/js/paragraph.ts b/docx-wasm/js/paragraph.ts index e14f4e0..63e4c92 100644 --- a/docx-wasm/js/paragraph.ts +++ b/docx-wasm/js/paragraph.ts @@ -5,11 +5,13 @@ import { BookmarkStart } from "./bookmark-start"; import { BookmarkEnd } from "./bookmark-end"; import { Comment } from "./comment"; import { CommentEnd } from "./comment-end"; +import { Hyperlink } from "./hyperlink"; export type ParagraphChild = | Run | Insert | Delete + | Hyperlink | BookmarkStart | BookmarkEnd | Comment @@ -102,6 +104,11 @@ export class Paragraph { return this; } + addHyperlink(link: Hyperlink) { + this.children.push(link); + return this; + } + addInsert(ins: Insert) { this.children.push(ins); return this; diff --git a/docx-wasm/src/hyperlink.rs b/docx-wasm/src/hyperlink.rs new file mode 100644 index 0000000..b2b5cd4 --- /dev/null +++ b/docx-wasm/src/hyperlink.rs @@ -0,0 +1,85 @@ +use wasm_bindgen::prelude::*; + +use super::*; + +#[wasm_bindgen] +#[derive(Debug)] +pub struct Hyperlink(docx_rs::Hyperlink); + +#[wasm_bindgen(js_name = createHyperlink)] +pub fn create_hyperlink() -> Hyperlink { + Hyperlink(docx_rs::Hyperlink::new()) +} + +#[wasm_bindgen] +impl Hyperlink { + pub fn rid(mut self, rid: &str) -> Self { + self.0 = self.0.rid(rid); + self + } + + pub fn anchor(mut self, anchor: &str) -> Self { + self.0 = self.0.anchor(anchor); + self + } + + pub fn history(mut self) -> Self { + self.0 = self.0.history(); + self + } + + pub fn add_run(mut self, run: Run) -> Self { + self.0 = self.0.add_run(run.take()); + self + } + + pub fn add_insert(mut self, i: Insert) -> Self { + self.0 + .children + .push(docx_rs::ParagraphChild::Insert(i.take())); + self + } + + pub fn add_delete(mut self, d: Delete) -> Self { + self.0 + .children + .push(docx_rs::ParagraphChild::Delete(d.take())); + self + } + + pub fn add_bookmark_start(mut self, id: usize, name: &str) -> Self { + self.0.children.push(docx_rs::ParagraphChild::BookmarkStart( + docx_rs::BookmarkStart::new(id, name), + )); + self + } + + pub fn add_bookmark_end(mut self, id: usize) -> Self { + self.0.children.push(docx_rs::ParagraphChild::BookmarkEnd( + docx_rs::BookmarkEnd::new(id), + )); + self + } + + pub fn add_comment_start(mut self, comment: Comment) -> Self { + self.0 + .children + .push(docx_rs::ParagraphChild::CommentStart(Box::new( + docx_rs::CommentRangeStart::new(comment.take()), + ))); + self + } + + pub fn add_comment_end(mut self, id: usize) -> Self { + self.0.children.push(docx_rs::ParagraphChild::CommentEnd( + docx_rs::CommentRangeEnd::new(id), + )); + self + } +} + +impl Hyperlink { + pub fn take(self) -> docx_rs::Hyperlink { + self.0 + } +} diff --git a/docx-wasm/src/lib.rs b/docx-wasm/src/lib.rs index 9ffe2d6..42bc7ed 100644 --- a/docx-wasm/src/lib.rs +++ b/docx-wasm/src/lib.rs @@ -5,6 +5,7 @@ mod delete; mod doc; mod footer; mod header; +mod hyperlink; mod insert; mod level; mod level_override; @@ -28,6 +29,7 @@ pub use delete::*; pub use doc::*; pub use footer::*; pub use header::*; +pub use hyperlink::*; pub use insert::*; pub use level::*; pub use level_override::*; diff --git a/docx-wasm/src/paragraph.rs b/docx-wasm/src/paragraph.rs index 827ae45..6785aee 100644 --- a/docx-wasm/src/paragraph.rs +++ b/docx-wasm/src/paragraph.rs @@ -17,6 +17,11 @@ impl Paragraph { self } + pub fn add_hyperlink(mut self, link: Hyperlink) -> Paragraph { + self.0 = self.0.add_hyperlink(link.take()); + self + } + pub fn add_insert(mut self, i: Insert) -> Paragraph { self.0 .children diff --git a/docx-wasm/test/__snapshots__/index.test.js.snap b/docx-wasm/test/__snapshots__/index.test.js.snap index bb4b50a..24ddf10 100644 --- a/docx-wasm/test/__snapshots__/index.test.js.snap +++ b/docx-wasm/test/__snapshots__/index.test.js.snap @@ -33241,6 +33241,24 @@ exports[`writer should write hello 3`] = ` " `; +exports[`writer should write hyperlink 1`] = ` +" + + + + + +" +`; + +exports[`writer should write hyperlink 2`] = ` +" + + Hello!! +World!! +" +`; + exports[`writer should write line spacing 1`] = ` " diff --git a/docx-wasm/test/index.test.js b/docx-wasm/test/index.test.js index 4b4ba97..50ba1da 100644 --- a/docx-wasm/test/index.test.js +++ b/docx-wasm/test/index.test.js @@ -442,4 +442,24 @@ describe("writer", () => { } } }); + + test("should write hyperlink", () => { + const p1 = new w.Paragraph().addHyperlink( + new w.Hyperlink().anchor("anchor").addRun(new w.Run().addText("Hello!!")) + ); + const p2 = new w.Paragraph() + .addBookmarkStart(1, "anchor") + .addRun(new w.Run().addText("World!!")) + .pageBreakBefore(true) + .addBookmarkEnd(1); + const buffer = new w.Docx().addParagraph(p1).addParagraph(p2).build(); + + writeFileSync("../output/hyperlink.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(); + } + } + }); });