Descriptor System
All OOXML XML parts in @office-open/docx, @office-open/pptx, and @office-open/xlsx are defined as descriptors — plain objects that declare how to map between TypeScript options objects and XML. The same descriptor drives both directions: stringify (JSON → XML) and parse (XML → JSON).
Core Concepts
A descriptor describes the bidirectional mapping between a TypeScript options interface and its XML representation. There are two kinds:
| Kind | When to use | Example |
|---|---|---|
ElementDescriptor<T> | Declarative attr/child mapping | Simple elements with predictable structure |
CustomDescriptor<T> | Complex logic that doesn't fit the declarative model | Elements with conditional content, cross-references |
Both are consumed by the same runtime functions: stringify(desc, value, ctx) and parse(desc, element, ctx).
ElementDescriptor
A declarative mapping built with the element() fluent builder:
import { element } from "@office-open/core";
interface AlignmentOptions {
val?: string;
}
const alignmentDesc = element<AlignmentOptions>("w:jc").attr("val", "w:val").build();
The builder supports these chainable methods:
| Method | Purpose | Example |
|---|---|---|
.attr(key, xmlName, opts?) | Map a property to an XML attribute | .attr("val", "w:val") |
.child(key, tag, desc) | Map a property to a single child element | .child("spacing", "w:spacing", spacingDesc) |
.children(key, tag, desc) | Map a property to repeating child elements | .children("runs", "w:r", runDesc) |
.union(key, variants) | Map a property to one-of-several child elements | .union("fill", [{ tag: "a:solidFill", ... }]) |
.text(key) | Map a property to text content | .text("content") |
.custom(spec) | Add a custom content handler | .custom(myCustomDesc) |
A more complete example:
import { element, stringify, parse } from "@office-open/core";
interface SpacingOptions {
before?: number;
after?: number;
line?: number;
lineRule?: string;
}
const spacingDesc = element<SpacingOptions>("w:spacing")
.attr("before", "w:before")
.attr("after", "w:after")
.attr("line", "w:line")
.attr("lineRule", "w:lineRule")
.build();
// Write path: Options → XML string
const xml = stringify(spacingDesc, { before: 240, after: 120 }, ctx);
// <w:spacing w:before="240" w:after="120"/>
// Read path: XML Element → Options
const opts = parse(spacingDesc, spacingElement, readCtx);
// { before: "240", after: "120" }
CustomDescriptor
For elements with complex logic that doesn't fit the declarative model:
import type { CustomDescriptor } from "@office-open/core";
interface MyComplexOptions {
items: string[];
separator?: string;
}
const myComplexDesc: CustomDescriptor<MyComplexOptions> = {
kind: "custom",
stringify(value, ctx) {
const parts = value.items.map((item) => `<w:item val="${item}"/>`).join("");
return `<w:container>${parts}</w:container>`;
},
parse(el, ctx) {
const items = [...el.children]
.filter((c) => c.name === "w:item")
.map((c) => c.attributes?.["w:val"] ?? "");
return { items } as any;
},
};
Runtime Functions
The runtime is a single step in each direction — no intermediate representation:
import { stringify, parse } from "@office-open/core";
stringify(desc, value, ctx)
Serialize an options object to an XML string. Returns undefined when an optional element should be omitted.
const xml = stringify(spacingDesc, { before: 240 }, writeCtx);
parse(desc, element, ctx)
Parse an XML element into a partial options object.
const result = parse(spacingDesc, element, readCtx);
Attribute Options
The .attr() method accepts optional encode/decode functions and a default value:
element<MyOpts>("w:b")
.attr("val", "w:val", {
default: true, // skip when value equals default
encode: (v) => (v ? undefined : "0"), // custom XML value
decode: (raw) => raw !== "0", // custom JS value
})
.build();
OOXML Helpers
Common encode/decode patterns for OOXML value types:
import { boolEncode, boolDecode, enumEncode, enumDecode } from "@office-open/core";
| Helper | Pattern | Usage |
|---|---|---|
boolEncode | CT_OnOff: true → omit val, false → "0" | .attr("bold", "w:val", { encode: boolEncode }) |
boolDecode | Absent/"true"/"1" → true, "0"/"false" → false | .attr("bold", "w:val", { decode: boolDecode }) |
enumEncode(map) | JS value → XML value via mapping | .attr("align", "w:val", { encode: enumEncode(ALIGN_MAP) }) |
enumDecode(map) | XML value → JS value via inverse mapping | .attr("align", "w:val", { decode: enumDecode(ALIGN_MAP) }) |
Context Objects
WriteContext
Passed during stringify (write path):
interface WriteContext {
addRelationship(type: string, target: string, mode?: string): string;
addMedia(data: Uint8Array, type: string): string;
}
ReadContext
Passed during parse (read path):
interface ReadContext {
resolveRelationship(rId: string): string | undefined;
getPart(path: string): XmlElement | undefined;
getRaw(path: string): Uint8Array | undefined;
}
DescriptorRegistry
Registers all descriptors for coverage tracking. Each format package registers its descriptors at module load time:
import { DescriptorRegistry } from "@office-open/core";
DescriptorRegistry.register("w:p", paragraphDesc);
DescriptorRegistry.has("w:p"); // true
DescriptorRegistry.get("w:p"); // paragraphDesc
DescriptorRegistry.tags(); // Set of all registered XML tags
DescriptorRegistry.size; // number of registered descriptors
When to Use
You typically don't need to call stringify() and parse() directly — the format packages' top-level functions handle everything internally:
generateDocument()/generatePresentation()/generateWorkbook()— call stringify internallyparseDocument()/parsePresentation()/parseWorkbook()— call parse internally
Use the descriptor system directly when you need to:
- Define custom OOXML elements not covered by the format packages
- Build format-specific extensions or plugins
- Implement bidirectional (stringify + parse) support for new XML parts
Naming Conventions
| Convention | Example |
|---|---|
Descriptor names: <part>Desc | spacingDesc, settingsDesc, slideDesc |
Options interfaces: <Part>Options | WorkbookOptions, DocumentOptions |
Helper functions: stringify*() / parse*() | stringifyWorksheet(), parseWorkbook() |