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.205main
parent
1f4a6cd2f4
commit
6c139144f3
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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\":[]}]}]}]"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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\\">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue