@docx4j/generated-objects-ts
v0.2.0
Published
Office Open XML (WordprocessingML, DrawingML, PresentationML, SpreadsheetML, VML, ...) as typed JavaScript: Jsonix mappings and TypeScript declarations generated from docx4j's schemas, with a docx4j-style facade.
Maintainers
Readme
@docx4j/generated-objects-ts
Office Open XML as typed JavaScript: Jsonix mappings and TypeScript declarations for the schema set
that docx4j maintains (WordprocessingML, DrawingML,
PresentationML, SpreadsheetML, VML, the Microsoft Office extensions, OPC relationships, document
properties, and more), generated by
jsonix-schema-compiler from docx4j's
xsd/ROOT.xsd, with a small facade that gives them docx4j's names.
It is the counterpart of docx4j's docx4j-generated-objects module: the object model alone, usable
on its own wherever the document arrives as XML (a Word add-in via Office JS, a custom XML part, a
file you unzip yourself). The packaging and resolution layer that corresponds to docx4j-core
(parts, relationships, styles, numbering) is the separate package @docx4j/core-ts (repository
plutext/docx4j-core-ts), which depends on this one and
re-exports its facade. "docx4j-ts" names the TypeScript line as a whole.
npm install @docx4j/generated-objects-tsWhy
- There is no Microsoft object model for OOXML in JavaScript. Office JS gives an add-in the
document's XML only as strings:
Body.getOoxml()andRange.getOoxml()return a flat OPC package,insertOoxml()takes one back. Everything in between, reading a paragraph's properties or building a table, is string and DOM work against a 6,000-page specification. - Word on the web has WordApi 1.4 custom XML parts, with
getXml(),setXml(),query(),insertElement()andupdateElement(), but again only XML strings: there is no mapping between the XML and JavaScript objects. - docx4j is that object model, in Java. Its generated classes, names and rules
(
org.docx4j.wml.P,getParent(),deepCopy,unwrap) are how a large body of code manipulates Office documents. This package is the same model for JavaScript and TypeScript, generated from the same schemas:org_docx4j_wml.Pis docx4j'sP, with the same properties, aTYPE_NAMEdiscriminant,PARENTpointers, and theContentAccessor/SdtElementinterfaces as union types. - Typed. The declarations say what may appear where: which elements a paragraph can hold, which attributes are required, which values an enumeration allows. The TypeScript compiler checks the tree you build before Word sees it.
The runtime is @docx4j/jsonix, a fork of the Jsonix
XML-to-JSON library with parent pointers and deep copy; it works in browsers and in Node.
Examples
An add-in: from getOoxml() to typed objects and back
getOoxml() returns a flat OPC package (pkg:package), mapped here as org_docx4j_xmlPackage.
Its parts are xsd:any processContents="skip", so plain unmarshalling leaves them as DOM;
unmarshalPackage additionally unmarshals every part whose root element the model knows
(w:document, w:styles, relationships, ...) into typed objects, and marshalPackage reverses
that for insertOoxml():
import { unmarshalPackage, marshalPackage, unwrap } from '@docx4j/generated-objects-ts';
import type { DocumentElement, P, R, Text } from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
async function shout(body: Word.Body): Promise<void> {
const ooxml = body.getOoxml(); // OfficeExtension.ClientResult<string>
await body.context.sync();
const pkg = await unmarshalPackage(ooxml.value);
const part = unwrap(pkg).part?.find((p) => p.name === '/word/document.xml');
const document = part?.xmlData?.any as DocumentElement | undefined; // typed: w:document is known to the model
if (!document) return;
for (const entry of unwrap(document).body?.content ?? []) {
if (entry.value.TYPE_NAME !== 'org_docx4j_wml.P') continue;
const paragraph: P = entry.value;
for (const run of paragraph.content ?? []) {
if (run.value.TYPE_NAME !== 'org_docx4j_wml.R') continue;
for (const item of (run.value as R).content ?? []) {
if (item.value.TYPE_NAME === 'org_docx4j_wml.Text') {
const text = item.value as Text;
text.value = text.value?.toUpperCase();
}
}
}
}
body.insertOoxml(await marshalPackage(pkg), 'Replace');
await body.context.sync();
}Building content as typed literals
import type { P } from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const paragraph: P = {
pPr: { pStyle: { val: 'Heading1' } },
content: [{
name: { namespaceURI: W, localPart: 'r' },
value: { rPr: { b: {} }, content: [{ name: { namespaceURI: W, localPart: 't' }, value: { value: 'Hello' } }] },
}],
};
// paragraph.content[0].value.rPr.bold -> compile error: no such property
const xml = await marshalString({ name: { namespaceURI: W, localPart: 'p' }, value: paragraph });Building content with the factories
Each module also ships docx4j's ObjectFactory (compiler CR-010), generated from the same model:
factory/<module> has a creator per class (createP(init?), which sets TYPE_NAME) and a wrapper
per element declaration with XJC's names (createRT(value) for w:t in a run,
createPElement(value) for the global w:p), and el/<module> has one wrapper per element name
of the module's namespace. Both are ES modules of named exports, so a bundle keeps only what a
program uses.
import { createP, createR, createText, createRElement, createRT } from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
const paragraph = createP({
pPr: { pStyle: { val: 'Heading1' } },
content: [createRElement(createR({ rPr: { b: {} }, content: [createRT(createText({ value: 'Hello' }))] }))],
});
const same = el.p({ pPr: { pStyle: { val: 'Heading1' } }, content: [el.r({ rPr: { b: {} }, content: [el.t({ value: 'Hello' })] })] });
// createRT(createP()) -> compile error: a run holds Text, not PA wrapper sets TYPE_NAME on a literal value when the declaration determines the type; el.sdt
and el.customXml, one element name with four types by scope, leave it to the caller (the scoped
createSdtPrAlias-style wrappers are the precise form). A creator's init is partial, so a
literal (above) remains the stricter form: it is what checks required properties at compile time.
Fragments, text sugar and traversal (builders/wml)
builders/wml is the counterpart of docx4j-core's XmlUtils.unmarshalString, TextUtils and
TraversalUtil (CR-002): things that need only the object model.
import { wml, p, r, tbl, textOf, find } from '@docx4j/generated-objects-ts/builders/wml';
const [heading, table] = await wml`
<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr>${r('Report', { bold: true })}</w:p>
${tbl([['Item', 'Qty'], ['Widget', '3']], { style: 'TableGrid' })}`;
const para = p('Hello', { style: 'Heading1', italic: true, highlightColor: '#FFFF00' });
textOf(heading); // 'Report'
find(table, 'org_docx4j_wml.Tc').length; // 4wml parses sibling elements as written inside document.xml, with docx4j's namespace
declarations added, and returns them typed: the fragment is wrapped in the container docx4j would
put it in (w:body, w:p, w:tbl, ...; a content control by its content), since the runtime
unmarshals global elements only, so w:r, w:tr or w:t fragments work too. In the tagged form
a typed element marshals in place, a string is escaped as text and wml.raw(xml) is inserted
verbatim; the plain form wml(xml, { wrapper, preprocess }) takes options. p, r, t, br,
tab and tbl build content over el; run options use the names of Office JS Word.Font
(bold, name, size, highlightColor, ...) through applyRunOptions / readRunOptions, the
one mapping to w:rPr that @docx4j/core-ts's Font view shares. textOf reads the text back;
runItemsOf gives a holder's run-level list under whichever property name the model uses
(w:ins, w:del, w:moveFrom, w:moveTo, a run-level content control);
walk, find and linkParents are TraversalUtil, ClassFinder and what the unmarshaller does
for PARENT.
Navigating with TYPE_NAME and PARENT
import { unmarshalString, unwrap, deepCopy } from '@docx4j/generated-objects-ts';
import type { DocumentElement, Tc } from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
const doc = unwrap(await unmarshalString<DocumentElement>(documentXml));
const firstParagraph = doc.body?.content?.find((e) => e.value.TYPE_NAME === 'org_docx4j_wml.P')?.value;
const container = firstParagraph?.PARENT; // Body | Tc | Hdr | Ftr | ... as docx4j's getParent()
if (container?.TYPE_NAME === 'org_docx4j_wml.Tc') {
const cell: Tc = container; // narrowed
}
const copy = deepCopy(firstParagraph); // children re-linked to the copy; copy.PARENT unset, as docx4j's copy()Custom XML parts
Custom XML parts hold your own schemas. The compiler that produced this package works on any XML
Schema: run jsonix-schema-compiler -generateTypeScript over yours, put its mapping in a context
next to these, and getXml() / setXml() become typed round trips too. For OOXML content in a
part (a w:sdt, document properties), the modules here already apply:
async function readPart(part: Word.CustomXmlPart): Promise<void> {
const xml = part.getXml();
await part.context.sync();
const element = await unmarshalString(xml.value); // TypedNamedValue<unknown>; narrow by TYPE_NAME or a type argument
part.setXml(await marshalString(element));
}Node
import JSZip from 'jszip';
import { readFile } from 'node:fs/promises';
import { unmarshalString, unwrap } from '@docx4j/generated-objects-ts';
import type { DocumentElement } from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
const zip = await JSZip.loadAsync(await readFile('report.docx'));
const doc = unwrap(await unmarshalString<DocumentElement>(await zip.file('word/document.xml')!.async('string')));
console.log(doc.body?.content?.length, 'block-level items');The Office JS signatures above (Body.getOoxml, insertOoxml, CustomXmlPart.getXml/setXml)
are the documented ones; the snippets are compile-checked against minimal stubs in
test/readme-examples.ts.
What is here
- The facade (
src/index.mts, built todist/):getContext()builds oneJsonix.Contextover all 103 modules, lazily on first use, withparentPointers: true;unmarshalString,marshalString,unmarshalNode,marshalNode,unwrapanddeepCopyare docx4j'sXmlUtilsnames over it, withdeepCopyAsfor a copy typed as a base type (w:pPrChangeholds aPPrBase, not aPPr) anddeepCopyAsSync/getContextSyncwhere a caller cannot await;unmarshalPackage/marshalPackagehandle flat OPC packages with typed parts;Jsonixis re-exported. The facade is asynchronous because the mappings load as ES modules; for a synchronous setup import the modules you need from@docx4j/generated-objects-ts/modules/<module>and build your own context. - Bundling: no bundler configuration is needed. The mappings are imported by literal specifiers
from a registry (
MODULES), so a bundler sees exactly the 103 modules a context loads, rather than the template specifier earlier versions used - which Vite could not resolve withoutdynamicImportVarsOptionsand esbuild expanded to a glob over every.mjsinmodules/, including the.eland.factorysiblings (a 3.5MB esbuild bundle against 2.0MB now). One obstacle remains and is the runtime's: bundled for Node,@docx4j/jsonixloses the@xmldom/xmldomit injects through its UMD wrapper and reaches for browser globals, so a Node bundle needsglobalThis.DOMParserandglobalThis.XMLSerializerset; a browser has them. - A smaller context:
getContext({ modules: modulesFor('org_docx4j_wml') })builds a context over one root's modules and their closure - twelve modules for WordprocessingML, which reads a Word 365document.xml- instead of all 103.modulesForfollows the type references in the mappings themselves, since a hand-picked list almost always misses one and fails with "Type info [...] is not known in this context". Options are read when the context is built, so callresetContext()first if one already exists; and a smaller context knows less, so a document carrying content its modules do not cover will throw where the full context would type it. - Namespace prefixes: the context uses docx4j's prefix table, exported as
NAMESPACE_PREFIXES(namespace URI to prefix:w,w14,mc,r,a,pkg, ...), so marshalled XML reads as Word writes it. The facade's marshal functions declare on the root element the namespaces the tree uses plus every prefix the root'smc:Ignorablenames, and nothing else; aRelationshipsroot uses the default namespace, as docx4j's relationships part mapper. To add or change prefixes, pass a table before the context is first used:getContext({ namespacePrefixes: { ...NAMESPACE_PREFIXES, 'urn:my-ns': 'my' } }). A table passed togetContextreplaces the default (spread it to extend it). One namespace is written as the default rather than with a prefix (SpreadsheetML main, as docx4j), so a prefixmc:Ignorablenames for it cannot come from the table;IGNORABLE_PREFIX_ALIASESsupplies it, and the prefix is declared beside the default declaration (Excel writesmc:Ignorable="x xr10"on slicer, slicer cache and timeline parts). A prefix neither table resolves is dropped frommc:Ignorable, with a warning, since Office repairs a file naming an undeclared prefix. - 103 modules under
modules/, one per JAXB package reachable fromROOT.xsd, named after the package with dots as underscores (docx4j'sorg.docx4j.wmlisorg_docx4j_wml). Each has<module>.js(UMD),<module>.mjs(ES module),<module>.d.ts(declarations) and<module>.d.mts(typed re-export for the.mjs). Modules reference each other by name, so a context needs all of them. @docx4j/generated-objects-ts/helpers/wml(src/helpers/wml.mts): docx4j's highlight colour table,isQFormat(viaPARENT) andisCustomStyle.@docx4j/generated-objects-ts/builders/wml(src/builders/wml.mts):wmlfragments,p/r/t/tbl, the run mapping,tr/tc,inlinePicture,sdt/sdtPr/sdtProperty/sdtKindOffor content controls,rPrToElements/rPrFromElements,walkAll,mcBranchOf,textOf,runItemsOf,walk/find/linkParents(CR-002, CR-003).modules/bindings.xjb: the Jsonix customizations the files were generated with (kept for reference; the source of truth is the compiler repository'sOfficeOpenXML/bindings.xjb).
Mapping names, module names and TYPE_NAME discriminants all use the docx4j package names, so
org_docx4j_wml.P is the type that docx4j calls org.docx4j.wml.P.
The declarations
- Required properties are non-optional; collections are arrays; choices are unions;
elementRefproperties (Body.content,P.content,R.content, ...) are unions ofTypedNamedValue<T>for every element the schema allows there, including substitution groups, plusstringwhere content is mixed. TYPE_NAMEis a literal union over the type and its subtypes and is set by Jsonix on unmarshal.readonly PARENT?lists the types that can contain each type, as docx4j'sgetParent()would return; the runtime fills it in (parentPointers, on by default in the facade) anddeepCopyre-links it.- docx4j's hand-written interfaces (
ContentAccessor,SdtElement,SdtContent,CTCustomXmlElement, the VML attribute interfaces) are union type aliases. - Dates are Jsonix calendars (
XmlCalendar), not JavaScriptDates;xs:doublevalues arenumber(includingNaNdefaults in DrawingML diagrams). - Attributes are emitted sorted by name, except
v:line(id style from to, which Word requires); elements keep schema order. An absentw:customStylemeans a built-in style (helpers/wml: isCustomStyle).
Regeneration
See generate.md. The files are regenerated from the compiler repository, which owns the bindings
and the generation script; commits here cite the compiler and docx4j commits used.
Licence
Apache-2.0, as docx4j. The schemas are ECMA-376 / Microsoft Open Specifications material as redistributed by docx4j.
