Support custom xml (#316)

* feat: Add CustomXML

* feat: Add custom items

* fix: custom items

* fix: test

* update

* fix: lint error

* fix

* allow empty prefix namespace

* 0.0.204

* fix

* 0.0.205
main
bokuweb 2021-07-13 18:46:15 +09:00 committed by GitHub
parent 1f4a6cd2f4
commit 6c139144f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 679 additions and 38 deletions

View File

@ -0,0 +1,12 @@
use docx_rs::*;
pub fn main() -> Result<(), DocxError> {
let path = std::path::Path::new("./output/custom_xml.docx");
let file = std::fs::File::create(&path).unwrap();
Docx::new()
.add_paragraph(Paragraph::new().add_run(Run::new().add_text("Hello")))
.add_custom_item("06AC5857-5C65-A94A-BCEC-37356A209BC3", "<root xmlns=\"https://exampple.com\"><item name=\"Cheap Item\" price=\"$193.95\"/><item name=\"Expensive Item\" price=\"$931.88\"/></root>")
.build()
.pack(file)?;
Ok(())
}

View File

@ -11,6 +11,7 @@ use crate::xml_builder::*;
pub struct ContentTypes {
types: BTreeMap<String, String>,
web_extension_count: usize,
custom_xml_count: usize,
}
impl ContentTypes {
@ -104,6 +105,15 @@ impl ContentTypes {
self.web_extension_count += 1;
self
}
pub fn add_custom_xml(mut self) -> Self {
self.types.insert(
format!("/customXml/itemProps{}.xml", self.web_extension_count),
"application/vnd.openxmlformats-officedocument.customXmlProperties+xml".to_owned(),
);
self.custom_xml_count += 1;
self
}
}
impl Default for ContentTypes {
@ -111,6 +121,7 @@ impl Default for ContentTypes {
ContentTypes {
types: BTreeMap::new(),
web_extension_count: 1,
custom_xml_count: 1,
}
}
}
@ -188,7 +199,8 @@ mod tests {
assert_eq!(
ContentTypes {
types,
web_extension_count: 1
web_extension_count: 1,
custom_xml_count: 1
},
c
);

View File

@ -0,0 +1,66 @@
use crate::documents::BuildXML;
use crate::{ParseXmlError, XmlDocument};
use serde::ser::SerializeSeq;
use serde::Serialize;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct CustomItem(XmlDocument);
impl FromStr for CustomItem {
type Err = ParseXmlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(CustomItem(XmlDocument::from_str(s)?))
}
}
impl Serialize for CustomItem {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.data.len()))?;
for e in self.0.data.iter() {
seq.serialize_element(e)?;
}
seq.end()
}
}
impl BuildXML for CustomItem {
fn build(&self) -> Vec<u8> {
self.0.to_string().as_bytes().to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
use pretty_assertions::assert_eq;
#[test]
fn test_custom_xml() {
let c = CustomItem::from_str(
r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ds:datastoreItem ds:itemID="{06AC5857-5C65-A94A-BCEC-37356A209BC3}"
xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml">
<ds:schemaRefs>
<ds:schemaRef ds:uri="https://hoge.com"/>
</ds:schemaRefs>
</ds:datastoreItem>"#,
)
.unwrap();
assert_eq!(
c.0.to_string(),
"<ds:datastoreItem ds:itemID=\"{06AC5857-5C65-A94A-BCEC-37356A209BC3}\" xmlns:ds=\"http://schemas.openxmlformats.org/officeDocument/2006/customXml\">\n <ds:schemaRefs>\n <ds:schemaRef ds:uri=\"https://hoge.com\">\n </ds:schemaRef>\n\n </ds:schemaRefs>\n\n</ds:datastoreItem>\n"
);
assert_eq!(
serde_json::to_string(&c).unwrap(),
"[{\"name\":\"ds:datastoreItem\",\"attributes\":[[\"ds:itemID\",\"{06AC5857-5C65-A94A-BCEC-37356A209BC3}\"],[\"xmlns:ds\",\"http://schemas.openxmlformats.org/officeDocument/2006/customXml\"]],\"data\":null,\"children\":[{\"name\":\"ds:schemaRefs\",\"attributes\":[],\"data\":null,\"children\":[{\"name\":\"ds:schemaRef\",\"attributes\":[[\"ds:uri\",\"https://hoge.com\"]],\"data\":null,\"children\":[]}]}]}]"
);
}
}

View File

@ -0,0 +1,30 @@
use serde::Serialize;
use crate::documents::BuildXML;
use crate::xml_builder::*;
#[derive(Debug, Clone, Serialize)]
pub struct CustomItemProperty {
id: String,
}
impl CustomItemProperty {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
impl BuildXML for CustomItemProperty {
fn build(&self) -> Vec<u8> {
let mut b = XMLBuilder::new();
b = b.declaration(Some(false));
b = b
.open_data_store_item(
"http://schemas.openxmlformats.org/officeDocument/2006/customXml",
&format!("{{{}}}", self.id),
)
.open_data_store_schema_refs()
.close();
b.close().build()
}
}

View File

@ -0,0 +1,41 @@
use serde::Serialize;
use crate::documents::BuildXML;
use crate::xml_builder::*;
#[derive(Debug, Clone, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CustomItemRels {
custom_item_count: usize,
}
impl CustomItemRels {
pub fn new() -> CustomItemRels {
Default::default()
}
pub fn add_item(mut self) -> Self {
self.custom_item_count += 1;
self
}
}
impl BuildXML for CustomItemRels {
fn build(&self) -> Vec<u8> {
let mut b = XMLBuilder::new();
b = b
.declaration(Some(true))
.open_relationships("http://schemas.openxmlformats.org/package/2006/relationships");
for id in 0..self.custom_item_count {
let id = id + 1;
b = b.relationship(
&format!("rId{}", id),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps",
&format!("itemProps{}.xml", id),
)
}
b.close().build()
}
}

View File

@ -10,12 +10,18 @@ pub struct DocumentRels {
pub has_comments: bool,
pub has_numberings: bool,
pub image_ids: Vec<usize>,
pub custom_xml_count: usize,
}
impl DocumentRels {
pub fn new() -> DocumentRels {
Default::default()
}
pub fn add_custom_item(mut self) -> Self {
self.custom_xml_count += 1;
self
}
}
impl Default for DocumentRels {
@ -24,6 +30,7 @@ impl Default for DocumentRels {
has_comments: false,
has_numberings: false,
image_ids: vec![],
custom_xml_count: 0,
}
}
}
@ -76,6 +83,14 @@ impl BuildXML for DocumentRels {
)
}
for i in 0..self.custom_xml_count {
b = b.relationship(
&format!("rId{}", i + 8),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml",
&format!("../customXml/item{}.xml", i + 1),
)
}
for id in self.image_ids.iter() {
b = b.relationship(
&create_pic_rid(*id),

View File

@ -1,9 +1,12 @@
use std::collections::HashMap;
use std::{collections::HashMap, str::FromStr};
mod build_xml;
mod comments;
mod comments_extended;
mod content_types;
mod custom_item;
mod custom_item_property;
mod custom_item_rels;
mod doc_props;
mod document;
mod document_rels;
@ -32,6 +35,9 @@ pub(crate) use pic_id::*;
pub use comments::*;
pub use comments_extended::*;
pub use content_types::*;
pub use custom_item::*;
pub use custom_item_property::*;
pub use custom_item_rels::*;
pub use doc_props::*;
pub use document::*;
pub use document_rels::*;
@ -50,7 +56,7 @@ pub use xml_docx::*;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Docx {
pub content_type: ContentTypes,
@ -70,6 +76,9 @@ pub struct Docx {
pub taskpanes: Option<Taskpanes>,
pub taskpanes_rels: TaskpanesRels,
pub web_extensions: Vec<WebExtension>,
pub custom_items: Vec<CustomItem>,
pub custom_item_props: Vec<CustomItemProperty>,
pub custom_item_rels: Vec<CustomItemRels>,
}
impl Default for Docx {
@ -107,6 +116,9 @@ impl Default for Docx {
taskpanes: None,
taskpanes_rels: TaskpanesRels::new(),
web_extensions: vec![],
custom_items: vec![],
custom_item_props: vec![],
custom_item_rels: vec![],
}
}
}
@ -291,6 +303,17 @@ impl Docx {
self
}
pub fn add_custom_item(mut self, id: &str, xml: &str) -> Self {
let x = CustomItem::from_str(xml).expect("should parse xml string");
self.content_type = self.content_type.add_custom_xml();
let rel = CustomItemRels::new().add_item();
self.custom_item_props.push(CustomItemProperty::new(id));
self.document_rels = self.document_rels.add_custom_item();
self.custom_item_rels.push(rel);
self.custom_items.push(x);
self
}
pub fn build(&mut self) -> XMLDocx {
self.reset();
@ -299,6 +322,13 @@ impl Docx {
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();
let custom_item_props = self.custom_item_props.iter().map(|p| p.build()).collect();
let custom_item_rels = self
.custom_item_rels
.iter()
.map(|rel| rel.build())
.collect();
self.document_rels.image_ids = image_ids;
@ -319,6 +349,9 @@ impl Docx {
taskpanes: self.taskpanes.map(|taskpanes| taskpanes.build()),
taskpanes_rels: self.taskpanes_rels.build(),
web_extensions,
custom_items,
custom_item_rels,
custom_item_props,
}
}

View File

@ -22,6 +22,9 @@ pub struct XMLDocx {
pub taskpanes: Option<Vec<u8>>,
pub taskpanes_rels: Vec<u8>,
pub web_extensions: Vec<Vec<u8>>,
pub custom_items: Vec<Vec<u8>>,
pub custom_item_rels: Vec<Vec<u8>>,
pub custom_item_props: Vec<Vec<u8>>,
}
impl XMLDocx {

View File

@ -4,9 +4,11 @@ mod escape;
mod reader;
mod types;
mod xml_builder;
mod xml_json;
mod zipper;
pub use documents::*;
pub use errors::*;
pub use reader::*;
pub use types::*;
pub use xml_json::*;

View File

@ -358,6 +358,15 @@ impl XMLBuilder {
);
closed!(webextensionref, "wetp:webextensionref", "xmlns:r", "r:id");
// customXML
open!(
open_data_store_item,
"ds:datastoreItem",
"xmlns:ds",
"ds:itemID"
);
open!(open_data_store_schema_refs, "ds:schemaRefs");
// CommentExtended
// w15:commentEx w15:paraId="00000001" w15:paraIdParent="57D1BD7C" w15:done="0"
pub(crate) fn comment_extended(

View File

@ -0,0 +1,280 @@
// Licensed under either of
//
// Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
// MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
// at your option.
//
// Contribution
// Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
// use serde::Serialize;
use serde::Serialize;
use std::fmt;
use std::io::prelude::*;
use std::io::Cursor;
use std::str::FromStr;
use xml::attribute::OwnedAttribute;
use xml::name::OwnedName;
use xml::namespace::{self, Namespace};
use xml::reader::{EventReader, XmlEvent};
/// An XML Document
#[derive(Debug, Clone)]
pub struct XmlDocument {
/// Data contained within the parsed XML Document
pub data: Vec<XmlData>,
}
// Print as JSON
impl fmt::Display for XmlDocument {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::new();
for d in self.data.iter() {
s = format!("{}{}", s, d);
}
s.fmt(f)
}
}
/// An XML Tag
///
/// For exammple:
///
/// ```XML
/// <foo bar="baz">
/// test text
/// <sub></sub>
/// </foo>
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct XmlData {
/// Name of the tag (i.e. "foo")
pub name: String,
/// Key-value pairs of the attributes (i.e. ("bar", "baz"))
pub attributes: Vec<(String, String)>,
/// Data (i.e. "test text")
pub data: Option<String>,
/// Sub elements (i.e. an XML element of "sub")
pub children: Vec<XmlData>,
}
// Generate indentation
fn indent(size: usize) -> String {
const INDENT: &str = " ";
(0..size)
.map(|_| INDENT)
.fold(String::with_capacity(size * INDENT.len()), |r, s| r + s)
}
// Get the attributes as a string
fn attributes_to_string(attributes: &[(String, String)]) -> String {
attributes
.iter()
.fold(String::new(), |acc, &(ref k, ref v)| {
format!("{} {}=\"{}\"", acc, k, v)
})
}
// Format the XML data as a string
fn format(data: &XmlData, depth: usize) -> String {
let sub = if data.children.is_empty() {
String::new()
} else {
let mut sub = "\n".to_string();
for elmt in data.children.iter() {
sub = format!("{}{}", sub, format(elmt, depth + 1));
}
sub
};
let indt = indent(depth);
let fmt_data = if let Some(ref d) = data.data {
format!("\n{}{}", indent(depth + 1), d)
} else {
"".to_string()
};
format!(
"{}<{}{}>{}{}\n{}</{}>\n",
indt,
data.name,
attributes_to_string(&data.attributes),
fmt_data,
sub,
indt,
data.name
)
}
impl fmt::Display for XmlData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", format(self, 0))
}
}
// Get the XML atributes as a string
fn map_owned_attributes(attrs: Vec<xml::attribute::OwnedAttribute>) -> Vec<(String, String)> {
attrs
.into_iter()
.map(|attr| {
let fmt_name = if attr.name.prefix.is_some() {
if !attr.name.local_name.is_empty() {
format!("{}:{}", attr.name.prefix.unwrap(), attr.name.local_name)
} else {
attr.name.prefix.unwrap()
}
} else {
attr.name.local_name.clone()
};
(fmt_name, attr.value)
})
.collect()
}
// Parse the data
fn parse(
mut data: Vec<XmlEvent>,
current: Option<XmlData>,
mut current_vec: Vec<XmlData>,
trim: bool,
current_namespace: Namespace,
) -> Result<(Vec<XmlData>, Vec<XmlEvent>), String> {
if let Some(elmt) = data.pop() {
match elmt {
XmlEvent::StartElement {
name,
attributes,
namespace,
} => {
let fmt_name = if name.prefix.is_some() {
if !name.local_name.is_empty() {
format!("{}:{}", name.prefix.unwrap(), name.local_name)
} else {
name.prefix.unwrap()
}
} else {
name.local_name
};
let attributes = if namespace == current_namespace {
attributes
} else {
let mut attributes = attributes;
let n = namespace.clone();
let ns = n
.into_iter()
.filter(|&(_k, v)| {
(v != namespace::NS_EMPTY_URI)
&& (v != namespace::NS_XMLNS_URI)
&& (v != namespace::NS_XML_URI)
})
.map(|(k, v)| OwnedAttribute {
name: OwnedName {
local_name: k.to_string(),
namespace: if v == namespace::NS_NO_PREFIX {
None
} else {
Some(v.to_string())
},
prefix: Some("xmlns".to_string()),
},
value: v.to_string(),
});
attributes.extend(ns);
attributes
};
let inner = XmlData {
name: fmt_name,
attributes: map_owned_attributes(attributes),
data: None,
children: Vec::new(),
};
let (inner, rest) = parse(data, Some(inner), Vec::new(), trim, namespace.clone())?;
if let Some(mut crnt) = current {
crnt.children.extend(inner);
parse(rest, Some(crnt), current_vec, trim, namespace)
} else {
current_vec.extend(inner);
parse(rest, None, current_vec, trim, namespace)
}
}
XmlEvent::Characters(chr) => {
let chr = if trim { chr.trim().to_string() } else { chr };
if let Some(mut crnt) = current {
crnt.data = Some(chr);
parse(data, Some(crnt), current_vec, trim, current_namespace)
} else {
Err("Invalid form of XML doc".to_string())
}
}
XmlEvent::EndElement { name } => {
let fmt_name = if name.prefix.is_some() {
if !name.local_name.is_empty() {
format!("{}:{}", name.prefix.unwrap(), name.local_name)
} else {
name.prefix.unwrap()
}
} else {
name.local_name.clone()
};
if let Some(crnt) = current {
if crnt.name == fmt_name {
current_vec.push(crnt);
Ok((current_vec, data))
} else {
Err(format!(
"Invalid end tag: expected {}, got {}",
crnt.name, name.local_name
))
}
} else {
Err(format!("Invalid end tag: {}", name.local_name))
}
}
_ => parse(data, current, current_vec, trim, current_namespace),
}
} else if let Some(_current) = current {
Err("Invalid end tag".to_string())
} else {
Ok((current_vec, Vec::new()))
}
}
impl XmlDocument {
pub fn from_reader<R>(source: R, trim: bool) -> Result<Self, ParseXmlError>
where
R: Read,
{
let parser = EventReader::new(source);
let mut events: Vec<XmlEvent> = parser.into_iter().map(|x| x.unwrap()).collect();
events.reverse();
parse(events, None, Vec::new(), trim, Namespace::empty())
.map(|(data, _)| XmlDocument { data })
.map_err(ParseXmlError)
}
}
/// Error when parsing XML
#[derive(Debug, Clone, PartialEq)]
pub struct ParseXmlError(String);
impl fmt::Display for ParseXmlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Coult not parse string to XML: {}", self.0)
}
}
// Generate an XML document from a string
impl FromStr for XmlDocument {
type Err = ParseXmlError;
fn from_str(s: &str) -> Result<XmlDocument, ParseXmlError> {
XmlDocument::from_reader(Cursor::new(s.to_string().into_bytes()), true)
}
}

View File

@ -75,6 +75,20 @@ where
}
}
if !xml.custom_items.is_empty() {
zip.add_directory("customXml/_rels", Default::default())?;
}
for (i, item) in xml.custom_items.into_iter().enumerate() {
let n = i + 1;
zip.start_file(format!("customXml/_rels/item{}.xml.rels", n), options)?;
zip.write_all(&xml.custom_item_rels[i])?;
zip.start_file(format!("customXml/item{}.xml", n), options)?;
zip.write_all(&item)?;
zip.start_file(format!("customXml/itemProps{}.xml", n), options)?;
zip.write_all(&xml.custom_item_props[i])?;
}
zip.finish()?;
Ok(())
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -81,6 +81,7 @@ export class Docx {
sectionProperty: SectionProperty = new SectionProperty();
_taskpanes: boolean = false;
webextensions: WebExtension[] = [];
customItems: { id: string; xml: string }[] = [];
styles = new Styles();
addParagraph(p: Paragraph) {
@ -194,6 +195,11 @@ export class Docx {
return this;
}
addCustomItem(id: string, xml: string) {
this.customItems.push({ id, xml });
return this;
}
buildRunFonts = (fonts: RunFonts | undefined) => {
let f = wasm.createRunFonts();
if (fonts?._ascii) {
@ -889,6 +895,10 @@ export class Docx {
}
}
for (const item of this.customItems) {
docx = docx.add_custom_item(item.id, item.xml);
}
return docx;
}

View File

@ -1,6 +1,6 @@
{
"name": "docx-wasm",
"version": "0.0.202",
"version": "0.0.205",
"main": "dist/node/index.js",
"browser": "dist/web/index.js",
"author": "bokuweb <bokuweb12@gmail.com>",

View File

@ -113,6 +113,11 @@ impl Docx {
self
}
pub fn add_custom_item(mut self, id: &str, xml: &str) -> Self {
self.0 = self.0.add_custom_item(id, xml);
self
}
pub fn doc_grid(
mut self,
grid_type: docx_rs::DocGridType,

View File

@ -9,6 +9,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -26,6 +27,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -151,6 +155,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -375,6 +380,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -392,6 +398,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -800,6 +809,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -1693,6 +1703,7 @@ Object {
],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -1710,6 +1721,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -2252,6 +2266,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": true,
"hasNumberings": false,
"imageIds": Array [],
@ -2476,6 +2491,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -2493,6 +2509,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -4741,6 +4760,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -5334,6 +5354,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -5351,6 +5372,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -5786,6 +5810,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -9067,6 +9092,7 @@ Object {
],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -9084,6 +9110,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -10307,6 +10336,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": true,
"hasNumberings": false,
"imageIds": Array [],
@ -13024,6 +13054,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -13041,6 +13072,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -13880,6 +13914,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -14626,6 +14661,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -14643,6 +14679,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -15149,6 +15188,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -15877,6 +15917,7 @@ Object {
"children": Array [],
},
"contentType": Object {
"custom_xml_count": 1,
"types": Object {
"/_rels/.rels": "application/vnd.openxmlformats-package.relationships+xml",
"/docProps/app.xml": "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -15894,6 +15935,9 @@ Object {
},
"web_extension_count": 1,
},
"customItemProps": Array [],
"customItemRels": Array [],
"customItems": Array [],
"docProps": Object {
"app": Object {},
"core": Object {
@ -16201,6 +16245,7 @@ Object {
},
},
"documentRels": Object {
"customXmlCount": 0,
"hasComments": false,
"hasNumberings": false,
"imageIds": Array [],
@ -16849,6 +16894,52 @@ exports[`writer should write custom props 4`] = `
</w:num></w:numbering>"
`;
exports[`writer should write customItem 1`] = `""`;
exports[`writer should write customItem 2`] = `""`;
exports[`writer should write customItem 3`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<Relationships xmlns=\\"http://schemas.openxmlformats.org/package/2006/relationships\\">
<Relationship Id=\\"rId1\\" Type=\\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\\" Target=\\"docProps/core.xml\\" />
<Relationship Id=\\"rId2\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\\" Target=\\"docProps/app.xml\\" />
<Relationship Id=\\"rId3\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\\" Target=\\"word/document.xml\\" />
<Relationship Id=\\"rId4\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties\\" Target=\\"docProps/custom.xml\\" />
</Relationships>"
`;
exports[`writer should write customItem 4`] = `
"<?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=\\"rId4\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header\\" Target=\\"header1.xml\\" />
<Relationship Id=\\"rId5\\" Type=\\"http://schemas.microsoft.com/office/2011/relationships/commentsExtended\\" Target=\\"commentsExtended.xml\\" />
<Relationship Id=\\"rId8\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml\\" Target=\\"../customXml/item1.xml\\" />
</Relationships>"
`;
exports[`writer should write customItem 5`] = `""`;
exports[`writer should write customItem 6`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" standalone=\\"yes\\"?>
<Relationships xmlns=\\"http://schemas.openxmlformats.org/package/2006/relationships\\">
<Relationship Id=\\"rId1\\" Type=\\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps\\" Target=\\"itemProps1.xml\\" />
</Relationships>"
`;
exports[`writer should write customItem 7`] = `
"<root xmlns=\\"https://example.com\\">
<item name=\\"Cheap Item\\" price=\\"$193.95\\">
</item>
<item name=\\"Expensive Item\\" price=\\"$931.88\\">
</item>
</root>
"
`;
exports[`writer should write default font 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<Relationships xmlns=\\"http://schemas.openxmlformats.org/package/2006/relationships\\">

View File

@ -318,4 +318,22 @@ describe("writer", () => {
}
}
});
test("should write customItem", () => {
const p = new w.Paragraph().addRun(new w.Run().addText("Hello!!"));
const buffer = new w.Docx()
.addParagraph(p)
.addCustomItem(
"06AC5857-5C65-A94A-BCEC-37356A209BC3",
'<root xmlns="https://example.com"><item name="Cheap Item" price="$193.95"/><item name="Expensive Item" price="$931.88"/></root>'
)
.build();
writeFileSync("../output/custom-item.docx", buffer);
const z = new Zip(Buffer.from(buffer));
for (const e of z.getEntries()) {
if (e.entryName.match(/item1.xml|_rels|item1Props/)) {
expect(z.readAsText(e)).toMatchSnapshot();
}
}
});
});