docxml
Version:
TypeScript (component) library for building and parsing a DOCX file
292 lines (291 loc) • 12.8 kB
JavaScript
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Docx_officeDocument, _Docx_customProperties, _Docx_renderer;
import { GenericRenderer } from '../deps/deno.land/x/xml_renderer@5.0.7/mod.js';
import { Archive } from './classes/Archive.js';
import { Bookmarks } from './classes/Bookmarks.js';
import { FileLocation, RelationshipType } from './enums.js';
import { ContentTypesXml } from './files/ContentTypesXml.js';
import { CustomPropertiesXml } from './files/CustomPropertiesXml.js';
import { DocumentXml } from './files/DocumentXml.js';
import { RelationshipsXml } from './files/RelationshipsXml.js';
import { parse } from './utilities/dom.js';
import { jsx } from './utilities/jsx.js';
/**
* Represents the DOCX file as a whole, and collates other responsibilities together. Provides
* access to DOCX content types ({@link ContentTypesXml}), relationships ({@link RelationshipsXml}),
* the document itself ({@link DocumentXml}).
*
* An instance of this class can access other classes that represent the various XML files in a
* DOCX archive, such as `ContentTypes.xml`, `word/document.xml`, and `_rels/.rels`.
*/
export class Docx {
constructor(contentTypes = new ContentTypesXml(FileLocation.contentTypes), relationships = new RelationshipsXml(FileLocation.relationships), rules = null) {
/**
* The JSX pragma.
*/
Object.defineProperty(this, "jsx", {
enumerable: true,
configurable: true,
writable: true,
value: jsx
});
/**
* The utility function dealing with the XML for recording content types. Every DOCX file has
* exactly one of these.
*/
Object.defineProperty(this, "contentTypes", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* The utility function dealing with the top-level XML file for recording relationships. Other
* relationships may have their own relationship XMLs.
*/
Object.defineProperty(this, "relationships", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "bookmarks", {
enumerable: true,
configurable: true,
writable: true,
value: new Bookmarks()
});
// Also not enumerable
_Docx_officeDocument.set(this, null);
_Docx_customProperties.set(this, null);
/**
* The XML renderer instance containing translation rules, going from your XML to this library's
* OOXML components.
*/
_Docx_renderer.set(this, new GenericRenderer());
this.contentTypes = contentTypes;
this.relationships = relationships;
if (rules) {
__classPrivateFieldGet(this, _Docx_renderer, "f").merge(rules);
}
if (!this.relationships.hasType(RelationshipType.officeDocument)) {
this.relationships.add(RelationshipType.officeDocument, new DocumentXml(FileLocation.mainDocument));
}
}
/**
* A short-cut to the relationship that represents visible document content.
*/
get document() {
// @TODO Invalidate the cached _officeDocument whenever that relationship changes.
if (!__classPrivateFieldGet(this, _Docx_officeDocument, "f")) {
__classPrivateFieldSet(this, _Docx_officeDocument, this.relationships.ensureRelationship(RelationshipType.officeDocument, () => new DocumentXml(FileLocation.mainDocument)), "f");
}
return __classPrivateFieldGet(this, _Docx_officeDocument, "f");
}
/**
* The API representing "docProps/custom.xml"
*/
get customProperties() {
if (!__classPrivateFieldGet(this, _Docx_customProperties, "f")) {
__classPrivateFieldSet(this, _Docx_customProperties, this.relationships.ensureRelationship(RelationshipType.customProperties, () => new CustomPropertiesXml(FileLocation.customProperties)), "f");
}
return __classPrivateFieldGet(this, _Docx_customProperties, "f");
}
/**
* Create a ZIP archive, which is the handler for `.docx` files as a ZIP archive.
*/
async toArchive() {
const styles = this.document.styles;
const roots = [
{
relationships: this.document.relationships,
componentRoot: this.document.children,
},
...this.document.headers.map((runningBlock) => ({
relationships: runningBlock.relationships,
componentRoot: runningBlock.children,
})),
...this.document.footers.map((runningBlock) => ({
relationships: runningBlock.relationships,
componentRoot: runningBlock.children,
})),
];
async function walkChildComponentsFromRoot(children, relationships) {
// Loop over all content to ensure styles are registered, relationships created etc.
await Promise.all((await children).map(async function walk(componentPromise) {
const component = await componentPromise;
if (typeof component === 'string') {
return;
}
if (Array.isArray(component)) {
await Promise.all(component.map(walk));
return;
}
const styleName = component.props.style;
if (styleName) {
styles.ensureStyle(styleName);
}
if (relationships !== null) {
await component.ensureRelationship(relationships);
}
await Promise.all(component.children.map(walk));
}));
}
await Promise.all(roots.map(({ relationships, componentRoot }) => walkChildComponentsFromRoot(componentRoot, relationships)));
const archive = new Archive();
// New relationships may be created as they are necessary for serializing content, eg. for
// images.
await this.relationships.addToArchive(archive);
await Promise.all(this.relationships
.getRelated()
.filter((related) => !(related instanceof RelationshipsXml))
.map(async (related) => {
this.contentTypes.addOverride(related.location, await related.contentType);
}));
await this.contentTypes.addToArchive(archive);
return archive;
}
/**
* Convenience method to create a DOCX archive from the current document and write it to your disk.
*/
async toFile(location) {
const archive = await this.toArchive();
return archive.toFile(location);
}
/**
* Instantiate this class by referencing an existing `.docx` archive.
*/
static async fromArchive(locationOrZipArchive) {
const archive = typeof locationOrZipArchive === 'string'
? await Archive.fromFile(locationOrZipArchive)
: locationOrZipArchive instanceof Uint8Array
? await Archive.fromUInt8Array(locationOrZipArchive)
: locationOrZipArchive;
const contentTypes = await ContentTypesXml.fromArchive(archive, FileLocation.contentTypes);
const relationships = await RelationshipsXml.fromArchive(archive, contentTypes, FileLocation.relationships);
return new Docx(contentTypes, relationships);
}
/**
* Create an empty DOCX, and populate it with the minimum viable contents to appease MS Word.
*/
static fromNothing() {
return new Docx();
}
/**
* Create a new DOCX with contents composed by this library's components. Needs a single JSX component
* as root, for example `<Section>` or `<Paragragh>`.
*/
static fromJsx(roots) {
const docx = Docx.fromNothing();
docx.document.set(roots);
return docx;
}
/**
* Add an XML translation rule, applied to an element that matches the given XPath test.
*
* If an element matches multiple rules, the rule with the most specific XPath test wins.
*/
withXmlRule(xPathTest, transformer) {
__classPrivateFieldGet(this, _Docx_renderer, "f").add(xPathTest, transformer);
return this;
}
/**
* Add _all_ the XML translatiom rules from another set of translation rules. Useful for
* cloning.
*/
withXmlRules(renderer) {
__classPrivateFieldGet(this, _Docx_renderer, "f").merge(renderer);
return this;
}
/**
* A convenience method to set a few settings for the document.
*/
withSettings(settingOverrides) {
Object.entries(settingOverrides).forEach(([key, value]) => {
this.document.settings.set(key, value);
});
return this;
}
/**
* Set the document contents to the provided XML, transformed using the rules previously
* registered through {@link Docx.withXmlRule}.
*/
withXml(dom, props) {
if (typeof dom === 'string') {
dom = parse(dom);
}
if (!__classPrivateFieldGet(this, _Docx_renderer, "f").length) {
throw new Error('No XML transformation rules were configured, creating a DOCX from XML is therefore not possible.');
}
const ast = __classPrivateFieldGet(this, _Docx_renderer, "f").render(dom, {
document: this.document,
...props,
});
const root = [ast].reduce(async function flatten(flatPromise, childPromise) {
const flat = await flatPromise;
const child = await childPromise;
if (child === null || typeof child === 'string') {
return flat;
}
if (Array.isArray(child)) {
return [...flat, ...(await child.reduce(flatten, Promise.resolve([])))];
}
flat.push(child);
return flat;
}, Promise.resolve([]));
// There is no guarantee that the rendering rules produce schema-valid XML.
// @TODO implement some kind of an errr-out mechanism
// @TODO validate that the children are correct?
this.document.set(root);
return this;
}
/**
* Clone some reusable configuration to a new instance of {@link Docx}:
*
* - XML rendering rules
* - Settings
* - Default content types
* - Custom styles
*
* Does _not_ clone other things, like:
* - Not content
* - Not content type overrides
* - Not relationships (unless required for settings)
* - Not anything else either
*/
cloneAsEmptyTemplate() {
const clone = Docx.fromNothing();
clone.withXmlRules(__classPrivateFieldGet(this, _Docx_renderer, "f"));
clone.withSettings(this.document.settings.entries().reduce((dict, [key, value]) => ({
...dict,
[key]: value,
}), {}));
clone.customProperties.add(this.customProperties.values());
clone.contentTypes.addDefaults(this.contentTypes.defaults);
clone.document.styles.addStyles(this.document.styles.styles);
return clone;
}
}
_Docx_officeDocument = new WeakMap(), _Docx_customProperties = new WeakMap(), _Docx_renderer = new WeakMap();
/**
* The JSX pragma.
*
* @deprecated This static property may be removed in the future since it does not have the context of
* a DOCX. If you can, use the instance JSX property. If you cannot, submit an issue.
*/
Object.defineProperty(Docx, "jsx", {
enumerable: true,
configurable: true,
writable: true,
value: jsx
});