CORE

Descriptor System

Declarative XML mapping for OOXML elements — stringify and parse

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:

KindWhen to useExample
ElementDescriptor<T>Declarative attr/child mappingSimple elements with predictable structure
CustomDescriptor<T>Complex logic that doesn't fit the declarative modelElements 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:

MethodPurposeExample
.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";
HelperPatternUsage
boolEncodeCT_OnOff: true → omit val, false → "0".attr("bold", "w:val", { encode: boolEncode })
boolDecodeAbsent/"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 internally
  • parseDocument() / 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

ConventionExample
Descriptor names: <part>DescspacingDesc, settingsDesc, slideDesc
Options interfaces: <Part>OptionsWorkbookOptions, DocumentOptions
Helper functions: stringify*() / parse*()stringifyWorksheet(), parseWorkbook()
Copyright © 2026