Allow custom text properties to be included

This commit is contained in:
Max Lay
2020-08-03 14:58:30 +12:00
parent a6eb8e01df
commit 870c222dd5
12 changed files with 203 additions and 10 deletions

View File

@ -22,10 +22,11 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(14);
expect(fileNames).has.length(15);
expect(fileNames).to.include("word/document.xml");
expect(fileNames).to.include("word/styles.xml");
expect(fileNames).to.include("docProps/core.xml");
expect(fileNames).to.include("docProps/custom.xml");
expect(fileNames).to.include("docProps/app.xml");
expect(fileNames).to.include("word/numbering.xml");
expect(fileNames).to.include("word/footnotes.xml");
@ -62,7 +63,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(22);
expect(fileNames).has.length(23);
expect(fileNames).to.include("word/header1.xml");
expect(fileNames).to.include("word/_rels/header1.xml.rels");
@ -88,7 +89,7 @@ describe("Compiler", () => {
const spy = sinon.spy(compiler["formatter"], "format");
compiler.compile(file);
expect(spy.callCount).to.equal(10);
expect(spy.callCount).to.equal(11);
});
});
});

View File

@ -23,6 +23,7 @@ interface IXmlifyedFileMapping {
readonly HeaderRelationships: IXmlifyedFile[];
readonly FooterRelationships: IXmlifyedFile[];
readonly ContentTypes: IXmlifyedFile;
readonly CustomProperties: IXmlifyedFile;
readonly AppProperties: IXmlifyedFile;
readonly FootNotes: IXmlifyedFile;
readonly Settings: IXmlifyedFile;
@ -179,6 +180,10 @@ export class Compiler {
data: xml(this.formatter.format(file.ContentTypes, file), prettify),
path: "[Content_Types].xml",
},
CustomProperties: {
data: xml(this.formatter.format(file.CustomProperties, file), prettify),
path: "docProps/custom.xml",
},
AppProperties: {
data: xml(this.formatter.format(file.AppProperties, file), prettify),
path: "docProps/app.xml",

View File

@ -54,6 +54,14 @@ describe("ContentTypes", () => {
},
});
expect(tree["Types"][11]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml",
PartName: "/docProps/custom.xml",
},
},
});
expect(tree["Types"][12]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -61,7 +69,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][12]).to.deep.equal({
expect(tree["Types"][13]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
@ -69,7 +77,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][13]).to.deep.equal({
expect(tree["Types"][14]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
@ -77,7 +85,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][14]).to.deep.equal({
expect(tree["Types"][15]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
@ -94,7 +102,7 @@ describe("ContentTypes", () => {
contentTypes.addFooter(102);
const tree = new Formatter().format(contentTypes);
expect(tree["Types"][15]).to.deep.equal({
expect(tree["Types"][16]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@ -103,7 +111,7 @@ describe("ContentTypes", () => {
},
});
expect(tree["Types"][16]).to.deep.equal({
expect(tree["Types"][17]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@ -120,7 +128,7 @@ describe("ContentTypes", () => {
contentTypes.addHeader(202);
const tree = new Formatter().format(contentTypes);
expect(tree["Types"][15]).to.deep.equal({
expect(tree["Types"][16]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
@ -129,7 +137,7 @@ describe("ContentTypes", () => {
},
});
expect(tree["Types"][16]).to.deep.equal({
expect(tree["Types"][17]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",

View File

@ -27,6 +27,7 @@ export class ContentTypes extends XmlComponent {
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", "/word/styles.xml"));
this.root.push(new Override("application/vnd.openxmlformats-package.core-properties+xml", "/docProps/core.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.custom-properties+xml", "/docProps/custom.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.extended-properties+xml", "/docProps/app.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", "/word/numbering.xml"));
this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml"));

View File

@ -0,0 +1,13 @@
import { XmlAttributeComponent } from "file/xml-components";
export interface ICustomPropertiesAttributes {
readonly xmlns: string;
readonly vt: string;
}
export class CustomPropertiesAttributes extends XmlAttributeComponent<ICustomPropertiesAttributes> {
protected readonly xmlKeys = {
xmlns: "xmlns",
vt: "xmlns:vt",
};
}

View File

@ -0,0 +1,67 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { CustomProperties } from "./custom-properties";
describe("CustomProperties", () => {
describe("#constructor()", () => {
it("sets the appropriate attributes on the top-level", () => {
const properties = new CustomProperties([]);
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
Properties: {
_attr: {
"xmlns": "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",
"xmlns:vt": "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
},
},
});
});
it("should create custom properties with all the attributes given", () => {
const properties = new CustomProperties([{ name: "Address", value: "123" }, { name: "Author", value: "456" }]);
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
Properties: [
{
_attr: {
"xmlns": "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",
"xmlns:vt": "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
},
},
{
property: [
{
_attr: {
fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",
pid: "2",
name: "Address",
},
},
{
"vt:lpwstr": [
"123",
],
},
],
},
{
property: [
{
_attr: {
fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",
pid: "3",
name: "Author",
},
},
{
"vt:lpwstr": [
"456",
],
},
],
},
],
});
});
});
});

View File

@ -0,0 +1,41 @@
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { CustomPropertiesAttributes } from "./custom-properties-attributes";
import { CustomProperty, ICustomPropertyOptions } from "./custom-property";
export class CustomProperties extends XmlComponent {
// tslint:disable-next-line:readonly-keyword
private nextId: number;
private readonly properties: CustomProperty[] = [];
constructor(properties: ICustomPropertyOptions[]) {
super("Properties");
this.root.push(
new CustomPropertiesAttributes({
xmlns: "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",
vt: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes",
}),
);
// I'm not sure why, but every example I have seen starts with 2
// https://docs.microsoft.com/en-us/office/open-xml/how-to-set-a-custom-property-in-a-word-processing-document
this.nextId = 2;
for (const property of properties) {
this.addCustomProperty(property);
}
}
public prepForXml(): IXmlableObject | undefined {
this.properties.forEach((x) => this.root.push(x));
return super.prepForXml();
}
public addCustomProperty(property: ICustomPropertyOptions): void {
this.properties.push(new CustomProperty(this.nextId++, property));
}
public get Properties(): CustomProperty[] {
return this.properties;
}
}

View File

@ -0,0 +1,15 @@
import { XmlAttributeComponent } from "file/xml-components";
export interface ICustomPropertyAttributes {
readonly fmtid: string;
readonly pid: string;
readonly name: string;
}
export class CustomPropertyAttributes extends XmlAttributeComponent<ICustomPropertyAttributes> {
protected readonly xmlKeys = {
fmtid: "fmtid",
pid: "pid",
name: "name",
};
}

View File

@ -0,0 +1,26 @@
import { XmlComponent } from "file/xml-components";
import { CustomPropertyAttributes } from "./custom-property-attributes";
export interface ICustomPropertyOptions {
readonly name: string;
readonly value: string;
}
export class CustomProperty extends XmlComponent {
constructor(id: number, properties: ICustomPropertyOptions) {
super("property");
this.root.push(new CustomPropertyAttributes({
fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",
pid: id.toString(),
name: properties.name,
}));
this.root.push(new CustomPropertyValue(properties.value));
}
}
export class CustomPropertyValue extends XmlComponent {
constructor(value: string) {
super("vt:lpwstr");
this.root.push(value);
}
}

View File

@ -0,0 +1,2 @@
export * from "./custom-properties";
export * from "./custom-property";

View File

@ -2,6 +2,7 @@ import * as shortid from "shortid";
import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties";
import { CustomProperties, ICustomPropertyOptions } from "./custom-properties";
import { Document } from "./document";
import {
FooterReferenceType,
@ -59,6 +60,7 @@ export class File {
private readonly footNotes: FootNotes;
private readonly settings: Settings;
private readonly contentTypes: ContentTypes;
private readonly customProperties: CustomProperties;
private readonly appProperties: AppProperties;
private readonly styles: Styles;
private readonly hyperlinkCache: { readonly [key: string]: Hyperlink } = {};
@ -71,6 +73,7 @@ export class File {
},
fileProperties: IFileProperties = {},
sections: ISectionOptions[] = [],
customProperties: ICustomPropertyOptions[] = [],
) {
this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering(
@ -82,6 +85,7 @@ export class File {
);
this.docRelationships = new Relationships();
this.fileRelationships = new Relationships();
this.customProperties = new CustomProperties(customProperties);
this.appProperties = new AppProperties();
this.footNotes = new FootNotes();
this.contentTypes = new ContentTypes();
@ -289,6 +293,11 @@ export class File {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
"docProps/app.xml",
);
this.fileRelationships.createRelationship(
4,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
"docProps/custom.xml",
);
this.docRelationships.createRelationship(
this.currentRelationshipId++,
@ -352,6 +361,10 @@ export class File {
return this.contentTypes;
}
public get CustomProperties(): CustomProperties {
return this.customProperties;
}
public get AppProperties(): AppProperties {
return this.appProperties;
}

View File

@ -13,6 +13,7 @@ export type RelationshipType =
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
| "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";