UNPKG

docxml

Version:

TypeScript (component) library for building and parsing a DOCX file

292 lines (291 loc) 12.8 kB
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 });