# Extract and reuse a style guide

> Read a DOCX's theme, document defaults, and styles into an editable guide, write the edits back, and apply the result to new drafts as a template.



A firm's own documents already define its house style: the theme, the document defaults, and the named styles. This guide reads them into an editable style guide, writes the guide's edits back, saves the result as a template package, and applies that template to a draft.

Every step uses the Document API. Nothing in it unzips the DOCX or parses XML.

## Run the complete workflow [#run-the-complete-workflow]

The example runs server-side with the Node SDK. Put the firm's document next to it as `firm-document.docx` and a draft as `draft.docx`:

```mjs
import { writeFile } from 'node:fs/promises';
import path from 'node:path';
import { SuperDocClient } from '@superdoc/sdk';

// Text from a heading in the firm's document. Its run has no direct
// formatting, so everything query.match reports for it comes from styles.
const SAMPLE_TEXT = 'Master Services Agreement';

const client = new SuperDocClient({
  user: { name: 'Style guide builder', email: 'styles@example.com' },
  // Allow a slow first start of the document host, for example on a cold server.
  startupTimeoutMs: 30_000,
});

try {
  await client.connect();

  // 1. Extract an editable guide from the firm's document.
  const firm = await client.open({ doc: './firm-document.docx' });
  try {
    const guide = await extractGuide(firm);
    await writeFile('./style-guide.json', JSON.stringify(guide, null, 2));

    // 2. Edit the guide. In an application, a person does this in your UI.
    editGuide(guide);

    // 3. Write the edits back and save the result as a template package.
    await writeGuide(firm, guide);
    await firm.save({ out: './style-guide-template.docx', force: true });
  } finally {
    await firm.close({ discard: true });
  }

  // 4. Preview the template on a draft, then apply it.
  const draft = await client.open({ doc: './draft.docx' });
  try {
    /** @type {{ kind: 'path', path: string }} */
    const source = { kind: 'path', path: path.resolve('./style-guide-template.docx') };

    // The SDK throws when an operation fails, so each result here is a success.
    const preview = await draft.templates.apply({ source, dryRun: true });
    console.log('Would apply:', preview.appliedScopes.map((entry) => entry.scope));
    console.log('Skipped:', preview.skippedScopes);
    console.log('Never applied:', preview.unsupportedItems);
    console.log('Warnings:', preview.warnings);

    await draft.templates.apply({ source });
    await draft.save({ out: './draft.styled.docx', force: true });
  } finally {
    await draft.close({ discard: true });
  }
} finally {
  await client.dispose();
}

/**
 * Read the theme, document defaults, every paragraph and character style, and
 * the effective formatting of one sample run.
 *
 * @param {any} doc
 */
async function extractGuide(doc) {
  const { theme } = await doc.styles.getTheme({});
  if (theme.source === 'fallback') {
    console.warn('The document has no usable theme. Theme values are the Office defaults.');
  }

  // `props` and `runProps` keep theme references raw. `resolved` and
  // `resolvedRun` follow basedOn, document defaults, and the theme.
  // Provenance says which layer each resolved value came from.
  const model = await doc.get({ options: { includeResolved: true, includeProvenance: true } });
  const catalog = await doc.styles.getCatalog({ includePreview: true });

  const styles = [];
  for (const item of catalog.styles) {
    const paragraphStyle = model.styles?.paragraph?.[item.id];
    const characterStyle = model.styles?.character?.[item.id];
    if (!paragraphStyle && !characterStyle) continue;

    styles.push({
      id: item.id,
      name: item.name,
      type: paragraphStyle ? 'paragraph' : 'character',
      basedOn: item.basedOn,
      next: item.next,
      metadata: {
        aliases: item.aliases,
        locked: item.locked,
        custom: item.custom,
        priority: item.priority,
        qFormat: item.qFormat,
        hidden: item.hidden,
        semiHidden: item.semiHidden,
        unhideWhenUsed: item.unhideWhenUsed,
      },
      // styles.create cannot redefine half of a linked pair, such as Heading 1.
      editable: item.link === null,
      authored: paragraphStyle
        ? { paragraph: paragraphStyle.props, run: paragraphStyle.runProps }
        : { run: characterStyle.props },
      resolved: paragraphStyle
        ? { paragraph: paragraphStyle.resolved, run: paragraphStyle.resolvedRun }
        : { run: characterStyle.resolved },
      provenance: paragraphStyle
        ? { paragraph: paragraphStyle.provenance, run: paragraphStyle.runProvenance }
        : { run: characterStyle.provenance },
      // CSS-ready values with their origin: `theme` or `literal`.
      preview: item.preview?.css,
      previewSources: item.preview?.sources,
    });
  }

  const match = await doc.query.match({
    select: { type: 'text', pattern: SAMPLE_TEXT },
    require: 'first',
  });
  const sample = match.items[0];
  const sampleRun = sample?.matchKind === 'text' ? sample.blocks[0]?.runs[0] : undefined;

  return {
    theme: {
      source: theme.source,
      colors: theme.colorScheme,
      fonts: theme.fontScheme,
      colorMapping: theme.colorSchemeMapping,
    },
    defaults: {
      run: model.docDefaults?.run,
      paragraph: model.docDefaults?.paragraph,
      resolvedRun: model.docDefaults?.resolvedRun,
    },
    styles,
    sample: sampleRun
      ? { text: SAMPLE_TEXT, styleId: sampleRun.styleId, effective: sampleRun.styles.effective }
      : null,
  };
}

/**
 * Stand-in for a person editing the guide.
 *
 * @param {any} guide
 */
function editGuide(guide) {
  // Body text: 12 pt instead of the firm's default.
  guide.defaults.run = { ...guide.defaults.run, fontSize: 12 };
  guide.defaults.changed = true;

  // Clause references: the primary accent color instead of a shaded secondary accent.
  const clause = guide.styles.find((/** @type {any} */ style) => style.id === 'ClauseRef');
  if (clause?.editable) {
    clause.authored.run = { ...clause.authored.run, color: { model: 'theme', theme: 'accent1' } };
    clause.changed = true;
  }

  // A new paragraph style for quoted language.
  guide.styles.push({
    id: 'GuideQuote',
    name: 'Guide Quote',
    type: 'paragraph',
    basedOn: 'Normal',
    next: null,
    metadata: { custom: true, qFormat: true },
    editable: true,
    authored: {
      paragraph: { spacing: { before: 6, after: 6 } },
      run: { italic: true, color: { model: 'theme', theme: 'accent2' } },
    },
    changed: true,
  });
}

/**
 * Write changed guide entries back to the open document.
 *
 * @param {any} doc
 * @param {any} guide
 */
async function writeGuide(doc, guide) {
  if (guide.defaults.changed) {
    await doc.styles.apply({
      target: { scope: 'docDefaults', channel: 'run' },
      patch: { fontSize: toHalfPoints(guide.defaults.run.fontSize) },
    });
  }

  for (const style of guide.styles) {
    if (!style.changed) continue;
    if (!style.editable) {
      console.warn(`Skipped ${style.id}: linked styles cannot be redefined yet.`);
      continue;
    }

    // styles.create replaces the whole definition, so send every property to keep.
    await doc.styles.create({
      id: style.id,
      name: style.name,
      type: style.type,
      basedOn: style.basedOn,
      ...(style.type === 'paragraph' ? { next: style.next } : {}),
      ...withoutNulls(style.metadata),
      ...(style.type === 'paragraph' ? { paragraph: toParagraphPatch(style.authored.paragraph) } : {}),
      run: toRunPatch(style.authored.run),
      conflictPolicy: 'replace',
    });
  }
}

// Reads report points and SDColorRef values. Style writes take the units
// Word stores: half-points for font size, twentieths of a point for spacing,
// and hex strings for theme tint and shade.

/** @param {any} run */
function toRunPatch(run = {}) {
  /** @type {Record<string, unknown>} */
  const patch = {};
  for (const [key, value] of Object.entries(run)) {
    if (key === 'bold' || key === 'italic') patch[key] = value;
    else if (key === 'fontSize') patch.fontSize = toHalfPoints(value);
    else if (key === 'color') patch.color = toColorPatch(value);
    else if (key === 'fontFamily') patch.fontFamily = { ascii: value, hAnsi: value };
    else if (key === 'fonts') patch.fontFamily = toFontPatch(value);
    else throw new Error(`This example does not write run property "${key}".`);
  }
  return patch;
}

/** @param {any} paragraph */
function toParagraphPatch(paragraph = {}) {
  /** @type {Record<string, unknown>} */
  const patch = {};
  for (const [key, value] of Object.entries(paragraph)) {
    if (key === 'keepWithNext') patch.keepNext = value;
    else if (key === 'spacing') {
      const { before, after, ...rest } = value;
      if (Object.keys(rest).length > 0) throw new Error('This example writes spacing before and after only.');
      patch.spacing = withoutNulls({ before: toTwips(before), after: toTwips(after) });
    } else throw new Error(`This example does not write paragraph property "${key}".`);
  }
  return patch;
}

/** @param {any} color */
function toColorPatch(color) {
  if (color.model === 'rgb') return { val: color.value };
  if (color.model === 'auto') return { val: 'auto' };
  return withoutNulls({
    themeColor: color.theme,
    themeTint: toHexByte(color.tint),
    themeShade: toHexByte(color.shade),
  });
}

/** @param {any} fonts */
function toFontPatch(fonts) {
  const { csTheme, ...rest } = fonts;
  return withoutNulls({ ...rest, cstheme: csTheme });
}

/** @param {number} points */
function toHalfPoints(points) {
  return Math.round(points * 2);
}

/** @param {number | undefined} points */
function toTwips(points) {
  return points === undefined ? undefined : Math.round(points * 20);
}

/** @param {number | undefined} byte */
function toHexByte(byte) {
  return byte === undefined ? undefined : byte.toString(16).toUpperCase().padStart(2, '0');
}

/** @param {Record<string, unknown>} value */
function withoutNulls(value) {
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null));
}

```

It writes three files:

| File                        | Contents                                                    |
| --------------------------- | ----------------------------------------------------------- |
| `style-guide.json`          | The extracted guide, before edits                           |
| `style-guide-template.docx` | The firm's document with the guide edits applied            |
| `draft.styled.docx`         | The draft with the template adopted. Its text is unchanged. |

The guide format is the example's own. Keep the fields your application needs.

## Raw, resolved, and provenance values [#raw-resolved-and-provenance-values]

`doc.get()` returns `styles`, `theme`, and `docDefaults` next to the body. Read options decide how much of each style you get:

| Field                         | Present with              | Contents                                                                                                                              |
| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `props`, `runProps`           | always                    | What the style itself sets. Theme references stay raw, such as `asciiTheme: 'majorHAnsi'` and `{ model: 'theme', theme: 'accent1' }`. |
| `resolved`, `resolvedRun`     | `includeResolved: true`   | The value after `basedOn`, document defaults, and the theme. A theme font becomes a family name and a theme color becomes RGB.        |
| `provenance`, `runProvenance` | `includeProvenance: true` | For each resolved value: `source` (`style`, `docDefaults`, ...), `sourceRef`, and the `basedOn` chain.                                |

Keep the raw value when the guide should stay tied to the theme, so a later theme change carries through. Show the resolved value to a person. Use provenance to tell a style's own setting from an inherited one.

`docDefaults` follows the same pattern: `run` and `paragraph` as authored, and `resolvedRun` and `resolvedParagraph` with `includeResolved`.

## Theme fonts and colors [#theme-fonts-and-colors]

`styles.getTheme()` returns the theme on its own: the color scheme as six-digit hex values, the major and minor fonts, and the color mapping from `word/settings.xml`. It is the same theme `doc.get()` returns.

Check `theme.source`. When the document has no theme part or the part cannot be read, the result is the default Office theme, labeled `fallback`, and `diagnostics` says why. A guide built from a fallback theme describes Office defaults, not the firm.

`styles.getCatalog({ includePreview: true })` adds CSS-ready values to each style. `preview.sources` says where `fontFamily` and `color` came from:

```json
{
  "css": { "fontFamily": "Georgia", "color": "#0B6E4F", "fontSize": "16pt" },
  "sources": {
    "fontFamily": { "origin": "theme", "themeFont": "majorHAnsi" },
    "color": { "origin": "theme", "themeColor": "accent1" }
  }
}
```

A literal value reports `{ "origin": "literal" }`. A theme color with a tint or shade carries `tint` or `shade`, a byte from 0 to 255. A value resolved against the fallback theme carries `fallback: true`.

## Effective formatting of real text [#effective-formatting-of-real-text]

The style definitions say what a style sets. `query.match()` says what a run of text actually renders with, after its paragraph style, character style, direct formatting, document defaults, and the theme:

```ts
const match = await doc.query.match({
  select: { type: 'text', pattern: 'Master Services Agreement' },
  require: 'first',
});
const run = match.items[0].blocks[0].runs[0];

run.styles.effective; // { bold: false, fontFamily: 'Georgia', color: '#0b6e4f', fontSizePt: 16, sources: { ... } }
run.styles.direct; // what the run sets itself: { bold: 'clear', italic: 'clear', ... }
```

`match.meta.effectiveResolved` is `true` when the effective values went through the style cascade. Top-level values such as `run.styles.fontFamily` stay direct-only.

## Write the edits back [#write-the-edits-back]

Two operations write a guide back to a document:

* `styles.apply` changes the document defaults. It takes a patch and changes only the keys you send.
* `styles.create` defines or redefines a paragraph or character style. With `conflictPolicy: 'replace'`, the input replaces the whole definition: a property you leave out is removed. Send every property the style should keep, including its metadata.

Reads report points and `SDColorRef` values. Writes take the units Word stores:

| Value               | Read as                                            | Write as                                      |
| ------------------- | -------------------------------------------------- | --------------------------------------------- |
| Font size           | points: `12`                                       | half-points: `24`                             |
| Spacing and indents | points: `6`                                        | twentieths of a point: `120`                  |
| RGB color           | `{ model: 'rgb', value: '1F3864' }`                | `{ val: '1F3864' }`                           |
| Theme color         | `{ model: 'theme', theme: 'accent2', shade: 191 }` | `{ themeColor: 'accent2', themeShade: 'BF' }` |
| Theme font          | `fonts: { asciiTheme: 'majorHAnsi' }`              | `fontFamily: { asciiTheme: 'majorHAnsi' }`    |

The example converts the properties it uses and throws on any other, so a property it does not know is never dropped without notice.

## Apply the template [#apply-the-template]

The example saves the edited firm document as the template. `templates.apply` adopts its styles, numbering, settings, theme, font table, headers and footers, and page setup into the draft. It never copies body content.

Preview first. A dry run returns the receipt the apply would return and leaves the draft unchanged. Check `appliedScopes`, `skippedScopes`, `unsupportedItems`, and `warnings` before applying. `DOC_DEFAULTS_ADOPTED` means the draft's document defaults were replaced. [Apply a template](/document-api/templates) covers each scope and receipt field.

After applying, read the draft the same way you read the firm's document. `query.match()` on a heading shows whether it now renders with the guide's font and color.

## Limits [#limits]

* **Linked styles.** `styles.create` cannot redefine half of a linked pair. Word links Heading 1 to Heading 1 Char, so built-in headings cannot be edited through the guide yet. The catalog's `link` field names the other half. The example marks those styles read-only.
* **Theme edits.** No operation writes the theme. The template carries the firm document's theme unchanged.
* **Table styles.** `doc.get()` reads table styles, including conditional formatting per region. `styles.create` writes paragraph and character styles only.
* **Headers and footers.** The template's headers and footers are adopted with the rest. Use a source document without them if drafts should keep their own.
* **Applying again.** Applying the same template a second time adds another copy of its headers, footers, and list definitions. Apply it once per draft.
* **Shared rooms.** `templates.apply` is not available in a shared room. Apply the template before the draft joins a room.
* **Browser.** In the browser editor, pass the template as base64. A `path` source needs a file system.

See the reference for [`doc.get`](/document-api/reference/get), [`styles.getTheme`](/document-api/reference/styles/get-theme), [`styles.getCatalog`](/document-api/reference/styles/get-catalog), [`query.match`](/document-api/reference/query/match), [`styles.apply`](/document-api/reference/styles/apply), [`styles.create`](/document-api/reference/styles/create), and [`templates.apply`](/document-api/reference/templates/apply).
