feat: Added a hyperlink element (#386)

* feat: Added a hyperlink element

* feat: fix comment collerctor

* feat: Add JS API
main
bokuweb 2021-12-18 01:03:02 +09:00 committed by GitHub
parent a4ddb3a5ee
commit 26edc02635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 501 additions and 55 deletions

View File

@ -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(())
}

View File

@ -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<String>,
pub anchor: Option<String>,
pub history: bool,
pub children: Vec<ParagraphChild>,
}
impl Hyperlink {
pub fn new() -> Self {
Hyperlink::default()
}
pub fn rid(mut self, rid: impl Into<String>) -> Self {
self.rid = Some(rid.into());
self
}
pub fn anchor(mut self, anchor: impl Into<String>) -> 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<String>) -> 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<u8> {
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#"<w:hyperlink w:anchor="ToC1"><w:r><w:rPr /><w:t xml:space="preserve">hello</w:t></w:r></w:hyperlink>"#
);
}
}

View File

@ -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::*;

View File

@ -32,6 +32,7 @@ pub enum ParagraphChild {
Insert(Insert),
Delete(Delete),
BookmarkStart(BookmarkStart),
Hyperlink(Hyperlink),
BookmarkEnd(BookmarkEnd),
CommentStart(Box<CommentRangeStart>),
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)));

View File

@ -487,6 +487,46 @@ impl Docx {
crate::reset_para_id();
}
fn insert_comment_to_map(
&self,
comment_map: &mut HashMap<usize, String>,
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<Comment>,
comments_extended: &mut Vec<CommentExtended>,
comment_map: &HashMap<usize, String>,
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<Comment> = vec![];
@ -498,14 +538,13 @@ impl Docx {
DocumentChild::Paragraph(paragraph) => {
for child in &paragraph.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 &paragraph.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 &paragraph.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 &paragraph.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,
);
}
}
}

View File

@ -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. <w:r ... >
open!(open_run, "w:r");
open!(open_run_property, "w:rPr");

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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;

View File

@ -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
}
}

View File

@ -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::*;

View File

@ -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

View File

@ -33241,6 +33241,24 @@ exports[`writer should write hello 3`] = `
</w:num></w:numbering>"
`;
exports[`writer should write hyperlink 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<Relationships xmlns=\\"http://schemas.openxmlformats.org/package/2006/relationships\\">
<Relationship Id=\\"rId1\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\\" Target=\\"styles.xml\\" />
<Relationship Id=\\"rId2\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable\\" Target=\\"fontTable.xml\\" />
<Relationship Id=\\"rId3\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings\\" Target=\\"settings.xml\\" />
<Relationship Id=\\"rId5\\" Type=\\"http://schemas.microsoft.com/office/2011/relationships/commentsExtended\\" Target=\\"commentsExtended.xml\\" />
</Relationships>"
`;
exports[`writer should write hyperlink 2`] = `
"<?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:p w14:paraId=\\"00000001\\"><w:pPr><w:rPr /></w:pPr><w:hyperlink w:anchor=\\"anchor\\"><w:r><w:rPr><w:rFonts /></w:rPr><w:t xml:space=\\"preserve\\">Hello!!</w:t></w:r></w:hyperlink></w:p><w:p w14:paraId=\\"00000002\\"><w:pPr><w:rPr /><w:pageBreakBefore />
</w:pPr><w:bookmarkStart w:id=\\"1\\" w:name=\\"anchor\\" /><w:r><w:rPr><w:rFonts /></w:rPr><w:t xml:space=\\"preserve\\">World!!</w:t></w:r><w:bookmarkEnd w:id=\\"1\\" /></w:p><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>"
`;
exports[`writer should write line spacing 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<Relationships xmlns=\\"http://schemas.openxmlformats.org/package/2006/relationships\\">

View File

@ -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();
}
}
});
});