@openpresentation/opf 0.26.10 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/browser.js +20 -17
- package/dist/{catalog-refs-BSfGELpt.d.ts → catalog-refs-kAefsZWT.d.ts} +2 -1
- package/dist/{catalog-schemas-DoxA2BgY.d.ts → catalog-schemas-CC1d_h8s.d.ts} +3 -2
- package/dist/{chart-data-BnNqgnC-.d.ts → chart-data-Ci9s_NDN.d.ts} +2 -1
- package/dist/{chunk-XRMV2GMB.js → chunk-2CUCYENF.js} +758 -232
- package/dist/chunk-43JWUIGF.js +8 -0
- package/dist/{chunk-F4HFDP6I.js → chunk-5Q4IYLMW.js} +31 -15
- package/dist/{chunk-GIWNHRTX.js → chunk-BQUWRXTR.js} +4 -4
- package/dist/chunk-CR72ZJ6W.js +236 -0
- package/dist/{chunk-L52N5FZZ.js → chunk-F6DCEGB3.js} +6 -6
- package/dist/chunk-GM7WFPGG.js +10 -0
- package/dist/chunk-JBARNYFR.js +23 -0
- package/dist/{chunk-EQ6S5SEZ.js → chunk-KELC4K62.js} +1 -1
- package/dist/{chunk-77KL2PMM.js → chunk-KXKN3W5Z.js} +3 -3
- package/dist/{chunk-D25BNPTW.js → chunk-LLEXM7TQ.js} +4 -4
- package/dist/{chunk-3S2XVDPN.js → chunk-MTFK7QEY.js} +6 -6
- package/dist/{chunk-TKQ2VFV5.js → chunk-N77N4AOD.js} +1 -1
- package/dist/{chunk-NVT2YDVC.js → chunk-OAIAFXMS.js} +2 -2
- package/dist/{chunk-PHP45MO7.js → chunk-P6PKYJTS.js} +1 -1
- package/dist/{chunk-H7XMO36O.js → chunk-PCU4XGKV.js} +5 -5
- package/dist/{chunk-Y4TZCE7R.js → chunk-SEZ4EPW6.js} +2 -2
- package/dist/{chunk-Y25DYRME.js → chunk-TNUTRV6E.js} +1 -1
- package/dist/{chunk-G5WJMJV3.js → chunk-UOHWG4OS.js} +1 -1
- package/dist/{chunk-A54NGT2K.js → chunk-VGGPAMP7.js} +2 -2
- package/dist/{chunk-IVPA6NG6.js → chunk-WPKCWQ6W.js} +494 -120
- package/dist/{chunk-VZBFQLCA.js → chunk-XZ23FORL.js} +6 -6
- package/dist/chunk-ZROVYWGU.js +1 -0
- package/dist/{composition-DIftf6iP.d.ts → composition-C-hd_Ci3.d.ts} +134 -14
- package/dist/composition.d.ts +3 -3
- package/dist/composition.js +2 -1
- package/dist/conversion-XJTDWQ5W.js +24 -0
- package/dist/{conversion-ChmGA8Ul.d.ts → conversion-hbZeS5kD.d.ts} +8 -7
- package/dist/convert.d.ts +6 -5
- package/dist/convert.js +9 -7
- package/dist/data.d.ts +4 -3
- package/dist/data.js +3 -2
- package/dist/{deck-report-5vwMGcQz.d.ts → deck-report-DQMsvyBI.d.ts} +5 -4
- package/dist/deck.d.ts +9 -8
- package/dist/deck.js +13 -11
- package/dist/diff.d.ts +3 -2
- package/dist/diff.js +6 -4
- package/dist/docs.d.ts +2 -1
- package/dist/docs.js +33 -19
- package/dist/examples.d.ts +3 -2
- package/dist/examples.js +92 -50
- package/dist/{explain-JSuESV-Y.d.ts → explain-iZeNOUc6.d.ts} +2 -1
- package/dist/font-policy.d.ts +2 -1
- package/dist/font-policy.js +1 -0
- package/dist/format.d.ts +2 -1
- package/dist/format.js +3 -2
- package/dist/ids.d.ts +114 -0
- package/dist/ids.js +6 -0
- package/dist/index.d.ts +40 -16
- package/dist/index.js +24 -21
- package/dist/markdown.d.ts +8 -7
- package/dist/markdown.js +10 -8
- package/dist/node-engine.d.ts +33 -10
- package/dist/node-engine.js +37 -28
- package/dist/{pagination-D_kJe4jH.d.ts → pagination-DyAKYpXu.d.ts} +4 -3
- package/dist/pagination.d.ts +4 -4
- package/dist/pagination.js +6 -4
- package/dist/patch.d.ts +89 -4
- package/dist/patch.js +5 -3
- package/dist/precompiled.d.ts +2 -0
- package/dist/precompiled.js +7 -0
- package/dist/{presentation-Bwo8Ko6D.d.ts → presentation-8jLSTFqL.d.ts} +11 -11
- package/dist/repo-readme.js +3 -1
- package/dist/schema-engine-ajv.js +37 -0
- package/dist/schema-engine-precompiled.js +3 -0
- package/dist/schema.d.ts +4 -3
- package/dist/schema.js +5 -3
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +2 -1
- package/dist/source.d.ts +5 -4
- package/dist/source.js +12 -10
- package/dist/spec/README.md +1 -1
- package/dist/spec/schemas/audience.schema.json +1 -0
- package/dist/spec/schemas/catalog-index.schema.json +1 -0
- package/dist/spec/schemas/catalog-manifest.schema.json +1 -0
- package/dist/spec/schemas/chart-type.schema.json +1 -0
- package/dist/spec/schemas/color-scheme.schema.json +1 -0
- package/dist/spec/schemas/finding.schema.json +1 -0
- package/dist/spec/schemas/font-scheme.schema.json +1 -0
- package/dist/spec/schemas/language.schema.json +1 -0
- package/dist/spec/schemas/layout-preview-index.schema.json +1 -0
- package/dist/spec/schemas/layout.schema.json +8 -6
- package/dist/spec/schemas/narrative.schema.json +1 -0
- package/dist/spec/schemas/opf.schema.json +9 -8
- package/dist/spec/schemas/purpose.schema.json +1 -0
- package/dist/spec/schemas/social-platform.schema.json +1 -0
- package/dist/spec/schemas/theme.schema.json +3 -1
- package/dist/spec/schemas/tone.schema.json +1 -0
- package/dist/spec-files.d.ts +2 -1
- package/dist/spec-files.js +1 -0
- package/dist/symbol-font-encodings.d.ts +2 -1
- package/dist/symbol-font-encodings.js +2 -0
- package/dist/types.d.ts +9 -9
- package/dist/{validation-types-3ARvTsB0.d.ts → validation-types-mNmfcY9A.d.ts} +4 -3
- package/dist/validator.d.ts +8 -8
- package/dist/validator.js +5 -3
- package/dist/yaml.d.ts +8 -7
- package/dist/yaml.js +9 -7
- package/package.json +30 -9
- package/dist/conversion-IVKCKMCN.js +0 -21
package/dist/docs.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import './chunk-GM7WFPGG.js';
|
|
2
|
+
|
|
1
3
|
// src/generated/docs.ts
|
|
2
4
|
var docsData = Object.freeze([
|
|
3
5
|
{
|
|
4
6
|
"slug": "agent-skills",
|
|
5
7
|
"file": "docs/agent-skills.md",
|
|
6
8
|
"title": "AI agent skills for OPF",
|
|
7
|
-
"markdown": "# AI agent skills for OPF\n\nThe repository ships six reusable skills in `skills/`. Each folder has a `SKILL.md` entrypoint and optional references, assets, or scripts. `agents/openai.yaml` supplies Codex display metadata; the instructions themselves are Markdown and do not require a hosted service.\n\n| Skill | Use it for |\n| --- | --- |\n| [opf-author](../skills/opf-author/SKILL.md) | Turn briefs and source material into valid OPF content; includes a complete starter deck |\n| [opf-layout](../skills/opf-layout/SKILL.md) | Dynamic composition, nested groups, promoted regions, overflow repair, and pagination |\n| [opf-presets](../skills/opf-presets/SKILL.md) | Catalog discovery, design inheritance, gallery reuse, colors, themes, and fonts |\n| [opf-edit](../skills/opf-edit/SKILL.md) | Precise JSON Patch edits, undo, canvas/schema integration, and copy/import |\n| [opf-export](../skills/opf-export/SKILL.md) | Browser previews, SVG/PNG/PDF/PPTX, assets, fonts, and export verification |\n| [opf-inspect](../skills/opf-inspect/SKILL.md) | Exact schema fields, catalog IDs, validation errors, and reference warnings |\n\nLoad only the skills relevant to the request. They distinguish the portable format from current renderer/editor capabilities, and distinguish imported document instructions from the user's request. They do not authorize publishing, sending decks, or changing unrelated project configuration.\n\n## Use from a checkout\n\nAn agent can read the entrypoint directly, for example:\n\n> Use `skills/opf-author/SKILL.md` to create a decision brief in OPF, then validate it using `skills/opf-inspect/SKILL.md`.\n\nThe root `AGENTS.md` points repository agents to these entrypoints. Skills read the current project's schema and package exports instead of hardcoding a historical field count or assuming a public package has unreleased APIs.\n\n## Install in an agent environment\n\nPublished CLI 0.24.1 bundles all six complete skill folders. Use Node 24 for the current packages and this checkout. From your project directory:\n\n```sh\nnpx @openpresentation/cli@0.24.1 skills install\n```\n\nThe default installs copies into `.agents/skills` in the current project, suitable for agents including Codex. It does not change AGENTS.md or any agent configuration. No symlink privileges, paid service, API key or AI provider is required. npm downloads the CLI on first use; the installed CLI then installs its bundled skills without network access. The explicit 0.24.1 pin makes this installation repeatable and matches the [current package train](compatibility-matrix.md). For CLI source development, use `node packages/cli/dist/index.js skills install` after building.\n\n| Target | Project directory | Personal directory with `--global` |\n| --- | --- | --- |\n| Default / `--agent universal` | `.agents/skills` | `~/.agents/skills` |\n| `--agent codex` | `.agents/skills` | `~/.codex/skills` |\n| `--agent claude-code` | `.claude/skills` | `~/.claude/skills` |\n| `--agent cursor` | `.cursor/skills` | `~/.cursor/skills` |\n\nFor example, `npx @openpresentation/cli@0.24.1 skills install --agent codex --global` installs personal Codex skills. For another compatible agent use `--directory <its-skills-directory>`; this option cannot be combined with `--agent` or `--global`. Restart or reload your agent if its skill discovery requires it. A compatible client can invoke the installed skills with names such as `$opf-author` or `$opf-inspect`.\n\nInspect or update the same destination:\n\n```sh\nnpx @openpresentation/cli@0.24.1 skills status\nnpx @openpresentation/cli@0.24.1 skills update\n```\n\nSupply the same target options used for installation. `status` is read-only and compares against the invoked CLI's bundled version; it does not query npm for newer releases. Repeated installation is idempotent. Updates check every installed file before changing any skill. Modified, added, deleted or unmanaged files cause the command to stop and list the conflicting folders; keep your customizations, move those folders outside the active skills directory, then retry. There is no force-overwrite option. A successful update returns backup paths outside the active skills directory for recovering the previous managed versions. Keep those backups until you have reviewed the update. Do not run concurrent writers: the installer lock coordinates other installer runs, but cannot lock an external editor.\n\nNo skills are installed by the repository build. Manual installation remains supported: copy whole folders from `skills/`, including references and scripts, to your agent's skill directory. Each folder is self-contained. The managed installer treats existing manual copies as unmanaged and preserves them.\n\nThe inspection helper requires Node 24 and `@openpresentation/opf` in the current project. In this checkout, build with `pnpm build` first. For an installed skill used outside the checkout, either run from an npm project that has the package or set `OPF_ROOT` to the built OPF checkout. It does not install dependencies, fetch catalogs, or modify input files.\n\n## Local CLI\n\nThe [installable CLI](../packages/cli/README.md) complements these skills with `opf create`, `opf validate`, `opf edit`, and schema/catalog lookup. It runs on the core it depends on; the inspection skill helper instead resolves the host project's core package. Check versions when moving between them. From code, core's `@openpresentation/opf` offers `convert`, `open` and `save` in Node (see the opf-export skill).\n\n## Examples of requests\n\n- \u201CUse $opf-author to turn these notes into a five-slide decision brief. Keep every factual claim sourced.\u201D\n- \u201CUse $opf-layout to fix this overflow without losing any text or notes.\u201D\n- \u201CUse $opf-presets to apply our brand colors while preserving slide-specific overrides.\u201D\n- \u201CUse $opf-edit to replace one table and retain all other document fields.\u201D\n- \u201CUse $opf-export to export the same reviewed slides to SVG and PPTX.\u201D (PPTX text is editable one line at a time unless you opt in to `--body-text native`, from opf-pptx and CLI 0.22.0; the skill says so.)\n- \u201CUse $opf-inspect to explain which background forms the installed schema accepts.\u201D\n\n## Maintenance\n\n`pnpm test:skills` checks skill links, schema-valid examples, and the inspection helper's actual behavior, including a copied standalone skill and a package installed in a consumer project. Run the skill-creator frontmatter validator when editing skill metadata. Behavioral tests are not evidence that every renderer option is visually complete.\n\nWhen schema/package APIs change, update only the affected skill/reference and its executable examples. Keep option lists in the canonical schema and catalogs. The format package and skill folders are separate distribution surfaces: CLI 0.24.1 includes the six skills; the core `@openpresentation/opf` package does not install agent configuration. Repository skill prose can be newer than the immutable snapshot bundled in an already-published CLI. These source documentation corrections do not change the CLI 0.24.1 tarball; `skills status` and `skills update` compare against the invoked CLI's bundled snapshot.\n"
|
|
9
|
+
"markdown": "# AI agent skills for OPF\n\nThe repository ships six reusable skills in `skills/`. Each folder has a `SKILL.md` entrypoint and optional references, assets, or scripts. `agents/openai.yaml` supplies Codex display metadata; the instructions themselves are Markdown and do not require a hosted service.\n\n| Skill | Use it for |\n| --- | --- |\n| [opf-author](../skills/opf-author/SKILL.md) | Turn briefs and source material into valid OPF content; includes a complete starter deck |\n| [opf-layout](../skills/opf-layout/SKILL.md) | Dynamic composition, nested groups, promoted regions, overflow repair, and pagination |\n| [opf-presets](../skills/opf-presets/SKILL.md) | Catalog discovery, design inheritance, gallery reuse, colors, themes, and fonts |\n| [opf-edit](../skills/opf-edit/SKILL.md) | Precise JSON Patch edits, undo, canvas/schema integration, and copy/import |\n| [opf-export](../skills/opf-export/SKILL.md) | Browser previews, SVG/PNG/PDF/PPTX, assets, fonts, and export verification |\n| [opf-inspect](../skills/opf-inspect/SKILL.md) | Exact schema fields, catalog IDs, validation errors, and reference warnings |\n\nLoad only the skills relevant to the request. They distinguish the portable format from current renderer/editor capabilities, and distinguish imported document instructions from the user's request. They do not authorize publishing, sending decks, or changing unrelated project configuration.\n\n## Use from a checkout\n\nAn agent can read the entrypoint directly, for example:\n\n> Use `skills/opf-author/SKILL.md` to create a decision brief in OPF, then validate it using `skills/opf-inspect/SKILL.md`.\n\nThe root `AGENTS.md` points repository agents to these entrypoints. Skills read the current project's schema and package exports instead of hardcoding a historical field count or assuming a public package has unreleased APIs.\n\n## Install in an agent environment\n\nPublished CLI 0.24.1 bundles all six complete skill folders. Use Node 24 for the current packages and this checkout. From your project directory:\n\n```sh\nnpx @openpresentation/cli@0.24.1 skills install\n```\n\nThe default installs copies into `.agents/skills` in the current project, suitable for agents including Codex. It does not change AGENTS.md or any agent configuration. No symlink privileges, paid service, API key or AI provider is required. npm downloads the CLI on first use; the installed CLI then installs its bundled skills without network access. The explicit 0.24.1 pin makes this installation repeatable and matches the [current package train](compatibility-matrix.md). For CLI source development, use `node packages/cli/dist/index.js skills install` after building.\n\n| Target | Project directory | Personal directory with `--global` |\n| --- | --- | --- |\n| Default / `--agent universal` | `.agents/skills` | `~/.agents/skills` |\n| `--agent codex` | `.agents/skills` | `~/.codex/skills` |\n| `--agent claude-code` | `.claude/skills` | `~/.claude/skills` |\n| `--agent cursor` | `.cursor/skills` | `~/.cursor/skills` |\n\nFor example, `npx @openpresentation/cli@0.24.1 skills install --agent codex --global` installs personal Codex skills. For another compatible agent use `--directory <its-skills-directory>`; this option cannot be combined with `--agent` or `--global`. Restart or reload your agent if its skill discovery requires it. A compatible client can invoke the installed skills with names such as `$opf-author` or `$opf-inspect`.\n\nInspect or update the same destination:\n\n```sh\nnpx @openpresentation/cli@0.24.1 skills status\nnpx @openpresentation/cli@0.24.1 skills update\n```\n\nSupply the same target options used for installation. `status` is read-only and compares against the invoked CLI's bundled version; it does not query npm for newer releases. Repeated installation is idempotent. Updates check every installed file before changing any skill. Modified, added, deleted or unmanaged files cause the command to stop and list the conflicting folders; keep your customizations, move those folders outside the active skills directory, then retry. There is no force-overwrite option. A successful update returns backup paths outside the active skills directory for recovering the previous managed versions. Keep those backups until you have reviewed the update. Do not run concurrent writers: the installer lock coordinates other installer runs, but cannot lock an external editor.\n\nNo skills are installed by the repository build. Manual installation remains supported: copy whole folders from `skills/`, including references and scripts, to your agent's skill directory. Each folder is self-contained. The managed installer treats existing manual copies as unmanaged and preserves them.\n\nThe inspection helper requires Node 24 and `@openpresentation/opf` in the current project. In this checkout, build with `pnpm build` first. For an installed skill used outside the checkout, either run from an npm project that has the package or set `OPF_ROOT` to the built OPF checkout. It does not install dependencies, fetch catalogs, or modify input files.\n\n## Local CLI\n\nThe [installable CLI](../packages/cli/README.md) complements these skills with `opf create`, `opf validate`, `opf edit`, and schema/catalog lookup. A deck is done only when `opf validate deck.opf.json --fail-on warning` passes. It runs on the core it depends on; the inspection skill helper instead resolves the host project's core package. Check versions when moving between them. From code, core's `@openpresentation/opf` offers `convert`, `open` and `save` in Node (see the opf-export skill).\n\n## Examples of requests\n\n- \u201CUse $opf-author to turn these notes into a five-slide decision brief. Keep every factual claim sourced.\u201D\n- \u201CUse $opf-layout to fix this overflow without losing any text or notes.\u201D\n- \u201CUse $opf-presets to apply our brand colors while preserving slide-specific overrides.\u201D\n- \u201CUse $opf-edit to replace one table and retain all other document fields.\u201D\n- \u201CUse $opf-export to export the same reviewed slides to SVG and PPTX.\u201D (PPTX text is editable one line at a time unless you opt in to `--body-text native`, from opf-pptx and CLI 0.22.0; the skill says so.)\n- \u201CUse $opf-inspect to explain which background forms the installed schema accepts.\u201D\n\n## Maintenance\n\n`pnpm test:skills` checks skill links, schema-valid examples, and the inspection helper's actual behavior, including a copied standalone skill and a package installed in a consumer project. Run the skill-creator frontmatter validator when editing skill metadata. Behavioral tests are not evidence that every renderer option is visually complete.\n\nWhen schema/package APIs change, update only the affected skill/reference and its executable examples. Keep option lists in the canonical schema and catalogs. The format package and skill folders are separate distribution surfaces: CLI 0.24.1 includes the six skills; the core `@openpresentation/opf` package does not install agent configuration. Repository skill prose can be newer than the immutable snapshot bundled in an already-published CLI. These source documentation corrections do not change the CLI 0.24.1 tarball; `skills status` and `skills update` compare against the invoked CLI's bundled snapshot.\n"
|
|
8
10
|
},
|
|
9
11
|
{
|
|
10
12
|
"slug": "catalog-schema-reference",
|
|
11
13
|
"file": "docs/catalog-schema-reference.md",
|
|
12
14
|
"title": "OPF Catalog Schema Reference",
|
|
13
|
-
"markdown": "# OPF Catalog Schema Reference\n\nCatalog records are reusable presets that OPF documents reference by id. This page summarizes every catalog companion schema in `spec/schemas/`. The top-level presentation schema has its own [schema reference](schema-reference.md), and the report format every tool shares has the [validation results reference](finding-schema-reference.md).\n\nOPF documents usually reference these records with string ids such as `design.theme = \"minimal\"`, `tone = \"formal\"`, or `chart.type = \"line\"`. Dense examples may also embed catalog sources or inline records under `catalogs`.\n\n## Audience\n\n- File: `spec/schemas/audience.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-audience/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for audience records. Each record names an audience archetype (e.g. 'executive', 'engineering-team', 'investor') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Documents reference an audience from 'audience', which also accepts free-form text. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-audience/v1\"` | Identifies a published record file as a audience record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The audience's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this f... |\n| `name` | yes | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience who they are and what they care about. |\n| `description` | no | `string` | Longer prose describing the audience archetype and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. Engines use this as a hint for default depth and pacing. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. AI generation uses this to decide whether to expand or assume technical terminology. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, to advise, or to actually decide. Shapes the strength of the closing ask. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on this audience's focused attention for a single presentation, in minutes. Used as a hint when comparing against duration and the resolved narrative's duration range. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this audience. Used by picker UIs to suggest narratives once an audience is chosen. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this audience. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Catalog Index\n\n- File: `spec/schemas/catalog-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Generic shape shared by every `<kind>/index.json` file of the @openpresentation/gallery package (packages/gallery/catalog in the OPF repo) and by the default-catalog index that pptx.gallery publishes at `https://www.pptx.gallery/<kind>/index.json` (the gallery package is a pinned snapshot of that catalog; see docs/default-catalog.md). An index is a lightweight, ordered summary of the full-record JSON files that live alongside it: each entry names the record's stable id, a human-readable name,...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-index/v1\"` | |\n| `kind` | no | `enum:audiences \\| chart-types \\| color-schemes \\| font-schemes \\| languages \\| layouts \\| narratives \\| purposes \\| social-platforms \\| themes \\| tones` | Catalog kind, as the URL segment of the default catalog (`https://www.pptx.gallery/<kind>`) and the `packages/gallery/catalog/<kind>` directory name. |\n| `version` | yes | `string` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of what this catalog kind holds and how entries are ordered. |\n| `contentSha256` | no | `string` | SHA-256 (lowercase hex) of the canonical JSON of the full records this index lists, in index order, with every top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace. Lets... |\n| `records` | yes | `array<ref:IndexRecord>` | Ordered list of lightweight record summaries. Order defines the catalog's canonical/display order; full record data lives in the sibling JSON file named by `file`. |\n\n### Nested Types\n\n#### IndexRecord\n\n- Type: `object`\n- Required fields: `id`, `name`, `file`\n- Purpose: Lightweight summary of one catalog record. Additional per-kind fields (e.g. `summary`, `tags`, `bcp47`, `duration`, `group`, `label`) are allowed and vary by catalog kind.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier, matching the `id` field inside the record file named by `file`. Lowercase kebab-case; chart-type ids may start with a digit (e.g. '100pct-stacked-bar'). |\n| `name` | yes | `string` | Human-readable name shown in pickers. |\n| `file` | yes | `string` | Filename of the full record, relative to this index file's directory. |\n| `deprecated` | no | `const:true` | Present on the entry of a record that carries `deprecation`. Pickers and default listings hide deprecated entries; the id keeps resolving. |\n| `replacedBy` | no | `string` | The deprecated record's `deprecation.replacedBy`, repeated so a picker can offer the replacement without loading the record. |\n\n## Catalog Snapshot Manifest\n\n- File: `spec/schemas/catalog-manifest.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-manifest/v1`\n- Type: `object`\n- Required fields: `$schema`, `description`, `publisher`, `source`, `kinds`\n- Purpose: Shape of the `manifest.json` of the @openpresentation/gallery package (packages/gallery/catalog/manifest.json), which pins the snapshot of the default OPF catalog published by pptx.gallery. It records the gallery commit the snapshot came from and, per kind, how the snapshot relates to the published catalog plus a content hash of the bundled records. Written by scripts/sync-gallery-catalog.mjs and checked by scripts/check-spec-integrity.mjs; see docs/default-catalog.md. This schema describes a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-manifest/v1\"` | |\n| `description` | yes | `string` | |\n| `publisher` | yes | `string` | Base URL of the default-catalog publisher. Each kind is published at `<publisher>/<kind>/index.json`. |\n| `source` | yes | `object` | The published catalog files the snapshot was taken from. |\n| `kinds` | yes | `object` | One entry per catalog kind, keyed by the kind's URL segment. |\n\n### Nested Types\n\n#### KindEntry\n\n- Type: `object`\n- Required fields: `mode`, `records`, `contentSha256`, `gallery`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | yes | `enum:mirror` | Always 'mirror': the snapshot holds every record the publisher serves for the kind (OPF 0.15). |\n| `records` | yes | `integer` | Number of records bundled for this kind. |\n| `contentSha256` | yes | `string` | contentSha256 of the bundled records, as defined by the catalog index schema. |\n| `gallery` | yes | `object` | The published catalog for this kind at the pinned commit. |\n\n## Chart Type\n\n- File: `spec/schemas/chart-type.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-chart-type/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `mappings`\n- Purpose: Schema for chart-type records: display metadata for chart pickers (name, group, expected series, complexity) and the Open XML mapping a picker can describe. One record per chart type that Aspose.Slides officially supports (see mappings.renderers[\"aspose-slides\"].chartType), plus the column-and-line combination 'combo'. A document's chart.type is an engine vocabulary validated directly; engines never look a chart-type record up. pptx.gallery publishes these records and @openpresentation/galler...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-chart-type/v1\"` | Identifies this record as a chart type in the open presentation catalog. |\n| `id` | yes | `string` | The chart-type value (`chart.type`) this record describes. Lowercase kebab-case; it may start with a digit (e.g., '100pct-stacked-column') to mirror conventional chart naming. |\n| `name` | yes | `string` | Human-readable name of the chart type, as a chart picker shows it. |\n| `summary` | no | `string` | One-sentence positioning: when to reach for this chart variant. |\n| `description` | no | `string` | Longer prose describing the chart and ideal use cases. |\n| `mappings` | yes | `ref:ChartTypeMappings` | Canonical and optional renderer-specific mappings used by engines to render this chart type. |\n| `group` | no | `string` | Top-level grouping in the chart picker. |\n| `groupSort` | no | `integer` | Display ordering hint within the chart group. |\n| `complexity` | no | `enum:simple \\| calculated \\| hierarchical \\| normalized` | Shape of the underlying data: a flat series ('simple'), one with engine-side calculation ('calculated'), parent-child rows ('hierarchical'), or pre-normalized rows ('normalized'). The editor offers only 'simple' types... |\n| `series` | no | `integer` | Number of data series this chart type expects: exactly that many, except that a stacked or percent-stacked type and a combination (composition 'mixed', such as combo) expect at least that many. The editor offers a typ... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ChartTypeMappings\n\n- Type: `object`\n- Required fields: `openxml`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `openxml` | yes | `ref:OpenXmlChartMapping` | Canonical mapping to Open XML chart structures. |\n| `renderers` | no | `object` | Optional renderer-specific mappings. Keys are renderer ids; values are intentionally opaque to OPF. The bundled catalog records the matching Aspose.Slides ChartType enumeration member under the \"aspose-slides\" key, e.... |\n\n#### OpenXmlChartMapping\n\n- Type: `object`\n- Required fields: none\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `element` | no | `string` | Primary Open XML chart element or extension chart element, such as 'barChart', 'lineChart', 'pieChart', 'treemapChart', or 'waterfallChart'. |\n| `barDir` | no | `enum:bar \\| col` | Bar direction for Open XML barChart mappings. |\n| `grouping` | no | `enum:standard \\| clustered \\| stacked \\| percentStacked` | Open XML chart grouping value when the chart family supports grouping. |\n| `marker` | no | `boolean` | Whether the chart type expects visible data markers. |\n| `radarStyle` | no | `enum:standard \\| marker \\| filled` | Open XML radarStyle value for radarChart mappings. |\n| `scatterStyle` | no | `enum:line \\| lineMarker \\| marker \\| smooth \\| smoothMarker` | Open XML scatterStyle value for scatterChart mappings. |\n| `composition` | no | `enum:single \\| mixed \\| extension` | Whether the chart maps to one standard chart element, multiple combined chart elements, or an Open XML extension chart. |\n| `extension` | no | `string` | Optional Open XML extension namespace or element hint for extension charts. |\n| `series` | no | `array<ref:OpenXmlChartMapping>` | Open XML chart elements used by mixed/composite chart types. |\n| `notes` | no | `string` | Short implementation note for mappings that need renderer interpretation. |\n\n## Color Scheme\n\n- File: `spec/schemas/color-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-color-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for color-scheme records. Each scheme is a named palette with the twelve PowerPoint color slots (six accents, two darks, two lights, plus hyperlink and followed-hyperlink), suitable for being mapped directly into OOXML theme XML. Documents reference a scheme from design.colorScheme (or design.colorScheme.id) and from a theme's colorScheme; slot overrides on design.colorScheme take precedence over the resolved scheme. The slot fields here mirror the inline-override fields on the in-docu...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-color-scheme/v1\"` | Identifies a published record file as a colour scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The colour scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without t... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the palette what mood it evokes and where to use it. |\n| `description` | no | `string` | Longer prose describing the palette and its intended use. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color: '#RGB', '#RRGGBB' or '#RRGGBBAA'. The same definition as HexColor in opf.schema.json.\n\n_No named properties._\n\n## Font Scheme\n\n- File: `spec/schemas/font-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-font-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `major`, `minor`\n- Purpose: Schema for font-scheme records. Each scheme pairs a major (heading) and minor (body) font family in the OOXML majorFont/minorFont sense, scoped to a target app (powerpoint or google-slides) and a language family (Latin, East Asian, or Complex Script). Documents reference a scheme from design.fontScheme (or design.fontScheme.id), from a theme's fontScheme and from a language's fontScheme / googleFontScheme. Role overrides on design.fontScheme (heading, body, accent, code), each a font family n...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-font-scheme/v1\"` | Identifies a published record file as a font scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The font scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without thi... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `major` | yes | `string` | Heading (major) font family mirrors the OOXML majorFont entry. |\n| `minor` | yes | `string` | Body (minor) font family mirrors the OOXML minorFont entry. |\n| `code` | no | `string` | Optional monospaced font family name for code blocks and for inline code runs (TextRun.code). It has the same shape as the OPF FontScheme 'code' role, so a record and an inline design.fontScheme override are interchan... |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `languages` | no | `array<string>` | Optional list of BCP-47 language tags this scheme is curated for. Useful for picker UIs that group fonts by language coverage. As the design font scheme, an 'ea' or 'cs' scheme fills its script slot only when this lis... |\n| `textSample` | no | `string` | Short specimen string used by picker UIs to preview the scheme. |\n| `summary` | no | `string` | One-sentence positioning of the font pairing. |\n| `description` | no | `string` | Longer prose describing the font scheme and where it shines. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Language\n\n- File: `spec/schemas/language.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-language/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `bcp47`\n- Purpose: Schema for language records: display metadata for the language pickers of authoring tools (name, ISO code, the default font schemes a picker suggests). The document's 'language' is an engine vocabulary, a BCP-47 tag validated directly; engines never look a language record up, and their script, direction, OOXML tag and default script fonts come from spec/reference/engine-vocabularies.json. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.languages.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-language/v1\"` | Identifies this record as a language in the openpresentation.org catalog. |\n| `id` | yes | `string` | The record's id in the gallery's language catalog, lowercase kebab-case. Documents name a language by its BCP-47 tag ('bcp47'), never by this id. |\n| `name` | yes | `string` | Human-readable language name. |\n| `code` | no | `string` | ISO 639-3 (or 639-2) three-letter language code. Carried for engines that prefer ISO codes. |\n| `bcp47` | yes | `string` | BCP-47 language tag for this record. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Curated culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Engine... |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting PowerPoint output. Display metadata: engines take a language's default script fonts from their own vocabulary. |\n| `googleFontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting Google Slides output. Display metadata. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Layout Preview Index\n\n- File: `spec/schemas/layout-preview-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout-preview-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Shape of previews/layouts/index.json in @openpresentation/gallery 2: one self-contained SVG preview per v2 intent layout, keyed by the matching catalog layout id. Each record names its SVG file and exact UTF-8 byte length. The gallery package owns the files; this checkout materializes ignored copies under packages/gallery/previews/layouts/. This internal index schema is excluded from the package's published/typed schema surface.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout-preview-index/v1\"` | |\n| `version` | yes | `const:\"2\"` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of the preview gallery and its rendering conventions. |\n| `records` | yes | `array<ref:PreviewRecord>` | One entry per v2 layout and its SVG preview. |\n\n### Nested Types\n\n#### PreviewRecord\n\n- Type: `object`\n- Required fields: `id`, `file`, `bytes`\n- Purpose: One layout template preview.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The matching layout record id, such as cover, chart or two-column. |\n| `file` | yes | `string` | SVG filename, relative to this index directory, using the matching layout id. |\n| `bytes` | yes | `integer` | Exact UTF-8 byte length of the referenced SVG file. |\n\n## Slide Layout\n\n- File: `spec/schemas/layout.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout/v2`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `regions`\n- Purpose: Schema for slide-layout records. A layout is one ordered list of regions, declared once, in reading order: 'direction' says whether the record's 'regions' sit side by side (row) or are stacked (column), and each region says which content kinds it takes and how several blocks share it. A split is an entry that nests a second list the other way, which is how a layout gets two dimensions. 'title' and 'subtitle' are reserved heading regions. Engines compile the list into a grid of tracks (the gui...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout/v2\"` | Identifies a published record file as a layout template. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The layout's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fie... |\n| `name` | yes | `string` | Human-readable layout name, shown in layout pickers and as the PowerPoint layout name. |\n| `summary` | no | `string` | One-sentence positioning of the layout when to reach for it. |\n| `description` | no | `string` | Longer prose describing the layout structure and ideal use cases. |\n| `design` | no | `ref:DesignHints` | The layout's design defaults, with the same keys and values as the deck's design and a slide's design (a slide overrides exactly what its layout sets, by the same name). An absent key means the layout has no opinion.... |\n| `direction` | no | `enum:row \\| column` | How the record's 'regions' are laid out: 'row' places them side by side, left to right in reading order; 'column' stacks them top to bottom. Default 'column'. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the content of every body region when it is shorter than the region. A split's 'anchor' replaces it for the regions under that split, and a region's own 'anchor' replaces both. Only 'anchor' is in... |\n| `regions` | yes | `array<ref:RegionEntry>` | The record's regions in reading order: 1 to 12 entries, each a region ({ name, size?, ...rules }) or a split ({ direction, regions, size?, anchor? }). Every region is declared once, here, with its content rules; a blo... |\n| `overflowRegion` | no | `string` | The region where content that no region accepts goes, whatever its 'accepts', while it has room; content beyond every region's room is drawn there too, with the warning opf/layout-unplaced. Omitted: the first primary... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `composition` | no | `ref:Composition` | Only 'gap', 'padding', 'minFontSize' and 'overflow', with their 0.18 meaning. A template's regions replace composition.mode, columns and weights. |\n\n### Nested Types\n\n#### RegionEntry\n\n- Type: `oneOf:ref:HeadingRegion / ref:Region / ref:Split`\n- Required fields: none\n- Purpose: One entry of a regions list: a heading region, a body region or a split.\n\n_No named properties._\n\n#### RegionSize\n\n- Type: `oneOf:number / const:\"auto\"`\n- Required fields: none\n- Purpose: The region's or split's share along its parent's direction: a number above 0 and up to 100 that siblings share in proportion (the gap is taken out first), default 1; or 'auto', the length the content needs at the starting text size (at most half the content box; a split sized auto is as tall as its tallest content, and every row of a column split sized auto is auto too; auto regions with no content take no space)....\n\n_No named properties._\n\n#### HeadingRegion\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: A reserved heading region: 'title' holds the slide's heading group (tag, title and subtitle, stacked) and 'subtitle' takes the subtitle out of it. A heading region takes only a size. At most one of each; a 'subtitle' needs a 'title'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `enum:title \\| subtitle` | 'title' or 'subtitle'. |\n| `size` | no | `ref:RegionSize` | |\n\n#### Split\n\n- Type: `object`\n- Required fields: `direction`, `regions`\n- Purpose: A split nests a list of regions the other way inside one cell of its parent: a row of regions inside a column, or a column inside a row. It has no name; its regions are declared in reading order like the record's own. Splits nest at most three levels (the record's own list is level 1).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `direction` | yes | `enum:row \\| column` | How the split's 'regions' are laid out: 'row' side by side, 'column' stacked. |\n| `regions` | yes | `array<ref:RegionEntry>` | The split's regions in reading order: 1 to 12 regions and splits. |\n| `size` | no | `ref:RegionSize` | |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the content of every body region under this split, unless the region sets its own. The heading regions 'title' and 'subtitle' never inherit it. |\n\n#### Region\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: One body region of a layout: its name, its share of the space and the rules for which content it takes and how several blocks share it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `string` | The region's name, lowercase kebab-case, unique across the whole record. A block targets it with 'region': '<name>'. It may not be 'title' (those are heading regions), 'tag', 'auto' or one of the promoted-region words... |\n| `size` | no | `ref:RegionSize` | |\n| `accepts` | no | `array<enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline \\| group>` | Content kinds the region takes: the body content kinds, and 'group' for a content group of any content. A group is also accepted where every one of its leaf kinds is. Omitted: every body content kind and 'group'. |\n| `role` | no | `enum:primary \\| secondary \\| media \\| supporting` | Which content the region wants first. Binding gives a picture (image, video) to a media region first, then primary, secondary, supporting; every other kind to a primary region first, then secondary, supporting, media;... |\n| `flow` | no | `enum:none \\| grid \\| column \\| auto` | How several blocks share the region: 'none' one block fills it; 'grid' by count (1 fills, 2 side by side, 3 in a row, 4 as 2 x 2, 5 as 3 + 2, 6 as 3 x 2, 7 to 12 in four columns; rows and columns swap in a portrait re... |\n| `max` | no | `integer` | Blocks the region holds on one slide. Default 1 for flow 'none' (which requires 1), else 6. Content beyond every region's room is drawn in the overflow region with the warning opf/layout-unplaced, and paginate moves i... |\n| `bleed` | no | `boolean` | The region's outer edges that lie on the content box's edge extend to the slide edge, as a placed image does. A bled region is never drawn as a card. Default false. |\n| `listColumns` | no | `oneOf:const:\"auto\" / const:1` | 'auto': a list in the region that does not fit its cell at the starting size (a lone list, or one of several blocks) flows into up to three columns before it shrinks, as a list payload's columns: 'auto'. Default 1. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the region's content when it is shorter than the region. Replaces the 'anchor' of the record and of the splits around it. Default: the nearest of those that sets one, else 'top'. |\n| `empty` | no | `enum:collapse \\| keep` | What the region does on a slide that gives it no content: 'collapse' (default) joins its cells to a neighbouring area that shares the full length of one of its edges and is not itself empty; 'keep' leaves the space. |\n\n#### DesignHints\n\n- Type: `object`\n- Required fields: none\n- Purpose: Design hints a layout carries. The keys and lowercase values are those of the deck's design and a slide's design in opf.schema.json (Design), so a slide overrides a layout by the same name and layout.design can be copied into slide.design unchanged. An absent key means the layout has no opinion. Only the keys the layout sets are stored. scripts/check-spec-integrity.mjs verifies that every key here has the same typ...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the title heading area. |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the body regions. |\n| `contentBox` | no | `boolean` | Whether the body regions are drawn inside a visible card or surface. |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. On a layout slide it turns every flowing region into one row or one column; the slide's own composition.mode, columns and... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content on automatic slides. Template layouts express this arrangement with their regions; chartPrimary does not override a template. |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of the slide's image blocks: 'cover' fills the frame and crops around focus, 'contain' shows the whole picture, 'stretch' scales it to the frame. |\n| `listBullet` | no | `enum:character \\| image` | Marker style of lists: 'character' draws the glyph, 'image' draws the deck's icon logo as a picture bullet. |\n| `mirror` | no | `boolean` | Draw the layout mirrored: each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Binding and reading order do no... |\n\n#### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: A template's composition: only gap, padding, minFontSize and overflow, with their 0.18 meaning. A slide's composition overrides them; a slide's composition.mode, columns and weights arrange its first primary flowing region.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n\n## Narrative Template\n\n- File: `spec/schemas/narrative.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-narrative/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `beats`\n- Purpose: Schema for narrative records. A narrative is a plan: a named story arc (e.g. 'problem-solution', 'scqa') as an ordered list of beats, each saying what its slide must do. A document points at one with the string 'narrative' and links its slides to beats with 'slides[].beat'; a narrative the document defines itself is a record in catalogs.custom.narratives. Core validate warns about a 'slides[].beat' id the resolved narrative does not define (opf/unknown-beat) and about a root 'duration' outsid...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-narrative/v1\"` | Identifies a published record file as a narrative record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The narrative's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this... |\n| `name` | yes | `string` | Human-readable template name, e.g. 'Problem Solution'. |\n| `summary` | no | `string` | One-sentence description of when and why to use this narrative. |\n| `description` | no | `string` | Longer prose describing the narrative arc and ideal use cases. Used by AI-driven generation to seed deck-level direction. |\n| `audienceFit` | no | `array<string>` | Audiences this narrative works well for, e.g. ['executive', 'investor', 'customer']. |\n| `duration` | no | `object` | Typical talk length this narrative suits, as a range in minutes. A deck's own target is the root 'duration' (one number); core validate warns when that target lies outside this range, and when 'min' is greater than 'm... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search, e.g. ['business', 'pitch', 'internal']. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `beats` | yes | `array<ref:Beat>` | Ordered list of beats that make up the narrative arc. |\n\n### Nested Types\n\n#### Beat\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A single narrative beat a labeled segment of the story arc with a specific dramatic purpose. One beat is one slide: 'type' is that slide's Slide.type and 'layout' is its Slide.layout, so a planner copies them straight into a skeleton deck. Split a heavy beat into several beats rather than giving it several slides.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable slug used by Slide.beat to reference this beat. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable beat name, e.g. 'The Problem'. |\n| `description` | no | `string` | Curator-written prose that explains what this beat should accomplish. |\n| `instructions` | no | `string` | Short author-facing instruction for the beat typically one phrase. Complements 'description' with a concise directive. |\n| `type` | no | `enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline` | Default content kind for the beat's slide: the same values as Slide.type. Helps engines and planners choose a layout when only the beat is known. |\n| `layout` | no | `string` | Suggested layout id for the beat's slide, e.g. 'section-divider', 'title-slide', 'text-left'. Resolves the same way as Slide.layout against catalogs.layouts and the default catalog at https://www.pptx.gallery/layouts. |\n| `thoughtCues` | no | `array<string>` | Optional speaker or thinking cues for the beat: questions the slide should answer. A planner or author turns them into the slide's notes; no engine copies them there on its own. |\n\n## Purpose\n\n- File: `spec/schemas/purpose.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-purpose/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for purpose records. Each record names a presentation objective such as informing, aligning, persuading, driving a decision, or selling. Documents reference a purpose from 'purpose', which also accepts free-form text and inline Purpose objects. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-purpose/v1\"` | Identifies a published record file as a purpose record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The purpose's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fi... |\n| `name` | yes | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose what this deck is trying to accomplish. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Social Platform\n\n- File: `spec/schemas/social-platform.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-social-platform/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for social-platform records: display metadata for authoring UIs (name, brand color, handle example) alongside the URL patterns. The keys of a Socials object are an engine vocabulary validated directly; engines link a handle with the patterns in spec/reference/engine-vocabularies.json and never look a social-platform record up. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.socialPlatforms.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-social-platform/v1\"` | Identifies this record as a social-platform entry in the openpresentation.org catalog. |\n| `id` | yes | `string` | The social platform's key, as written in a Socials object. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable platform name shown in pickers and footers. |\n| `summary` | no | `string` | One-sentence positioning of the platform what it's used for and who's on it. |\n| `description` | no | `string` | Longer prose describing the platform and any rendering conventions (e.g., handle prefixes, distributed instances). |\n| `baseUrl` | no | `string` | Canonical base URL of the platform used as the prefix when normalizing handles to full URLs. |\n| `profileUrlPattern` | no | `string` | URL pattern for individual member profiles. Use '{handle}' as the placeholder for the handle (with the prefix already stripped). |\n| `companyUrlPattern` | no | `string` | Optional URL pattern for organization / company pages, when the platform distinguishes them from member profiles. Use '{handle}' as the placeholder. |\n| `handlePrefix` | no | `string` | Conventional prefix character displayed before the handle (e.g. '@' for X / Mastodon / Threads / TikTok). Empty string when no prefix is used. Renderers strip it before substituting into URL patterns. |\n| `handleExample` | no | `string` | Example handle in its conventional rendered form, used by picker UIs and validation hints. |\n| `brandColor` | no | `string` | Brand color (hex) for branded icon chips, link styling, or section accents in authoring UIs. Catalog metadata: engines do not draw it. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Theme\n\n- File: `spec/schemas/theme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-theme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for theme records. Each theme is a small, named bundle that pairs a color scheme, a font scheme, a default theme-controlled background, and a slide size. Documents reference a theme from design.theme; design.colorScheme, design.fontScheme, design.background and design.dimensions take precedence over the resolved theme. The theme's own colorScheme and fontScheme references resolve in the theme's group first, so a catalog's theme finds that catalog's schemes. A document references a reco...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-theme/v1\"` | Identifies a published record file as a theme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The theme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable theme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the theme when to reach for it. |\n| `description` | no | `string` | Longer prose describing what the theme looks and feels like and the kinds of decks it suits. |\n| `colorScheme` | no | `string` | The theme's default colour scheme: a reference (a bare id or 'name:id') resolved like design.colorScheme, but in the theme's own catalogs group first. |\n| `fontScheme` | no | `string` | The theme's default font scheme: a reference (a bare id or 'name:id') resolved like design.fontScheme, but in the theme's own catalogs group first. |\n| `background` | no | `ref:ThemeBackground` | |\n| `variants` | no | `array<ref:ThemeVariant>` | The color variants the theme offers (OPF 0.23), for example light, light-alt, dark and dark-alt: named values of design.variant over the theme's one color scheme, so a dark deck is a variant of a theme and not a theme... |\n| `dimensions` | no | `enum:16:9 \\| 4:3 \\| 16:10 \\| 1:1 \\| 4:5 \\| 9:16 \\| letter \\| a4 \\| widescreen \\| standard` | Default slide size for this theme. Accepts the same preset values as design.dimensions.preset. |\n| `design` | no | `object` | The theme's design defaults (OPF 0.19), so a house style such as boxed content or centred titles is set once: the keys and values of the deck's design for titleAlignment, contentAlignment, contentBox, contentDirection... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ThemeVariant\n\n- Type: `object`\n- Required fields: `id`, `name`, `variant`\n- Purpose: One named color variant of a theme.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The variant's id within the theme, lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable variant name shown in pickers. |\n| `variant` | yes | `oneOf:ref:ThemeBackgroundSlot / ref:ColorVariant` | The value design.variant takes when the variant is chosen: a background slot ('light1', 'light2', 'dark1', 'dark2'; the same as { background: slot }) or the ColorVariant object of opf.schema.json, which adds an accent... |\n\n#### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n#### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n## Tone\n\n- File: `spec/schemas/tone.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-tone/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for tone records. Each record names a presentation tone (e.g. 'formal', 'casual', 'inspirational') and carries voice cues, anti-patterns, and sample phrases that AI-driven generation uses to shape output. Documents reference a tone from 'tone'. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-tone/v1\"` | Identifies a published record file as a tone record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The tone's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone when to reach for it. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. Phrased as imperatives, e.g. 'use second-person', 'favor short sentences', 'lead with the recommendation'. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. Used by picker UIs and as few-shot examples for AI generation. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog this tone pairs well with. Used by picker UIs to suggest narratives once a tone is chosen. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n"
|
|
15
|
+
"markdown": "# OPF Catalog Schema Reference\n\nCatalog records are reusable presets that OPF documents reference by id. This page summarizes every catalog companion schema in `spec/schemas/`. The top-level presentation schema has its own [schema reference](schema-reference.md), and the report format every tool shares has the [validation results reference](finding-schema-reference.md).\n\nOPF documents usually reference these records with string ids such as `design.theme = \"minimal\"`, `tone = \"formal\"`, or `chart.type = \"line\"`. Dense examples may also embed catalog sources or inline records under `catalogs`.\n\n## Audience\n\n- File: `spec/schemas/audience.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-audience/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for audience records. Each record names an audience archetype (e.g. 'executive', 'engineering-team', 'investor') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Documents reference an audience from 'audience', which also accepts free-form text. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-audience/v1\"` | Identifies a published record file as a audience record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The audience's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this f... |\n| `name` | yes | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience who they are and what they care about. |\n| `description` | no | `string` | Longer prose describing the audience archetype and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. Engines use this as a hint for default depth and pacing. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. AI generation uses this to decide whether to expand or assume technical terminology. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, to advise, or to actually decide. Shapes the strength of the closing ask. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on this audience's focused attention for a single presentation, in minutes. Used as a hint when comparing against duration and the resolved narrative's duration range. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this audience. Used by picker UIs to suggest narratives once an audience is chosen. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this audience. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Catalog Index\n\n- File: `spec/schemas/catalog-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Generic shape shared by every `<kind>/index.json` file of the @openpresentation/gallery package (packages/gallery/catalog in the OPF repo) and by the default-catalog index that pptx.gallery publishes at `https://www.pptx.gallery/<kind>/index.json` (the gallery package is a pinned snapshot of that catalog; see docs/default-catalog.md). An index is a lightweight, ordered summary of the full-record JSON files that live alongside it: each entry names the record's stable id, a human-readable name,...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-index/v1\"` | |\n| `kind` | no | `enum:audiences \\| chart-types \\| color-schemes \\| font-schemes \\| languages \\| layouts \\| narratives \\| purposes \\| social-platforms \\| themes \\| tones` | Catalog kind, as the URL segment of the default catalog (`https://www.pptx.gallery/<kind>`) and the `packages/gallery/catalog/<kind>` directory name. |\n| `version` | yes | `string` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of what this catalog kind holds and how entries are ordered. |\n| `contentSha256` | no | `string` | SHA-256 (lowercase hex) of the canonical JSON of the full records this index lists, in index order, with every top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace. Lets... |\n| `records` | yes | `array<ref:IndexRecord>` | Ordered list of lightweight record summaries. Order defines the catalog's canonical/display order; full record data lives in the sibling JSON file named by `file`. |\n\n### Nested Types\n\n#### IndexRecord\n\n- Type: `object`\n- Required fields: `id`, `name`, `file`\n- Purpose: Lightweight summary of one catalog record. Additional per-kind fields (e.g. `summary`, `tags`, `bcp47`, `duration`, `group`, `label`) are allowed and vary by catalog kind.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier, matching the `id` field inside the record file named by `file`. Lowercase kebab-case; chart-type ids may start with a digit (e.g. '100pct-stacked-bar'). |\n| `name` | yes | `string` | Human-readable name shown in pickers. |\n| `file` | yes | `string` | Filename of the full record, relative to this index file's directory. |\n| `deprecated` | no | `const:true` | Present on the entry of a record that carries `deprecation`. Pickers and default listings hide deprecated entries; the id keeps resolving. |\n| `replacedBy` | no | `string` | The deprecated record's `deprecation.replacedBy`, repeated so a picker can offer the replacement without loading the record. |\n\n## Catalog Snapshot Manifest\n\n- File: `spec/schemas/catalog-manifest.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-manifest/v1`\n- Type: `object`\n- Required fields: `$schema`, `description`, `publisher`, `source`, `kinds`\n- Purpose: Shape of the `manifest.json` of the @openpresentation/gallery package (packages/gallery/catalog/manifest.json), which pins the snapshot of the default OPF catalog published by pptx.gallery. It records the gallery commit the snapshot came from and, per kind, how the snapshot relates to the published catalog plus a content hash of the bundled records. Written by scripts/sync-gallery-catalog.mjs and checked by scripts/check-spec-integrity.mjs; see docs/default-catalog.md. This schema describes a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-manifest/v1\"` | |\n| `description` | yes | `string` | |\n| `publisher` | yes | `string` | Base URL of the default-catalog publisher. Each kind is published at `<publisher>/<kind>/index.json`. |\n| `source` | yes | `object` | The published catalog files the snapshot was taken from. |\n| `kinds` | yes | `object` | One entry per catalog kind, keyed by the kind's URL segment. |\n\n### Nested Types\n\n#### KindEntry\n\n- Type: `object`\n- Required fields: `mode`, `records`, `contentSha256`, `gallery`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | yes | `enum:mirror` | Always 'mirror': the snapshot holds every record the publisher serves for the kind (OPF 0.15). |\n| `records` | yes | `integer` | Number of records bundled for this kind. |\n| `contentSha256` | yes | `string` | contentSha256 of the bundled records, as defined by the catalog index schema. |\n| `gallery` | yes | `object` | The published catalog for this kind at the pinned commit. |\n\n## Chart Type\n\n- File: `spec/schemas/chart-type.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-chart-type/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `mappings`\n- Purpose: Schema for chart-type records: display metadata for chart pickers (name, group, expected series, complexity) and the Open XML mapping a picker can describe. One record per chart type that Aspose.Slides officially supports (see mappings.renderers[\"aspose-slides\"].chartType), plus the column-and-line combination 'combo'. A document's chart.type is an engine vocabulary validated directly; engines never look a chart-type record up. pptx.gallery publishes these records and @openpresentation/galler...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-chart-type/v1\"` | Identifies this record as a chart type in the open presentation catalog. |\n| `id` | yes | `string` | The chart-type value (`chart.type`) this record describes. Lowercase kebab-case; it may start with a digit (e.g., '100pct-stacked-column') to mirror conventional chart naming. |\n| `name` | yes | `string` | Human-readable name of the chart type, as a chart picker shows it. |\n| `summary` | no | `string` | One-sentence positioning: when to reach for this chart variant. |\n| `description` | no | `string` | Longer prose describing the chart and ideal use cases. |\n| `mappings` | yes | `ref:ChartTypeMappings` | Canonical and optional renderer-specific mappings used by engines to render this chart type. |\n| `group` | no | `string` | Top-level grouping in the chart picker. |\n| `groupSort` | no | `integer` | Display ordering hint within the chart group. |\n| `complexity` | no | `enum:simple \\| calculated \\| hierarchical \\| normalized` | Shape of the underlying data: a flat series ('simple'), one with engine-side calculation ('calculated'), parent-child rows ('hierarchical'), or pre-normalized rows ('normalized'). The editor offers only 'simple' types... |\n| `series` | no | `integer` | Number of data series this chart type expects: exactly that many, except that a stacked or percent-stacked type and a combination (composition 'mixed', such as combo) expect at least that many. The editor offers a typ... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ChartTypeMappings\n\n- Type: `object`\n- Required fields: `openxml`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `openxml` | yes | `ref:OpenXmlChartMapping` | Canonical mapping to Open XML chart structures. |\n| `renderers` | no | `object` | Optional renderer-specific mappings. Keys are renderer ids; values are intentionally opaque to OPF. The bundled catalog records the matching Aspose.Slides ChartType enumeration member under the \"aspose-slides\" key, e.... |\n\n#### OpenXmlChartMapping\n\n- Type: `object`\n- Required fields: none\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `element` | no | `string` | Primary Open XML chart element or extension chart element, such as 'barChart', 'lineChart', 'pieChart', 'treemapChart', or 'waterfallChart'. |\n| `barDir` | no | `enum:bar \\| col` | Bar direction for Open XML barChart mappings. |\n| `grouping` | no | `enum:standard \\| clustered \\| stacked \\| percentStacked` | Open XML chart grouping value when the chart family supports grouping. |\n| `marker` | no | `boolean` | Whether the chart type expects visible data markers. |\n| `radarStyle` | no | `enum:standard \\| marker \\| filled` | Open XML radarStyle value for radarChart mappings. |\n| `scatterStyle` | no | `enum:line \\| lineMarker \\| marker \\| smooth \\| smoothMarker` | Open XML scatterStyle value for scatterChart mappings. |\n| `composition` | no | `enum:single \\| mixed \\| extension` | Whether the chart maps to one standard chart element, multiple combined chart elements, or an Open XML extension chart. |\n| `extension` | no | `string` | Optional Open XML extension namespace or element hint for extension charts. |\n| `series` | no | `array<ref:OpenXmlChartMapping>` | Open XML chart elements used by mixed/composite chart types. |\n| `notes` | no | `string` | Short implementation note for mappings that need renderer interpretation. |\n\n## Color Scheme\n\n- File: `spec/schemas/color-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-color-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for color-scheme records. Each scheme is a named palette with the twelve PowerPoint color slots (six accents, two darks, two lights, plus hyperlink and followed-hyperlink), suitable for being mapped directly into OOXML theme XML. Documents reference a scheme from design.colorScheme (or design.colorScheme.id) and from a theme's colorScheme; slot overrides on design.colorScheme take precedence over the resolved scheme. The slot fields here mirror the inline-override fields on the in-docu...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-color-scheme/v1\"` | Identifies a published record file as a colour scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The colour scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without t... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the palette what mood it evokes and where to use it. |\n| `description` | no | `string` | Longer prose describing the palette and its intended use. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color: '#RGB', '#RRGGBB' or '#RRGGBBAA'. The same definition as HexColor in opf.schema.json.\n\n_No named properties._\n\n## Font Scheme\n\n- File: `spec/schemas/font-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-font-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `major`, `minor`\n- Purpose: Schema for font-scheme records. Each scheme pairs a major (heading) and minor (body) font family in the OOXML majorFont/minorFont sense, scoped to a target app (powerpoint or google-slides) and a language family (Latin, East Asian, or Complex Script). Documents reference a scheme from design.fontScheme (or design.fontScheme.id), from a theme's fontScheme and from a language's fontScheme / googleFontScheme. Role overrides on design.fontScheme (heading, body, accent, code), each a font family n...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-font-scheme/v1\"` | Identifies a published record file as a font scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The font scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without thi... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `major` | yes | `string` | Heading (major) font family mirrors the OOXML majorFont entry. |\n| `minor` | yes | `string` | Body (minor) font family mirrors the OOXML minorFont entry. |\n| `code` | no | `string` | Optional monospaced font family name for code blocks and for inline code runs (TextRun.code). It has the same shape as the OPF FontScheme 'code' role, so a record and an inline design.fontScheme override are interchan... |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `languages` | no | `array<string>` | Optional list of BCP-47 language tags this scheme is curated for. Useful for picker UIs that group fonts by language coverage. As the design font scheme, an 'ea' or 'cs' scheme fills its script slot only when this lis... |\n| `textSample` | no | `string` | Short specimen string used by picker UIs to preview the scheme. |\n| `summary` | no | `string` | One-sentence positioning of the font pairing. |\n| `description` | no | `string` | Longer prose describing the font scheme and where it shines. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Language\n\n- File: `spec/schemas/language.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-language/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `bcp47`\n- Purpose: Schema for language records: display metadata for the language pickers of authoring tools (name, ISO code, the default font schemes a picker suggests). The document's 'language' is an engine vocabulary, a BCP-47 tag validated directly; engines never look a language record up, and their script, direction, OOXML tag and default script fonts come from spec/reference/engine-vocabularies.json. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.languages.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-language/v1\"` | Identifies this record as a language in the openpresentation.org catalog. |\n| `id` | yes | `string` | The record's id in the gallery's language catalog, lowercase kebab-case. Documents name a language by its BCP-47 tag ('bcp47'), never by this id. |\n| `name` | yes | `string` | Human-readable language name. |\n| `code` | no | `string` | ISO 639-3 (or 639-2) three-letter language code. Carried for engines that prefer ISO codes. |\n| `bcp47` | yes | `string` | BCP-47 language tag for this record. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Curated culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Engine... |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting PowerPoint output. Display metadata: engines take a language's default script fonts from their own vocabulary. |\n| `googleFontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting Google Slides output. Display metadata. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Layout Preview Index\n\n- File: `spec/schemas/layout-preview-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout-preview-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Shape of previews/layouts/index.json in @openpresentation/gallery 2: one self-contained SVG preview per v2 intent layout, keyed by the matching catalog layout id. Each record names its SVG file and exact UTF-8 byte length. The gallery package owns the files; this checkout materializes ignored copies under packages/gallery/previews/layouts/. This internal index schema is excluded from the package's published/typed schema surface.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout-preview-index/v1\"` | |\n| `version` | yes | `const:\"2\"` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of the preview gallery and its rendering conventions. |\n| `records` | yes | `array<ref:PreviewRecord>` | One entry per v2 layout and its SVG preview. |\n\n### Nested Types\n\n#### PreviewRecord\n\n- Type: `object`\n- Required fields: `id`, `file`, `bytes`\n- Purpose: One layout template preview.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The matching layout record id, such as cover, chart or two-column. |\n| `file` | yes | `string` | SVG filename, relative to this index directory, using the matching layout id. |\n| `bytes` | yes | `integer` | Exact UTF-8 byte length of the referenced SVG file. |\n\n## Slide Layout\n\n- File: `spec/schemas/layout.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout/v2`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `regions`\n- Purpose: Schema for slide-layout records. A layout is one ordered list of regions, declared once, in reading order: 'direction' says whether the record's 'regions' sit side by side (row) or are stacked (column), and each region says which content kinds it takes and how several blocks share it. A split is an entry that nests a second list the other way, which is how a layout gets two dimensions. 'title' and 'subtitle' are reserved heading regions. Engines compile the list into a grid of tracks (the gui...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout/v2\"` | Identifies a published record file as a layout template. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The layout's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fie... |\n| `name` | yes | `string` | Human-readable layout name, shown in layout pickers and as the PowerPoint layout name. |\n| `summary` | no | `string` | One-sentence positioning of the layout when to reach for it. |\n| `description` | no | `string` | Longer prose describing the layout structure and ideal use cases. |\n| `design` | no | `ref:DesignHints` | The layout's design defaults, with the same keys and values as the deck's design and a slide's design (a slide overrides exactly what its layout sets, by the same name). An absent key means the layout has no opinion.... |\n| `direction` | no | `enum:row \\| column` | How the record's 'regions' are laid out: 'row' places them side by side, left to right in reading order; 'column' stacks them top to bottom. Default 'column'. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the content of every body region when it is shorter than the region. A split's 'anchor' replaces it for the regions under that split, and a region's own 'anchor' replaces both. Only 'anchor' is in... |\n| `regions` | yes | `array<ref:RegionEntry>` | The record's regions in reading order: 1 to 12 entries, each a region ({ name, size?, ...rules }) or a split ({ direction, regions, size?, anchor? }). Every region is declared once, here, with its content rules; a blo... |\n| `overflowRegion` | no | `string` | The region where content that no region accepts goes, whatever its 'accepts', while it has room; content beyond every region's room is drawn there too, with the warning opf/layout-unplaced. Omitted: the first primary... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `composition` | no | `ref:Composition` | Only 'gap', 'padding', 'minFontSize' and 'overflow', with their 0.18 meaning. A template's regions replace composition.mode, columns and weights. |\n\n### Nested Types\n\n#### RegionEntry\n\n- Type: `oneOf:ref:HeadingRegion / ref:Region / ref:Split`\n- Required fields: none\n- Purpose: One entry of a regions list: a heading region, a body region or a split.\n\n_No named properties._\n\n#### RegionSize\n\n- Type: `oneOf:number / const:\"auto\"`\n- Required fields: none\n- Purpose: The region's or split's share along its parent's direction: a number above 0 and up to 100 that siblings share in proportion (the gap is taken out first), default 1; or 'auto', the length the content needs at the starting text size (up to the space the heading rows and the content of the numeric rows leave; auto rows that do not all fit shrink together, each giving up the same fraction of what it can give up above...\n\n_No named properties._\n\n#### HeadingRegion\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: A reserved heading region: 'title' holds the slide's heading group (tag, title and subtitle, stacked) and 'subtitle' takes the subtitle out of it. A heading region takes only a size. At most one of each; a 'subtitle' needs a 'title'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `enum:title \\| subtitle` | 'title' or 'subtitle'. |\n| `size` | no | `ref:RegionSize` | |\n\n#### Split\n\n- Type: `object`\n- Required fields: `direction`, `regions`\n- Purpose: A split nests a list of regions the other way inside one cell of its parent: a row of regions inside a column, or a column inside a row. It has no name; its regions are declared in reading order like the record's own. Splits nest at most three levels (the record's own list is level 1).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `direction` | yes | `enum:row \\| column` | How the split's 'regions' are laid out: 'row' side by side, 'column' stacked. |\n| `regions` | yes | `array<ref:RegionEntry>` | The split's regions in reading order: 1 to 12 regions and splits. |\n| `size` | no | `ref:RegionSize` | |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the content of every body region under this split, unless the region sets its own. The heading regions 'title' and 'subtitle' never inherit it. |\n\n#### Region\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: One body region of a layout: its name, its share of the space and the rules for which content it takes and how several blocks share it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `string` | The region's name, lowercase kebab-case, unique across the whole record. A block targets it with 'region': '<name>'. It may not be 'title' (those are heading regions), 'tag', 'auto' or one of the promoted-region words... |\n| `size` | no | `ref:RegionSize` | |\n| `accepts` | no | `array<enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline \\| group>` | Content kinds the region takes: the body content kinds, and 'group' for a content group of any content. A group is also accepted where every one of its leaf kinds is. Omitted: every body content kind and 'group'. |\n| `role` | no | `enum:primary \\| secondary \\| media \\| supporting` | Which content the region wants first. Binding gives a picture (image, video) to a media region first, then primary, secondary, supporting; every other kind to a primary region first, then secondary, supporting, media;... |\n| `flow` | no | `enum:none \\| grid \\| column \\| auto` | How several blocks share the region: 'none' one block fills it; 'grid' by count (1 fills, 2 side by side, 3 in a row, 4 as 2 x 2, 5 as 3 + 2, 6 as 3 x 2, 7 to 12 in four columns; rows and columns swap in a portrait re... |\n| `max` | no | `integer` | Blocks the region holds on one slide. Default 1 for flow 'none' (which requires 1), else 6. Content beyond every region's room is drawn in the overflow region with the warning opf/layout-unplaced, and paginate moves i... |\n| `bleed` | no | `boolean` | The region's outer edges that lie on the content box's edge extend to the slide edge, as a placed image does. A bled region is never drawn as a card. Default false. |\n| `listColumns` | no | `oneOf:const:\"auto\" / const:1` | 'auto': a list in the region that does not fit its cell at the starting size (a lone list, or one of several blocks) flows into up to three columns before it shrinks, as a list payload's columns: 'auto'. Default 1. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the region's content when it is shorter than the region. Replaces the 'anchor' of the record and of the splits around it. Default: the nearest of those that sets one, else 'top'. A picture shown w... |\n| `empty` | no | `enum:collapse \\| keep` | What the region does on a slide that gives it no content: 'collapse' (default) joins its cells to a neighbouring area that shares the full length of one of its edges and is not itself empty; 'keep' leaves the space. |\n\n#### DesignHints\n\n- Type: `object`\n- Required fields: none\n- Purpose: Design hints a layout carries. The keys and lowercase values are those of the deck's design and a slide's design in opf.schema.json (Design), so a slide overrides a layout by the same name and layout.design can be copied into slide.design unchanged. An absent key means the layout has no opinion. Only the keys the layout sets are stored. scripts/check-spec-integrity.mjs verifies that every key here has the same typ...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the title heading area. |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the body regions. |\n| `contentBox` | no | `boolean` | Whether the body regions are drawn inside a visible card or surface. |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. On a layout slide it turns every flowing region into one row or one column; the slide's own composition.mode, columns and... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content on automatic slides. Template layouts express this arrangement with their regions; chartPrimary does not override a template. |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of the slide's image blocks: 'cover' fills the frame and crops around focus, 'contain' shows the whole picture, 'stretch' scales it to the frame. |\n| `listBullet` | no | `enum:character \\| image \\| none` | Marker style of lists: 'character' draws the glyph, 'image' draws the deck's icon logo as a picture bullet, 'none' draws no marker and no hanging indent (label-and-description lists). |\n| `mirror` | no | `boolean` | Draw the layout mirrored: each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Binding and reading order do no... |\n\n#### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: A template's composition: only gap, padding, minFontSize and overflow, with their 0.18 meaning. A slide's composition overrides them; a slide's composition.mode, columns and weights arrange its first primary flowing region.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n\n## Narrative Template\n\n- File: `spec/schemas/narrative.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-narrative/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `beats`\n- Purpose: Schema for narrative records. A narrative is a plan: a named story arc (e.g. 'problem-solution', 'scqa') as an ordered list of beats, each saying what its slide must do. A document points at one with the string 'narrative' and links its slides to beats with 'slides[].beat'; a narrative the document defines itself is a record in catalogs.custom.narratives. Core validate warns about a 'slides[].beat' id the resolved narrative does not define (opf/unknown-beat) and about a root 'duration' outsid...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-narrative/v1\"` | Identifies a published record file as a narrative record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The narrative's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this... |\n| `name` | yes | `string` | Human-readable template name, e.g. 'Problem Solution'. |\n| `summary` | no | `string` | One-sentence description of when and why to use this narrative. |\n| `description` | no | `string` | Longer prose describing the narrative arc and ideal use cases. Used by AI-driven generation to seed deck-level direction. |\n| `audienceFit` | no | `array<string>` | Audiences this narrative works well for, e.g. ['executive', 'investor', 'customer']. |\n| `duration` | no | `object` | Typical talk length this narrative suits, as a range in minutes. A deck's own target is the root 'duration' (one number); core validate warns when that target lies outside this range, and when 'min' is greater than 'm... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search, e.g. ['business', 'pitch', 'internal']. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `beats` | yes | `array<ref:Beat>` | Ordered list of beats that make up the narrative arc. |\n\n### Nested Types\n\n#### Beat\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A single narrative beat a labeled segment of the story arc with a specific dramatic purpose. One beat is one slide: 'type' is that slide's Slide.type and 'layout' is its Slide.layout, so a planner copies them straight into a skeleton deck. Split a heavy beat into several beats rather than giving it several slides.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable slug used by Slide.beat to reference this beat. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable beat name, e.g. 'The Problem'. |\n| `description` | no | `string` | Curator-written prose that explains what this beat should accomplish. |\n| `instructions` | no | `string` | Short author-facing instruction for the beat typically one phrase. Complements 'description' with a concise directive. |\n| `type` | no | `enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline` | Default content kind for the beat's slide: the same values as Slide.type. Helps engines and planners choose a layout when only the beat is known. |\n| `layout` | no | `string` | Suggested layout id for the beat's slide, e.g. 'section-divider', 'title-slide', 'text-left'. Resolves the same way as Slide.layout against catalogs.layouts and the default catalog at https://www.pptx.gallery/layouts. |\n| `thoughtCues` | no | `array<string>` | Optional speaker or thinking cues for the beat: questions the slide should answer. A planner or author turns them into the slide's notes; no engine copies them there on its own. |\n\n## Purpose\n\n- File: `spec/schemas/purpose.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-purpose/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for purpose records. Each record names a presentation objective such as informing, aligning, persuading, driving a decision, or selling. Documents reference a purpose from 'purpose', which also accepts free-form text and inline Purpose objects. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-purpose/v1\"` | Identifies a published record file as a purpose record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The purpose's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fi... |\n| `name` | yes | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose what this deck is trying to accomplish. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Social Platform\n\n- File: `spec/schemas/social-platform.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-social-platform/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for social-platform records: display metadata for authoring UIs (name, brand color, handle example) alongside the URL patterns. The keys of a Socials object are an engine vocabulary validated directly; engines link a handle with the patterns in spec/reference/engine-vocabularies.json and never look a social-platform record up. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.socialPlatforms.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-social-platform/v1\"` | Identifies this record as a social-platform entry in the openpresentation.org catalog. |\n| `id` | yes | `string` | The social platform's key, as written in a Socials object. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable platform name shown in pickers and footers. |\n| `summary` | no | `string` | One-sentence positioning of the platform what it's used for and who's on it. |\n| `description` | no | `string` | Longer prose describing the platform and any rendering conventions (e.g., handle prefixes, distributed instances). |\n| `baseUrl` | no | `string` | Canonical base URL of the platform used as the prefix when normalizing handles to full URLs. |\n| `profileUrlPattern` | no | `string` | URL pattern for individual member profiles. Use '{handle}' as the placeholder for the handle (with the prefix already stripped). |\n| `companyUrlPattern` | no | `string` | Optional URL pattern for organization / company pages, when the platform distinguishes them from member profiles. Use '{handle}' as the placeholder. |\n| `handlePrefix` | no | `string` | Conventional prefix character displayed before the handle (e.g. '@' for X / Mastodon / Threads / TikTok). Empty string when no prefix is used. Renderers strip it before substituting into URL patterns. |\n| `handleExample` | no | `string` | Example handle in its conventional rendered form, used by picker UIs and validation hints. |\n| `brandColor` | no | `string` | Brand color (hex) for branded icon chips, link styling, or section accents in authoring UIs. Catalog metadata: engines do not draw it. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Theme\n\n- File: `spec/schemas/theme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-theme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for theme records. Each theme is a small, named bundle that pairs a color scheme, a font scheme, a default theme-controlled background, and a slide size. Documents reference a theme from design.theme; design.colorScheme, design.fontScheme, design.background and design.dimensions take precedence over the resolved theme. The theme's own colorScheme and fontScheme references resolve in the theme's group first, so a catalog's theme finds that catalog's schemes. A document references a reco...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-theme/v1\"` | Identifies a published record file as a theme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The theme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable theme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the theme when to reach for it. |\n| `description` | no | `string` | Longer prose describing what the theme looks and feels like and the kinds of decks it suits. |\n| `colorScheme` | no | `string` | The theme's default colour scheme: a reference (a bare id or 'name:id') resolved like design.colorScheme, but in the theme's own catalogs group first. |\n| `fontScheme` | no | `string` | The theme's default font scheme: a reference (a bare id or 'name:id') resolved like design.fontScheme, but in the theme's own catalogs group first. |\n| `background` | no | `ref:ThemeBackground` | |\n| `variants` | no | `array<ref:ThemeVariant>` | The color variants the theme offers (OPF 0.23), for example light, light-alt, dark and dark-alt: named values of design.variant over the theme's one color scheme, so a dark deck is a variant of a theme and not a theme... |\n| `dimensions` | no | `enum:16:9 \\| 4:3 \\| 16:10 \\| 1:1 \\| 4:5 \\| 9:16 \\| letter \\| a4 \\| widescreen \\| standard` | Default slide size for this theme. Accepts the same preset values as design.dimensions.preset. |\n| `design` | no | `object` | The theme's design defaults (OPF 0.19), so a house style such as boxed content or centred titles is set once: the keys and values of the deck's design for titleAlignment, contentAlignment, contentBox, contentDirection... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ThemeVariant\n\n- Type: `object`\n- Required fields: `id`, `name`, `variant`\n- Purpose: One named color variant of a theme.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The variant's id within the theme, lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable variant name shown in pickers. |\n| `variant` | yes | `oneOf:ref:ThemeBackgroundSlot / ref:ColorVariant` | The value design.variant takes when the variant is chosen: a background slot ('light1', 'light2', 'dark1', 'dark2'; the same as { background: slot }) or the ColorVariant object of opf.schema.json, which adds an accent... |\n\n#### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n#### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n## Tone\n\n- File: `spec/schemas/tone.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-tone/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for tone records. Each record names a presentation tone (e.g. 'formal', 'casual', 'inspirational') and carries voice cues, anti-patterns, and sample phrases that AI-driven generation uses to shape output. Documents reference a tone from 'tone'. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-tone/v1\"` | Identifies a published record file as a tone record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The tone's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone when to reach for it. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. Phrased as imperatives, e.g. 'use second-person', 'favor short sentences', 'lead with the recommendation'. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. Used by picker UIs and as few-shot examples for AI generation. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog this tone pairs well with. Used by picker UIs to suggest narratives once a tone is chosen. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n"
|
|
14
16
|
},
|
|
15
17
|
{
|
|
16
18
|
"slug": "chart-options",
|
|
@@ -28,13 +30,13 @@ var docsData = Object.freeze([
|
|
|
28
30
|
"slug": "cli",
|
|
29
31
|
"file": "docs/cli.md",
|
|
30
32
|
"title": "The `opf` CLI",
|
|
31
|
-
"markdown": '# The `opf` CLI\n\nThe CLI (`@openpresentation/cli`, binary `opf`, Node 22 or later; verified on Node 24) creates, checks, edits, converts and\ninspects decks (see [its README](../packages/cli/README.md)). `opf validate` is the one checker ([validate](validate.md)).\n`opf convert` is the one command that changes formats: a deck form, PDF, PPTX, PNG and SVG. Every command runs core\'s\nfunctions, the ones applications call from [`@openpresentation/opf`](node.md); the commands add the flags, the JSON report and\nthe exit codes.\n\nEverything is deterministic and local: no network, no model, no telemetry, no system fonts. The same document, options and\ninstalled package versions give the same bytes on every operating system.\n\n## Commands\n\n| Command | What it does |\n| --- | --- |\n| `opf create [output] [--title <text> \\| --example <slug>]` | A one-slide starter deck, or one of core\'s bundled examples (`opf catalog examples` lists them). |\n| `opf validate <file\\|dir\\|->...` | The one checker: format, references, policy, accessibility, layout, content. Several files, folders and stdin in one run; `--format json\\|text\\|github`. |\n| `opf convert <input> <output>` | Any change of format: `.json`, `.opf.yaml`, `.opf.md` (and a plain `.md` input), `.pptx`, `.pdf`, `.png`, `.svg`, `.zip`, or stdin and stdout. |\n| `opf edit <file> [output] --patch <patch>` | A JSON Patch, applied whole or not at all, the result checked. |\n| `opf format <file>... [output]` | Canonical key order and layout, in the form the file has. |\n| `opf diff <a> <b>` | What changed between two decks (`--format text\\|json\\|patch`). |\n| `opf merge <base> <ours> <theirs> [output]` | A three-way merge; conflicts are listed. |\n| `opf stats <file>` | Neutral facts about a deck (`--format json\\|text`). |\n| `opf paginate <input> [output]` | Overflowing slides split into continuation slides, measured with the renderer\'s fonts when it is installed. |\n| `opf embed <input> [output]` | The catalog records a deck references, copied into it. |\n| `opf fill <template> <data> [output]` | A template\'s variables filled: one deck per record (the output is a name pattern), or `--combine`. The second argument is always the data (`-` for stdin). |\n| `opf ingest <data> [output] --as table\\|chart` | CSV, TSV or JSON rows as a table or chart slide, alone or added `--into` a deck. |\n| `opf doctor [deck]` | Which formats this install can write, and the one command that installs what is missing. |\n| `opf schemas`, `opf schema [name] [pointer]` | The OPF schemas. |\n| `opf catalogs`, `opf catalog <kind> [id]`, `opf catalog examples [slug]` | The pptx.gallery catalog (`@openpresentation/gallery`) and the bundled example decks. |\n| `opf skills <install\\|update\\|status>` | The six bundled agent skills. |\n\n`opf <command> --help` (or `-h`, or `opf help <command>`) prints a command\'s flags and examples.\n\nRemoved in 0.18 (each is a usage error, exit 2, that names its replacement): `opf render`, `opf export`, `opf import`,\n`opf from-md`, `opf to-md`, `opf from-yaml` and `opf to-yaml` (all `opf convert`), `opf create --from` (`opf convert`), and the\nflags `--output`, `--out`, `--out-dir` and `--name` (a positional output), `--input-format` (`--from`), `--json` (reports are\nJSON), `--dry-run` (stdout), `--data` (fill\'s second argument) and the deck values of `--format` (`--to`).\n\n## One output convention\n\nEvery command that writes a deck takes the same arguments:\n\n| Form | Meaning |\n| --- | --- |\n| `opf embed deck.opf.json` | No output: the deck goes to stdout, the report to stderr. `-` says the same. |\n| `opf embed deck.opf.json out.opf.json` | An output after the inputs: written atomically (a temporary sibling, then a rename); an existing file needs `--force`. |\n| `opf embed deck.opf.json -i` | `-i` (`--in-place`) rewrites the input, refusing if it changed since it was read. Not for stdin, and not with an output. |\n| `opf embed deck.opf.json --check` | Writes nothing and exits 1 when the command would change something: `format --check` (not canonical), `embed --check` (not self-contained) and `paginate --check` (a slide overflows). |\n\nThe form written is the output\'s extension (`.json`, `.opf.yaml`/`.yml`, `.opf.md`), else `--to json|yaml|md`, else the form\nthat was read. An existing output is refused before anything is printed or written.\n\n`opf fill` writes one deck per record through a file name pattern: `{n}` is the record number (padded to the width of the largest, the rule of `opf convert`), `{column}` a slug of that\ncolumn\'s value (`opf fill t.opf.md clients.csv decks/qbr-{client}.opf.md`); `--combine` writes one deck with every record\'s slides.\n\n## What each flag means\n\n| Flag | Meaning |\n| --- | --- |\n| `--format json\\|text\\|github\\|patch` | The report format only: `json` (the default), `text`, `github` (validate\'s workflow annotations) or `patch` (diff\'s JSON Patch). |\n| `--from json\\|yaml\\|md` | The form of a deck input: stdin, and names that say none. Without it, a name ending `.json`, `.opf.yaml`, `.yaml`, `.yml` or `.opf.md` says the form, and text that starts with `{` or `[` is JSON; anything else is a usage error that asks for `--from yaml\\|md`. `opf convert` also takes `--from pptx`. |\n| `--to json\\|yaml\\|md` | The form of a deck output, for stdout and names that say none. `opf convert` also takes `pdf`, `pptx`, `png`, `svg` and `zip`. A `--to` that disagrees with the output\'s extension is refused. |\n| `--data-format csv\\|tsv\\|json` | The form of the data of `opf fill` and `opf ingest` (default: the extension, else JSON when it starts with `{` or `[`, else CSV). |\n| `--fail-on error\\|warning\\|info` | Findings at or above this severity fail the command (exit 1) and nothing is written (default `error`). |\n| `--force` | Replace an existing output. |\n\n## Install\n\n**Published set (2026-10-24):** CLI 0.26.1 (core `^0.26.1`, Gallery `^0.26.1`,\noptional renderer/PPTX `^0.26.1`) with core, renderer, PPTX, editor and Gallery 0.26.1. Use the complete pinned [quickstart installation](quickstart.md#versions) and its\nrequired font/converter peers.\n\nDeck forms (JSON, YAML, Markdown) need nothing beyond the CLI. PDF, PNG and SVG need `@openpresentation/opf-render`; PPTX output\nand `.pptx` input also need `@openpresentation/opf-pptx`. Both are **optional peer dependencies** of the CLI and of core (decision\nRR-27, below), loaded the first time a command needs them, from beside the CLI first and from the working directory second.\n\n`opf doctor` says what this install can write and prints the one command that installs the rest:\n\n```sh\nopf doctor --format text # per format: ready, or what is missing; then one install command\nopf doctor deck.opf.md # JSON; with a deck, a PDF needs sharp only when the deck has pictures\n```\n\nThe command matches the package manager that runs the CLI (`npm_config_user_agent`: npm, pnpm, yarn or bun) and the way the CLI\nis installed: global (`npm install -g ...`), a project (`npm install ...`), or one npx run (`npx -p @openpresentation/cli -p ... opf <command>`).\nA conversion that misses a peer exits 2 with `code: "peer-not-installed"` (or `peer-too-old`), and its `install` field and message\ncarry that one command for the format asked for. What each output needs beside `@openpresentation/opf-render`:\n\n| Output | Also install |\n| --- | --- |\n| every drawn format (the office font pack) | `@expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2` |\n| `png` | `@resvg/resvg-js@^2.6.2 sharp@^0.35.5` |\n| `pdf` | nothing for text in the default vector mode; `pdf-lib@^1.17.1` for `--raster`; `sharp@^0.35.5` when the deck has pictures |\n| `svg` | the fonts only |\n| `pptx`, a `.pptx` input | `@openpresentation/opf-pptx` |\n\n## From code\n\nThe engine the commands run is core\'s `@openpresentation/opf` in Node, which an application imports instead of spawning `opf`.\nIn Node, `validate`, `stats`, `paginate`, `embed` and `edit` register the gallery (`@openpresentation/gallery`) when a call names none, as the commands\ndo, so `opf validate deck` and `validate(deck)` agree:\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf"); // opf convert deck.opf.md deck.pdf\nconst deck = await opf.open("deck.pptx"); // opf convert deck.pptx -\nconst { files } = await opf.convert(deck, { format: "png", scale: 2 }); // bytes, nothing written\nconst { presentation } = opf.edit(deck, patch); // opf edit deck --patch patch.json\nconst { decks } = opf.fill(template, csvText); // opf fill template data.csv out-{n}.opf.json\n```\n\nThe options are the flags in camel case (`--raster` is `raster: true`, `--fonts` is `fonts` (a folder or a list of folders),\n`--asset-dir` is `assetDir`, `--include-hidden` is `includeHidden`). The commands refuse an existing output without `--force`;\n`convert` replaces it unless `overwrite: false`. See [OPF files in Node](node.md).\n\n## `opf convert`\n\n```sh\nopf convert deck.opf.md deck.pdf\nopf convert deck.opf.md slides/deck.png --slides 1,3-5 --scale 2 # slides/deck-1.png, -3, -4, -5\nopf convert deck.opf.md "slides/slide-{n}.png" # slides/slide-1.png, slide-2.png, ... (always numbered)\nopf convert deck.opf.md slides.zip --to svg\nopf convert deck.pptx deck.opf.md --signals signals.json\nopf convert outline.md deck.opf.yaml # a plain .md input is OPF Markdown\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf convert deck.opf.md - --to pdf > deck.pdf\n```\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide beside the output, named after it: `slides/deck.png` writes `slides/deck-1.png`, `-2`, ... (the deck\'s slide numbers). One selected slide, or a one-slide deck, is written to the output itself. The extension is the format: `deck.png` is always PNG. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide or one selected slide, and `{n}` is the deck\'s slide number (2 and 5 with `--slides 2,5`). `{n}` is the only placeholder: any other `{key}`, and `{n}` in `.pdf`, `.pptx`, `.zip` or a deck form, is a usage error (exit 2, `invalid-option`). |\n\nNumbers are padded with zeros to the width of the largest number written: `1`...`9` for slides 1 to 9, `01`...`99` up to 99, `001` from 100. With `--slides` the width comes from the largest slide selected (`--slides 2,5` gives `deck-2.png`, `deck-5.png`). The same rule pads `{n}` in `opf fill` (the largest record number) and the zip entries below.\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered, and no `{n}` in the archive\'s own name): PNG, or SVG with `--to svg`. Entries are stored with a fixed timestamp, so the archive is byte-identical everywhere. |\n| `.json`, `.opf.yaml`, `.yml`, `.opf.md` | the deck in that form |\n| `-` | stdout, with `--to`: one file (`pdf`, `pptx`, `zip`, a deck form, or one slide as `png` or `svg`) |\n\nA flag applies only when its format is involved; anywhere else it is a usage error (exit 2, `option-not-applicable`):\n\n| Involved | Flags |\n| --- | --- |\n| a Markdown input | `--split auto\\|rules\\|headings` (default `auto`: slides at `---` lines, and an outline with none at its `#` headings), `--title <text>` (the deck name unless the front matter sets one) |\n| a YAML input | `--aliases` (expand anchors, aliases and merge keys, at most 100) |\n| a `.pptx` input | `--signals <file>` (also write the raw per-shape layout and style signals, `fromPptx` with `signals: true`) |\n| a YAML output | `--schema-comment` (the `# yaml-language-server: $schema=...` line) |\n| a Markdown output | `--drop-unsupported` (leave out what the dialect has no syntax for and list it as loss, instead of embedding it as YAML in a fence) |\n| pdf, png, svg | `--slides 1,3-5` (one-based; `2-` to the end), `--include-hidden` |\n| pdf, png, svg, pptx | `--paginate`, `--date YYYY-MM-DD`, `--fonts <dir>` (repeatable), `--asset-dir <dir>` |\n| png, raster pdf | `--scale 0.1-8` (1 draws the 1280 x 720 reference slide at 1280 x 720 pixels) |\n| pdf | `--raster` (each page a picture instead of selectable vector text) |\n| svg | `--text fonts\\|system\\|paths` (embed the faces the slide uses, none, or draw glyph outlines) |\n| pptx | `--charts auto\\|native\\|picture`, `--images compatible\\|preserve`, `--provenance full\\|references-only\\|none`, `--body-text measured\\|native`, `--slide-numbers static\\|live` |\n\nA `.pptx` input is imported, checked and then written or drawn; `findings` holds both steps (`import/` rules from the importer).\nPer-slide images and the PDF skip slides marked `hidden: true` unless `--include-hidden`; slides named with `--slides` are always\nwritten, and the report lists the slides left out in `skippedHidden`. The PPTX keeps a hidden slide as a hidden slide.\n\n### PPTX text (RR-88, RR-91)\n\nBy default (`--body-text measured`) a wrapped paragraph becomes one text box per line, laid out exactly as the preview draws it, so editing a line in PowerPoint or Keynote does not reflow the rest. `--body-text native` opts in to whole paragraphs: a title that wraps is one Title placeholder, and a text, bullets or numbered-list region is one body that the application re-wraps when edited. What native mode cannot carry (groups, cards, nested or described lists, restarted numbering, titles with a hard break) stays measured and is reported as `native-body-fallback`. `--slide-numbers` decides a footer such as `{{slide.number}} / {{deck.slideCount}}`: `static` (the default) writes the number as fixed text, so Keynote shows the words around it, and `live` keeps the slide-number field, which Keynote draws as the digit alone because it drops the words next to a live number. A footer that is only `{{slide.number}}` stays a live field in both modes (the total is always fixed text). Both flags need an opf-pptx that reads them (0.22.0 or later); an older one is refused (`peer-too-old`) rather than ignoring them, and they are exit 2 for any other output.\n\n**Which to choose.** Pick `measured` (the default) when the deck is final and the PPTX is a delivery format: every line sits where the preview put it, and the deck is edited in OPF. Pick `native` when people will edit the text in PowerPoint or Keynote: a longer bullet wraps, a title keeps its placeholder (so outline view and slide titles work), and numbered lists renumber. Native text is re-wrapped by the application\'s own fonts and engine, so line breaks can differ from the preview; native wrapping is not yet qualified in every application, and anything it cannot carry (see above) stays measured with a `native-body-fallback` finding, so read the findings. `--slide-numbers static` is the default because Keynote replaces any paragraph that holds a live slide-number field with the number alone, so `{{slide.number}} / {{deck.slideCount}}` showed only the digit; the cost is that PowerPoint does not renumber a static number when you reorder slides there (choose `live` for that, and accept the digit-only footer in Keynote).\n\n## The other commands\n\n- **`opf create`** writes a starter deck (`--title`) or a bundled example (`--example <slug>`, from `opf catalog examples`).\n- **`opf validate`** checks files, folders (their `*.opf.json`, `*.opf.yaml`, `*.opf.yml` and `*.opf.md`, sorted, skipping\n `node_modules` and dot folders) and `-`. One input prints its report; several print `{ command, ok, files: [reports], counts }`.\n The exit status is the worst over every file. `--format github` prints `::error`, `::warning` and `::notice` annotations with the\n file, line and column. See [validate](validate.md).\n- **`opf paginate`** measures the page breaks with the fonts `opf convert --paginate` draws with when `@openpresentation/opf-render`\n is installed (`layout: "measured"`); without it they are estimated (`layout: "estimated"`) and `hint` names the install command.\n- **`opf fill`** reads CSV, TSV or JSON data; a blank cell keeps the variable\'s declared value; an unfilled required variable fails\n unless `--partial`; `--examples` fills from the declared examples. The second argument is always the data, whatever its name\n (`-` reads it from stdin), and the third is the output; a template alone fills from its own values, to stdout.\n- **`opf ingest`** gives the same slide id and bytes for the same inputs; `--dataset <id>` stores the rows in the deck\'s datasets.\n\n## Reports and errors\n\nEvery command that reports prints one envelope:\n\n```json\n{ "command": "convert", "ok": true, "input": { "file": "/abs/deck.opf.md", "sha256": "\u2026", "bytes": 1234 },\n "outputs": [{ "file": "/abs/deck.pdf", "sha256": "\u2026", "bytes": 56789, "mediaType": "application/pdf" }],\n "findings": [], "counts": { "error": 0, "warning": 0, "info": 0 }, "format": "pdf", "\u2026": "\u2026" }\n```\n\n`input` is `null` when nothing was read and a list when several were (diff, merge, fill). The report is on stdout, or on stderr when\nstdout carries the document. Findings have the [`opf validate`](validate.md) shape (`ruleId`, `severity`, `category`, JSON Pointer\n`path`, `message`, `help`, and `location` when read from text); engine findings carry the prefix `render/`, `pptx/`, `pdf/`,\n`fonts/`, `import/` or `cli/`. The lookup commands (`schemas`, `schema`, `catalogs`, `catalog`, `--version`) print the data asked for.\n\nErrors are `{ command, ok: false, code, error, ... }` on stderr. The message states the fix: a missing argument names it and\nprints the usage, an unknown command, option or value suggests the nearest one ("Did you mean --check?"), and a removed command\nor flag names its replacement. Codes include `usage`, `unknown-command`, `unknown-option`, `removed-command`, `removed-option`,\n`missing-argument`, `extra-argument`, `missing-value`, `invalid-value`, `option-not-applicable`, `unknown-input-format`,\n`input-not-found`, `invalid-json`, `invalid-yaml`, `invalid-markdown`, `invalid-document`, `findings-at-fail-on`,\n`output-exists`, `merge-conflict`, `fill-failed`, `peer-not-installed` and `peer-too-old`.\n\nExit `0`: success (warnings allowed). Exit `1`: an invalid document, a finding at or above `--fail-on`, an existing output, a\nmerge conflict, or a `--check` that found a change. Exit `2`: usage, a read or I/O problem, text that is not valid JSON, YAML or\nMarkdown, or a missing or too-old peer. (`opf validate` reports text that does not parse as a finding, exit 1.)\n\n## Fonts\n\nThe renderer\'s bundled open font pack (the office pack: Carlito, Intos for Aptos, the open families font schemes select and the\nopen replacements the font policy routes proprietary families to) with the visual substitution policy, plus the files from\n`--fonts`. **System fonts are never loaded**, and nothing is downloaded. Font substitutions are listed under\n`fonts.substitutions` in the report. The PPTX keeps the font names the document chose.\n\nScripts beyond Latin, Greek and Cyrillic need the optional Noto script packages of the renderer (`@expo-google-fonts/noto-sans-jp`\nand so on); install the ones named in the `fonts/script-font-not-installed` diagnostic next to the CLI. A standalone SVG\n(`--text fonts`, the default) embeds the faces its text uses; `--text system` omits all font bytes; `--text paths` draws the text\nas glyph outlines (the real text kept invisible beside it for selection and search), so the SVG needs no font.\n\n## Images and assets\n\nRelative image paths and `file:` paths resolve against the input\'s folder (or `--asset-dir`). Only `.png`, `.jpg`, `.jpeg`,\n`.gif`, `.webp` and `.svg` files whose content matches are read, and only inside that folder (symlinks are resolved first). URLs\nare never fetched. For SVG and PNG an unreadable image draws the renderer\'s placeholder with an `unresolved-asset` or\n`cli/asset-blocked` warning. For PPTX (opf-pptx 0.24.1 and later) an unreadable or blocked local path and an unresolved remote URL\nare one case: the deck exports without that image (an unavailable-image placeholder, or the fallback background for a background\npicture) and the report carries a `pptx/unresolved-asset` warning. Use `--fail-on warning` to reject that output before any file is\nwritten. Before opf-pptx 0.24.1 an unreadable or blocked local path stopped the export (`pptx/asset-unresolved`, an error).\n\n## Markdown and YAML\n\nJSON stays the canonical form of a deck. YAML (`deck.opf.yaml`) and Markdown (`deck.opf.md`) are authoring forms of the same data,\nand every command that reads or writes a deck reads and writes all three:\n\n```sh\nopf convert deck.md deck.opf.json # Markdown in the OPF dialect (or an outline) to a deck\nopf convert deck.opf.json deck.opf.md # a deck as Markdown that reads back unchanged\nopf convert deck.opf.yaml deck.opf.json --aliases # strict YAML (--aliases expands anchors)\nopf validate deck.opf.yaml deck.opf.md # every command reads .yaml, .yml and .opf.md\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md --check # canonical Markdown\nopf validate - --from yaml < deck.txt # stdin and other names: --from\n```\n\nSee [Markdown and outlines](markdown.md) and [OPF as YAML](yaml.md). A syntax error exits 2 with the line and column (`opf\nvalidate` reports it as a `yaml/<rule>` or `markdown/<rule>` finding and exits 1); a command that rewrites a YAML file does not\nkeep its comments and says so on stderr, and one that has to put content into an `opf-slide`/`opf-block` fence says so too.\n\n## Runtime choices\n\n- **Peers, not bundled.** opf-render and opf-pptx are optional peer dependencies loaded lazily. opf-pptx pulls the native `sharp`\n engine, opf-render `resvg`, `fontkit` and the font packs (about 135 MB installed); bundling them would make the small CLI\n impossible to install offline or on a locked-down agent host, and validating or editing a deck would pay for it.\n- **JSON reports.** `--format text` gives human-readable output where a command has it; `--format github` gives CI annotations.\n- **No clock.** `--date` is explicit so a rerun tomorrow gives the same bytes.\n- **Fonts are the bundled pack plus `--fonts`.** Never system fonts (owner font policy).\n- **Stored zip entries.** Deflate output differs between zlib builds, which would make archive hashes host-dependent.\n\n## Catalog names\n\n`opf catalogs` lists canonical camelCase names such as `colorSchemes` and `fontSchemes`, and `examples`. `opf catalog` also\naccepts the website-style `color-schemes`, `font-schemes` and `chart-types` spellings.\n'
|
|
33
|
+
"markdown": '# The `opf` CLI\n\nThe CLI (`@openpresentation/cli`, binary `opf`, Node 22 or later; verified on Node 24) creates, checks, edits, converts and\ninspects decks (see [its README](../packages/cli/README.md)). `opf validate` is the one checker ([validate](validate.md)).\n`opf convert` is the one command that changes formats: a deck form, PDF, PPTX, PNG and SVG. Every command runs core\'s\nfunctions, the ones applications call from [`@openpresentation/opf`](node.md); the commands add the flags, the JSON report and\nthe exit codes.\n\nEverything is deterministic and local: no network, no model, no telemetry, no system fonts. The same document, options and\ninstalled package versions give the same bytes on every operating system.\n\n## Commands\n\n| Command | What it does |\n| --- | --- |\n| `opf create [output] [--title <text> \\| --example <slug>]` | A one-slide starter deck, or one of core\'s bundled examples (`opf catalog examples` lists them). |\n| `opf validate <file\\|dir\\|->...` | The one checker: format, references, policy, accessibility, layout, content. Several files, folders and stdin in one run; `--format json\\|text\\|github`. |\n| `opf convert <input> <output>` | Any change of format: `.json`, `.opf.yaml`, `.opf.md` (and a plain `.md` input), `.pptx`, `.pdf`, `.png`, `.svg`, `.zip`, or stdin and stdout. |\n| `opf edit <file> [output] --patch <patch>` | A JSON Patch, applied whole or not at all, the result checked. |\n| `opf format <file>... [output]` | Canonical key order and layout, in the form the file has. |\n| `opf diff <a> <b>` | What changed between two decks (`--format text\\|json\\|patch`). |\n| `opf merge <base> <ours> <theirs> [output]` | A three-way merge; conflicts are listed. |\n| `opf stats <file>` | Neutral facts about a deck (`--format json\\|text`). |\n| `opf paginate <input> [output]` | Overflowing slides split into continuation slides, measured with the renderer\'s fonts when it is installed. |\n| `opf embed <input> [output]` | The catalog records a deck references, copied into it. |\n| `opf fill <template> <data> [output]` | A template\'s variables filled: one deck per record (the output is a name pattern), or `--combine`. The second argument is always the data (`-` for stdin). |\n| `opf ingest <data> [output] --as table\\|chart` | CSV, TSV or JSON rows as a table or chart slide, alone or added `--into` a deck. |\n| `opf doctor [deck]` | Which formats this install can write, and the one command that installs what is missing. |\n| `opf schemas`, `opf schema [name] [pointer]` | The OPF schemas. `opf schemas` lists the `@openpresentation/opf` version each came from next to its id, and `opf --version` names the schema id ([schema versioning](schema-versioning.md)). |\n| `opf catalogs`, `opf catalog <kind> [id]`, `opf catalog examples [slug]` | The pptx.gallery catalog (`@openpresentation/gallery`) and the bundled example decks. |\n| `opf skills <install\\|update\\|status>` | The six bundled agent skills. |\n| `opf mcp` | Runs the MCP server of `@openpresentation/mcp` on stdio when that package is installed; otherwise prints `npx -y @openpresentation/mcp` and exits 2. See [the MCP server](mcp.md). |\n\n`opf <command> --help` (or `-h`, or `opf help <command>`) prints a command\'s flags and examples.\n\nRemoved in 0.18 (each is a usage error, exit 2, that names its replacement): `opf render`, `opf export`, `opf import`,\n`opf from-md`, `opf to-md`, `opf from-yaml` and `opf to-yaml` (all `opf convert`), `opf create --from` (`opf convert`), and the\nflags `--output`, `--out`, `--out-dir` and `--name` (a positional output), `--input-format` (`--from`), `--json` (reports are\nJSON), `--dry-run` (stdout), `--data` (fill\'s second argument) and the deck values of `--format` (`--to`).\n\n## One output convention\n\nEvery command that writes a deck takes the same arguments:\n\n| Form | Meaning |\n| --- | --- |\n| `opf embed deck.opf.json` | No output: the deck goes to stdout, the report to stderr. `-` says the same. |\n| `opf embed deck.opf.json out.opf.json` | An output after the inputs: written atomically (a temporary sibling, then a rename); an existing file needs `--force`. |\n| `opf embed deck.opf.json -i` | `-i` (`--in-place`) rewrites the input, refusing if it changed since it was read. Not for stdin, and not with an output. |\n| `opf embed deck.opf.json --check` | Writes nothing and exits 1 when the command would change something: `format --check` (not canonical), `embed --check` (not self-contained) and `paginate --check` (a slide overflows). |\n\nThe form written is the output\'s extension (`.json`, `.opf.yaml`/`.yml`, `.opf.md`), else `--to json|yaml|md`, else the form\nthat was read. An existing output is refused before anything is printed or written.\n\n`opf fill` writes one deck per record through a file name pattern: `{n}` is the record number (padded to the width of the largest, the rule of `opf convert`), `{column}` a slug of that\ncolumn\'s value (`opf fill t.opf.md clients.csv decks/qbr-{client}.opf.md`); `--combine` writes one deck with every record\'s slides.\n\n## What each flag means\n\n| Flag | Meaning |\n| --- | --- |\n| `--format json\\|text\\|github\\|patch` | The report format only: `json` (the default), `text`, `github` (validate\'s workflow annotations) or `patch` (diff\'s JSON Patch). |\n| `--from json\\|yaml\\|md` | The form of a deck input: stdin, and names that say none. Without it, a name ending `.json`, `.opf.yaml`, `.yaml`, `.yml` or `.opf.md` says the form, and text that starts with `{` or `[` is JSON; anything else is a usage error that asks for `--from yaml\\|md`. `opf convert` also takes `--from pptx`. |\n| `--to json\\|yaml\\|md` | The form of a deck output, for stdout and names that say none. `opf convert` also takes `pdf`, `pptx`, `png`, `svg` and `zip`. A `--to` that disagrees with the output\'s extension is refused. |\n| `--data-format csv\\|tsv\\|json` | The form of the data of `opf fill` and `opf ingest` (default: the extension, else JSON when it starts with `{` or `[`, else CSV). |\n| `--fail-on error\\|warning\\|info` | Findings at or above this severity fail the command (exit 1) and nothing is written (default `error`). |\n| `--force` | Replace an existing output. |\n\n## Install\n\n**Published set (2026-10-24):** CLI 0.26.1 (core `^0.26.1`, Gallery `^0.26.1`,\noptional renderer/PPTX `^0.26.1`) with core, renderer, PPTX, editor and Gallery 0.26.1. Use the complete pinned [quickstart installation](quickstart.md#versions) and its\nrequired font/converter peers.\n\nDeck forms (JSON, YAML, Markdown) need nothing beyond the CLI. PDF, PNG and SVG need `@openpresentation/opf-render`; PPTX output\nand `.pptx` input also need `@openpresentation/opf-pptx`. Both are **optional peer dependencies** of the CLI and of core (decision\nRR-27, below), loaded the first time a command needs them, from beside the CLI first and from the working directory second.\n\n`opf doctor` says what this install can write and prints the one command that installs the rest:\n\n```sh\nopf doctor --format text # per format: ready, or what is missing; then one install command\nopf doctor deck.opf.md # JSON; with a deck, a PDF needs sharp only when the deck has pictures\n```\n\nThe command matches the package manager that runs the CLI (`npm_config_user_agent`: npm, pnpm, yarn or bun) and the way the CLI\nis installed: global (`npm install -g ...`), a project (`npm install ...`), or one npx run (`npx -p @openpresentation/cli -p ... opf <command>`).\nA conversion that misses a peer exits 2 with `code: "peer-not-installed"` (or `peer-too-old`), and its `install` field and message\ncarry that one command for the format asked for. What each output needs beside `@openpresentation/opf-render`:\n\n| Output | Also install |\n| --- | --- |\n| every drawn format (the office font pack) | `@expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2` |\n| `png` | `@resvg/resvg-js@^2.6.2 sharp@^0.35.5` |\n| `pdf` | nothing for text in the default vector mode; `pdf-lib@^1.17.1` for `--raster`; `sharp@^0.35.5` when the deck has pictures |\n| `svg` | the fonts only |\n| `pptx`, a `.pptx` input | `@openpresentation/opf-pptx` |\n\n## From code\n\nThe engine the commands run is core\'s `@openpresentation/opf` in Node, which an application imports instead of spawning `opf`.\nIn Node, `validate`, `stats`, `paginate`, `embed` and `edit` register the gallery (`@openpresentation/gallery`) when a call names none, as the commands\ndo, so `opf validate deck` and `validate(deck)` agree:\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf"); // opf convert deck.opf.md deck.pdf\nconst deck = await opf.open("deck.pptx"); // opf convert deck.pptx -\nconst { files } = await opf.convert(deck, { format: "png", scale: 2 }); // bytes, nothing written\nconst { presentation } = opf.edit(deck, patch); // opf edit deck --patch patch.json\nconst { decks } = opf.fill(template, csvText); // opf fill template data.csv out-{n}.opf.json\n```\n\nThe options are the flags in camel case (`--raster` is `raster: true`, `--fonts` is `fonts` (a folder or a list of folders),\n`--asset-dir` is `assetDir`, `--include-hidden` is `includeHidden`). The commands refuse an existing output without `--force`;\n`convert` replaces it unless `overwrite: false`. See [OPF files in Node](node.md).\n\n## `opf convert`\n\n```sh\nopf convert deck.opf.md deck.pdf\nopf convert deck.opf.md slides/deck.png --slides 1,3-5 --scale 2 # slides/deck-1.png, -3, -4, -5\nopf convert deck.opf.md "slides/slide-{n}.png" # slides/slide-1.png, slide-2.png, ... (always numbered)\nopf convert deck.opf.md slides.zip --to svg\nopf convert deck.pptx deck.opf.md --signals signals.json\nopf convert outline.md deck.opf.yaml # a plain .md input is OPF Markdown\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf convert deck.opf.md - --to pdf > deck.pdf\n```\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide beside the output, named after it: `slides/deck.png` writes `slides/deck-1.png`, `-2`, ... (the deck\'s slide numbers). One selected slide, or a one-slide deck, is written to the output itself. The extension is the format: `deck.png` is always PNG. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide or one selected slide, and `{n}` is the deck\'s slide number (2 and 5 with `--slides 2,5`). `{n}` is the only placeholder: any other `{key}`, and `{n}` in `.pdf`, `.pptx`, `.zip` or a deck form, is a usage error (exit 2, `invalid-option`). |\n\nNumbers are padded with zeros to the width of the largest number written: `1`...`9` for slides 1 to 9, `01`...`99` up to 99, `001` from 100. With `--slides` the width comes from the largest slide selected (`--slides 2,5` gives `deck-2.png`, `deck-5.png`). The same rule pads `{n}` in `opf fill` (the largest record number) and the zip entries below.\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered, and no `{n}` in the archive\'s own name): PNG, or SVG with `--to svg`. Entries are stored with a fixed timestamp, so the archive is byte-identical everywhere. |\n| `.json`, `.opf.yaml`, `.yml`, `.opf.md` | the deck in that form |\n| `-` | stdout, with `--to`: one file (`pdf`, `pptx`, `zip`, a deck form, or one slide as `png` or `svg`) |\n\nA flag applies only when its format is involved; anywhere else it is a usage error (exit 2, `option-not-applicable`):\n\n| Involved | Flags |\n| --- | --- |\n| a Markdown input | `--split auto\\|rules\\|headings` (default `auto`: slides at `---` lines, and an outline with none at its `#` headings), `--title <text>` (the deck name unless the front matter sets one) |\n| a YAML input | `--aliases` (expand anchors, aliases and merge keys, at most 100) |\n| a `.pptx` input | `--signals <file>` (also write the raw per-shape layout and style signals, `fromPptx` with `signals: true`) |\n| a YAML output | `--schema-comment` (the `# yaml-language-server: $schema=...` line) |\n| a Markdown output | `--drop-unsupported` (leave out what the dialect has no syntax for and list it as loss, instead of embedding it as YAML in a fence) |\n| pdf, png, svg | `--slides 1,3-5` (one-based; `2-` to the end), `--include-hidden` |\n| pdf, png, svg, pptx | `--paginate`, `--date YYYY-MM-DD`, `--fonts <dir>` (repeatable), `--asset-dir <dir>` |\n| png, raster pdf | `--scale 0.1-8` (1 draws the 1280 x 720 reference slide at 1280 x 720 pixels) |\n| pdf | `--raster` (each page a picture instead of selectable vector text) |\n| svg | `--text fonts\\|system\\|paths` (embed the faces the slide uses, none, or draw glyph outlines) |\n| pptx | `--charts auto\\|native\\|picture`, `--images compatible\\|preserve`, `--provenance full\\|references-only\\|none`, `--body-text measured\\|native`, `--slide-numbers static\\|live` |\n\nA `.pptx` input is imported, checked and then written or drawn; `findings` holds both steps (`import/` rules from the importer).\nPer-slide images and the PDF skip slides marked `hidden: true` unless `--include-hidden`; slides named with `--slides` are always\nwritten, and the report lists the slides left out in `skippedHidden`. The PPTX keeps a hidden slide as a hidden slide.\n\n### PPTX text (RR-88, RR-91)\n\nBy default (`--body-text measured`) a wrapped paragraph becomes one text box per line, laid out exactly as the preview draws it, so editing a line in PowerPoint or Keynote does not reflow the rest. `--body-text native` opts in to whole paragraphs: a title that wraps is one Title placeholder, and a text, bullets or numbered-list region is one body that the application re-wraps when edited. What native mode cannot carry (groups, cards, nested or described lists, restarted numbering, titles with a hard break) stays measured and is reported as `native-body-fallback`. `--slide-numbers` decides a footer such as `{{slide.number}} / {{deck.slideCount}}`: `static` (the default) writes the number as fixed text, so Keynote shows the words around it, and `live` keeps the slide-number field, which Keynote draws as the digit alone because it drops the words next to a live number. A footer that is only `{{slide.number}}` stays a live field in both modes (the total is always fixed text). Both flags need an opf-pptx that reads them (0.22.0 or later); an older one is refused (`peer-too-old`) rather than ignoring them, and they are exit 2 for any other output.\n\n**Which to choose.** Pick `measured` (the default) when the deck is final and the PPTX is a delivery format: every line sits where the preview put it, and the deck is edited in OPF. Pick `native` when people will edit the text in PowerPoint or Keynote: a longer bullet wraps, a title keeps its placeholder (so outline view and slide titles work), and numbered lists renumber. Native text is re-wrapped by the application\'s own fonts and engine, so line breaks can differ from the preview; native wrapping is not yet qualified in every application, and anything it cannot carry (see above) stays measured with a `native-body-fallback` finding, so read the findings. `--slide-numbers static` is the default because Keynote replaces any paragraph that holds a live slide-number field with the number alone, so `{{slide.number}} / {{deck.slideCount}}` showed only the digit; the cost is that PowerPoint does not renumber a static number when you reorder slides there (choose `live` for that, and accept the digit-only footer in Keynote).\n\n## The other commands\n\n- **`opf create`** writes a starter deck (`--title`) or a bundled example (`--example <slug>`, from `opf catalog examples`).\n- **`opf validate`** checks files, folders (their `*.opf.json`, `*.opf.yaml`, `*.opf.yml` and `*.opf.md`, sorted, skipping\n `node_modules` and dot folders) and `-`. One input prints its report; several print `{ command, ok, files: [reports], counts }`.\n The exit status is the worst over every file. `--format github` prints `::error`, `::warning` and `::notice` annotations with the\n file, line and column. See [validate](validate.md).\n- **`opf paginate`** measures the page breaks with the fonts `opf convert --paginate` draws with when `@openpresentation/opf-render`\n is installed (`layout: "measured"`); without it they are estimated (`layout: "estimated"`) and `hint` names the install command.\n- **`opf fill`** reads CSV, TSV or JSON data; a blank cell keeps the variable\'s declared value; an unfilled required variable fails\n unless `--partial`; `--examples` fills from the declared examples. The second argument is always the data, whatever its name\n (`-` reads it from stdin), and the third is the output; a template alone fills from its own values, to stdout.\n- **`opf ingest`** gives the same slide id and bytes for the same inputs; `--dataset <id>` stores the rows in the deck\'s datasets.\n\n## Reports and errors\n\nEvery command that reports prints one envelope:\n\n```json\n{ "command": "convert", "ok": true, "input": { "file": "/abs/deck.opf.md", "sha256": "\u2026", "bytes": 1234 },\n "outputs": [{ "file": "/abs/deck.pdf", "sha256": "\u2026", "bytes": 56789, "mediaType": "application/pdf" }],\n "findings": [], "counts": { "error": 0, "warning": 0, "info": 0 }, "format": "pdf", "\u2026": "\u2026" }\n```\n\n`input` is `null` when nothing was read and a list when several were (diff, merge, fill). The report is on stdout, or on stderr when\nstdout carries the document. Findings have the [`opf validate`](validate.md) shape (`ruleId`, `severity`, `category`, JSON Pointer\n`path`, `message`, `help`, and `location` when read from text); engine findings carry the prefix `render/`, `pptx/`, `pdf/`,\n`fonts/`, `import/` or `cli/`. The lookup commands (`schemas`, `schema`, `catalogs`, `catalog`, `--version`) print the data asked for.\n\nErrors are `{ command, ok: false, code, error, ... }` on stderr. The message states the fix: a missing argument names it and\nprints the usage, an unknown command, option or value suggests the nearest one ("Did you mean --check?"), and a removed command\nor flag names its replacement. Codes include `usage`, `unknown-command`, `unknown-option`, `removed-command`, `removed-option`,\n`missing-argument`, `extra-argument`, `missing-value`, `invalid-value`, `option-not-applicable`, `unknown-input-format`,\n`input-not-found`, `invalid-json`, `invalid-yaml`, `invalid-markdown`, `invalid-document`, `findings-at-fail-on`,\n`output-exists`, `merge-conflict`, `fill-failed`, `peer-not-installed` and `peer-too-old`.\n\nExit `0`: success (warnings allowed). Exit `1`: an invalid document, a finding at or above `--fail-on`, an existing output, a\nmerge conflict, or a `--check` that found a change. Exit `2`: usage, a read or I/O problem, text that is not valid JSON, YAML or\nMarkdown, or a missing or too-old peer. (`opf validate` reports text that does not parse as a finding, exit 1.)\n\n## Fonts\n\nThe renderer\'s bundled open font pack (the office pack: Carlito, Intos for Aptos, the open families font schemes select and the\nopen replacements the font policy routes proprietary families to) with the visual substitution policy, plus the files from\n`--fonts`. **System fonts are never loaded**, and nothing is downloaded. Font substitutions are listed under\n`fonts.substitutions` in the report. The PPTX keeps the font names the document chose.\n\nScripts beyond Latin, Greek and Cyrillic need the optional Noto script packages of the renderer (`@expo-google-fonts/noto-sans-jp`\nand so on); install the ones named in the `fonts/script-font-not-installed` diagnostic next to the CLI. A standalone SVG\n(`--text fonts`, the default) embeds the faces its text uses; `--text system` omits all font bytes; `--text paths` draws the text\nas glyph outlines (the real text kept invisible beside it for selection and search), so the SVG needs no font.\n\n## Images and assets\n\nRelative image paths and `file:` paths resolve against the input\'s folder (or `--asset-dir`). Only `.png`, `.jpg`, `.jpeg`,\n`.gif`, `.webp` and `.svg` files whose content matches are read, and only inside that folder (symlinks are resolved first). URLs\nare never fetched. For SVG and PNG an unreadable image draws the renderer\'s placeholder with an `unresolved-asset` or\n`cli/asset-blocked` warning. For PPTX (opf-pptx 0.24.1 and later) an unreadable or blocked local path and an unresolved remote URL\nare one case: the deck exports without that image (an unavailable-image placeholder, or the fallback background for a background\npicture) and the report carries a `pptx/unresolved-asset` warning. Use `--fail-on warning` to reject that output before any file is\nwritten. Before opf-pptx 0.24.1 an unreadable or blocked local path stopped the export (`pptx/asset-unresolved`, an error).\n\n## Markdown and YAML\n\nJSON stays the canonical form of a deck. YAML (`deck.opf.yaml`) and Markdown (`deck.opf.md`) are authoring forms of the same data,\nand every command that reads or writes a deck reads and writes all three:\n\n```sh\nopf convert deck.md deck.opf.json # Markdown in the OPF dialect (or an outline) to a deck\nopf convert deck.opf.json deck.opf.md # a deck as Markdown that reads back unchanged\nopf convert deck.opf.yaml deck.opf.json --aliases # strict YAML (--aliases expands anchors)\nopf validate deck.opf.yaml deck.opf.md # every command reads .yaml, .yml and .opf.md\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md --check # canonical Markdown\nopf validate - --from yaml < deck.txt # stdin and other names: --from\n```\n\nSee [Markdown and outlines](markdown.md) and [OPF as YAML](yaml.md). A syntax error exits 2 with the line and column (`opf\nvalidate` reports it as a `yaml/<rule>` or `markdown/<rule>` finding and exits 1); a command that rewrites a YAML file does not\nkeep its comments and says so on stderr, and one that has to put content into an `opf-slide`/`opf-block` fence says so too.\n\n## Runtime choices\n\n- **Peers, not bundled.** opf-render and opf-pptx are optional peer dependencies loaded lazily. opf-pptx pulls the native `sharp`\n engine, opf-render `resvg`, `fontkit` and the font packs (about 135 MB installed); bundling them would make the small CLI\n impossible to install offline or on a locked-down agent host, and validating or editing a deck would pay for it.\n- **JSON reports.** `--format text` gives human-readable output where a command has it; `--format github` gives CI annotations.\n- **No clock.** `--date` is explicit so a rerun tomorrow gives the same bytes.\n- **Fonts are the bundled pack plus `--fonts`.** Never system fonts (owner font policy).\n- **Stored zip entries.** Deflate output differs between zlib builds, which would make archive hashes host-dependent.\n\n## Catalog names\n\n`opf catalogs` lists canonical camelCase names such as `colorSchemes` and `fontSchemes`, and `examples`. `opf catalog` also\naccepts the website-style `color-schemes`, `font-schemes` and `chart-types` spellings.\n'
|
|
32
34
|
},
|
|
33
35
|
{
|
|
34
36
|
"slug": "compatibility-matrix",
|
|
35
37
|
"file": "docs/compatibility-matrix.md",
|
|
36
38
|
"title": "Compatibility matrix",
|
|
37
|
-
"markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\n**Published set (2026-10-24):** core, renderer, PPTX, editor, CLI and Gallery,\nall at the shared version 0.26.1. `node scripts/registry-plan.mjs` prints each\nversion with the registry `gitHead` of its publisher commit.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published layout contract and the 0.20 engines\n\nA layout record is a `direction` and one ordered `regions` list of regions and\nsplits, nested for two dimensions (FA-33: core 0.25.0 and Gallery 5.0.0, catalog\ncompatibility generation 4). `areas`, `columns`, `rows` and the `regions` map are\ngone, and Gallery 3 and 4 records (generation 3, written as nested `areas`\narrays) no longer validate on core 0.25. Gallery owns every default record;\nper-kind schema versions remain v2. Core 0.19 and later consume the Gallery\npackage and do not export `@openpresentation/opf/catalog` or ship a catalog\nsnapshot.\n\nThe 0.20 engines share measured quote allocation, the 32 reference pixel\nstarting size for lists without an authored size, neutral structural borders\nand default right alignment for numeric table columns. Core and CLI forward\nregistered catalogs during PPTX import. PPTX preserves stretch images with\nzero crop through native save/re-import. These published contracts do not\nimply universal Office or full-catalog visual parity.\n\n## Current published package contracts\n\n| Package | Version | Dependency contract |\n| --- | --- | --- |\n| Core | 0.26.1 | Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| CLI | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| Renderer | 0.26.1 | Core ^0.26.1; optional pinned font/converter peers |\n| PPTX | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer ^0.26.1 |\n| Editor | 0.26.1 | Core ^0.26.1; optional renderer ^0.26.1 |\n| Gallery | 0.26.1 | Owner of every default catalog record; 28 intent layouts; same records as 5.0.0 |\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | `engines.node` is `>=22` on every package below; **Node 24** is the coordinated toolchain these records are verified on (the 0.12.2 set and later are tested on Node 22, 24 and 26) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Historical coordinated 0.18 published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.18.4 | optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0`, which the Node build loads for `convert`, `open` and `save` |\n| `@openpresentation/cli` | 0.18.1 | `@openpresentation/opf@^0.18.3` (a regular dependency, no bundled copy); optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0` |\n| `@openpresentation/opf-render` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peers (RR-63): the converters `sharp`, `@resvg/resvg-js` and `pdf-lib` and the `@expo-google-fonts/*` font packages, which a host installs for the formats and fonts it uses |\n| `@openpresentation/opf-editor` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n| `@openpresentation/opf-pptx` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n\nThe 0.18 set is the consistent-API release ([design](programs/release-readiness/0.18-consistent-api.md)), breaking:\n- **One import (RR-70).** `import * as opf from \"@openpresentation/opf\"` works in every runtime through conditional\n exports, and `/node` is removed. `defaultCatalog` lives at `@openpresentation/opf/catalog` only.\n- **Names (RR-73) and options (RR-74).**\n - `ingest` replaces `importData` (and `opf ingest` replaces `opf import-data`).\n - `toSvg`, `toPng`, `toPdf` and `toHtml` work on decks.\n - The editor exports with `convert`.\n - `convert` takes `text`, `raster`, `charts`, `images`, `fonts` and `name`.\n- **Logos (RR-71).** Logos come from `organizations[].logo` (`full`, `stacked`, `icon`, `wordmark`, `onLight`/`onDark`),\n placed with `var:organization.logo.*`. `LogoSet` and zone `logo: true` are removed.\n- **PowerPoint (RR-72).** Shared furniture is written once on the PPTX slide master or layout.\n- **CLI (RR-75).** `opf convert` absorbs `render`, `export` and `import`. There is one output convention, plus\n `opf doctor` and a multi-file `validate`.\n\nSee the 0.18.0 and 0.18.1 entries of the [changelog](../CHANGELOG.md) and the 0.18.0 entry of the [CLI changelog](../packages/cli/CHANGELOG.md).\n\nCore starts at **0.18.1**. `opf-v0.18.0` was tagged, but its publish run failed before `npm publish`: a core test\nspawned the CLI, which the publish workflow had not built. So 0.18.1 is the first published 0.18 core.\n\nThe 0.17 set (core, editor and PPTX 0.17.0, renderer 0.17.1, CLI 0.17.1) was the lockstep minor after 0.16, also breaking:\n- **Furniture variables (FA-31):** header and footer text uses the `{{slide.number}}`, `{{slide.section}}` and\n `{{deck.slideCount}}` variables, and the `HeaderFooterItem` flags are removed.\n- **`@openpresentation/opf/node` (RR-62):** `convert`, `open` and `save` work from files and bytes. `parse` and\n `stringify` replace `readDeck` and `writeDeck`. `opf convert` is new, and `@openpresentation/cli/api` is removed.\n- **Fixes and additions:**\n - deterministic data import;\n - `validate` and `paginate` that plan script faces (RR-59);\n - the Arabic and Latin preview fixes;\n - `renderSvg(deck, { textAsPaths: true })` (RR-64 phase 1);\n - the Windows CLI path fixes (RR-66).\n\nSee the 0.17.0 entries of the [changelog](../CHANGELOG.md) and the 0.17.1 entry of the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nRenderer 0.17.1 is a patch on the 0.17 line: SVG never embeds a face whose OS/2 `fsType` forbids it (RR-76), embedded faces are cut to the glyphs a slide draws (RR-65, mean example slide 2.4 MB to 59 KB), and `textAsPaths` shapes with HarfBuzz and keeps an invisible text layer (RR-64 phases 2 and 3).\n\nThe CLI starts at **0.17.1**. `cli-v0.17.0` was tagged, but its publish run failed before `npm publish`, because the\nrelease commit carried a stale generated gallery tracker. So 0.17.1 is the first published 0.17 CLI.\n\nThe 0.16 set (core, CLI, renderer and editor 0.16.0, PPTX 0.16.1) was the lockstep minor after 0.15 (owner decision 2026-10-08: no 0.15.x patch train). It adds nested\nplaceholder groups in layout records (FA-26), `Table.alt` (FA-27), the `opf/language-tag` warning (FA-29), Markdown\ndecks (`.opf.md`) in every command with core `readDeck`/`writeDeck` (RR-60), SVGs that embed only the faces they draw\n(RR-61), the CLI's programmatic `@openpresentation/cli/api` (RR-62), a lighter renderer install with optional\nconverters and fonts and the `/png`, `/pdf` and `/html` subpaths (RR-63), and the developer-contract fixes (RR-59).\n0.15 documents are 0.16 documents. See the 0.16.0 entries of the [changelog](../CHANGELOG.md) and the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nThe 0.15 set (core 0.15.1, CLI 0.15.0, renderer, PPTX and editor 0.15.0) was the breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nSee the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore 0.15 started at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nwas the first published 0.15 core and the floor of every 0.15 sibling.\n\nOPF 0.15 is a breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nThe 0.15 set reads and writes only 0.15 documents; see the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore starts at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nis the first published 0.15 core and the floor of every 0.15 sibling.\n\nFrom CLI 0.16.0 the CLI depends on core (`@openpresentation/opf`, a regular dependency, not a bundled copy). CLI 0.16.0\nexposed `@openpresentation/cli/api`, and 0.17 removes it in favour of core's `@openpresentation/opf/node`. The renderer\nand PPTX stay optional peers. CLI 0.15.0 and earlier bundled their core.\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The selected renderer, PPTX and editor 0.26.1 require core ^0.26.1;\nCLI 0.26.1 accepts core ^0.26.1 (CLI 0.25.0 and earlier accept only their own minor). Pin core 0.26.1 alongside them and\nverify one installed Core so preview and export use the same composition.\nCore 0.20.2 and later center metrics when alignment is omitted; explicit alignment remains\nauthoritative.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\nPPTX 0.18 (RR-72) writes a tagged part that is the same on two or more slides once,\non the slide master or a shared layout (one per background tone for `onLight`/`onDark`\nlogos; a hidden footer uses a layout that hides the master's), and reads it back from\nthere ([rr-72-master-furniture.md](programs/release-readiness/rr-72-master-furniture.md)).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Independently released packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/gallery` | 0.26.1 (the shared version, with records identical to 5.0.0, the last number of its own line; 4.0.0 (2026-10-19), 3.0.0, 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nGallery 4.0.0 (published and independently verified 2026-10-19) adds theme `variants` and removes the `dark` theme; core 0.24 and its siblings depended on `^4.0.0`. It declares catalog compatibility generation 3. Gallery 5.0.0 (published and independently verified; layouts as one list of regions, catalog generation 4) is the range core 0.25.0 and this checkout depend on (`^5.0.0`); the published set depends on it from core 0.25.0, and so does CLI 0.25.0 (CLI 0.24.1 and earlier, on Gallery `^4.0.0`, cannot read it).\n\nThe gallery (RR-78) is the package `packages/gallery` of this repository. Since 0.26.0 it is released with the other five at one\nshared version (`vX.Y.Z` tags, `npm-publish.yml`); until 0.25.0 it was released from its own repository on its own line, which\nended at 5.0.0 (Gallery 1.0.0 to 5.0.0 stay on npm). New catalog records go in any release; a removed id or a changed drawing needs a\nminor release ([release process](release-process.md)). From OPF 0.19 core and the\nCLI consume the Gallery package (Gallery 2 in OPF 0.19; Gallery 3 in OPF 0.20), and core has no `/catalog` subpath; up to 0.18 core ships the same records as\n`@openpresentation/opf/catalog`.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **127** decks; the quickstart JSON is a docs fixture, not a 128th catalog entry |\n| Validate | `validate` / `opf validate` | One read-only checker: format, references, policy, accessibility, layout and content findings ([guide](validate.md)); no network catalog fetch |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Supported theme-slot colors write native `schemeClr` references; arbitrary native Office recoloring and full visual fidelity remain separately scoped. |\n| Embed catalog records | `embed` / `opf embed` | Embeds every record a document references, in the group it resolves in, so it renders with no catalog registered; remote media and data remain explicit host concerns. |\n| Offline fonts | `loadFonts` (`/fonts-node`) | Bundled Roboto pack (`pack: 'base'`) or the office pack; hashed files; the handle is passed as `{ fonts }` to every deck-level call |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginate` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `toSvg(deck)` (every slide) / `toSvg(deck, n)` (slide n, counted from 1) (opf-render 0.18; `renderSvg` / `renderSlideSvg` before) | Local; the same `{ fonts }` as layout; `text: \"paths\"` draws the text as glyph outlines |\n| Convert, export and import files from code | `convert`, `open` and `save` of `@openpresentation/opf` in Node (core 0.18.0 and later; core 0.17 had them at `@openpresentation/opf/node`; CLI 0.16.0 had `exportDeck` / `importDeck` in `@openpresentation/cli/api`) | PDF, PNG, SVG and PPTX out, PPTX in, deck forms both ways, with the bundled fonts, no network and no clock; calls opf-render and opf-pptx as optional peers and throws `OPFExportError` / `OPFImportError` with the install command when one is missing. `opf convert` runs the same engine (CLI 0.18; `opf render`, `opf export` and `opf import` before it) ([OPF files in Node](node.md)) |\n| Read and write a deck in JSON, YAML or Markdown | `parse` / `stringify` (core 0.17.0 and later; `readDeck` / `writeDeck` in 0.16) | One entry point for the three text formats; `parse` throws `OPFValidationError` with located findings |\n| PNG | `toPng` (opf-render 0.18; `svgToPng` before) | Node raster of a deck or of SVG slides |\n| PDF | `toPdf` (opf-render 0.18; `svgToPdf` before) | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `raster: true` keeps the image-per-slide output (`mode: \"raster\"` before 0.18). Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Text is one editable box per wrapped line, so edited text does not reflow in PowerPoint or Keynote, and a wrapped title is not the Title placeholder; the opt-in `bodyText: \"native\"` mode (CLI `--body-text native`, from opf-pptx and CLI 0.22.0) writes whole paragraphs, a wrapped title as the Title placeholder. Shared furniture is written on the master or layout; supported footer placeholders and tagged parts retain controlled re-import provenance. General native Office behavior remains separately scoped |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current coordinated adoption is recorded in the [final adoption note](evidence/rr-85-final-adoption-2026-10-16/README.md). The following source, CI and canonical production results are the historical September 21 checkpoint, recorded in the\n[font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\nApp47 corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged App53 at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\nApp54 first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\nimmutable 93-file app bundle\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe startup-link correction at 4338e57\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. Immutable app evidence\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe immutable app bundle\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. Postmerge CI 35660464578\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\ndraft app #58: independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity beyond the supported footer placeholders | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; supported native footer placeholders, self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `toPng` / `toPdf`\n(`svgToPng` / `svgToPdf` before 0.18) APIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\nCore's file API (`open`, `save`, `convert`) is Node-only too: it reads files, font files and the engines' native packages. From\ncore 0.18 the root resolves to a browser-safe build in a browser or a worker, with the same names, where those three reject\nwith `node-only`; every other core entry is browser-safe. In a browser, use the engines' browser entries.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.17.0, CLI 0.17.1, renderer 0.17.1, PPTX 0.17.0, editor 0.17.0 | Previous coordinated set (FA-31 furniture variables, `@openpresentation/opf/node`, `parse`/`stringify`, `opf convert`). The 0.18 set is breaking: one root import replaces `/node`, the `toSvg`/`ingest` names and `convert` options, organization logos, and the consolidated CLI. |\n| core 0.16.0, CLI 0.16.0, renderer 0.16.0, PPTX 0.16.1, editor 0.16.0 | Previous coordinated set (Markdown decks, used-face SVG embedding, optional renderer converters, nested placeholder groups, `@openpresentation/cli/api`). The 0.17 set is breaking: header and footer flags become `{{slide.number}}`-style variables (FA-31), `readDeck`/`writeDeck` become `parse`/`stringify`, and `@openpresentation/cli/api` is replaced by `@openpresentation/opf/node`. |\n| core 0.15.1, CLI 0.15.0, renderer 0.15.0, PPTX 0.15.0, editor 0.15.0 | Previous coordinated set (OPF 0.15, format audit wave C; CLI 0.15.0 bundles core 0.15.1). The 0.16 set is additive for documents; the renderer's converters and font packages became optional peers (RR-63), so a 0.16 host installs the ones it uses, and the CLI depends on core instead of bundling it (RR-62). |\n| core 0.14.0, CLI 0.11.0, renderer 0.14.0, PPTX 0.14.0, editor 0.14.2 | Previous coordinated set (format audit waves A and B, RR-55 and RR-56; CLI 0.11.0 bundles core 0.13.0). The 0.15 set is breaking: a 0.14 document's catalog shape (`catalogs.<kind>.records`), catalog-id languages and `design.slideImage` are invalid in 0.15, and the 0.15 engines need the host to register the default catalog. |\n| core 0.13.0, CLI 0.11.0, renderer 0.13.1, PPTX 0.13.2, editor 0.12.1 | Previous coordinated set (chart and table data, RR-54). |\n| core 0.12.2, CLI 0.10.1, renderer 0.12.2, PPTX 0.12.4, editor 0.11.3 | Previous coordinated set (the first set whose `engines.node` is the open-ended `>=22`). The 0.13 set adds chart and table data (RR-54, [contract](chart-table-data.md)): strict chart numbers (a value is a finite number or a strict decimal string; `\"12%\"`, `\"$5\"` and `\"Q1\"` are a gap with a `chart-value-not-numeric` warning, never a guessed number), number formats (`DataColumn` `{ name, format }`), top-level `datasets` with `{ \"dataset\", \"fields\" }` references and `chart.mapping`, in core 0.13.0, previewed by renderer 0.13.0, exported and re-imported by PPTX 0.13.0 (`OPF_DATASETS_V1` and `OPF_DATA_V1` provenance), edited in the editor 0.12.x data grid and imported by CLI 0.11.0 (`opf import-data --dataset`). PPTX 0.13.0 also vendors pptxgenjs-plus 4.3.4 (was PptxGenJS 4.0.1) and PPTX 0.13.1 bundles for browsers again. Renderer 0.13.1 and PPTX 0.13.2 pin Sharp 0.35.5 (GHSA-wq5f-xc86-pv6w; output unchanged); editor 0.12.1 replaces 0.12.0, which was never published. Renderer, PPTX and editor raise their core floor to `^0.13.0` and the renderer peer to `^0.13.1` together. |\n| core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. PPTX 0.12.3 corrects the table range of a chart's embedded workbook (apostrophe-quoted sheet references and bubble-series references), which made Keynote drop category charts on import (opf-pptx#162, opf-pptx#163; every other part of the package is byte-identical). Core, renderer and editor are unchanged and keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. Core 0.12.1 deprecates the six plural audience ids (`executives`, `investors`, `customers`, `sales-team`, `marketing-team`, `regulators`) in favour of the singular ids: additive catalog data, validation warns and never errors, and no geometry moves, so renderer, PPTX and editor keep the core floor `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 | Previous coordinated set (PowerPoint lists only the deck's fonts). PPTX 0.12.2 also names the deck's font in the theme's `Viet` (Vietnamese) and `Uigh` (Uyghur) per-language script entries, which PowerPoint applies to `vi-VN` and `ug` runs and which kept Office's Times New Roman and Arial for those two languages (RR-17; every other language is byte-identical). Editor 0.11.2 adds the `slide-sizes` and `purposes` switch dimensions, `SLIDE_SIZE_PRESETS` and the Slide size and Purpose selects in the Design panel (RR-41, additive API). Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 | Not valid for these packages |\n| Node 22 / 26 | Declared by the current set (`engines.node` `>=22`) and tested, but Node 24 is the toolchain the evidence runs on. Releases up to core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2 declared `24.x`, so npm on Node 22 or 26 silently picked an older release |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
|
|
39
|
+
"markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\n**Published set (2026-10-24):** core, renderer, PPTX, editor, CLI and Gallery,\nall at the shared version 0.26.1. `node scripts/registry-plan.mjs` prints each\nversion with the registry `gitHead` of its publisher commit.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published layout contract and the 0.20 engines\n\nA layout record is a `direction` and one ordered `regions` list of regions and\nsplits, nested for two dimensions (FA-33: core 0.25.0 and Gallery 5.0.0, catalog\ncompatibility generation 4). `areas`, `columns`, `rows` and the `regions` map are\ngone, and Gallery 3 and 4 records (generation 3, written as nested `areas`\narrays) no longer validate on core 0.25. Gallery owns every default record;\nper-kind schema versions remain v2. Core 0.19 and later consume the Gallery\npackage and do not export `@openpresentation/opf/catalog` or ship a catalog\nsnapshot.\n\nThe 0.20 engines share measured quote allocation, the 32 reference pixel\nstarting size for lists without an authored size, neutral structural borders\nand default right alignment for numeric table columns. Core and CLI forward\nregistered catalogs during PPTX import. PPTX preserves stretch images with\nzero crop through native save/re-import. These published contracts do not\nimply universal Office or full-catalog visual parity.\n\n## Current published package contracts\n\n| Package | Version | Dependency contract |\n| --- | --- | --- |\n| Core | 0.26.1 | Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| CLI | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| Renderer | 0.26.1 | Core ^0.26.1; optional pinned font/converter peers |\n| PPTX | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer ^0.26.1 |\n| Editor | 0.26.1 | Core ^0.26.1; optional renderer ^0.26.1 |\n| Gallery | 0.26.1 | Owner of every default catalog record; 30 intent layouts; same records as 5.0.0 |\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | `engines.node` is `>=22` on every package below; **Node 24** is the coordinated toolchain these records are verified on (the 0.12.2 set and later are tested on Node 22, 24 and 26) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Historical coordinated 0.18 published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.18.4 | optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0`, which the Node build loads for `convert`, `open` and `save` |\n| `@openpresentation/cli` | 0.18.1 | `@openpresentation/opf@^0.18.3` (a regular dependency, no bundled copy); optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0` |\n| `@openpresentation/opf-render` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peers (RR-63): the converters `sharp`, `@resvg/resvg-js` and `pdf-lib` and the `@expo-google-fonts/*` font packages, which a host installs for the formats and fonts it uses |\n| `@openpresentation/opf-editor` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n| `@openpresentation/opf-pptx` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n\nThe 0.18 set is the consistent-API release ([design](programs/release-readiness/0.18-consistent-api.md)), breaking:\n- **One import (RR-70).** `import * as opf from \"@openpresentation/opf\"` works in every runtime through conditional\n exports, and `/node` is removed. `defaultCatalog` lives at `@openpresentation/opf/catalog` only.\n- **Names (RR-73) and options (RR-74).**\n - `ingest` replaces `importData` (and `opf ingest` replaces `opf import-data`).\n - `toSvg`, `toPng`, `toPdf` and `toHtml` work on decks.\n - The editor exports with `convert`.\n - `convert` takes `text`, `raster`, `charts`, `images`, `fonts` and `name`.\n- **Logos (RR-71).** Logos come from `organizations[].logo` (`full`, `stacked`, `icon`, `wordmark`, `onLight`/`onDark`),\n placed with `var:organization.logo.*`. `LogoSet` and zone `logo: true` are removed.\n- **PowerPoint (RR-72).** Shared furniture is written once on the PPTX slide master or layout.\n- **CLI (RR-75).** `opf convert` absorbs `render`, `export` and `import`. There is one output convention, plus\n `opf doctor` and a multi-file `validate`.\n\nSee the 0.18.0 and 0.18.1 entries of the [changelog](../CHANGELOG.md) and the 0.18.0 entry of the [CLI changelog](../packages/cli/CHANGELOG.md).\n\nCore starts at **0.18.1**. `opf-v0.18.0` was tagged, but its publish run failed before `npm publish`: a core test\nspawned the CLI, which the publish workflow had not built. So 0.18.1 is the first published 0.18 core.\n\nThe 0.17 set (core, editor and PPTX 0.17.0, renderer 0.17.1, CLI 0.17.1) was the lockstep minor after 0.16, also breaking:\n- **Furniture variables (FA-31):** header and footer text uses the `{{slide.number}}`, `{{slide.section}}` and\n `{{deck.slideCount}}` variables, and the `HeaderFooterItem` flags are removed.\n- **`@openpresentation/opf/node` (RR-62):** `convert`, `open` and `save` work from files and bytes. `parse` and\n `stringify` replace `readDeck` and `writeDeck`. `opf convert` is new, and `@openpresentation/cli/api` is removed.\n- **Fixes and additions:**\n - deterministic data import;\n - `validate` and `paginate` that plan script faces (RR-59);\n - the Arabic and Latin preview fixes;\n - `renderSvg(deck, { textAsPaths: true })` (RR-64 phase 1);\n - the Windows CLI path fixes (RR-66).\n\nSee the 0.17.0 entries of the [changelog](../CHANGELOG.md) and the 0.17.1 entry of the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nRenderer 0.17.1 is a patch on the 0.17 line: SVG never embeds a face whose OS/2 `fsType` forbids it (RR-76), embedded faces are cut to the glyphs a slide draws (RR-65, mean example slide 2.4 MB to 59 KB), and `textAsPaths` shapes with HarfBuzz and keeps an invisible text layer (RR-64 phases 2 and 3).\n\nThe CLI starts at **0.17.1**. `cli-v0.17.0` was tagged, but its publish run failed before `npm publish`, because the\nrelease commit carried a stale generated gallery tracker. So 0.17.1 is the first published 0.17 CLI.\n\nThe 0.16 set (core, CLI, renderer and editor 0.16.0, PPTX 0.16.1) was the lockstep minor after 0.15 (owner decision 2026-10-08: no 0.15.x patch train). It adds nested\nplaceholder groups in layout records (FA-26), `Table.alt` (FA-27), the `opf/language-tag` warning (FA-29), Markdown\ndecks (`.opf.md`) in every command with core `readDeck`/`writeDeck` (RR-60), SVGs that embed only the faces they draw\n(RR-61), the CLI's programmatic `@openpresentation/cli/api` (RR-62), a lighter renderer install with optional\nconverters and fonts and the `/png`, `/pdf` and `/html` subpaths (RR-63), and the developer-contract fixes (RR-59).\n0.15 documents are 0.16 documents. See the 0.16.0 entries of the [changelog](../CHANGELOG.md) and the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nThe 0.15 set (core 0.15.1, CLI 0.15.0, renderer, PPTX and editor 0.15.0) was the breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nSee the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore 0.15 started at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nwas the first published 0.15 core and the floor of every 0.15 sibling.\n\nOPF 0.15 is a breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nThe 0.15 set reads and writes only 0.15 documents; see the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore starts at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nis the first published 0.15 core and the floor of every 0.15 sibling.\n\nFrom CLI 0.16.0 the CLI depends on core (`@openpresentation/opf`, a regular dependency, not a bundled copy). CLI 0.16.0\nexposed `@openpresentation/cli/api`, and 0.17 removes it in favour of core's `@openpresentation/opf/node`. The renderer\nand PPTX stay optional peers. CLI 0.15.0 and earlier bundled their core.\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The selected renderer, PPTX and editor 0.26.1 require core ^0.26.1;\nCLI 0.26.1 accepts core ^0.26.1 (CLI 0.25.0 and earlier accept only their own minor). Pin core 0.26.1 alongside them and\nverify one installed Core so preview and export use the same composition.\nCore 0.20.2 and later center metrics when alignment is omitted; explicit alignment remains\nauthoritative.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\nPPTX 0.18 (RR-72) writes a tagged part that is the same on two or more slides once,\non the slide master or a shared layout (one per background tone for `onLight`/`onDark`\nlogos; a hidden footer uses a layout that hides the master's), and reads it back from\nthere ([rr-72-master-furniture.md](programs/release-readiness/rr-72-master-furniture.md)).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Independently released packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/gallery` | 0.26.1 (the shared version, with records identical to 5.0.0, the last number of its own line; 4.0.0 (2026-10-19), 3.0.0, 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nGallery 4.0.0 (published and independently verified 2026-10-19) adds theme `variants` and removes the `dark` theme; core 0.24 and its siblings depended on `^4.0.0`. It declares catalog compatibility generation 3. Gallery 5.0.0 (published and independently verified; layouts as one list of regions, catalog generation 4) is the range core 0.25.0 and this checkout depend on (`^5.0.0`); the published set depends on it from core 0.25.0, and so does CLI 0.25.0 (CLI 0.24.1 and earlier, on Gallery `^4.0.0`, cannot read it).\n\nThe gallery (RR-78) is the package `packages/gallery` of this repository. Since 0.26.0 it is released with the other five at one\nshared version (`vX.Y.Z` tags, `npm-publish.yml`); until 0.25.0 it was released from its own repository on its own line, which\nended at 5.0.0 (Gallery 1.0.0 to 5.0.0 stay on npm). New catalog records go in any release; a removed id or a changed drawing needs a\nminor release ([release process](release-process.md)). From OPF 0.19 core and the\nCLI consume the Gallery package (Gallery 2 in OPF 0.19; Gallery 3 in OPF 0.20), and core has no `/catalog` subpath; up to 0.18 core ships the same records as\n`@openpresentation/opf/catalog`.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **127** decks; the quickstart JSON is a docs fixture, not a 128th catalog entry |\n| Validate | `validate` / `opf validate` | One read-only checker: format, references, policy, accessibility, layout and content findings ([guide](validate.md)); no network catalog fetch |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Supported theme-slot colors write native `schemeClr` references; arbitrary native Office recoloring and full visual fidelity remain separately scoped. |\n| Embed catalog records | `embed` / `opf embed` | Embeds every record a document references, in the group it resolves in, so it renders with no catalog registered; remote media and data remain explicit host concerns. |\n| Offline fonts | `loadFonts` (`/fonts-node`) | Bundled Roboto pack (`pack: 'base'`) or the office pack; hashed files; the handle is passed as `{ fonts }` to every deck-level call |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginate` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `toSvg(deck)` (every slide) / `toSvg(deck, n)` (slide n, counted from 1) (opf-render 0.18; `renderSvg` / `renderSlideSvg` before) | Local; the same `{ fonts }` as layout; `text: \"paths\"` draws the text as glyph outlines |\n| Convert, export and import files from code | `convert`, `open` and `save` of `@openpresentation/opf` in Node (core 0.18.0 and later; core 0.17 had them at `@openpresentation/opf/node`; CLI 0.16.0 had `exportDeck` / `importDeck` in `@openpresentation/cli/api`) | PDF, PNG, SVG and PPTX out, PPTX in, deck forms both ways, with the bundled fonts, no network and no clock; calls opf-render and opf-pptx as optional peers and throws `OPFExportError` / `OPFImportError` with the install command when one is missing. `opf convert` runs the same engine (CLI 0.18; `opf render`, `opf export` and `opf import` before it) ([OPF files in Node](node.md)) |\n| Read and write a deck in JSON, YAML or Markdown | `parse` / `stringify` (core 0.17.0 and later; `readDeck` / `writeDeck` in 0.16) | One entry point for the three text formats; `parse` throws `OPFValidationError` with located findings |\n| PNG | `toPng` (opf-render 0.18; `svgToPng` before) | Node raster of a deck or of SVG slides |\n| PDF | `toPdf` (opf-render 0.18; `svgToPdf` before) | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `raster: true` keeps the image-per-slide output (`mode: \"raster\"` before 0.18). Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Text is one editable box per wrapped line, so edited text does not reflow in PowerPoint or Keynote, and a wrapped title is not the Title placeholder; the opt-in `bodyText: \"native\"` mode (CLI `--body-text native`, from opf-pptx and CLI 0.22.0) writes whole paragraphs, a wrapped title as the Title placeholder. Shared furniture is written on the master or layout; supported footer placeholders and tagged parts retain controlled re-import provenance. General native Office behavior remains separately scoped |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current coordinated adoption is recorded in the [final adoption note](evidence/rr-85-final-adoption-2026-10-16/README.md). The following source, CI and canonical production results are the historical September 21 checkpoint, recorded in the\n[font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\nApp47 corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged App53 at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\nApp54 first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\nimmutable 93-file app bundle\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe startup-link correction at 4338e57\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. Immutable app evidence\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe immutable app bundle\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. Postmerge CI 35660464578\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\ndraft app #58: independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity beyond the supported footer placeholders | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; supported native footer placeholders, self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `toPng` / `toPdf`\n(`svgToPng` / `svgToPdf` before 0.18) APIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\nCore's file API (`open`, `save`, `convert`) is Node-only too: it reads files, font files and the engines' native packages. From\ncore 0.18 the root resolves to a browser-safe build in a browser or a worker, with the same names, where those three reject\nwith `node-only`; every other core entry is browser-safe. In a browser, use the engines' browser entries.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.17.0, CLI 0.17.1, renderer 0.17.1, PPTX 0.17.0, editor 0.17.0 | Previous coordinated set (FA-31 furniture variables, `@openpresentation/opf/node`, `parse`/`stringify`, `opf convert`). The 0.18 set is breaking: one root import replaces `/node`, the `toSvg`/`ingest` names and `convert` options, organization logos, and the consolidated CLI. |\n| core 0.16.0, CLI 0.16.0, renderer 0.16.0, PPTX 0.16.1, editor 0.16.0 | Previous coordinated set (Markdown decks, used-face SVG embedding, optional renderer converters, nested placeholder groups, `@openpresentation/cli/api`). The 0.17 set is breaking: header and footer flags become `{{slide.number}}`-style variables (FA-31), `readDeck`/`writeDeck` become `parse`/`stringify`, and `@openpresentation/cli/api` is replaced by `@openpresentation/opf/node`. |\n| core 0.15.1, CLI 0.15.0, renderer 0.15.0, PPTX 0.15.0, editor 0.15.0 | Previous coordinated set (OPF 0.15, format audit wave C; CLI 0.15.0 bundles core 0.15.1). The 0.16 set is additive for documents; the renderer's converters and font packages became optional peers (RR-63), so a 0.16 host installs the ones it uses, and the CLI depends on core instead of bundling it (RR-62). |\n| core 0.14.0, CLI 0.11.0, renderer 0.14.0, PPTX 0.14.0, editor 0.14.2 | Previous coordinated set (format audit waves A and B, RR-55 and RR-56; CLI 0.11.0 bundles core 0.13.0). The 0.15 set is breaking: a 0.14 document's catalog shape (`catalogs.<kind>.records`), catalog-id languages and `design.slideImage` are invalid in 0.15, and the 0.15 engines need the host to register the default catalog. |\n| core 0.13.0, CLI 0.11.0, renderer 0.13.1, PPTX 0.13.2, editor 0.12.1 | Previous coordinated set (chart and table data, RR-54). |\n| core 0.12.2, CLI 0.10.1, renderer 0.12.2, PPTX 0.12.4, editor 0.11.3 | Previous coordinated set (the first set whose `engines.node` is the open-ended `>=22`). The 0.13 set adds chart and table data (RR-54, [contract](chart-table-data.md)): strict chart numbers (a value is a finite number or a strict decimal string; `\"12%\"`, `\"$5\"` and `\"Q1\"` are a gap with a `chart-value-not-numeric` warning, never a guessed number), number formats (`DataColumn` `{ name, format }`), top-level `datasets` with `{ \"dataset\", \"fields\" }` references and `chart.mapping`, in core 0.13.0, previewed by renderer 0.13.0, exported and re-imported by PPTX 0.13.0 (`OPF_DATASETS_V1` and `OPF_DATA_V1` provenance), edited in the editor 0.12.x data grid and imported by CLI 0.11.0 (`opf import-data --dataset`). PPTX 0.13.0 also vendors pptxgenjs-plus 4.3.4 (was PptxGenJS 4.0.1) and PPTX 0.13.1 bundles for browsers again. Renderer 0.13.1 and PPTX 0.13.2 pin Sharp 0.35.5 (GHSA-wq5f-xc86-pv6w; output unchanged); editor 0.12.1 replaces 0.12.0, which was never published. Renderer, PPTX and editor raise their core floor to `^0.13.0` and the renderer peer to `^0.13.1` together. |\n| core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. PPTX 0.12.3 corrects the table range of a chart's embedded workbook (apostrophe-quoted sheet references and bubble-series references), which made Keynote drop category charts on import (opf-pptx#162, opf-pptx#163; every other part of the package is byte-identical). Core, renderer and editor are unchanged and keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. Core 0.12.1 deprecates the six plural audience ids (`executives`, `investors`, `customers`, `sales-team`, `marketing-team`, `regulators`) in favour of the singular ids: additive catalog data, validation warns and never errors, and no geometry moves, so renderer, PPTX and editor keep the core floor `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 | Previous coordinated set (PowerPoint lists only the deck's fonts). PPTX 0.12.2 also names the deck's font in the theme's `Viet` (Vietnamese) and `Uigh` (Uyghur) per-language script entries, which PowerPoint applies to `vi-VN` and `ug` runs and which kept Office's Times New Roman and Arial for those two languages (RR-17; every other language is byte-identical). Editor 0.11.2 adds the `slide-sizes` and `purposes` switch dimensions, `SLIDE_SIZE_PRESETS` and the Slide size and Purpose selects in the Design panel (RR-41, additive API). Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 | Not valid for these packages |\n| Node 22 / 26 | Declared by the current set (`engines.node` `>=22`) and tested, but Node 24 is the toolchain the evidence runs on. Releases up to core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2 declared `24.x`, so npm on Node 22 or 26 silently picked an older release |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
|
|
38
40
|
},
|
|
39
41
|
{
|
|
40
42
|
"slug": "content-item-design-overrides",
|
|
@@ -64,25 +66,25 @@ var docsData = Object.freeze([
|
|
|
64
66
|
"slug": "default-catalog",
|
|
65
67
|
"file": "docs/default-catalog.md",
|
|
66
68
|
"title": "Catalogs and the gallery package",
|
|
67
|
-
"markdown": '# Catalogs and the gallery package\n\nOPF 0.15 keeps catalogs out of the engine. A document embeds every record it uses, hosts register the catalogs they\ntrust, and core ships no records. The **default catalog** is the npm package **`@openpresentation/gallery`**, authored in\n`packages/gallery` of this repository (RR-78, moved in by MR-23) and released with the other packages under one shared version;\npptx.gallery presents it and consumes the published package. Core and the CLI\ndepend on it and register it in Node; core has no `/catalog` subpath (removed in 0.19, with no alias).\n\nThis page is the contract: the document shape, the one resolution rule, how hosts register catalogs, the authoring\nhelpers, the diagnostics, and who authors and releases the gallery package. The design behind it is\n[0.15-design.md](programs/format-audit/0.15-design.md).\n\n## Catalogs in a document\n\n`catalogs` groups the records a document embeds by the catalog they came from. Inside a group, records are keyed by\nkind and then by id:\n\n```json\n"catalogs": {\n "default": {\n "source": "https://www.pptx.gallery",\n "layouts": { "two-column": { "name": "Two column", "regions": [{ "name": "title" }, { "direction": "row", "regions": [{ "name": "first", "accepts": ["text"], "flow": "auto" }, { "name": "second", "accepts": ["text"], "flow": "auto" }] }] } },\n "themes": { "minimal": { "name": "Minimal", "colorScheme": "cool-horizon", "fontScheme": "aptos" } }\n },\n "acme": { "source": "pkg:@acme/opf-catalog", "layouts": { "hero": { "name": "Hero", "regions": [{ "name": "title" }, { "name": "media", "accepts": ["image"], "flow": "none" }] } } },\n "custom": { "layouts": { "q4-special": { "name": "Q4 special", "regions": [{ "name": "title" }, { "name": "chart", "accepts": ["chart"], "flow": "none" }] } } }\n}\n```\n\n- **`default`** is the catalog bare ids come from. It needs a `source` when it holds records. Omitted: bare ids fall\n back to the host\'s default catalog. `"default": false`: no catalog fallback at all, so every bare id must be embedded.\n- **`custom`** holds the records the document defines itself. It has no source. Forking a catalog record copies it here\n under a new id, so the change of ownership is visible.\n- **Any other name** (lowercase kebab-case) is an extra catalog, identified by its `source`: an HTTPS URL or a `pkg:`\n package reference. The name is the prefix its references use (`acme:hero`).\n- The kinds are `layouts`, `themes`, `colorSchemes`, `fontSchemes`, `narratives`, `audiences`, `purposes` and `tones`.\n- An embedded record has the fields of its kind\'s companion schema without `$schema` and `id` (the key is the id), and\n without the catalog\'s `x-*` display metadata. `validate` checks each one against the companion schema\n (`opf/catalog-record`).\n\n## References\n\nEvery content reference is a bare `id` or `name:id`, and every prefix must name a group (`opf/undeclared-catalog`\notherwise, a format error that engines reject at their format check). URLs and `pkg:` strings are not references; a named group replaces them. The references are\n`Slide.layout`, `design.theme`, `design.colorScheme` (or its `id`), `design.fontScheme` (or its `id`), the same three on\n`Slide.design`, `narrative`, `audience`, `purpose` and `tone` (a string or an object\'s `id`), a theme record\'s\n`colorScheme` and `fontScheme`, and a `language` object\'s `fontScheme` and `googleFontScheme`. An `audience` or\n`purpose` string that is not an id (it has spaces, capitals or punctuation) is free text, never looked up.\n\n## Resolution\n\nOne rule for every kind; nothing is ever fetched:\n\n1. A bare id: `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host\n registered for `default.source` (the host\'s default catalog when `default` is omitted; nothing when it is `false`).\n2. `name:id`: the records embedded under `catalogs.<name>`, then the catalog the host registered for its `source`.\n3. A reference written inside an embedded record (a theme\'s `colorScheme`) resolves in that record\'s own group first,\n so acme\'s `brand` theme finds acme\'s `ocean` before the default catalog\'s.\n4. Otherwise the reference is **unresolved**: the `opf/unresolved-reference` warning names the reference and the\n source it was looked for in, a slide composes automatically, and a design uses the engine default. A strict export\n fails with the same reference.\n\nA record embedded under `catalogs.default` or a named group whose registered catalog has no record of that kind and id is the warning `opf/catalog-record-not-in-source`: it is the document\'s own record and belongs in `catalogs.custom`. The check is silent when no catalog is registered for the group\'s source, for a record the catalog has with other content (an update difference) and for `custom`; rendering is unchanged.\n\nA slide with no `layout` is automatic composition, not a missing reference, and has no finding. Engines never\nsubstitute a different layout for one that does not resolve.\n\n## Registering catalogs\n\nEvery entry point that resolves references takes the same option, `catalogs`: the catalogs the host registered,\nmatched by `source`.\n\n> **The host-default rule.** The first registered catalog is the default for documents that omit `catalogs.default`:\n> their bare ids resolve in `custom`, then in that catalog. A document whose `default` names a `source` uses the\n> registered catalog with that source, and nothing when the host did not register it, even if it registered others.\n> `"default": false` uses no catalog for bare ids. Without the `catalogs` option the browser build and every engine\n> register nothing, so only what the document embeds resolves; in Node, core\'s file API and `validate`, `stats`,\n> `paginate`, `embed` and `edit` register `gallery`, as the CLI does. Hosts (the CLI, the editor app, the sites)\n> register `@openpresentation/gallery`; libraries and engines pass the option through.\n\n```js\nimport { validate, resolveSlideContext, paginate, embed } from \'@openpresentation/opf\';\nimport { gallery } from \'@openpresentation/gallery\';\n\nconst catalogs = [gallery, acmeCatalog]; // acmeCatalog = { source: \'pkg:@acme/opf-catalog\', layouts: { hero: { \u2026 } } }\nvalidate(document, { catalogs });\nresolveSlideContext(document, 0, { catalogs, strictReferences: true }); // throws OPFUnresolvedReferenceError\npaginate(document, { catalogs });\n```\n\nA registered catalog has the shape of a document group with `source` required: `{ source, layouts?: { <id>: record },\nthemes?, \u2026 }`. Records may keep their `x-*` display metadata there; embedding strips it. `resolveSlideContext`,\n`paginate`, `validate`, `stats`, `resolveScriptFonts` (`/composition`), `resolveReference`, `catalogRecords`, `embed`,\n`copySlides`, `moveToCustom` and `updateFromCatalog` all take it, and opf-render, opf-pptx and opf-editor pass it through to core, so a\nrecord a host registers resolves the same in preview, editor and export. Without it only what the document embeds\nresolves. A `catalogs` value that is not an array of registered catalogs throws `OPFCatalogsOptionError`\n(`code: "invalid-catalogs"`) at every entry point. `resolveSlideContext(...).resolved.provenance` (and\n`resolveDesignRecords(...).provenance`) says where each resolved layout, theme, colour scheme and font scheme came from\n(`{ kind, reference, id, group, source?, origin }`, origin `document` or `host`), so engines record provenance without\nresolving again.\n\n`@openpresentation/gallery` exports:\n\n| Export | What it is |\n| --- | --- |\n| `gallery` | The content records, keyed by kind and id, registered under `GALLERY_SOURCE` |\n| `GALLERY_SOURCE` | `https://www.pptx.gallery` |\n| `CATALOG_SCHEMA` | The version of core\'s catalog record schemas the records target (also `opf.catalogSchema` in its `package.json`) |\n| `catalogDisplay` | Display metadata for pickers: `chartTypes`, `languages` and `socialPlatforms` by id |\n| `catalogIndexes` | Each kind\'s `index.json` |\n| `catalogManifest` | A content hash per kind, and the pptx.gallery commit of the one-time 1.x snapshot (never re-synced: records have been authored in the gallery package since 2.0.0) |\n\n`@openpresentation/gallery/previews` exports the static HTML layout previews: `layoutPreviews`, `layoutPreviewIndex`,\n`layoutPreviewSlugs`, `getLayoutPreview` and `hasLayoutPreview`.\n\nThe region-list source contract (FA-33) reads catalog record generation `CATALOG_SCHEMA = 4` (exported from core).\nThis package compatibility generation is separate from the layout schema URI: layout records retain\n`https://openpresentation.org/schema/opf-layout/v2` and per-kind `schemaVersion: 2`. Generation 4 refuses\ngeneration-3 Gallery records because `areas`, `columns`, `rows` and the `regions` map no longer validate; like generation 3\n(nested area arrays) it does not introduce a `/v3` schema URI.\n\nCore reads catalog schema `CATALOG_SCHEMA` (exported from its root). When the installed gallery declares another one,\ncore never registers it: the Node defaults throw `OPFApiError` `gallery-schema-mismatch` and the CLI stops with the same\ncode, naming both versions. No browser entry of core imports catalog data; `pnpm check:catalog-free` holds that with an\nesbuild metafile budget.\n\nOPF 0.19 removed `@openpresentation/opf/catalog` (`defaultCatalog`, `DEFAULT_CATALOG_SOURCE`, `catalogDisplay`,\n`catalogIndexes` and the layout previews) and core\'s `spec/catalogs/` and `spec/previews/` files, with no alias: import\nthe same records from `@openpresentation/gallery`, where `defaultCatalog` is `gallery` and `DEFAULT_CATALOG_SOURCE` is\n`GALLERY_SOURCE`.\n\n## Authoring helpers\n\n- **`embed(document, { catalogs })`** embeds each referenced record once, in the group it resolves in, together with\n the records it references (a theme\'s colour and font schemes). A bare id from the host default goes under\n `catalogs.default` with the catalog\'s source. Records already embedded are kept as they are; `x-*`, `$schema` and `id`\n are stripped. It returns `{ document, added, unresolved }` and is idempotent. Authoring tools call it on save and\n export; `opf embed` does it on the command line.\n- **`copySlides(from, to, indexes, { catalogs, at })`** copies slides with their records. Groups match by `source`, not\n by name: references are rewritten to the target\'s name for that source, and a missing group is added (renamed\n `<name>-2` when its name is taken). A source document without `catalogs.default` inherits the host default\'s\n source; when the target\'s default has another source, or is `false`, that catalog is added to the target as a named\n group and the copied bare references get its prefix, so copied slides never change catalog. A record the target\n already has with the same content is reused. A `custom` id\n the target uses for different content is renamed `<id>-2`. A catalog record whose revision differs from the one the\n target resolves moves into `custom` as `<id>-2`, so the copied slides look the same, and is listed in `renamed`.\n `renamed` lists only the records a call creates under a new id: a later copy that reuses one is not a rename.\n- **`moveToCustom(document, { kind, reference }, { catalogs })`** moves a record embedded under `default` or a named\n group into `custom`: the fix `opf/catalog-record-not-in-source` suggests. Every reference that named it is rewritten\n to name it in `custom`, and its own references keep naming what they named. It keeps its id unless `custom` holds a\n different record under it (then `<id>-2` and up, reported in `renamed`; an identical one is reused). It returns\n `{ document, from, to, references, patch, renamed? }`, where `patch` is RFC 6902 for review and undo, and throws\n `OPFMoveToCustomError` (`invalid-reference`, `already-custom`, `not-embedded`, `invalid-id`). Fork:\n `moveToCustom(document, ref, { id })` copies the record into `custom` under the new id (`<id>-2` on conflict) and\n rewrites every reference to the copy, qualified references inside other records included, so the original is dropped\n from its group; an editor calls it on the first edit of a catalog record and applies the edit to the copy.\n- **`updateFromCatalog(document, catalogs, refs?)`** compares the records embedded under `default` and the named groups\n with the registered catalogs\' current ones and returns `{ changes, patch }`. Nothing changes until the author applies\n `patch` (`applyPatch`); `custom` records are never compared.\n- **`resolveReference`**, **`parseReference`** and **`catalogRecords`** (the records a picker can offer, with the\n reference to write for each) are the building blocks.\n\n## Engine vocabularies and engine defaults\n\nValues an engine must understand to draw are not catalog references:\n\n- `chart.type` is a schema enum;\n- the keys of `Organization.socials` and `Speaker.socials` are a schema enum, linked with URL patterns in code;\n- `language` is a BCP-47 tag (or a `Language` object with a `bcp47` tag); engines know the script, direction, OOXML\n culture tag and default script fonts of the vocabulary\'s tags and infer the script of any other well-formed tag.\n\nThey are listed in [`spec/reference/engine-vocabularies.json`](../spec/reference/engine-vocabularies.json) and exported\nas `CHART_TYPES`, `SOCIAL_PLATFORMS` and `LANGUAGES`. Their labels, descriptions and icons stay in the catalog as\ndisplay metadata (`catalogDisplay`).\n\nWhen nothing names a theme, colour scheme or font scheme, or a reference resolves nowhere, every engine draws with the\nsame engine defaults: [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json), exported as\n`ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME` and `ENGINE_DEFAULT_FONT_SCHEME`. They are the drawing fields of\nthe gallery\'s `minimal`, `cool-horizon` and `aptos` records, compiled into code.\n\n## Ownership\n\n- **The gallery package owns every catalog record.** Since gallery 2.0.0 records, layouts and theme variants are authored\n by pull requests in `packages/gallery` (the product statement "pptx.gallery owns every default record" is retired, D11 of the\n monorepo program: the site presents the catalog, it does not author it); each id has one owner. Its\n display metadata lives under `x-*` members. pptx.gallery (the site, a separate repository) consumes the published package,\n adds its own presentation overlay and does not write records. A record that needs an engine change is one pull request,\n tested by one `verify` run.\n- **The gallery package owns the records and releases with the other packages.** `packages/gallery` is a tracked package of this repository (the monorepo program, D13): records, layouts and theme variants are authored by pull request here, its manifest holds a content hash per kind and the commit of the one-time 1.x snapshot from pptx.gallery, and it takes the repository\'s one shared version line (0.26.0 and later; gallery 1.0.0 to 5.0.0 stay on npm). The earlier `pnpm install` materializer of ignored fixtures is gone.\n- **A host chooses its catalog version.** Hosts register the catalog they choose; documents carry the records they were\n saved with. The packages are released together, so the pair a release ships (core and the gallery at the same version) is\n tested together.\n\n## The `@openpresentation/gallery` package\n\nRR-78: the same records are published as the npm package\n[`@openpresentation/gallery`](https://www.npmjs.com/package/@openpresentation/gallery) from `packages/gallery`, with the\nrepository\'s one shared version (from 0.26.0; the earlier independent line ended at 5.0.0). A record change is an ordinary\npull request (`catalog/<kind>/<id>.json` plus its index entry, then `pnpm rehash` and `pnpm previews` in the package, checked\nby `pnpm check:changed`); a `changes/<slug>.md` fragment records it and the release workflow publishes it with the rest:\n\n```js\nimport { gallery } from \'@openpresentation/gallery\';\nvalidate(document, { catalogs: [gallery] });\n```\n\n`gallery` replaces what `@openpresentation/opf/catalog` exported as `defaultCatalog` (`source` and the eight content kinds, records\nkeyed by id without `$schema`, `id` or `x-*`); the package also exports `GALLERY_SOURCE`, `CATALOG_SCHEMA`,\n`catalogDisplay`, `catalogIndexes` and `catalogManifest`, and `/previews`. New records go in any release; an id removed\nor renamed, a changed drawing or a record schema change is declared in the `changes/` fragment and makes the release a minor\none (a patch is refused), which the pixel-stability check (`pnpm check:stability`, against the `latest` release) enforces.\nFrom OPF 0.19 core and the CLI depend on it (`workspace:^` in this checkout, published as `^X.Y.Z` of the same release), core\'s\nNode defaults register `gallery`, and core drops `/catalog` and `spec/catalogs/`. Gallery 1.0.0 to 5.0.0 stay on npm and\nare never deprecated, because published core 0.25.0 depends on `^5.0.0`; the shared line cannot use those versions again\n(the first stable major of all six packages is 6.0.0; see [the release process](release-process.md)).\n\n### Theme variants (gallery 4)\n\nA theme record may list `variants`, an array of `{ "id", "name", "variant" }` whose `variant` is a value of `design.variant`\n(a background slot such as `"dark1"`, or `{ "background", "accents" }`; the same definition, `ColorVariant` in\n`opf.schema.json`, validates it). Gallery 4 gives `minimal`, `classic` and `bold` four each: `light`, `light-alt`, `dark`\nand `dark-alt`. A picker offers them and writes the chosen `variant` into `design.variant`; documents never store the id, and\nthe engines read only `design.variant`. Light and dark are therefore variants of every theme, and gallery 4 has no `dark`\ntheme: `design.theme: "dark"` is an unresolved reference (`opf/unresolved-reference`). Use a theme with a dark variant, for\nexample `{ "theme": "classic", "variant": "dark1" }`, and `colorScheme: "boost"` with `fontScheme: "seaford"` for the old\ndark look.\n\n## Endpoints\n\n| Request | Response |\n| --- | --- |\n| `GET https://www.pptx.gallery/<kind>/index.json` | Catalog index |\n| `GET https://www.pptx.gallery/<kind>` with `Accept: application/json` | Same catalog index |\n| `GET https://www.pptx.gallery/<kind>/<id>.json` | One published record |\n| `GET https://www.pptx.gallery/<kind>/<id>` with `Accept: application/json` | Same record |\n| Either URL from a browser | The gallery\'s HTML page |\n\nEngines never call these. A host may, to build the catalog it registers. Catalog files are served with\n`Access-Control-Allow-Origin: *`, and negotiated URLs send `Vary: Accept`. The older\n`https://www.pptx.gallery/api/<dimension>.json` envelopes carry the gallery\'s presentation data in its own shape; they\nare not OPF records and link their catalog index with `Link: <\u2026/<kind>/index.json>; rel="alternate"`.\n\n## Kinds\n\n| `<kind>` (URL and snapshot directory) | Key | Record schema | Role | Snapshot mode |\n| --- | --- | --- | --- | --- |\n| `audiences` | `audiences` | `opf-audience/v1` | content, `audience` | mirror |\n| `color-schemes` | `colorSchemes` | `opf-color-scheme/v1` | content, `design.colorScheme` | mirror |\n| `font-schemes` | `fontSchemes` | `opf-font-scheme/v1` | content, `design.fontScheme` | mirror |\n| `layouts` | `layouts` | `opf-layout/v2` since 0.19 | content, `Slide.layout` | authored in `packages/gallery` |\n| `narratives` | `narratives` | `opf-narrative/v1` | content, `narrative` | mirror |\n| `purposes` | `purposes` | `opf-purpose/v1` | content, `purpose` | mirror |\n| `themes` | `themes` | `opf-theme/v1` | content, `design.theme` | mirror |\n| `tones` | `tones` | `opf-tone/v1` | content, `tone` | mirror |\n| `chart-types` | `chartTypes` | `opf-chart-type/v1` | display metadata for `chart.type` | mirror |\n| `languages` | `languages` | `opf-language/v1` | display metadata for `language` | mirror |\n| `social-platforms` | `socialPlatforms` | `opf-social-platform/v1` | display metadata for `socials` keys | mirror |\n\nRecord schema ids are `https://openpresentation.org/schema/<name>`. A font scheme\'s `languages` list holds BCP-47 tags.\n\n## Index and record shape\n\nAn index validates against `spec/schemas/catalog-index.schema.json`:\n\n```json\n{\n "$schema": "https://openpresentation.org/schema/opf-catalog-index/v1",\n "kind": "tones",\n "version": "1",\n "description": "\u2026",\n "contentSha256": "b5a8d093\u2026",\n "records": [{ "id": "formal", "name": "Formal", "summary": "\u2026", "file": "formal.json" }]\n}\n```\n\n- `records` is in canonical order. `file` is relative to the index.\n- `version` is the index format version.\n- `contentSha256` is the lowercase hex SHA-256 of the canonical JSON of the full records in index order, with every\n top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace (`canonicalJson()`\n in `scripts/catalog-snapshot.mjs`). The bundled index and the published index carry the same value for a mirrored kind.\n\nA **published record file** names its companion schema in `$schema` and carries its `id`; `validateCatalogRecord(kind,\nrecord)` checks one. Publishers may add top-level `x-*` members (pptx.gallery puts page URLs, mood tags, contrast\nnotes and font stacks in `x-gallery`); the snapshot never carries them. The copy a document embeds has neither\n`$schema` nor `id` nor `x-*` members.\n\nBefore v1 an id is renamed or removed outright: there are no deprecated records, aliases or redirects. A document that\nnames a retired id gets `opf/unresolved-reference`.\n\n## Historical snapshot (before 0.19)\n\nBefore 0.19, Core stored the snapshot at `spec/catalogs/manifest.json` (schema `spec/schemas/catalog-manifest.schema.json`):\n\n```json\n{\n "publisher": "https://www.pptx.gallery",\n "source": { "repository": "https://github.com/Data-Advantage/pptx-gallery", "commit": "<sha>", "path": "public" },\n "kinds": {\n "tones": { "mode": "mirror", "records": 7, "contentSha256": "\u2026", "gallery": { "records": 7, "contentSha256": "\u2026" } }\n }\n}\n```\n\nThat historical snapshot used `mirror` for every kind and held every record the gallery published (278 layouts at that checkpoint). The `packages/gallery` package owns the snapshot and the 28 v2 layout templates; core consumes the package, and the records are tracked in this repository. No browser entry of Core imports catalog records (`pnpm check:catalog-free`).\n\nThe 1.x design synchronized the snapshot from pptx.gallery and refused a publisher that stopped serving a held id\n(waived per id with `--allow-removed`). That sync was retired with gallery 2.0.0; a record is now removed by a pull\nrequest that deletes it and its index entry, and the stability check requires the removal to be declared and the release to be a minor version.\n\n### Updating it\n\nEdit records by pull request in `packages/gallery`: change the record and its index entry, run `pnpm --filter @openpresentation/gallery rehash` and `previews`, and add a `changes/<slug>.md` fragment (`packages: [gallery]`; a drawing change is declared with `drawing:`, see `changes/README.md`). The package is released with the other five under the repository\'s one shared version. `pnpm check:spec` and `pnpm check:catalog` verify the indexes, manifest and records, and `pnpm check:stability` holds a drawing change or a removed id to the declaration rule. Core\'s browser entries remain catalog-free (`pnpm check:catalog-free`).\n\n## Reconciliation status\n\nThe per-kind divergence between the gallery and this snapshot is in\n[`programs/font-fidelity-everywhere/ff-37-catalog-divergence.md`](programs/font-fidelity-everywhere/ff-37-catalog-divergence.md).\n'
|
|
69
|
+
"markdown": '# Catalogs and the gallery package\n\nOPF 0.15 keeps catalogs out of the engine. A document embeds every record it uses, hosts register the catalogs they\ntrust, and core ships no records. The **default catalog** is the npm package **`@openpresentation/gallery`**, authored in\n`packages/gallery` of this repository (RR-78, moved in by MR-23) and released with the other packages under one shared version;\npptx.gallery presents it and consumes the published package. Core and the CLI\ndepend on it and register it in Node; core has no `/catalog` subpath (removed in 0.19, with no alias).\n\nThis page is the contract: the document shape, the one resolution rule, how hosts register catalogs, the authoring\nhelpers, the diagnostics, and who authors and releases the gallery package. The design behind it is\n[0.15-design.md](programs/format-audit/0.15-design.md).\n\n## Catalogs in a document\n\n`catalogs` groups the records a document embeds by the catalog they came from. Inside a group, records are keyed by\nkind and then by id:\n\n```json\n"catalogs": {\n "default": {\n "source": "https://www.pptx.gallery",\n "layouts": { "two-column": { "name": "Two column", "regions": [{ "name": "title" }, { "direction": "row", "regions": [{ "name": "first", "accepts": ["text"], "flow": "auto" }, { "name": "second", "accepts": ["text"], "flow": "auto" }] }] } },\n "themes": { "minimal": { "name": "Minimal", "colorScheme": "cool-horizon", "fontScheme": "aptos" } }\n },\n "acme": { "source": "pkg:@acme/opf-catalog", "layouts": { "hero": { "name": "Hero", "regions": [{ "name": "title" }, { "name": "media", "accepts": ["image"], "flow": "none" }] } } },\n "custom": { "layouts": { "q4-special": { "name": "Q4 special", "regions": [{ "name": "title" }, { "name": "chart", "accepts": ["chart"], "flow": "none" }] } } }\n}\n```\n\n- **`default`** is the catalog bare ids come from. It needs a `source` when it holds records. Omitted: bare ids fall\n back to the host\'s default catalog. `"default": false`: no catalog fallback at all, so every bare id must be embedded.\n- **`custom`** holds the records the document defines itself. It has no source. Forking a catalog record copies it here\n under a new id, so the change of ownership is visible.\n- **Any other name** (lowercase kebab-case) is an extra catalog, identified by its `source`: an HTTPS URL or a `pkg:`\n package reference. The name is the prefix its references use (`acme:hero`).\n- The kinds are `layouts`, `themes`, `colorSchemes`, `fontSchemes`, `narratives`, `audiences`, `purposes` and `tones`.\n- An embedded record has the fields of its kind\'s companion schema without `$schema` and `id` (the key is the id), and\n without the catalog\'s `x-*` display metadata. `validate` checks each one against the companion schema\n (`opf/catalog-record`).\n\n## References\n\nEvery content reference is a bare `id` or `name:id`, and every prefix must name a group (`opf/undeclared-catalog`\notherwise, a format error that engines reject at their format check). URLs and `pkg:` strings are not references; a named group replaces them. The references are\n`Slide.layout`, `design.theme`, `design.colorScheme` (or its `id`), `design.fontScheme` (or its `id`), the same three on\n`Slide.design`, `narrative`, `audience`, `purpose` and `tone` (a string or an object\'s `id`), a theme record\'s\n`colorScheme` and `fontScheme`, and a `language` object\'s `fontScheme` and `googleFontScheme`. An `audience` or\n`purpose` string that is not an id (it has spaces, capitals or punctuation) is free text, never looked up.\n\n## Resolution\n\nOne rule for every kind; nothing is ever fetched:\n\n1. A bare id: `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host\n registered for `default.source` (the host\'s default catalog when `default` is omitted; nothing when it is `false`).\n2. `name:id`: the records embedded under `catalogs.<name>`, then the catalog the host registered for its `source`.\n3. A reference written inside an embedded record (a theme\'s `colorScheme`) resolves in that record\'s own group first,\n so acme\'s `brand` theme finds acme\'s `ocean` before the default catalog\'s.\n4. Otherwise the reference is **unresolved**: the `opf/unresolved-reference` warning names the reference and the\n source it was looked for in, a slide composes automatically, and a design uses the engine default. A strict export\n fails with the same reference.\n\nA record embedded under `catalogs.default` or a named group whose registered catalog has no record of that kind and id is the warning `opf/catalog-record-not-in-source`: it is the document\'s own record and belongs in `catalogs.custom`. The check is silent when no catalog is registered for the group\'s source, for a record the catalog has with other content (an update difference) and for `custom`; rendering is unchanged.\n\nA slide with no `layout` is automatic composition, not a missing reference, and has no finding. Engines never\nsubstitute a different layout for one that does not resolve.\n\n## Registering catalogs\n\nEvery entry point that resolves references takes the same option, `catalogs`: the catalogs the host registered,\nmatched by `source`.\n\n> **The host-default rule.** The first registered catalog is the default for documents that omit `catalogs.default`:\n> their bare ids resolve in `custom`, then in that catalog. A document whose `default` names a `source` uses the\n> registered catalog with that source, and nothing when the host did not register it, even if it registered others.\n> `"default": false` uses no catalog for bare ids. Without the `catalogs` option the browser build and every engine\n> register nothing, so only what the document embeds resolves; in Node, core\'s file API and `validate`, `stats`,\n> `paginate`, `embed` and `edit` register `gallery`, as the CLI does. Hosts (the CLI, the editor app, the sites)\n> register `@openpresentation/gallery`; libraries and engines pass the option through.\n\n```js\nimport { validate, resolveSlideContext, paginate, embed } from \'@openpresentation/opf\';\nimport { gallery } from \'@openpresentation/gallery\';\n\nconst catalogs = [gallery, acmeCatalog]; // acmeCatalog = { source: \'pkg:@acme/opf-catalog\', layouts: { hero: { \u2026 } } }\nvalidate(document, { catalogs });\nresolveSlideContext(document, 0, { catalogs, strictReferences: true }); // throws OPFUnresolvedReferenceError\npaginate(document, { catalogs });\n```\n\nA registered catalog has the shape of a document group with `source` required: `{ source, layouts?: { <id>: record },\nthemes?, \u2026 }`. Records may keep their `x-*` display metadata there; embedding strips it. `resolveSlideContext`,\n`paginate`, `validate`, `stats`, `resolveScriptFonts` (`/composition`), `resolveReference`, `catalogRecords`, `embed`,\n`copySlides`, `moveToCustom` and `updateFromCatalog` all take it, and opf-render, opf-pptx and opf-editor pass it through to core, so a\nrecord a host registers resolves the same in preview, editor and export. Without it only what the document embeds\nresolves. A `catalogs` value that is not an array of registered catalogs throws `OPFCatalogsOptionError`\n(`code: "invalid-catalogs"`) at every entry point. `resolveSlideContext(...).resolved.provenance` (and\n`resolveDesignRecords(...).provenance`) says where each resolved layout, theme, colour scheme and font scheme came from\n(`{ kind, reference, id, group, source?, origin }`, origin `document` or `host`), so engines record provenance without\nresolving again.\n\n`@openpresentation/gallery` exports:\n\n| Export | What it is |\n| --- | --- |\n| `gallery` | The content records, keyed by kind and id, registered under `GALLERY_SOURCE` |\n| `GALLERY_SOURCE` | `https://www.pptx.gallery` |\n| `CATALOG_SCHEMA` | The version of core\'s catalog record schemas the records target (also `opf.catalogSchema` in its `package.json`) |\n| `catalogDisplay` | Display metadata for pickers: `chartTypes`, `languages` and `socialPlatforms` by id |\n| `catalogIndexes` | Each kind\'s `index.json` |\n| `catalogManifest` | A content hash per kind, and the pptx.gallery commit of the one-time 1.x snapshot (never re-synced: records have been authored in the gallery package since 2.0.0) |\n\n`@openpresentation/gallery/previews` exports the static HTML layout previews: `layoutPreviews`, `layoutPreviewIndex`,\n`layoutPreviewSlugs`, `getLayoutPreview` and `hasLayoutPreview`.\n\nThe region-list source contract (FA-33) reads catalog record generation `CATALOG_SCHEMA = 4` (exported from core).\nThis package compatibility generation is separate from the layout schema URI: layout records retain\n`https://openpresentation.org/schema/opf-layout/v2` and per-kind `schemaVersion: 2`. Generation 4 refuses\ngeneration-3 Gallery records because `areas`, `columns`, `rows` and the `regions` map no longer validate; like generation 3\n(nested area arrays) it does not introduce a `/v3` schema URI.\n\nCore reads catalog schema `CATALOG_SCHEMA` (exported from its root). When the installed gallery declares another one,\ncore never registers it: the Node defaults throw `OPFApiError` `gallery-schema-mismatch` and the CLI stops with the same\ncode, naming both versions. No browser entry of core imports catalog data; `pnpm check:catalog-free` holds that with an\nesbuild metafile budget.\n\nOPF 0.19 removed `@openpresentation/opf/catalog` (`defaultCatalog`, `DEFAULT_CATALOG_SOURCE`, `catalogDisplay`,\n`catalogIndexes` and the layout previews) and core\'s `spec/catalogs/` and `spec/previews/` files, with no alias: import\nthe same records from `@openpresentation/gallery`, where `defaultCatalog` is `gallery` and `DEFAULT_CATALOG_SOURCE` is\n`GALLERY_SOURCE`.\n\n## Authoring helpers\n\n- **`embed(document, { catalogs })`** embeds each referenced record once, in the group it resolves in, together with\n the records it references (a theme\'s colour and font schemes). A bare id from the host default goes under\n `catalogs.default` with the catalog\'s source. Records already embedded are kept as they are; `x-*`, `$schema` and `id`\n are stripped. It returns `{ document, added, unresolved }` and is idempotent. Authoring tools call it on save and\n export; `opf embed` does it on the command line.\n- **`copySlides(from, to, indexes, { catalogs, at })`** copies slides with their records. Groups match by `source`, not\n by name: references are rewritten to the target\'s name for that source, and a missing group is added (renamed\n `<name>-2` when its name is taken). A source document without `catalogs.default` inherits the host default\'s\n source; when the target\'s default has another source, or is `false`, that catalog is added to the target as a named\n group and the copied bare references get its prefix, so copied slides never change catalog. A record the target\n already has with the same content is reused. A `custom` id\n the target uses for different content is renamed `<id>-2`. A catalog record whose revision differs from the one the\n target resolves moves into `custom` as `<id>-2`, so the copied slides look the same, and is listed in `renamed`.\n `renamed` lists only the records a call creates under a new id: a later copy that reuses one is not a rename.\n- **`moveToCustom(document, { kind, reference }, { catalogs })`** moves a record embedded under `default` or a named\n group into `custom`: the fix `opf/catalog-record-not-in-source` suggests. Every reference that named it is rewritten\n to name it in `custom`, and its own references keep naming what they named. It keeps its id unless `custom` holds a\n different record under it (then `<id>-2` and up, reported in `renamed`; an identical one is reused). It returns\n `{ document, from, to, references, patch, renamed? }`, where `patch` is RFC 6902 for review and undo, and throws\n `OPFMoveToCustomError` (`invalid-reference`, `already-custom`, `not-embedded`, `invalid-id`). Fork:\n `moveToCustom(document, ref, { id })` copies the record into `custom` under the new id (`<id>-2` on conflict) and\n rewrites every reference to the copy, qualified references inside other records included, so the original is dropped\n from its group; an editor calls it on the first edit of a catalog record and applies the edit to the copy.\n- **`updateFromCatalog(document, catalogs, refs?)`** compares the records embedded under `default` and the named groups\n with the registered catalogs\' current ones and returns `{ changes, patch }`. Nothing changes until the author applies\n `patch` (`applyPatch`); `custom` records are never compared.\n- **`resolveReference`**, **`parseReference`** and **`catalogRecords`** (the records a picker can offer, with the\n reference to write for each) are the building blocks.\n\n## Engine vocabularies and engine defaults\n\nValues an engine must understand to draw are not catalog references:\n\n- `chart.type` is a schema enum;\n- the keys of `Organization.socials` and `Speaker.socials` are a schema enum, linked with URL patterns in code;\n- `language` is a BCP-47 tag (or a `Language` object with a `bcp47` tag); engines know the script, direction, OOXML\n culture tag and default script fonts of the vocabulary\'s tags and infer the script of any other well-formed tag.\n\nThey are listed in [`spec/reference/engine-vocabularies.json`](../spec/reference/engine-vocabularies.json) and exported\nas `CHART_TYPES`, `SOCIAL_PLATFORMS` and `LANGUAGES`. Their labels, descriptions and icons stay in the catalog as\ndisplay metadata (`catalogDisplay`).\n\nWhen nothing names a theme, colour scheme or font scheme, or a reference resolves nowhere, every engine draws with the\nsame engine defaults: [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json), exported as\n`ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME` and `ENGINE_DEFAULT_FONT_SCHEME`. They are the drawing fields of\nthe gallery\'s `minimal`, `cool-horizon` and `aptos` records, compiled into code.\n\n## Ownership\n\n- **The gallery package owns every catalog record.** Since gallery 2.0.0 records, layouts and theme variants are authored\n by pull requests in `packages/gallery` (the product statement "pptx.gallery owns every default record" is retired, D11 of the\n monorepo program: the site presents the catalog, it does not author it); each id has one owner. Its\n display metadata lives under `x-*` members. pptx.gallery (the site, a separate repository) consumes the published package,\n adds its own presentation overlay and does not write records. A record that needs an engine change is one pull request,\n tested by one `verify` run.\n- **The gallery package owns the records and releases with the other packages.** `packages/gallery` is a tracked package of this repository (the monorepo program, D13): records, layouts and theme variants are authored by pull request here, its manifest holds a content hash per kind and the commit of the one-time 1.x snapshot from pptx.gallery, and it takes the repository\'s one shared version line (0.26.0 and later; gallery 1.0.0 to 5.0.0 stay on npm). The earlier `pnpm install` materializer of ignored fixtures is gone.\n- **A host chooses its catalog version.** Hosts register the catalog they choose; documents carry the records they were\n saved with. The packages are released together, so the pair a release ships (core and the gallery at the same version) is\n tested together.\n\n## The `@openpresentation/gallery` package\n\nRR-78: the same records are published as the npm package\n[`@openpresentation/gallery`](https://www.npmjs.com/package/@openpresentation/gallery) from `packages/gallery`, with the\nrepository\'s one shared version (from 0.26.0; the earlier independent line ended at 5.0.0). A record change is an ordinary\npull request (`catalog/<kind>/<id>.json` plus its index entry, then `pnpm rehash` and `pnpm previews` in the package, checked\nby `pnpm check:changed`); a `changes/<slug>.md` fragment records it and the release workflow publishes it with the rest:\n\n```js\nimport { gallery } from \'@openpresentation/gallery\';\nvalidate(document, { catalogs: [gallery] });\n```\n\n`gallery` replaces what `@openpresentation/opf/catalog` exported as `defaultCatalog` (`source` and the eight content kinds, records\nkeyed by id without `$schema`, `id` or `x-*`); the package also exports `GALLERY_SOURCE`, `CATALOG_SCHEMA`,\n`catalogDisplay`, `catalogIndexes` and `catalogManifest`, and `/previews`. New records go in any release; an id removed\nor renamed, a changed drawing or a record schema change is declared in the `changes/` fragment and makes the release a minor\none (a patch is refused), which the pixel-stability check (`pnpm check:stability`, against the `latest` release) enforces.\nFrom OPF 0.19 core and the CLI depend on it (`workspace:^` in this checkout, published as `^X.Y.Z` of the same release), core\'s\nNode defaults register `gallery`, and core drops `/catalog` and `spec/catalogs/`. Gallery 1.0.0 to 5.0.0 stay on npm and\nare never deprecated, because published core 0.25.0 depends on `^5.0.0`; the shared line cannot use those versions again\n(the first stable major of all six packages is 6.0.0; see [the release process](release-process.md)).\n\n### Theme variants (gallery 4)\n\nA theme record may list `variants`, an array of `{ "id", "name", "variant" }` whose `variant` is a value of `design.variant`\n(a background slot such as `"dark1"`, or `{ "background", "accents" }`; the same definition, `ColorVariant` in\n`opf.schema.json`, validates it). Gallery 4 gives `minimal`, `classic` and `bold` four each: `light`, `light-alt`, `dark`\nand `dark-alt`. A picker offers them and writes the chosen `variant` into `design.variant`; documents never store the id, and\nthe engines read only `design.variant`. Light and dark are therefore variants of every theme, and gallery 4 has no `dark`\ntheme: `design.theme: "dark"` is an unresolved reference (`opf/unresolved-reference`). Use a theme with a dark variant, for\nexample `{ "theme": "classic", "variant": "dark1" }`, and `colorScheme: "boost"` with `fontScheme: "seaford"` for the old\ndark look.\n\n## Endpoints\n\n| Request | Response |\n| --- | --- |\n| `GET https://www.pptx.gallery/<kind>/index.json` | Catalog index |\n| `GET https://www.pptx.gallery/<kind>` with `Accept: application/json` | Same catalog index |\n| `GET https://www.pptx.gallery/<kind>/<id>.json` | One published record |\n| `GET https://www.pptx.gallery/<kind>/<id>` with `Accept: application/json` | Same record |\n| Either URL from a browser | The gallery\'s HTML page |\n\nEngines never call these. A host may, to build the catalog it registers. Catalog files are served with\n`Access-Control-Allow-Origin: *`, and negotiated URLs send `Vary: Accept`. The older\n`https://www.pptx.gallery/api/<dimension>.json` envelopes carry the gallery\'s presentation data in its own shape; they\nare not OPF records and link their catalog index with `Link: <\u2026/<kind>/index.json>; rel="alternate"`.\n\n## Kinds\n\n| `<kind>` (URL and snapshot directory) | Key | Record schema | Role | Snapshot mode |\n| --- | --- | --- | --- | --- |\n| `audiences` | `audiences` | `opf-audience/v1` | content, `audience` | mirror |\n| `color-schemes` | `colorSchemes` | `opf-color-scheme/v1` | content, `design.colorScheme` | mirror |\n| `font-schemes` | `fontSchemes` | `opf-font-scheme/v1` | content, `design.fontScheme` | mirror |\n| `layouts` | `layouts` | `opf-layout/v2` since 0.19 | content, `Slide.layout` | authored in `packages/gallery` |\n| `narratives` | `narratives` | `opf-narrative/v1` | content, `narrative` | mirror |\n| `purposes` | `purposes` | `opf-purpose/v1` | content, `purpose` | mirror |\n| `themes` | `themes` | `opf-theme/v1` | content, `design.theme` | mirror |\n| `tones` | `tones` | `opf-tone/v1` | content, `tone` | mirror |\n| `chart-types` | `chartTypes` | `opf-chart-type/v1` | display metadata for `chart.type` | mirror |\n| `languages` | `languages` | `opf-language/v1` | display metadata for `language` | mirror |\n| `social-platforms` | `socialPlatforms` | `opf-social-platform/v1` | display metadata for `socials` keys | mirror |\n\nRecord schema ids are `https://openpresentation.org/schema/<name>`. A font scheme\'s `languages` list holds BCP-47 tags.\n\n## Index and record shape\n\nAn index validates against `spec/schemas/catalog-index.schema.json`:\n\n```json\n{\n "$schema": "https://openpresentation.org/schema/opf-catalog-index/v1",\n "kind": "tones",\n "version": "1",\n "description": "\u2026",\n "contentSha256": "b5a8d093\u2026",\n "records": [{ "id": "formal", "name": "Formal", "summary": "\u2026", "file": "formal.json" }]\n}\n```\n\n- `records` is in canonical order. `file` is relative to the index.\n- `version` is the index format version.\n- `contentSha256` is the lowercase hex SHA-256 of the canonical JSON of the full records in index order, with every\n top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace (`canonicalJson()`\n in `scripts/catalog-snapshot.mjs`). The bundled index and the published index carry the same value for a mirrored kind.\n\nA **published record file** names its companion schema in `$schema` and carries its `id`; `validateCatalogRecord(kind,\nrecord)` checks one. Publishers may add top-level `x-*` members (pptx.gallery puts page URLs, mood tags, contrast\nnotes and font stacks in `x-gallery`); the snapshot never carries them. The copy a document embeds has neither\n`$schema` nor `id` nor `x-*` members.\n\nBefore v1 an id is renamed or removed outright: there are no deprecated records, aliases or redirects. A document that\nnames a retired id gets `opf/unresolved-reference`.\n\n## Historical snapshot (before 0.19)\n\nBefore 0.19, Core stored the snapshot at `spec/catalogs/manifest.json` (schema `spec/schemas/catalog-manifest.schema.json`):\n\n```json\n{\n "publisher": "https://www.pptx.gallery",\n "source": { "repository": "https://github.com/Data-Advantage/pptx-gallery", "commit": "<sha>", "path": "public" },\n "kinds": {\n "tones": { "mode": "mirror", "records": 7, "contentSha256": "\u2026", "gallery": { "records": 7, "contentSha256": "\u2026" } }\n }\n}\n```\n\nThat historical snapshot used `mirror` for every kind and held every record the gallery published (278 layouts at that checkpoint). The `packages/gallery` package owns the snapshot and the 30 v2 layout templates; core consumes the package, and the records are tracked in this repository. No browser entry of Core imports catalog records (`pnpm check:catalog-free`).\n\nThe 1.x design synchronized the snapshot from pptx.gallery and refused a publisher that stopped serving a held id\n(waived per id with `--allow-removed`). That sync was retired with gallery 2.0.0; a record is now removed by a pull\nrequest that deletes it and its index entry, and the stability check requires the removal to be declared and the release to be a minor version.\n\n### Updating it\n\nEdit records by pull request in `packages/gallery`: change the record and its index entry, run `pnpm --filter @openpresentation/gallery rehash` and `previews`, and add a `changes/<slug>.md` fragment (`packages: [gallery]`; a drawing change is declared with `drawing:`, see `changes/README.md`). The package is released with the other five under the repository\'s one shared version. `pnpm check:spec` and `pnpm check:catalog` verify the indexes, manifest and records, and `pnpm check:stability` holds a drawing change or a removed id to the declaration rule. Core\'s browser entries remain catalog-free (`pnpm check:catalog-free`).\n\n## Reconciliation status\n\nThe per-kind divergence between the gallery and this snapshot is in\n[`programs/font-fidelity-everywhere/ff-37-catalog-divergence.md`](programs/font-fidelity-everywhere/ff-37-catalog-divergence.md).\n'
|
|
68
70
|
},
|
|
69
71
|
{
|
|
70
72
|
"slug": "design-resolution",
|
|
71
73
|
"file": "docs/design-resolution.md",
|
|
72
74
|
"title": "Design Resolution",
|
|
73
|
-
"markdown": '# Design Resolution\n\nHow an engine decides the effective design for any given slide. The schema spreads these rules across field descriptions; this page states them once, as an algorithm.\n\n## Precedence\n\nFor every design field independently, the most specific source wins:\n\n```\n wins +--------------------------------------------------------+\n ^ | 1. slide design slides[i].design.* |\n | +--------------------------------------------------------+\n | | 2. deck design design.* on the presentation root |\n | +--------------------------------------------------------+\n | | 3. resolved theme colorScheme, fontScheme, |\n | | background, dimensions from the |\n | | theme record |\n | +--------------------------------------------------------+\n loses | 4. engine defaults e.g. spec/reference/ |\n v | engine-defaults.json |\n +--------------------------------------------------------+\n```\n\n1. **Slide design** \u2014 `slides[].design.*`\n2. **Deck design** \u2014 `design.*` on the presentation root\n3. **Resolved theme** \u2014 defaults carried by the theme record (`colorScheme`, `fontScheme`, `background`, `dimensions`)\n4. **Engine defaults** \u2014 [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json), compiled into core as `ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME` and `ENGINE_DEFAULT_FONT_SCHEME`\n\nResolution is **per field**, not per object. A slide that sets only `design.contentAlignment` inherits everything else from the deck design; a deck that sets only `design.colorScheme` keeps the theme\'s font scheme and background.\n\nTwo field-level rules complete the picture:\n\n- **Base-plus-overrides within one object.** Wherever a reference object carries an `id` (`ColorScheme`, `FontScheme`, and the inline `Audience`, `Purpose` and `Tone`), the `id` resolves a catalog record as the base and sibling fields override the resolved record per key. `design.theme` is a reference string only. The string shorthand (`"colorScheme": "cool-horizon"`) is equivalent to setting only `id`.\n\n ```\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n\n catalog record "cool-horizon" sibling fields on the object\n accent1: "#2874A6" <-- replaced -- accent1: "#0F4C81"\n accent2: "#1B4F72" <-- kept\n light1: "#FFFFFF" <-- kept\n |\n v\n effective scheme: accent1 from the override, everything else\n from the record\n ```\n- **Explicit suppression.** `watermark`, `header`, and `footer` accept `false` to switch off an inherited value \u2014 distinct from omitting the field, which inherits.\n\nEvery reference inside this chain resolves the one way every content reference does: `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered for its source (`name:id` in `catalogs.<name>`, then the host catalog for its source); see [`how-opf-works.md`](./how-opf-works.md) and [the catalogs page](default-catalog.md).\n\n## Worked example 1: color scheme through every level\n\n```json\n{\n "design": {\n "theme": "classic",\n "colorScheme": "forest-green"\n },\n "slides": [\n { "title": "Inherits the deck" },\n {\n "title": "Slide override",\n "design": {\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n }\n }\n ]\n}\n```\n\n- Slide 1: the `classic` theme record supplies its own default color scheme, but the deck design sets `colorScheme` explicitly, so `forest-green` wins (level 2 beats level 3). Fonts, background, and dimensions still come from `classic`.\n- Slide 2: slide design beats deck design (level 1 beats level 2). The `cool-horizon` record resolves as the base, then `accent1` is replaced by `#0F4C81`. All other `cool-horizon` slots survive.\n\nThere is no ambiguity between "override" and "reference": every scheme value *is* a reference, and any sibling fields on the same object are overrides applied after the reference resolves.\n\n## Worked example 2: backgrounds and suppression\n\n```json\n{\n "design": {\n "theme": "minimal",\n "background": "light1",\n "footer": {\n "left": { "text": "Acme Corp" },\n "right": { "text": "{{slide.number}}" }\n }\n },\n "slides": [\n { "title": "Light slide on a theme that defaults to dark" },\n {\n "title": "Section divider",\n "design": {\n "background": {\n "type": "gradient",\n "gradient": {\n "angle": 90,\n "stops": [\n { "color": "#0B1B2B", "position": 0 },\n { "color": "#123A5F", "position": 1 }\n ]\n }\n },\n "footer": false\n }\n }\n ]\n}\n```\n\n- Slide 1: the deck-level `background: "light1"` overrides the `minimal` theme\'s default background (`dark2`). `light1` is a theme slot \u2014 it resolves through the effective color scheme, which itself resolved through the chain above.\n- Slide 2: the gradient replaces the deck background for this slide only, and `footer: false` suppresses the inherited footer rather than inheriting or replacing it.\n\n## Worked example 3: font scheme models\n\n```json\n{\n "design": {\n "fontScheme": {\n "id": "aptos",\n "code": "JetBrains Mono"\n }\n }\n}\n```\n\nThe `aptos` record supplies the OOXML pair (`major`/`minor`). The `code` role is an OPF-specific addition with no OOXML slot, so it layers on top without disturbing the pair. When serializing to PowerPoint, engines write `major`/`minor` to `majorFont`/`minorFont` and map abstract roles (`heading`, `body`) onto those slots. `accent` has no slot; the `code` family is written directly on code runs. The same slot-versus-role split applies to color schemes: OOXML slots (`accent1`\u2013`accent6`, `dark1/2`, `light1/2`) round-trip directly, abstract roles (`primary`, `text`, `surface`, \u2026) are mapped onto slots by the engine.\n\n### Code font\n\nThe `code` role resolves per key like every other override:\n\n1. `code` on the effective `design.fontScheme` object;\n2. `code` on the resolved font-scheme record (the `consolas` and `courier-new` records carry `"Consolas"` and `"Courier New"`);\n3. otherwise **Roboto Mono**, the documented fallback that `@openpresentation/opf-render` bundles.\n\nThe heading and body families are never reused as the code fallback, so choosing `aptos` still gives Roboto Mono code unless the deck sets `code`. `resolveFontFamilies()` in `@openpresentation/opf/composition` applies these rules for all engines.\n\n### Engine default font scheme\n\nThe last-resort font scheme applies only when neither the slide, the deck nor the resolved theme names one, or when the reference resolves nowhere. It is an engine default in code, not a catalog record: `ENGINE_DEFAULT_FONT_SCHEME` (`{ major: "Aptos Display", minor: "Aptos", languageFamily: "latin" }`, the drawing fields of the gallery\'s `aptos` scheme), recorded in [`engine-defaults.json`](../spec/reference/engine-defaults.json) and exported from the package root and `@openpresentation/opf/composition`. A document with no `design` uses the engine default theme (`ENGINE_DEFAULT_THEME`, which names no font scheme), so it gets Aptos in every engine without any catalog registered.\n\nEvery engine shares this one last resort, so a theme without `fontScheme` is measured, paginated, previewed and exported in the same fonts:\n\n| Engine | Last resort | Where |\n| --- | --- | --- |\n| Core pagination and validate | `ENGINE_DEFAULT_FONT_SCHEME` | `packages/javascript/src/design-records.ts` |\n| opf-render preview | core\'s `resolveSlideContext` | `src/svg.js` |\n| opf-editor composition and slide transfer | core\'s `resolveSlideContext` | `src/font-defaults.js` |\n| opf-pptx export | core\'s `resolveSlideContext` | `src/index.js` |\n\nAptos is not openly licensed, so no OPF package bundles it. Previews take the same path for the last resort as for any `aptos` deck:\n\n- **Estimated layout** (no `fonts`): the SVG names `Aptos` and `Aptos Display`, and the default raster engine draws them with its bundled sans-serif fallback (Roboto).\n- **Measured layout** with the opf-render office font pack (`loadFonts({pack: \'office\'})`): `Aptos` and `Aptos Display` resolve to Intos and Intos Display, metric-compatible replacements from the OPF font policy (0.000% mean width difference against Aptos 2.01), and the substitution report lists both. With only the base pack and `substitutionPolicy: "visual"` they fall back to the visual alternates Roboto and Carlito. The PPTX always names Aptos. See [font-fidelity.md](font-fidelity.md#font-policy-ff-31).\n- **Measured layout with only the base pack under the metric policy**: `font-unavailable` for Aptos, as for a document with no `design`. Supply licensed Aptos faces, allow visual substitution, or set a `fallbackFamily`.\n\n`packages/javascript/test/font-scheme-defaults.test.mjs` checks the shared default in core pagination, and each sibling repository has a parity test.\n\n### References that resolve nowhere\n\nA layout, theme, colour scheme or font scheme reference that neither the document embeds nor a registered catalog defines falls back, and no engine throws for it (unless the host asks for a strict export, `strictReferences`, which throws `OPFUnresolvedReferenceError`):\n\n- **Layout.** A slide with no `layout` is composed with **no layout record**, and that is not a finding: title, subtitle, tag and content are arranged automatically, a slide with no body payload is a cover (the title block vertically centred) and the design hints apply. A layout reference that resolves nowhere composes the same way. An engine never substitutes a different layout.\n- **Theme.** A theme reference that resolves nowhere uses the engine default theme (`ENGINE_DEFAULT_THEME`: the background and dimensions of the gallery\'s `minimal`).\n- **Colour scheme.** A colour-scheme reference that resolves nowhere uses `ENGINE_DEFAULT_COLOR_SCHEME` (the twelve slots of `cool-horizon`). The theme\'s own colour scheme counts as the reference when the deck and slide name none, and resolves in the theme\'s catalog group first. An object without an `id` is laid over the engine default, so a partial inline scheme keeps the slots it does not name.\n- **Font scheme.** A font-scheme reference that resolves nowhere uses `ENGINE_DEFAULT_FONT_SCHEME`. Sibling fields on an object reference still override it per key, so `{ "id": "no-such-scheme", "major": "Inter", "minor": "Inter" }` uses Inter, and a `code` role still applies. An object without `id` is an inline scheme on the same base and reports nothing.\n\nEach is one `unresolved-reference` diagnostic: `{ code, kind, reference, path, group, source?, fallback, message }`, with `path` where the reference is written (`slides.N.layout`, `slides.N.design.theme`, `design.colorScheme`, `design.fontScheme.id`, or `catalogs.<group>.themes.<id>.colorScheme` for a reference inside an embedded theme), `fallback` `automatic` for a layout and `engine-default` otherwise, and a message that names the reference and the catalog source it was looked for in. `validate` reports the same references as `opf/unresolved-reference` warnings.\n\n`resolveSlideContext(presentation, index, { catalogs, fonts })` (package root) applies these rules with the slide, deck and theme precedence above. It returns the `ComposeSlideOptions` for one slide (canvas, layout, families as `fontFamilies`, measurement, `slideNumber`, `slideCount`), the slide to compose and draw (`slide`, with `{{slide.number}}`, `{{slide.section}}` and `{{deck.slideCount}}` substituted for those numbers), the diagnostics, and `darkBackground` (the preview\'s decision: the one colour the background names, resolved through the colour scheme and the deck\'s `variables` so `var:brand`, a scheme slot or `primary`/`secondary`/`accent` work, has a WCAG relative luminance below 0.179; a gradient counts as light, a picture reads `light1`, a pattern its `backgroundColor`; no opacity is applied), which `composeSlide` uses to pick logo variants. `resolveDesignRecords` and `resolveFontScheme` expose the same resolution for hosts that need only the records. Core pagination continues with the fallbacks and passes each diagnostic to `onDiagnostic`.\n\n| Engine | Diagnostic channel | Reported |\n| --- | --- | --- |\n| Core pagination | `paginate(..., { onDiagnostic })` | once per path per call |\n| opf-render preview | `toSvg` `onDiagnostic` | once per path per rendered slide |\n| opf-editor | `session.composeSlide` / `paginateSlide` `onDiagnostic` option | once per call |\n| opf-pptx export | `toPptx(..., { onDiagnostic })` | once per path per export |\n\nEvery engine passes the catalogs its host registered (the `catalogs` option) to core, so a record a host registers resolves the same in preview, editor and export.\n\n## Color references in content\n\nContent color fields (`TextRun.color`, styled table cell `style.fill` / `style.color`, table cell border `color`) accept references as well as literal hex, and those references resolve through the same chain above:\n\n```\n "color": "accent2" slot name -> effective color scheme slot\n "color": "text" role name -> role-to-slot mapping, then the slot\n "color": "var:risk" variable -> top-level variables map\n "color": "#B42318" literal -> used as-is (frozen at authoring time)\n```\n\n- **Slot names** (`accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`) read the named slot from the *effective* color scheme \u2014 the one produced by the slide \u2192 deck \u2192 theme \u2192 engine-default precedence at the top of this page. A slide-level `design.colorScheme` override therefore recolors that slide\'s named runs too.\n- **Role names** (`primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`) resolve through one shared definition, `resolveColorRoles()`, which the opf-render preview, the PPTX export and `validate` all call, so the same deck draws the same colors in each. A role defined on the effective scheme wins; otherwise it defaults from a slot:\n\n | Role | Default (through the slide\'s variant; see [Color variants](#color-variants)) |\n | --- | --- |\n | `primary`, `secondary`, `accent` | the first three accents of the variant: `accent1`, `accent2`, `accent3` unless `variant.accents` reorders them |\n | `background` | the slide\'s own resolved background when it is one color (solid or pattern background color); else the variant\'s `bg1` (`light1` by default, the scheme\'s `background` role overriding it there). A gradient or picture background uses the variant, or its `tone`, or `light1`. |\n | `surface` | `bg2` of the variant: `light2` on a light slide, `dark2` on a dark one |\n | `text` | `tx1` of the variant: `dark1`, or `light1` on a dark slide. A `text` override applies on a light slide only, so a dark slide always keeps readable light text. |\n | `textSecondary` | `tx2` of the variant: `dark2`, or `light2` on a dark slide |\n\n A slide is dark when its background\'s WCAG relative luminance is under 0.179, the point where white and black text contrast equally.\n- **Links.** A link run (`link`) with no `color` of its own is drawn underlined in the scheme\'s `hyperlink` slot (the OOXML `hlink` color; Office blue `#0563C1` when the scheme sets none), in the preview and in the PPTX export (`a:schemeClr hlink` where the deck theme holds that color). Where that color has under 4.5:1 contrast against the slide background (the default Office blue on a dark slide), the slide\'s `text` color is used instead, as the slide tag does for a low-contrast primary color. A link run with its own `color` keeps it, and a `hyperlink` ColorRef always names the slot. `followedHyperlink` is written to the PPTX theme; a static preview cannot know which links were visited, so it draws every link in `hyperlink`.\n- **Variable references** (`var:<id>`) resolve against the document\'s top-level `variables` map, independent of the scheme. Variables are deck-scoped named colors \u2014 use them for values that have meaning (`var:risk`) or repeat across slides. An unknown id is a validation warning, never an error, and engines fall back to their default text color.\n\nThe styled table cell and border color fields enforce the reference forms at the schema level (a typo like `"acent2"` is a schema error there \u2014 neither hex, a known name, nor a `var:` reference). Run colors stay open strings so imported decks keep validating: an unrecognized run color is a validation warning, and renderers fall back to the theme text color \u2014 the same warn-don\'t-error posture unknown catalog ids get. Unknown `var:` ids are warnings everywhere.\n\n### Color variants\n\nA deck has one color scheme of twelve slots. A **variant** is a mapping over those same slots, as PowerPoint\'s `clrMap` is, never a second scheme: which slot is the background, and so which are the text and the alternate surface, plus an optional order for the accents. Everything OPF draws refers to the roles, so a slide that changes variant recolors text, lines, cards, chart furniture and code together.\n\n`variant` is a string, the same way `colorScheme` is: the slot that is the background.\n\n```json\n{\n "design": { "variant": "dark1" },\n "slides": [{ "title": "Light one", "design": { "variant": "light1" } }]\n}\n```\n\n`"dark1"` is `{ "background": "dark1" }`. The object form is only needed to reorder the accents: `{ "background": "light1", "accents": ["accent2", "accent1", "accent3", "accent4", "accent5", "accent6"] }`. Both forms are allowed on `design` and on `slides[n].design`, and what `resolveColorRoles` and `resolveSlideContext` return is always the normalized object (`normalizeColorVariant`).\n\n| `variant` (or `variant.background`) | bg1 | tx1 | bg2 | tx2 |\n| --- | --- | --- | --- | --- |\n| `light1` (default) | `light1` | `dark1` | `light2` | `dark2` |\n| `dark1` | `dark1` | `light1` | `dark2` | `light2` |\n| `light2` | `light2` | `dark1` | `light2` | `dark2` |\n| `dark2` | `dark2` | `light1` | `dark2` | `light2` |\n\n(`light2` and `dark2` keep the background slot as the surface too, as a slide with that theme background has always been drawn, so the engine default theme, whose background is `dark2`, is unchanged.) `variant.accents` (object form) is a permutation of `accent1` to `accent6`; entry `i` is the slot drawn as accent `i + 1`, which reorders `primary`, `secondary`, `accent` and the chart series colors.\n\nWhich variant a slide has, from the most specific to the least (slide design, deck design, then the theme record), at the first level that says anything:\n\n1. a `variant`;\n2. a theme-slot background (`"dark1"`, `{ "type": "theme", "slot": "dark1" }`), which is the variant of that slot, so `design.background` stays a shorthand;\n3. a picture or gradient background with a `tone` (`"dark"` or `"light"`), which is the `dark1` or `light1` variant. Without a tone only a variant of a less specific level can apply; with none the slide counts as light, as before;\n4. any other background (a solid or pattern color) decides by luminance (dark below 0.179), as before.\n\nA theme record can list the variants it offers (`variants`, from gallery 4): named values of `variant`, such as `light`, `light-alt`, `dark` and `dark-alt` on `minimal`, `classic` and `bold`. They are for pickers: choosing one writes its `variant` into `design.variant` (or a slide\'s), and the document keeps that value, not the entry id. The engines read only `design.variant`. There is no `dark` theme in the default catalog: a dark deck is a theme with a dark variant, for example `{ "theme": "classic", "variant": "dark1" }`.\n\nA slide\'s variant replaces the deck\'s whole, accent order included. `resolveSlideContext(...).resolved.roles` is the resolved record, and `resolveColorRoles(scheme, { variant, tone, background })` is the function: besides the colors it returns `variant`, `map` (bg1, tx1, bg2, tx2 and the accents; `clrMapAttributes(map)` gives the PowerPoint `clrMap` attributes for the master and for a slide\'s `clrMapOvr`), `accents` (the six colors in order) and `neutrals`.\n\n#### Scheme-derived neutrals\n\nLines and quiet colors are not fixed grey. `roles.neutrals` derives each from the map, as PowerPoint\'s chart styles do: the text color (tx1) at a share over the background (bg1), chosen as a DrawingML luminance transform of the slot so a PPTX can write `<a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr>` and the preview draws the same color. Each neutral is `{ slot, hex, lumMod?, lumOff?, alpha? }` (the transforms in thousandths of a percent; `hex` is the transform applied, `applyLuminance(slotHex, lumMod, lumOff)`).\n\n| Neutral | Derivation |\n| --- | --- |\n| `border` (also `roles.border`) | tx1 at 20% over bg1: card, rule and table borders |\n| `gridline` | tx1 at 15%: chart gridlines |\n| `axisLine` | tx1 at 25%: chart axis lines |\n| `chartText` | tx1 at 65%: chart axis labels and legend text |\n| `outline` | bg1: pie and treemap outlines, the video play icon, anything drawn on a mark |\n| `trendNeutral` | tx2: the neutral metric trend |\n| `codePanel` | tx1 on a light map, bg2 on a dark one: the code block is a dark card on either |\n| `codeText` | bg1 or tx1, whichever reads better on the panel |\n| `codeComment` | the code text at 65% over the panel (comments and line numbers) |\n\n`codeSyntaxPaletteForRoles(roles)` builds the syntax palette from `codePanel`, `codeText`, `codeComment` and the variant\'s first three accents, and `codeHighlightColors(scheme, palette, codePanel.hex)` takes the panel the highlight band sits on. `readableSchemeColor(fill, roles)` returns bg1 or tx1 (as a scheme color), whichever reads on a fill the scheme does not name, for table header text and text on marks. The validator\'s contrast rules read the same roles, so a dark variant is checked against its own text and surface.\n\n### Background colors are ColorRefs\n\nThe colors of a solid background (`SolidBackground.color`), of each gradient stop and of a pattern (`foregroundColor`, `backgroundColor`) take the same forms as the content fields above: a literal hex, a slot or role name, or a `var:<id>` reference. The schema keeps these fields as plain strings, so a reference validates; engines resolve it through `resolveColorRef()` against the effective color scheme and the deck `variables`, and a reference that resolves to nothing is drawn as the engine default (white for a background), as for any other unresolvable color. The default text color of the slide follows the resolved background. A PPTX export writes `a:schemeClr` where the deck theme holds the named slot or role exactly, and the resolved literal otherwise (FF-24 conventions).\n\n> **Decision, 2026-09-30 (agent decision, vetoable).** The spec coverage audit found that `SolidBackground.color` accepted `var:` and slot names (the field is a string) but both engines painted white, while [`content-item-design-overrides.md`](./content-item-design-overrides.md) already states that every color field must accept the ColorRef forms and never hex alone. Rather than tighten the schema (which would break documents that validate today), the engines resolve ColorRefs in backgrounds. The owner can veto this by restricting the three background color fields to `HexColor` in the schema and the engines to hex only.\n\n`@openpresentation/opf/composition` exports `resolveColorRef()` with the shared slot, role, variable, and hex rules above so renderers and exporters do not drift. Pass the effective color scheme, optional resolved role colors, the deck `variables` map, and a theme-text `fallback` for unrecognized references.\n\n## Script fonts and language\n\nOOXML gives each theme font (major and minor) three script slots: `latin`, East Asian (`ea`) and complex script (`cs`). The presentation `language` and the effective font scheme resolve to all three:\n\n```\n latin design font scheme heading/body (the chain above)\n eastAsian 1. design.fontScheme.eastAsian explicit slot\n complexScript 2. the scheme\'s own major/minor when languageFamily is ea / cs (or eastAsian /\n complexScript, the same values) and its\n languages list (language ids) is empty or\n names the language\n 3. the language\'s font scheme when the language\'s script uses the slot\n 4. the latin family otherwise\n```\n\n- A language record\'s `script` (ISO 15924) picks its slot. East Asian scripts (`Jpan`, `Hans`, `Hant`, `Kore`, ...) use `eastAsian`. Complex scripts (`Arab`, `Hebr`, `Deva`, `Thai`, ...) use `complexScript`. Latin, Cyrillic, Greek and other scripts use `latin`. `direction` defaults from the script (Arabic and Hebrew are right-to-left).\n- The language\'s `fontScheme` applies to PowerPoint output and `googleFontScheme` to Google Slides output. For Latin-script languages, the design font scheme always supplies the latin slot.\n- A language sets `lang`, the text direction and the script slots, and never the Latin scheme: only `design.fontScheme` (slide, then deck, then theme, then the shared default `aptos`) sets the latin fonts, and a language record\'s `fontScheme` is a default for its own script slot, not a deck font. The PPTX theme\'s `a:ea` and `a:cs` are written only for a slot a script font is selected for (the scheme\'s explicit slot or the language\'s script font) and stay empty otherwise, as in Office\'s own themes. See [Language contract](./programs/font-fidelity-everywhere/script-font-model.md#language-contract-ff-50-model-c) and [Theme slots](./programs/font-fidelity-everywhere/script-font-model.md#theme-slots-ff-49).\n- A Latin deck therefore repeats its heading/body family in `ea`/`cs`. A Japanese deck with `design.fontScheme: { "major": "Carlito", "minor": "Carlito" }` keeps the Latin family in `latin` and uses Meiryo (PowerPoint) or Noto Sans JP (Google Slides) in `ea`. `design.fontScheme.eastAsian` / `.complexScript` (`{ "major": ..., "minor": ... }`) name a script font explicitly, for example for CJK text inside a Latin deck.\n\n`@openpresentation/opf/composition` exports `resolveScriptFonts(document, { app, slideIndex })` (`app` is `"powerpoint"`, the default, or `"google-slides"`), and the package root exports `normalizeLanguageFamily(value)` (which reads a font scheme\'s `languageFamily`, `eastAsian` as `ea` and `complexScript` as `cs`). `resolveScriptFonts` returns the heading and body slots, the OOXML `lang` (a curated `ooxmlLang` culture tag such as `ja-JP` or `ms-MY`, or an authored region tag), the canonical `bcp47` tag, `script`, `direction`/`rtl`, and the per-script supplemental theme font. Renderers and exporters should use it rather than re-deriving slots. The model, the OOXML mapping and the open questions are in [`programs/font-fidelity-everywhere/script-font-model.md`](./programs/font-fidelity-everywhere/script-font-model.md). opf-render and opf-pptx implement it (FF-07, FF-19, FF-49).\n\n## Brand assets and layout hints\n\nThe 2026-09-30 spec coverage audit found that the deck logo, `organization.logo`, `speaker.photo`, `design.contentDirection`, `design.chartPrimary`, `design.listBullet` and `fontScheme.accent` validated, edited and round-tripped but changed nothing in any engine. This section states what they do now. Every rule below is implemented once, in `composeSlide()` and `resolveLogo()` of `@openpresentation/opf`, and consumed by the renderer and the exporter; the decisions marked **(vetoable)** are agent decisions the owner can overturn.\n\n### Layout templates and design\n\nAn OPF layout record (`opf-layout/v2`) is one ordered list of `regions`, declared once, in reading order, with a `direction` (`row` or `column`) and splits for two dimensions. A region has a `name`, a relative `size` and rules: accepted content kinds, role, flow, capacity and optional bleed. `layoutContent(record)` and `layoutStructure(record)` summarize those regions for hosts; `composeSlide()` reports their binding and boxes in `geometry.regions`. A slide may use `layout: "auto"` to bypass a record explicitly. [Layout templates](layout-templates.md) documents the binding and flow rules.\n\nThe effective `design` value of each key comes from the slide, then the deck, then the resolved theme, then the layout record, then the engine default. This includes `titleAlignment`, `contentAlignment`, `contentBox`, `contentDirection`, `chartPrimary`, `imageFit`, `listBullet` and `mirror`. `resolveDesignHints()` reports the supplying level, and `SlideComposition.design` holds the merged result. A layout\'s regions determine placement; `design.mirror` reverses its tracks, while the slide\'s own `composition` arranges the first primary flowing region. A top-level image block may still carry its own `placement` band. A layout sits on any slide background; it does not opt into a picture.\n\nThe `tag` (eyebrow) follows `titleAlignment` on every slide, and is left-aligned when none resolves, like the title, so a layout that centres its content does not centre the tag over a left-aligned title. On a cover the `subtitle` joins them. Only the slide\'s own `design.contentAlignment` keeps the tag (and a cover\'s subtitle) on content alignment, preserving the 2026-10-06 authoring decision.\n\n### Logos live on the organization (RR-71, OPF 0.18)\n\nA logo belongs to an organization. `Organization.logo` is one path or Asset for every shape, or up to four shapes:\n\n```json\n"organization": {\n "id": "acme", "name": "Acme",\n "logo": {\n "full": { "onLight": "./assets/acme-logo.svg", "onDark": "./assets/acme-logo-white.svg" },\n "icon": "./assets/acme-mark.svg"\n }\n}\n```\n\n`resolveLogo(presentation, slide, { shape, onDark, slideIndex, reference })` (`@openpresentation/opf/composition`) returns `{ source, path, shape, variant, reference, designPath? }` or `null`:\n\n```\n organization 1. options.reference var:organization.beta.logo.icon\n 2. slides[i].design.logo, then design.logo:\n false draws no logo; a reference picks the organization\n (and the shape, when it names one)\n 3. the primary organization role "primary", else the first\n shape the reference\'s shape, else options.shape (default full)\n missing shape -> full -> first of wordmark, stacked, icon\n variant onDark on a dark background, else onLight; a missing one uses\n the other; a plain asset is "default"\n path organization.logo, organization.1.logo.icon,\n organization.logo.full.onDark, ...\n```\n\nAn override that names an organization without a logo draws nothing: it never falls back to another organization. Hosts pass their own background luminance test as `composeSlide(..., { darkBackground })` (`resolveSlideContext` computes it); core never inspects colors.\n\nThe same logos are placed anywhere through slide-scoped built-in references (`var:organization.logo`, `var:organization.logo.icon`, `var:organization.beta.logo.wordmark`), which resolve for each slide\'s background: see [templates and variables](templates-and-variables.md#organization-logos).\n\n### Where the logo is drawn (vetoable)\n\n1. **Cover and section slides.** A slide with no body payload on a title-only template, after empty template regions collapse, or without a layout (the same rule that centers covers) draws the primary organization\'s `full` logo (or the `design.logo` override) at the top-left of the free area, inside the slide padding and below any header furniture. `composeSlide` returns it as `geometry.logo` (`{ box, shape, path, source, variant, reference, anchor: \'left\' }`): `x = area.left + padding`, `y` at the image-safe heading top, `height = 56` reference pixels at a 720-pixel short edge, `width = min(4 * height, free width)`. Headings start one gap below the box and the cover-centering rule centers the tag/title/subtitle group in the remaining span; the logo itself does not move. Consumers fit the image inside the box preserving its aspect ratio, anchored left and vertically centered (SVG `preserveAspectRatio="xMinYMid meet"`; PPTX computes the fitted size from the raster dimensions and places it at `box.x`). Nothing is drawn when no logo resolves or `design.logo` is `false`. **Content slides never get an automatic logo** (vetoable: it would move every content area).\n2. **Headers and footers.** A zone places a logo like any picture: `"image": "var:organization.logo.icon"`. `layoutFurniture` resolves the reference for the slide\'s background; the image part carries the asset in `image`, the organization path in `sourcePath` and the reference in `reference`. Without a logo the engine reports `unresolved-content` at `<zone>.image`. The zone\'s parts sit in one row (see [dynamic composition](dynamic-composition.md)).\n3. **Picture bullets.** See `listBullet` below: the `icon` shape.\n\n> **Decision, 2026-09-30 (agent decision, vetoable).** 84 of the 126 bundled example decks carry a logo, so 81 cover slides gain a logo and their heading group moves down. The placement (top-left, 56 px, full logo) and the content-slide exclusion are the reference-engine defaults; a layout-driven logo slot is a separate design.\n\n### `speaker.photo` is a built-in image variable (vetoable)\n\nNo reference engine draws a speaker photo by itself: the schema has no speaker slot on any slide and no slide-to-speaker link, and a speaker block on covers would be a separate design. The author places it explicitly with the built-in image variable (`"image": "var:speaker.photo"`, see [templates and variables](templates-and-variables.md#built-in-variables)); the field also round-trips through PPTX provenance.\n\n### `contentDirection`\n\nThe effective value follows the normal design precedence: slide, deck, theme, layout, engine default. On automatic slides `vertical` sets the root arrangement to `column` and `horizontal` to `row`, unless the slide explicitly sets `composition.mode`. Promoted regions keep their geometry and nested content groups keep their own composition.\n\nOn a v2 template, `contentDirection` changes every flowing region into a row or column without changing the template\'s regions. An atomic `flow: "none"` region remains atomic. Without the direction hint each region uses its declared flow. Layout records do not have `composition.mode`, `columns` or `weights`; their region list defines the grid.\n\n### `titleAlignment` and `contentAlignment` in right-to-left decks\n\n> **Decision, 2026-10-01 (RR-05, supervisor decision, vetoable).** Alignment is logical for right-to-left text. In a deck whose language is written right to left, `left` means the start edge and `right` the end edge of each paragraph: an Arabic or Hebrew paragraph with the default (or authored) `left` is drawn against the right edge, with its bullets, indents and table cells to match, while an English paragraph in the same deck keeps the left edge. `center` is unchanged and a left-to-right deck is unchanged. The effective value (slide design, then deck design, then `left`) is still what `item.alignment` reports; `placement.lines[i].alignment` and the exported `algn` carry the physical edge. A right-to-left deck that wants a paragraph at its end edge sets `right`. See [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n### `chartPrimary`\n\nThe resolved `design.chartPrimary` applies on automatic slides only. It changes the root arrangement when the slide has no promoted regions, no `composition.mode` of its own, and its root nodes contain at least one chart leaf and at least one node that is not a chart. The **first chart node is primary** and the other root nodes form one synthetic sub-grid:\n\n- `left` / `right`: the root is a two-track row with weights `[3, 2]` (chart first for `left`, last for `right`); the rest arrange in `auto` mode inside their track.\n- `top` / `bottom`: a two-track column with weights `[3, 2]` (chart first for `top`).\n- `none`, or any other value: no change, the existing automatic grid with equal weight.\n\nAutomatic composition builds the primary chart track and a direct content group for the other root nodes, preserving the 3:2 chart weight and source content order. A v2 template remains authoritative and does not apply this automatic hint. OPF 0.19 removes the v1 record-construction helper `chartPrimaryLayout`; authors use `chart-beside` or a custom v2 template for an authored grid.\n\nThe synthetic container has no OPF path, so it records no `groups`, `flows` or explanation entry; the root decision has `reason: \'chart-primary\'` and `selectedColumns` 2 (row) or 1 (column). Explicit root `columns` and `weights` are ignored while the automatic hint applies. A chart inside a nested group, a chart-only root, or a single root `chart` payload leaves the arrangement unchanged. A slide using a v2 template follows that template instead.\n\n### `listBullet` (vetoable)\n\n`character` (the default) draws the current glyph marker. `image` draws the organization\'s icon logo (`resolveLogo(..., { shape: \'icon\', onDark })`: the primary organization\'s, or the organization and shape `design.logo` names) as a picture bullet: every `items`/`bullets` item in `composeSlide` and every `listEntries[]` entry of its fit carry `bulletImage: { source, path }`. Marker geometry is unchanged, and each entry carries `bulletBox`, where the image draws: a square of side `marker.fontSize * PICTURE_BULLET_SCALE` (0.65, exported) whose bottom sits on the marker baseline (`marker.y`) with its left edge at `marker.x`. The scale is what desktop PowerPoint draws for an `a:buBlip` at `a:buSzPct 100000` (what the exporter writes), measured as a square 10, 15, 16, 20 and 31 px wide at font sizes of 16, 24, 25, 32 and 48 px (0.625 to 0.646; the heights run a pixel more from anti-aliasing) with its bottom on the text baseline, in Arial, Aptos, Georgia and Courier New alike, so it does not depend on the typeface; the text start and hanging indent are identical in both. The exporter sets no size, because PowerPoint sizes the bullet itself; consumers that draw it themselves (the preview) use `bulletBox`. Vetoable: the constant is a measurement, not a rule of the format. When `image` is set and no logo resolves, the glyph stays and the slide reports one `unresolved-content` diagnostic at `slides.N.design.listBullet` or `design.listBullet`, only when the slide has a list. The renderer draws an `<image>` per marker; the exporter writes native picture bullets (`a:buBlip`).\n\n### `fontScheme.accent`\n\n`resolveFontFamilies()` returns `accent` only when the effective scheme defines an `accent` role (a family string or a `Font` object). The slide `tag` (eyebrow) and the quote body use `fonts.accent ?? <current family>` (body for the tag, heading for the quote body); nothing else changes. The renderer loads and embeds it, the exporter writes it on those runs (`a:latin`) while the theme fonts stay major/minor, and import keeps restoring `design.fontScheme` from provenance. None of the bundled examples sets an accent font.\n\n## What is *not* part of this chain\n\nBeyond the color references above, content payloads carry no design controls in v1 \u2014 `position`, `fontSize` overrides at payload level, and the like were deliberately kept out while the content model stabilizes (see [`content-item-design-overrides.md`](./content-item-design-overrides.md); styled table cells and rich-text runs carry the only per-content styling, and their color fields take the reference forms above). The design system, plus layout hints (`titleAlignment`, `contentBox`, `chartPrimary`, ...) and dynamic composition, is the styling surface of an OPF document.\n'
|
|
75
|
+
"markdown": '# Design Resolution\n\nHow an engine decides the effective design for any given slide. The schema spreads these rules across field descriptions; this page states them once, as an algorithm.\n\n## Precedence\n\nFor every design field independently, the most specific source wins:\n\n```\n wins +--------------------------------------------------------+\n ^ | 1. slide design slides[i].design.* |\n | +--------------------------------------------------------+\n | | 2. deck design design.* on the presentation root |\n | +--------------------------------------------------------+\n | | 3. resolved theme colorScheme, fontScheme, |\n | | background, dimensions from the |\n | | theme record |\n | +--------------------------------------------------------+\n loses | 4. engine defaults e.g. spec/reference/ |\n v | engine-defaults.json |\n +--------------------------------------------------------+\n```\n\n1. **Slide design** \u2014 `slides[].design.*`\n2. **Deck design** \u2014 `design.*` on the presentation root\n3. **Resolved theme** \u2014 defaults carried by the theme record (`colorScheme`, `fontScheme`, `background`, `dimensions`)\n4. **Engine defaults** \u2014 [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json), compiled into core as `ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME` and `ENGINE_DEFAULT_FONT_SCHEME`\n\nResolution is **per field**, not per object. A slide that sets only `design.contentAlignment` inherits everything else from the deck design; a deck that sets only `design.colorScheme` keeps the theme\'s font scheme and background.\n\nTwo field-level rules complete the picture:\n\n- **Base-plus-overrides within one object.** Wherever a reference object carries an `id` (`ColorScheme`, `FontScheme`, and the inline `Audience`, `Purpose` and `Tone`), the `id` resolves a catalog record as the base and sibling fields override the resolved record per key. `design.theme` is a reference string only. The string shorthand (`"colorScheme": "cool-horizon"`) is equivalent to setting only `id`.\n\n ```\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n\n catalog record "cool-horizon" sibling fields on the object\n accent1: "#2874A6" <-- replaced -- accent1: "#0F4C81"\n accent2: "#1B4F72" <-- kept\n light1: "#FFFFFF" <-- kept\n |\n v\n effective scheme: accent1 from the override, everything else\n from the record\n ```\n- **Explicit suppression.** `watermark`, `header`, and `footer` accept `false` to switch off an inherited value \u2014 distinct from omitting the field, which inherits.\n\nEvery reference inside this chain resolves the one way every content reference does: `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered for its source (`name:id` in `catalogs.<name>`, then the host catalog for its source); see [`how-opf-works.md`](./how-opf-works.md) and [the catalogs page](default-catalog.md).\n\n## Worked example 1: color scheme through every level\n\n```json\n{\n "design": {\n "theme": "classic",\n "colorScheme": "forest-green"\n },\n "slides": [\n { "title": "Inherits the deck" },\n {\n "title": "Slide override",\n "design": {\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n }\n }\n ]\n}\n```\n\n- Slide 1: the `classic` theme record supplies its own default color scheme, but the deck design sets `colorScheme` explicitly, so `forest-green` wins (level 2 beats level 3). Fonts, background, and dimensions still come from `classic`.\n- Slide 2: slide design beats deck design (level 1 beats level 2). The `cool-horizon` record resolves as the base, then `accent1` is replaced by `#0F4C81`. All other `cool-horizon` slots survive.\n\nThere is no ambiguity between "override" and "reference": every scheme value *is* a reference, and any sibling fields on the same object are overrides applied after the reference resolves.\n\n## Worked example 2: backgrounds and suppression\n\n```json\n{\n "design": {\n "theme": "minimal",\n "background": "light1",\n "footer": {\n "left": { "text": "Acme Corp" },\n "right": { "text": "{{slide.number}}" }\n }\n },\n "slides": [\n { "title": "Light slide on a theme that defaults to dark" },\n {\n "title": "Section divider",\n "design": {\n "background": {\n "type": "gradient",\n "gradient": {\n "angle": 90,\n "stops": [\n { "color": "#0B1B2B", "position": 0 },\n { "color": "#123A5F", "position": 1 }\n ]\n }\n },\n "footer": false\n }\n }\n ]\n}\n```\n\n- Slide 1: the deck-level `background: "light1"` overrides the `minimal` theme\'s default background (`dark2`). `light1` is a theme slot \u2014 it resolves through the effective color scheme, which itself resolved through the chain above.\n- Slide 2: the gradient replaces the deck background for this slide only, and `footer: false` suppresses the inherited footer rather than inheriting or replacing it.\n\n## Worked example 3: font scheme models\n\n```json\n{\n "design": {\n "fontScheme": {\n "id": "aptos",\n "code": "JetBrains Mono"\n }\n }\n}\n```\n\nThe `aptos` record supplies the OOXML pair (`major`/`minor`). The `code` role is an OPF-specific addition with no OOXML slot, so it layers on top without disturbing the pair. When serializing to PowerPoint, engines write `major`/`minor` to `majorFont`/`minorFont` and map abstract roles (`heading`, `body`) onto those slots. `accent` has no slot; the `code` family is written directly on code runs. The same slot-versus-role split applies to color schemes: OOXML slots (`accent1`\u2013`accent6`, `dark1/2`, `light1/2`) round-trip directly, abstract roles (`primary`, `text`, `surface`, \u2026) are mapped onto slots by the engine.\n\n### Code font\n\nThe `code` role resolves per key like every other override:\n\n1. `code` on the effective `design.fontScheme` object;\n2. `code` on the resolved font-scheme record (the `consolas` and `courier-new` records carry `"Consolas"` and `"Courier New"`);\n3. otherwise **Roboto Mono**, the documented fallback that `@openpresentation/opf-render` bundles.\n\nThe heading and body families are never reused as the code fallback, so choosing `aptos` still gives Roboto Mono code unless the deck sets `code`. `resolveFontFamilies()` in `@openpresentation/opf/composition` applies these rules for all engines.\n\n### Engine default font scheme\n\nThe last-resort font scheme applies only when neither the slide, the deck nor the resolved theme names one, or when the reference resolves nowhere. It is an engine default in code, not a catalog record: `ENGINE_DEFAULT_FONT_SCHEME` (`{ major: "Aptos Display", minor: "Aptos", languageFamily: "latin" }`, the drawing fields of the gallery\'s `aptos` scheme), recorded in [`engine-defaults.json`](../spec/reference/engine-defaults.json) and exported from the package root and `@openpresentation/opf/composition`. A document with no `design` uses the engine default theme (`ENGINE_DEFAULT_THEME`, which names no font scheme), so it gets Aptos in every engine without any catalog registered.\n\nEvery engine shares this one last resort, so a theme without `fontScheme` is measured, paginated, previewed and exported in the same fonts:\n\n| Engine | Last resort | Where |\n| --- | --- | --- |\n| Core pagination and validate | `ENGINE_DEFAULT_FONT_SCHEME` | `packages/javascript/src/design-records.ts` |\n| opf-render preview | core\'s `resolveSlideContext` | `src/svg.js` |\n| opf-editor composition and slide transfer | core\'s `resolveSlideContext` | `src/font-defaults.js` |\n| opf-pptx export | core\'s `resolveSlideContext` | `src/index.js` |\n\nAptos is not openly licensed, so no OPF package bundles it. Previews take the same path for the last resort as for any `aptos` deck:\n\n- **Estimated layout** (no `fonts`): the SVG names `Aptos` and `Aptos Display`, and the default raster engine draws them with its bundled sans-serif fallback (Roboto).\n- **Measured layout** with the opf-render office font pack (`loadFonts({pack: \'office\'})`): `Aptos` and `Aptos Display` resolve to Intos and Intos Display, metric-compatible replacements from the OPF font policy (0.000% mean width difference against Aptos 2.01), and the substitution report lists both. With only the base pack and `substitutionPolicy: "visual"` they fall back to the visual alternates Roboto and Carlito. The PPTX always names Aptos. See [font-fidelity.md](font-fidelity.md#font-policy-ff-31).\n- **Measured layout with only the base pack under the metric policy**: `font-unavailable` for Aptos, as for a document with no `design`. Supply licensed Aptos faces, allow visual substitution, or set a `fallbackFamily`.\n\n`packages/javascript/test/font-scheme-defaults.test.mjs` checks the shared default in core pagination, and each sibling repository has a parity test.\n\n### References that resolve nowhere\n\nA layout, theme, colour scheme or font scheme reference that neither the document embeds nor a registered catalog defines falls back, and no engine throws for it (unless the host asks for a strict export, `strictReferences`, which throws `OPFUnresolvedReferenceError`):\n\n- **Layout.** A slide with no `layout` is composed with **no layout record**, and that is not a finding: title, subtitle, tag and content are arranged automatically, a slide with no body payload is a cover (the title block vertically centred) and the design hints apply. A layout reference that resolves nowhere composes the same way. An engine never substitutes a different layout.\n- **Theme.** A theme reference that resolves nowhere uses the engine default theme (`ENGINE_DEFAULT_THEME`: the background and dimensions of the gallery\'s `minimal`).\n- **Colour scheme.** A colour-scheme reference that resolves nowhere uses `ENGINE_DEFAULT_COLOR_SCHEME` (the twelve slots of `cool-horizon`). The theme\'s own colour scheme counts as the reference when the deck and slide name none, and resolves in the theme\'s catalog group first. An object without an `id` is laid over the engine default, so a partial inline scheme keeps the slots it does not name.\n- **Font scheme.** A font-scheme reference that resolves nowhere uses `ENGINE_DEFAULT_FONT_SCHEME`. Sibling fields on an object reference still override it per key, so `{ "id": "no-such-scheme", "major": "Inter", "minor": "Inter" }` uses Inter, and a `code` role still applies. An object without `id` is an inline scheme on the same base and reports nothing.\n\nEach is one `unresolved-reference` diagnostic: `{ code, kind, reference, path, group, source?, fallback, message }`, with `path` where the reference is written (`slides.N.layout`, `slides.N.design.theme`, `design.colorScheme`, `design.fontScheme.id`, or `catalogs.<group>.themes.<id>.colorScheme` for a reference inside an embedded theme), `fallback` `automatic` for a layout and `engine-default` otherwise, and a message that names the reference and the catalog source it was looked for in. `validate` reports the same references as `opf/unresolved-reference` warnings.\n\n`resolveSlideContext(presentation, index, { catalogs, fonts })` (package root) applies these rules with the slide, deck and theme precedence above. It returns the `ComposeSlideOptions` for one slide (canvas, layout, families as `fontFamilies`, measurement, `slideNumber`, `slideCount`), the slide to compose and draw (`slide`, with `{{slide.number}}`, `{{slide.section}}` and `{{deck.slideCount}}` substituted for those numbers), the diagnostics, and `darkBackground` (the preview\'s decision: the one colour the background names, resolved through the colour scheme and the deck\'s `variables` so `var:brand`, a scheme slot or `primary`/`secondary`/`accent` work, has a WCAG relative luminance below 0.179; a gradient counts as light, a picture reads `light1`, a pattern its `backgroundColor`; no opacity is applied), which `composeSlide` uses to pick logo variants. `resolveDesignRecords` and `resolveFontScheme` expose the same resolution for hosts that need only the records. Core pagination continues with the fallbacks and passes each diagnostic to `onDiagnostic`.\n\n| Engine | Diagnostic channel | Reported |\n| --- | --- | --- |\n| Core pagination | `paginate(..., { onDiagnostic })` | once per path per call |\n| opf-render preview | `toSvg` `onDiagnostic` | once per path per rendered slide |\n| opf-editor | `session.composeSlide` / `paginateSlide` `onDiagnostic` option | once per call |\n| opf-pptx export | `toPptx(..., { onDiagnostic })` | once per path per export |\n\nEvery engine passes the catalogs its host registered (the `catalogs` option) to core, so a record a host registers resolves the same in preview, editor and export.\n\n## Color references in content\n\nContent color fields (`TextRun.color`, styled table cell `style.fill` / `style.color`, table cell border `color`) accept references as well as literal hex, and those references resolve through the same chain above:\n\n```\n "color": "accent2" slot name -> effective color scheme slot\n "color": "text" role name -> role-to-slot mapping, then the slot\n "color": "var:risk" variable -> top-level variables map\n "color": "#B42318" literal -> used as-is (frozen at authoring time)\n```\n\n- **Slot names** (`accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`) read the named slot from the *effective* color scheme \u2014 the one produced by the slide \u2192 deck \u2192 theme \u2192 engine-default precedence at the top of this page. A slide-level `design.colorScheme` override therefore recolors that slide\'s named runs too.\n- **Role names** (`primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`) resolve through one shared definition, `resolveColorRoles()`, which the opf-render preview, the PPTX export and `validate` all call, so the same deck draws the same colors in each. A role defined on the effective scheme wins; otherwise it defaults from a slot:\n\n | Role | Default (through the slide\'s variant; see [Color variants](#color-variants)) |\n | --- | --- |\n | `primary`, `secondary`, `accent` | the first three accents of the variant: `accent1`, `accent2`, `accent3` unless `variant.accents` reorders them |\n | `background` | the slide\'s own resolved background when it is one color (solid or pattern background color); else the variant\'s `bg1` (`light1` by default, the scheme\'s `background` role overriding it there). A gradient or picture background uses the variant, or its `tone`, or `light1`. |\n | `surface` | `bg2` of the variant: `light2` on a light slide, `dark2` on a dark one |\n | `text` | `tx1` of the variant: `dark1`, or `light1` on a dark slide. A `text` override applies on a light slide only, so a dark slide always keeps readable light text. |\n | `textSecondary` | `tx2` of the variant: `dark2`, or `light2` on a dark slide |\n\n A slide is dark when its background\'s WCAG relative luminance is under 0.179, the point where white and black text contrast equally.\n- **Links.** A link run (`link`) with no `color` of its own is drawn underlined in the scheme\'s `hyperlink` slot (the OOXML `hlink` color; Office blue `#0563C1` when the scheme sets none), in the preview and in the PPTX export (`a:schemeClr hlink` where the deck theme holds that color). Where that color has under 4.5:1 contrast against the slide background (the default Office blue on a dark slide), the slide\'s `text` color is used instead, as the slide tag does for a low-contrast primary color. A link run with its own `color` keeps it, and a `hyperlink` ColorRef always names the slot. `followedHyperlink` is written to the PPTX theme; a static preview cannot know which links were visited, so it draws every link in `hyperlink`.\n- **Variable references** (`var:<id>`) resolve against the document\'s top-level `variables` map, independent of the scheme. Variables are deck-scoped named colors \u2014 use them for values that have meaning (`var:risk`) or repeat across slides. An unknown id is a validation warning, never an error, and engines fall back to their default text color.\n\nThe styled table cell and border color fields enforce the reference forms at the schema level (a typo like `"acent2"` is a schema error there \u2014 neither hex, a known name, nor a `var:` reference). Run colors stay open strings so imported decks keep validating: an unrecognized run color is a validation warning, and renderers fall back to the theme text color \u2014 the same warn-don\'t-error posture unknown catalog ids get. Unknown `var:` ids are warnings everywhere.\n\n### Color variants\n\nA deck has one color scheme of twelve slots. A **variant** is a mapping over those same slots, as PowerPoint\'s `clrMap` is, never a second scheme: which slot is the background, and so which are the text and the alternate surface, plus an optional order for the accents. Everything OPF draws refers to the roles, so a slide that changes variant recolors text, lines, cards, chart furniture and code together.\n\n`variant` is a string, the same way `colorScheme` is: the slot that is the background.\n\n```json\n{\n "design": { "variant": "dark1" },\n "slides": [{ "title": "Light one", "design": { "variant": "light1" } }]\n}\n```\n\n`"dark1"` is `{ "background": "dark1" }`. The object form is only needed to reorder the accents: `{ "background": "light1", "accents": ["accent2", "accent1", "accent3", "accent4", "accent5", "accent6"] }`. Both forms are allowed on `design` and on `slides[n].design`, and what `resolveColorRoles` and `resolveSlideContext` return is always the normalized object (`normalizeColorVariant`).\n\n| `variant` (or `variant.background`) | bg1 | tx1 | bg2 | tx2 |\n| --- | --- | --- | --- | --- |\n| `light1` (default) | `light1` | `dark1` | `light2` | `dark2` |\n| `dark1` | `dark1` | `light1` | `dark2` | `light2` |\n| `light2` | `light2` | `dark1` | `light2` | `dark2` |\n| `dark2` | `dark2` | `light1` | `dark2` | `light2` |\n\n(`light2` and `dark2` keep the background slot as the surface too, as a slide with that theme background has always been drawn, so the engine default theme, whose background is `dark2`, is unchanged.) `variant.accents` (object form) is a permutation of `accent1` to `accent6`; entry `i` is the slot drawn as accent `i + 1`, which reorders `primary`, `secondary`, `accent` and the chart series colors.\n\nWhich variant a slide has, from the most specific to the least (slide design, deck design, then the theme record), at the first level that says anything:\n\n1. a `variant`;\n2. a theme-slot background (`"dark1"`, `{ "type": "theme", "slot": "dark1" }`), which is the variant of that slot, so `design.background` stays a shorthand;\n3. a picture or gradient background with a `tone` (`"dark"` or `"light"`), which is the `dark1` or `light1` variant. Without a tone only a variant of a less specific level can apply; with none the slide counts as light, as before;\n4. any other background (a solid or pattern color) decides by luminance (dark below 0.179), as before.\n\nA theme record can list the variants it offers (`variants`, from gallery 4): named values of `variant`, such as `light`, `light-alt`, `dark` and `dark-alt` on `minimal`, `classic` and `bold`. They are for pickers: choosing one writes its `variant` into `design.variant` (or a slide\'s), and the document keeps that value, not the entry id. The engines read only `design.variant`. There is no `dark` theme in the default catalog: a dark deck is a theme with a dark variant, for example `{ "theme": "classic", "variant": "dark1" }`.\n\nA slide\'s variant replaces the deck\'s whole, accent order included. `resolveSlideContext(...).resolved.roles` is the resolved record, and `resolveColorRoles(scheme, { variant, tone, background })` is the function: besides the colors it returns `variant`, `map` (bg1, tx1, bg2, tx2 and the accents; `clrMapAttributes(map)` gives the PowerPoint `clrMap` attributes for the master and for a slide\'s `clrMapOvr`), `accents` (the six colors in order) and `neutrals`.\n\n#### Scheme-derived neutrals\n\nLines and quiet colors are not fixed grey. `roles.neutrals` derives each from the map, as PowerPoint\'s chart styles do: the text color (tx1) at a share over the background (bg1), chosen as a DrawingML luminance transform of the slot so a PPTX can write `<a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr>` and the preview draws the same color. Each neutral is `{ slot, hex, lumMod?, lumOff?, alpha? }` (the transforms in thousandths of a percent; `hex` is the transform applied, `applyLuminance(slotHex, lumMod, lumOff)`).\n\n| Neutral | Derivation |\n| --- | --- |\n| `border` (also `roles.border`) | tx1 at 20% over bg1: card, rule and table borders |\n| `gridline` | tx1 at 15%: chart gridlines |\n| `axisLine` | tx1 at 25%: chart axis lines |\n| `chartText` | tx1 at 65%: chart axis labels and legend text |\n| `outline` | bg1: pie and treemap outlines, the video play icon, anything drawn on a mark |\n| `trendNeutral` | tx2: the neutral metric trend |\n| `codePanel` | tx1 on a light map, bg2 on a dark one: the code block is a dark card on either |\n| `codeText` | bg1 or tx1, whichever reads better on the panel |\n| `codeComment` | the code text at 65% over the panel (comments and line numbers) |\n\n`codeSyntaxPaletteForRoles(roles)` builds the syntax palette from `codePanel`, `codeText`, `codeComment` and the variant\'s first three accents, and `codeHighlightColors(scheme, palette, codePanel.hex)` takes the panel the highlight band sits on. `readableSchemeColor(fill, roles)` returns bg1 or tx1 (as a scheme color), whichever reads on a fill the scheme does not name, for table header text and text on marks. The validator\'s contrast rules read the same roles, so a dark variant is checked against its own text and surface.\n\n### Background colors are ColorRefs\n\nThe colors of a solid background (`SolidBackground.color`), of each gradient stop and of a pattern (`foregroundColor`, `backgroundColor`) take the same forms as the content fields above: a literal hex, a slot or role name, or a `var:<id>` reference. The schema keeps these fields as plain strings, so a reference validates; engines resolve it through `resolveColorRef()` against the effective color scheme and the deck `variables`, and a reference that resolves to nothing is drawn as the engine default (white for a background), as for any other unresolvable color. The default text color of the slide follows the resolved background. A PPTX export writes `a:schemeClr` where the deck theme holds the named slot or role exactly, and the resolved literal otherwise (FF-24 conventions).\n\n> **Decision, 2026-09-30 (agent decision, vetoable).** The spec coverage audit found that `SolidBackground.color` accepted `var:` and slot names (the field is a string) but both engines painted white, while [`content-item-design-overrides.md`](./content-item-design-overrides.md) already states that every color field must accept the ColorRef forms and never hex alone. Rather than tighten the schema (which would break documents that validate today), the engines resolve ColorRefs in backgrounds. The owner can veto this by restricting the three background color fields to `HexColor` in the schema and the engines to hex only.\n\n`@openpresentation/opf/composition` exports `resolveColorRef()` with the shared slot, role, variable, and hex rules above so renderers and exporters do not drift. Pass the effective color scheme, optional resolved role colors, the deck `variables` map, and a theme-text `fallback` for unrecognized references.\n\n## Script fonts and language\n\nOOXML gives each theme font (major and minor) three script slots: `latin`, East Asian (`ea`) and complex script (`cs`). The presentation `language` and the effective font scheme resolve to all three:\n\n```\n latin design font scheme heading/body (the chain above)\n eastAsian 1. design.fontScheme.eastAsian explicit slot\n complexScript 2. the scheme\'s own major/minor when languageFamily is ea / cs (or eastAsian /\n complexScript, the same values) and its\n languages list (language ids) is empty or\n names the language\n 3. the language\'s font scheme when the language\'s script uses the slot\n 4. the latin family otherwise\n```\n\n- A language record\'s `script` (ISO 15924) picks its slot. East Asian scripts (`Jpan`, `Hans`, `Hant`, `Kore`, ...) use `eastAsian`. Complex scripts (`Arab`, `Hebr`, `Deva`, `Thai`, ...) use `complexScript`. Latin, Cyrillic, Greek and other scripts use `latin`. `direction` defaults from the script (Arabic and Hebrew are right-to-left).\n- The language\'s `fontScheme` applies to PowerPoint output and `googleFontScheme` to Google Slides output. For Latin-script languages, the design font scheme always supplies the latin slot.\n- A language sets `lang`, the text direction and the script slots, and never the Latin scheme: only `design.fontScheme` (slide, then deck, then theme, then the shared default `aptos`) sets the latin fonts, and a language record\'s `fontScheme` is a default for its own script slot, not a deck font. The PPTX theme\'s `a:ea` and `a:cs` are written only for a slot a script font is selected for (the scheme\'s explicit slot or the language\'s script font) and stay empty otherwise, as in Office\'s own themes. See [Language contract](./programs/font-fidelity-everywhere/script-font-model.md#language-contract-ff-50-model-c) and [Theme slots](./programs/font-fidelity-everywhere/script-font-model.md#theme-slots-ff-49).\n- A Latin deck therefore repeats its heading/body family in `ea`/`cs`. A Japanese deck with `design.fontScheme: { "major": "Carlito", "minor": "Carlito" }` keeps the Latin family in `latin` and uses Meiryo (PowerPoint) or Noto Sans JP (Google Slides) in `ea`. `design.fontScheme.eastAsian` / `.complexScript` (`{ "major": ..., "minor": ... }`) name a script font explicitly, for example for CJK text inside a Latin deck.\n\n`@openpresentation/opf/composition` exports `resolveScriptFonts(document, { app, slideIndex })` (`app` is `"powerpoint"`, the default, or `"google-slides"`), and the package root exports `normalizeLanguageFamily(value)` (which reads a font scheme\'s `languageFamily`, `eastAsian` as `ea` and `complexScript` as `cs`). `resolveScriptFonts` returns the heading and body slots, the OOXML `lang` (a curated `ooxmlLang` culture tag such as `ja-JP` or `ms-MY`, or an authored region tag), the canonical `bcp47` tag, `script`, `direction`/`rtl`, and the per-script supplemental theme font. Renderers and exporters should use it rather than re-deriving slots. The model, the OOXML mapping and the open questions are in [`programs/font-fidelity-everywhere/script-font-model.md`](./programs/font-fidelity-everywhere/script-font-model.md). opf-render and opf-pptx implement it (FF-07, FF-19, FF-49).\n\n## Brand assets and layout hints\n\nThe 2026-09-30 spec coverage audit found that the deck logo, `organization.logo`, `speaker.photo`, `design.contentDirection`, `design.chartPrimary`, `design.listBullet` and `fontScheme.accent` validated, edited and round-tripped but changed nothing in any engine. This section states what they do now. Every rule below is implemented once, in `composeSlide()` and `resolveLogo()` of `@openpresentation/opf`, and consumed by the renderer and the exporter; the decisions marked **(vetoable)** are agent decisions the owner can overturn.\n\n### Layout templates and design\n\nAn OPF layout record (`opf-layout/v2`) is one ordered list of `regions`, declared once, in reading order, with a `direction` (`row` or `column`) and splits for two dimensions. A region has a `name`, a relative `size` and rules: accepted content kinds, role, flow, capacity and optional bleed. `layoutContent(record)` and `layoutStructure(record)` summarize those regions for hosts; `composeSlide()` reports their binding and boxes in `geometry.regions`. A slide may use `layout: "auto"` to bypass a record explicitly. [Layout templates](layout-templates.md) documents the binding and flow rules.\n\nThe effective `design` value of each key comes from the slide, then the deck, then the resolved theme, then the layout record, then the engine default. This includes `titleAlignment`, `contentAlignment`, `contentBox`, `contentDirection`, `chartPrimary`, `imageFit`, `listBullet` and `mirror`. `resolveDesignHints()` reports the supplying level, and `SlideComposition.design` holds the merged result. A layout\'s regions determine placement; `design.mirror` reverses its tracks, while the slide\'s own `composition` arranges the first primary flowing region. A top-level image block may still carry its own `placement` band. A layout sits on any slide background; it does not opt into a picture.\n\nThe `tag` (eyebrow) follows `titleAlignment` on every slide, and is left-aligned when none resolves, like the title, so a layout that centres its content does not centre the tag over a left-aligned title. The `subtitle` joins them whenever it sits in the title area (a cover, or a layout with no `subtitle` region of its own), so the heading group shares one alignment axis; a layout with a separate `subtitle` region keeps it on `contentAlignment`. Only the slide\'s own `design.contentAlignment` keeps the tag and that subtitle on content alignment, preserving the 2026-10-06 authoring decision.\n\n### Logos live on the organization (RR-71, OPF 0.18)\n\nA logo belongs to an organization. `Organization.logo` is one path or Asset for every shape, or up to four shapes:\n\n```json\n"organization": {\n "id": "acme", "name": "Acme",\n "logo": {\n "full": { "onLight": "./assets/acme-logo.svg", "onDark": "./assets/acme-logo-white.svg" },\n "icon": "./assets/acme-mark.svg"\n }\n}\n```\n\n`resolveLogo(presentation, slide, { shape, onDark, slideIndex, reference })` (`@openpresentation/opf/composition`) returns `{ source, path, shape, variant, reference, designPath? }` or `null`:\n\n```\n organization 1. options.reference var:organization.beta.logo.icon\n 2. slides[i].design.logo, then design.logo:\n false draws no logo; a reference picks the organization\n (and the shape, when it names one)\n 3. the primary organization role "primary", else the first\n shape the reference\'s shape, else options.shape (default full)\n missing shape -> full -> first of wordmark, stacked, icon\n variant onDark on a dark background, else onLight; a missing one uses\n the other; a plain asset is "default"\n path organization.logo, organization.1.logo.icon,\n organization.logo.full.onDark, ...\n```\n\nAn override that names an organization without a logo draws nothing: it never falls back to another organization. Hosts pass their own background luminance test as `composeSlide(..., { darkBackground })` (`resolveSlideContext` computes it); core never inspects colors.\n\nThe same logos are placed anywhere through slide-scoped built-in references (`var:organization.logo`, `var:organization.logo.icon`, `var:organization.beta.logo.wordmark`), which resolve for each slide\'s background: see [templates and variables](templates-and-variables.md#organization-logos).\n\n### Where the logo is drawn (vetoable)\n\n1. **Cover and section slides.** A slide with no body payload on a title-only template, after empty template regions collapse, or without a layout (the same rule that centers covers) draws the primary organization\'s `full` logo (or the `design.logo` override) at the top-left of the free area, inside the slide padding and below any header furniture. `composeSlide` returns it as `geometry.logo` (`{ box, shape, path, source, variant, reference, anchor: \'left\' }`): `x = area.left + padding`, `y` at the image-safe heading top, `height = 56` reference pixels at a 720-pixel short edge, `width = min(4 * height, free width)`. Headings start one gap below the box and the cover-centering rule centers the tag/title/subtitle group in the remaining span; the logo itself does not move. Consumers fit the image inside the box preserving its aspect ratio, anchored left and vertically centered (SVG `preserveAspectRatio="xMinYMid meet"`; PPTX computes the fitted size from the raster dimensions and places it at `box.x`). Nothing is drawn when no logo resolves or `design.logo` is `false`. **Content slides never get an automatic logo** (vetoable: it would move every content area).\n2. **Headers and footers.** A zone places a logo like any picture: `"image": "var:organization.logo.icon"`. `layoutFurniture` resolves the reference for the slide\'s background; the image part carries the asset in `image`, the organization path in `sourcePath` and the reference in `reference`. Without a logo the engine reports `unresolved-content` at `<zone>.image`. The zone\'s parts sit in one row (see [dynamic composition](dynamic-composition.md)).\n3. **Picture bullets.** See `listBullet` below: the `icon` shape.\n\n> **Decision, 2026-09-30 (agent decision, vetoable).** 84 of the 126 bundled example decks carry a logo, so 81 cover slides gain a logo and their heading group moves down. The placement (top-left, 56 px, full logo) and the content-slide exclusion are the reference-engine defaults; a layout-driven logo slot is a separate design.\n\n### `speaker.photo` is a built-in image variable (vetoable)\n\nNo reference engine draws a speaker photo by itself: the schema has no speaker slot on any slide and no slide-to-speaker link, and a speaker block on covers would be a separate design. The author places it explicitly with the built-in image variable (`"image": "var:speaker.photo"`, see [templates and variables](templates-and-variables.md#built-in-variables)); the field also round-trips through PPTX provenance.\n\n### `contentDirection`\n\nThe effective value follows the normal design precedence: slide, deck, theme, layout, engine default. On automatic slides `vertical` sets the root arrangement to `column` and `horizontal` to `row`, unless the slide explicitly sets `composition.mode`. Promoted regions keep their geometry and nested content groups keep their own composition.\n\nOn a v2 template, `contentDirection` changes every flowing region into a row or column without changing the template\'s regions. An atomic `flow: "none"` region remains atomic. Without the direction hint each region uses its declared flow. Layout records do not have `composition.mode`, `columns` or `weights`; their region list defines the grid.\n\n### `titleAlignment` and `contentAlignment` in right-to-left decks\n\n> **Decision, 2026-10-01 (RR-05, supervisor decision, vetoable).** Alignment is logical for right-to-left text. In a deck whose language is written right to left, `left` means the start edge and `right` the end edge of each paragraph: an Arabic or Hebrew paragraph with the default (or authored) `left` is drawn against the right edge, with its bullets, indents and table cells to match, while an English paragraph in the same deck keeps the left edge. `center` is unchanged and a left-to-right deck is unchanged. The effective value (slide design, then deck design, then `left`) is still what `item.alignment` reports; `placement.lines[i].alignment` and the exported `algn` carry the physical edge. A right-to-left deck that wants a paragraph at its end edge sets `right`. See [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n### `chartPrimary`\n\nThe resolved `design.chartPrimary` applies on automatic slides only. It changes the root arrangement when the slide has no promoted regions, no `composition.mode` of its own, and its root nodes contain at least one chart leaf and at least one node that is not a chart. The **first chart node is primary** and the other root nodes form one synthetic sub-grid:\n\n- `left` / `right`: the root is a two-track row with weights `[3, 2]` (chart first for `left`, last for `right`); the rest arrange in `auto` mode inside their track.\n- `top` / `bottom`: a two-track column with weights `[3, 2]` (chart first for `top`).\n- `none`, or any other value: no change, the existing automatic grid with equal weight.\n\nAutomatic composition builds the primary chart track and a direct content group for the other root nodes, preserving the 3:2 chart weight and source content order. A v2 template remains authoritative and does not apply this automatic hint. OPF 0.19 removes the v1 record-construction helper `chartPrimaryLayout`; authors use `chart-beside` or a custom v2 template for an authored grid.\n\nThe synthetic container has no OPF path, so it records no `groups`, `flows` or explanation entry; the root decision has `reason: \'chart-primary\'` and `selectedColumns` 2 (row) or 1 (column). Explicit root `columns` and `weights` are ignored while the automatic hint applies. A chart inside a nested group, a chart-only root, or a single root `chart` payload leaves the arrangement unchanged. A slide using a v2 template follows that template instead.\n\n### `listBullet` (vetoable)\n\n`character` (the default) draws the current glyph marker. `image` draws the organization\'s icon logo (`resolveLogo(..., { shape: \'icon\', onDark })`: the primary organization\'s, or the organization and shape `design.logo` names) as a picture bullet: every `items`/`bullets` item in `composeSlide` and every `listEntries[]` entry of its fit carry `bulletImage: { source, path }`. Marker geometry is unchanged, and each entry carries `bulletBox`, where the image draws: a square of side `marker.fontSize * PICTURE_BULLET_SCALE` (0.65, exported) whose bottom sits on the marker baseline (`marker.y`) with its left edge at `marker.x`. The scale is what desktop PowerPoint draws for an `a:buBlip` at `a:buSzPct 100000` (what the exporter writes), measured as a square 10, 15, 16, 20 and 31 px wide at font sizes of 16, 24, 25, 32 and 48 px (0.625 to 0.646; the heights run a pixel more from anti-aliasing) with its bottom on the text baseline, in Arial, Aptos, Georgia and Courier New alike, so it does not depend on the typeface; the text start and hanging indent are identical in both. The exporter sets no size, because PowerPoint sizes the bullet itself; consumers that draw it themselves (the preview) use `bulletBox`. Vetoable: the constant is a measurement, not a rule of the format. When `image` is set and no logo resolves, the glyph stays and the slide reports one `unresolved-content` diagnostic at `slides.N.design.listBullet` or `design.listBullet`, only when the slide has a list. The renderer draws an `<image>` per marker; the exporter writes native picture bullets (`a:buBlip`).\n\n`none` draws no marker and no hanging indent, for lists that are a label with its explanation (pillars, team members, questions with answers). Every entry\'s text and description start at the left edge of the list box, and each `listEntries[]` marker is `{ none: true, text: \'\', indent: 0 }` (its `x`, `y` and `style` still give the first baseline and font). A nested `level` still steps in by 1.1 em. A list that sets `numbering` keeps its numbers, as it does for `image`. The preview draws nothing for the marker, and the exporter writes `a:buNone` with no margin or indent; a re-import reads the list back through the line roles recorded in the slide\'s provenance (a PPTX without that record keeps one text shape per line).\n\n### `fontScheme.accent`\n\n`resolveFontFamilies()` returns `accent` only when the effective scheme defines an `accent` role (a family string or a `Font` object). The slide `tag` (eyebrow) and the quote body use `fonts.accent ?? <current family>` (body for the tag, heading for the quote body); nothing else changes. The renderer loads and embeds it, the exporter writes it on those runs (`a:latin`) while the theme fonts stay major/minor, and import keeps restoring `design.fontScheme` from provenance. None of the bundled examples sets an accent font.\n\n## What is *not* part of this chain\n\nBeyond the color references above, content payloads carry no design controls in v1 \u2014 `position`, `fontSize` overrides at payload level, and the like were deliberately kept out while the content model stabilizes (see [`content-item-design-overrides.md`](./content-item-design-overrides.md); styled table cells and rich-text runs carry the only per-content styling, and their color fields take the reference forms above). The design system, plus layout hints (`titleAlignment`, `contentBox`, `chartPrimary`, ...) and dynamic composition, is the styling surface of an OPF document.\n'
|
|
74
76
|
},
|
|
75
77
|
{
|
|
76
78
|
"slug": "dynamic-composition",
|
|
77
79
|
"file": "docs/dynamic-composition.md",
|
|
78
80
|
"title": "Dynamic composition",
|
|
79
|
-
"markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe published Node 24 set is the registry\'s `latest` of each package (`node scripts/registry-plan.mjs`); a layout is one list of regions (FA-33, Gallery 5). Use the exact pins it prints; the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nComposed font sizes lie on PowerPoint\'s 0.01 pt grid (RR-16). PowerPoint stores a run size (`sz`) in hundredths of a point and a point is 4/3 reference pixels, so every size a fit accepts is a whole multiple of 1/75 px (`FONT_SIZE_GRID_PER_PX`), and the preview draws exactly the size the export writes. Fitting evaluates each trial size on the grid before breaking lines and placing text: trial sizes stay anchored to the unsnapped request (no drift) and round down (`snapFontSizeDown`), so a size that fit before snapping still fits; a readability floor rounds up (`snapFontSizeUp`), so `minFontSize` is never undercut; and a result that reports no overflow was measured at the size it carries. The rule covers plain, rich, list (marker, description and picture-bullet side), table, quote, code, metric, timeline, furniture and heading text, including each rich run and script. `wrapText` measures at exactly the size it is given.\n\nHeadings reserve space according to their wrapped text. Title and subtitle share the padded width of the free area, and a missing tag or subtitle leaves no gap.\n\nCover slides vertically center the combined tag/title/subtitle group in the free heading area. A cover is a slide with no body payload (no root content field, where a root `image` counts unless it carries its own `placement`, no `blocks` other than placed images, no promoted regions; empty payloads such as `blocks: []`, `text: ""`, empty lists and regions with nothing in them count as no body; whitespace-only text is still body) on a v2 template or with no layout record. Empty template regions collapse according to the record and its heading regions place the headings. The rule reads the record, never its id. The free area is the slide minus the bands of placed images, header and footer furniture, and the usual padding; a picture background reserves nothing. A wrapped heading makes the group taller and the group recenters. A group that already fills the free area is not moved. Accepted line and outline origins move with the boxes. Explicit heading `alignment` positions ink inside the box and never changes the vertical position. A flowed image is body, so image slides keep the top-aligned content origin; a placed image is not. Content slides are not affected: headings stay at the top and the body follows them. This is a reference-engine default, not a schema field.\n\n## Adaptive layout templates (OPF 0.19)\n\nA layout record is one ordered list of regions, declared once in reading order, and describes the content each body region accepts. The 28 built-in layouts are named by intent, such as `cover`, `text`, `chart`, `list`, `metrics` and `image-beside`. `layout: "auto"` explicitly asks for automatic composition; omitting `layout` does the same. See [Layout templates](layout-templates.md) for the record shape, the built-ins, region binding and overflow rules.\n\n```json\n{\n "name": "Evidence beside the recommendation",\n "slides": [{\n "layout": "two-column",\n "title": "Approve the next phase",\n "blocks": [\n { "region": "first", "text": "The pilot met its acceptance criteria." },\n { "region": "second", "text": "Fund the next two sites." }\n ]\n }]\n}\n```\n\nA block\'s `region` pin is resolved before the layout binds unpinned content by kind, role and reading order. A region\'s `flow` arranges several blocks; `max` limits how many it accepts, and `overflowRegion` receives content no region accepts. If there is no overflow region, unplaced content receives a row below the grid. An empty area collapses into a neighbouring area unless its record says `empty: "keep"`. The geometry exposes the result in `geometry.regions`, including each region\'s box, bound paths, overflow, collapse and bleed. Pagination repeats the selected layout and carries a region pin forward for a block moved to a continuation slide.\n\nThe slide\'s own `composition.mode`, `columns` and `weights` arrange the first primary flowing region. `design.contentDirection` asks flowing regions to use a row or column; `design.mirror` reverses template tracks. Effective design keys resolve per key from slide, deck, theme, layout, then engine default. Promoted regions retain their 3\xD73 vocabulary and visual reading order. The layout template does not replace content groups inside a slide; groups still keep related blocks together.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. A zone logo reference (`var:organization.logo.icon`) to an organization without a logo, missing socials, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nGenerated values are variables inside a zone\'s `text` (FA-31): `{{organization.name}}`, `{{speaker.name}}, {{speaker.title}}` and the other deck-wide built-ins resolve with `resolveVariables` before composition, and the slide-scoped `{{slide.number}}` (the displayed number), `{{slide.section}}` (the slide\'s `section`, empty when it has none) and `{{deck.slideCount}}` (the displayed slide count: `options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count) resolve in `layoutFurniture` for the slide it lays out. One `text` holds several values in the author\'s order, with `\\n` between lines (`"{{organization.name}}\\n{{slide.section}}"`, `"Page {{slide.number}} of {{deck.slideCount}}"`). An escaped `\\{{slide.number}}` draws as the literal token. Every zone `text` is one part with `field: \'text\'` and `sourcePath` at the authored text. `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields`: one `{ type: \'slideNumber\', start, end }` per substituted `{{slide.number}}` and one `{ type: \'date\', start, end, format }` for a whole current date, as half-open UTF-16 ranges into the part\'s `text`, so exporters can write native live fields while `{{deck.slideCount}}`, `{{slide.section}}` and fixed dates stay fixed text. `formatFurnitureDate()` is exported for hosts. All configured fields in one zone sit side by side in one row, in the order image, text, socials, date (see the row geometry below). An organization logo is `image: "var:organization.logo.icon"` (or any shape and organization): `layoutFurniture` resolves it for the slide\'s background and the image part keeps the reference in `reference` (see [design resolution](design-resolution.md#brand-assets-and-layout-hints)). Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\nThe slide-scoped built-ins work in body text too. Engines compose and draw the slide `resolveSlideContext(deck, index, { slideNumber, slideCount })` returns as `context.slide` (`resolveSlideVariables(slide, { slideNumber, slideCount, presentation, darkBackground })` substitutes them in the slide\'s own strings and resolves its organization logo references for the background, leaving its own header and footer to `layoutFurniture`), with the same numbers in `context.options`, so body text is measured and drawn with the real value. `paginate` measures continuation pages with the substituted values but returns the slides with the tokens kept, never splitting a page inside a token, and runs its slide-count fixed point whenever `{{deck.slideCount}}` appears in the deck.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, owner)` formats each value without network access, through the engine\'s social-platform vocabulary (`SOCIAL_PLATFORMS`, [`engine-vocabularies.json`](../spec/reference/engine-vocabularies.json)). A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. A key outside the vocabulary (the schema rejects one) is shown raw with no link, which is the Socials engine fallback. No catalog is involved: the Socials keys are an engine vocabulary. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` lays the left, center and right zones on the content inset (`composition.padding`, the body\'s own left and right edge) as three columns with a 2% gutter, and an empty zone lends its column to its neighbours: a lone `left` footer spans the content width, a `left` and a `right` zone share the free centre, and a `center` zone stays centred. The validator warns (`opf/furniture-wraps`) when a zone\'s text wraps past two lines; move sources to a caption or the speaker notes. **Zones lay out in a row (RR-71, vetoable).** A zone\'s parts sit side by side in the order image, text, socials, date (in a right-to-left deck the row runs from the right, the image at the start edge), `FURNITURE_GAP` (12 reference pixels at a 720-pixel short edge) apart, the row flush with the zone\'s left edge, centered, or flush with its right edge, and each part vertically centered on the row\'s tallest part. `\n` still breaks lines inside a text or socials part. With other parts, a text part\'s box is as wide as its longest line; a zone with a lone text part keeps the whole zone width (it wraps there, as before). When the row is wider than the zone, its text parts share what the images and gaps leave: a part narrower than an equal share keeps its width and the others wrap at the share; beside other parts an image is at most `FURNITURE_IMAGE_SHARE` (40%) of the zone\'s width, and a word that would break inside itself at the readability floor (letters or digits of a space-separated script on both sides of a soft break) is a `text-overflow` diagnostic at the part\'s text path, never a silent break. The tallest row sets the natural band height; rows start at the band\'s top edge. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. An image part (`type: \'image\'`) alone in its zone aligns like the zone\'s text: its `box` is as wide as the image\'s own proportions make it at the band height (a 36 px band at 1280 px wide), at most the zone, flush with the zone\'s left edge in the left zone, centered in the center zone and flush with its right edge in the right zone, at the same vertical position as before; consumers fit the image inside `box`. Core reads the proportions without fetching, from an embedded PNG, JPEG, GIF, WebP or SVG data URI or an `asset:` reference to one, decoding only a bounded prefix of the payload (64 KiB, 1 MiB for a JPEG whose frame header sits behind large metadata) and memoizing per source, so a multi-megabyte logo costs nothing per slide; a JPEG whose header is past 1 MiB, an SVG whose root tag is past 64 KiB, a path or a URL is unreadable is placed in a square box (vetoable), so a wide image behind a path or URL is drawn small and flush to its zone edge until it is embedded. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX (0.9.1 and later) draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates. Since PPTX 0.11.9 (RR-11), the first footer text, date and slide number that fit one accepted line become real PowerPoint `ftr`, `dt` and `sldNum` placeholders at exactly this geometry, with master/layout placeholders and `p:hf` flags (see [native header and footer](https://github.com/OpenPresentation/opf-pptx/blob/main/docs/native-header-footer.md)); header parts, organization, section, socials, images and multi-line text stay tagged shapes because PowerPoint has no object for them. No core geometry or schema changed. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Footnote areas and caption bands\n\nRR-34 (core after 0.11.4) adds two reserved regions that exist only for decks that use the fields. A slide whose runs carry `cite` or `footnote` markers gets `geometry.footnotes` (`footnote-area-v1`): a rule and the slide\'s notes in number order, directly above the footer band (or the bottom padding) with the content area\'s left edge and width, in the body family at the furniture size; the content area shrinks by exactly the area\'s height plus half the slide gap, and nothing above it moves. The area takes at most 35% of the span between the heading top and the footer band; a note that does not fit reports `text-overflow` at its source path (`references.N` or the run\'s path), which pagination treats like any other fit overflow. `slideCitations(slide, slideIndex, presentation)` is the numbering `composeSlide` uses; pass the same `presentation` and `slideIndex` to every consumer. An `image`, `chart`, `table` or `video` payload with a `caption` reserves a caption band inside its region (`item.caption`; `item.box` becomes the media box), below or above the media, at most 35% of the region; automatic grid selection scores the leaf on its media box. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Backgrounds and placed images\n\nCore 0.15 composes pictures in two ways (FA-22); [images](image-treatments.md) has the full vocabulary.\n\n- A picture background (the slide\'s `design.background`, then the deck\'s, then the theme\'s, when it is `{ type: "image", ... }` or an image-source string) is `geometry.backgroundImage`: the whole canvas as its frame, `fit` (`cover`, `contain`, `stretch`, `tile`), `focus`, `opacity`, `recolor` and the overlay box (whole picture or an edge band). It never moves content. `resolveSlideContext` passes the theme\'s background as `ComposeSlideOptions.themeBackground`.\n- Every image item carries `item.image`: the frame inside its region, `fit` (the block\'s own, else the effective `design.imageFit`, else `cover`), `focus`, the mask with its DrawingML preset and SVG outline, line, opacity, recolor and overlay. `fitImage(frame, fit, aspect, focus)` is the shared fit math; core fills `item.image.picture` itself when it can read the picture\'s proportions (a data URI, or an `asset:` reference to one).\n\nA top-level image block with `placement: { edge, size, inset }` takes a band along that slide edge: `size` of the slide width (left, right) or height (top, bottom), default 0.5, edge to edge, or inside the slide padding with `inset`. Headings, footnotes and the body compose in the free area that remains, with the usual padding; header and footer bands keep their full-width placement. Bands are taken in block order and each spans the free area the earlier ones left; a second block on a used edge flows as content (the validator reports it). Placed items are listed after the headings and before the flowed body, and their placeholder slot is not reserved in the flow. A slide whose only body is placed images is composed like a cover. A right-to-left deck mirrors `left` and `right`.\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including the current core. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title and the tag (eyebrow) use `titleAlignment`; every other item, including subtitle, body text, lists, tables and metrics, uses `contentAlignment`. The value is the effective `design` key: the slide\'s design, then the deck\'s design (or the host\'s resolved deck option), then the theme\'s design, then the layout record\'s `design`. With no explicit alignment from those layers, metrics default to `center` and other items to `left`; any resolved `contentAlignment` still overrides that fallback. A layout that sets `contentAlignment: center` therefore centers its content when no higher-priority layer sets the key. The title never inherits `contentAlignment`. A cover (a slide with no body payload on a heading-only layout) has no content region, so its subtitle joins the title\'s alignment too: it follows `titleAlignment`. On every slide, only a `contentAlignment` set on the slide\'s own design keeps the tag (and a cover\'s subtitle) apart (a deck or layout value does not). Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size, `fontFamilies` and `textMeasurement`; `resolveSlideContext(deck, index, { fonts })` from the package root does the catalog resolution for one slide of a deck and returns those options, plus the slide to pass (`context.slide`, with the slide-scoped built-ins substituted). The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from 25 reference pixels for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginate } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginate(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Right-to-left decks\n\nWhen the presentation `language` is written right to left (Arabic, Hebrew, Syriac, Thaana and the other right-to-left scripts) or the host passes `direction: \'rtl\'`, `composeSlide` mirrors the composition and reports each paragraph\'s direction; `SlideComposition.direction` is `\'rtl\'`. The first column and the `left` region are drawn at the right, banded slide images, cover logos and header/footer zones swap sides, lists put their markers at the right, tables run right to left, and `TextFit.directions` gives the direction of the paragraph each line belongs to. Alignment is logical: the authored `left` is the start edge, drawn at the right edge of a right-to-left paragraph (`physicalAlignment`). A left-to-right deck composes exactly as before. The rules and the PPTX mapping are in [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, pass the same `fonts` handle to every step (see [pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate)). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Preview polish shared by preview and export (RR-07)\n\nThree accepted spec fields used to be drawn plain; core now owns the shared tables so the SVG preview and the PPTX export draw them identically. Consumers import them from `@openpresentation/opf`; none changes geometry, and an older core simply leaves the preview and the export plain, without a new diagnostic.\n\n- **Code syntax colours.** `tokenizeCode(source, language)` returns sorted, non-overlapping token ranges (`keyword`, `string`, `number`, `comment`, `function`, `type`, `property`) over the exact `code.source`; `codeLineRuns(tokens, start, end, source?)` clips them to one accepted source line, so concatenating the runs returns the line unchanged (a tab is always its own plain run). The scanner is deterministic and dependency-free (no network, clock or locale). `code.language` resolves through aliases (`ts`, `tsx`, `py`, `sh`, `yml`, `c++`, `terraform`, and so on) to `javascript`, `typescript`, `python`, `rust`, `go`, `bash`, `json`, `yaml`, `toml`, `ini`, `html`/`xml`, `css`, `sql`, `java`, `csharp`, `kotlin`, `swift`, `ruby`, `php`, `c`/`cpp`, `hcl` and `dockerfile` (`resolveCodeLanguage`, `CODE_HIGHLIGHT_LANGUAGES`); any other language, a missing one, or source over 200,000 characters stays plain. `codeSyntaxPaletteForScheme(colorScheme)` gives one colour per kind from the deck theme (keyword = primary, string = accent, number = secondary; comment, function, type and property are fixed slate, blue, cyan and pink, rotated away from a theme colour they would collide with), each lightened until it is at least 4.5:1 on the #111827 code panel. The renderer nests coloured tspans inside each accepted segment tspan; PPTX writes the same colours as native runs in the same one-text-box-per-line shapes, so editing and import are unchanged. The language is not inferred from `code.filename`. `code.highlight` (1-based source lines and inclusive ranges) adds a band behind marked lines and dims the rest: `codeHighlightLines` resolves it against the source, `codeLineNumbers` numbers each displayed line (a wrapped line repeats its number), `codeHighlightBands` returns the runs of displayed lines to draw behind, and `codeHighlightColors(colorScheme)` gives the band and the marked/dimmed palettes, all at least 4.5:1 on what they sit on. The SVG band is one `rect` per run behind the line text; PPTX writes one native rectangle per run (`OPF code N highlight M`) after the panel and before the line text boxes, so the one-text-box-per-line shapes and the code provenance tags are unchanged.\n- **Metric trend.** A `metric.trend` (`up`, `down`, `flat`) keeps its word as the visible, editable text and gains one arrow: `metricTrendMark(layout, {background})` derives the arrow from the accepted trend line (square, 0.72 of the font size, on the baseline, 0.3 of the font size from the word; after the word for left alignment, before it for centre and right, and omitted if it would leave the field). The arrow is the DrawingML `upArrow`, `downArrow` or `rightArrow` preset at default adjustments (`metricTrendPoints` is the outline the preview draws), coloured by the metric\'s `sentiment`: positive green, negative red, neutral a neutral colour, kept at 4.5:1 or more against the slide background (`metricTrendColor`); the delta and trend text take the same colour. `layoutMetric` passes `sentiment` through as `MetricLayout.sentiment` (omitted when the metric has none) and `metricTrendMark` reads it, so the arrow direction always follows `trend` and only the colour follows `sentiment`. Absent, the sentiment is the conventional reading of the trend (up positive, down negative, flat neutral), which is the colour drawn before `sentiment` existed. A `sentiment` without a `trend` draws no arrow and colours nothing. The arrow carries the alternative text "Trend: up" (`aria-label` in SVG, `descr` in PPTX); the word and the arrow carry the direction, the colour the verdict.\n- **Pattern fills.** `PATTERN_PRESETS` lists the 54 ECMA-376 ST_PresetPatternVal names and `patternBitmap(preset)` / `patternRuns(preset)` give each as an 8 x 8 one-bit tile, one pixel per 1/96 inch, anchored at the slide\'s top-left, foreground where a bit is set (`diagStripe` still resolves to `wdUpDiag`). ECMA-376 names the presets but does not define their pixels: the tiles are measured from desktop PowerPoint (Office 365, Windows, 2026-10-01) by exporting each preset as a full-slide background to a 1280 x 720 PNG and voting every pixel into its (x mod 8, y mod 8) cell with opf-render `scripts/derive-pattern-bitmaps.mjs`. Every tile was uniform across all repeats (confidence 1.00) at one image pixel per pattern pixel, so a pattern pixel is one 1/96 inch unit, and the phase is the slide\'s top-left corner. The tool also reports any tile that later differs. PPTX already wrote every preset as native `a:pattFill`.\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from `@openpresentation/opf/composition` (the package root no longer exports engine names). It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?, sentiment?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fontFamilies`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts were included in the core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train; current pins are what `node scripts/registry-plan.mjs` prints. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/18 reference pixels, raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the `@openpresentation/opf/composition` entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fontFamilies`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n### Quote role and photo (FA-12)\n\n`QuoteContent` also takes `role` and `photo`. The footer text is the attribution, then `role` after a line break, then `source` after ` - `; each field keeps its own source mapping (`<path>.role`). Without a `role` the footer text is unchanged. A `photo` (an `Asset` value) adds `QuoteLayout.photo`: `{path, value, box, shape}`, where `shape` is the circle mask of `imageShape(\'circle\', box)` (DrawingML `ellipse` and the same outline as an SVG path). The diameter is three times the footer font size and the gap beside it three quarters of that size, so the allocator picks the footer size (nominal or the readability floor) with the photo in the footer height and the footer text in the remaining width. The photo sits at the start edge of the inset area: the left, or the right when `options.direction` is `rtl`, where the footer text is its mirror. The text block is centered on the photo. A photo whose height cannot fit at the floor adds the `photo-fit` diagnostic reason (and `overflow`); a photo with no footer text still draws. Without a photo `layoutQuote` returns exactly what it returned before, with no `photo` key. The renderer and the PPTX exporter draw `photo` from the result and do not recompute it.\n\n## Timeline internals\n\n`layoutTimeline(value, box, options)` (`timeline-flow-v1`) places an optional name and description, then the events as a marker on a connector with each event\'s `when`, `what` and `description` as separate text parts. It tries an alternating arrangement (labels above and below a horizontal connector) and a vertical one (a marker rail down the start edge, labels beside it), at most 50 candidates in all, and keeps the first that fits or the one with the fewest diagnostics. Right-to-left decks put the vertical rail on the right edge.\n\n### Event status (FA-11)\n\n`TimelineEvent.status` is `done`, `current` or `planned`. It is a progress state that the engines draw from the deck\'s own colors without judging it; schedule health (at risk, blocked) is deliberately not a status. Layout copies the status to `TimelineLayout.markers[i].status` and to each of that event\'s text parts (`TimelineTextPart.status`), and `timelineMarkerShapes(marker, colors)` and `timelineTextColor(part, colors)` turn it into the shapes and text color that both engines draw:\n\n| Status | Marker | Text |\n| --- | --- | --- |\n| absent | A filled circle in the primary color. | Normal text color. |\n| `done` | The same filled circle, so it looks like an event without a status. | Normal text color. |\n| `current` ("we are here") | The filled circle inside a ring: a circle 1.6 times the marker\'s radius with a primary-color outline over the slide background. | The label (`what`) is bold, in the normal text color. |\n| `planned` | A hollow-looking circle of the marker\'s radius: a primary-color outline filled with the slide background. | All of the event\'s text is the muted secondary text color. |\n\n- **Geometry.** A marker\'s `radius` is the radius of its ellipse, for every status. A `current` marker also has `ring.radius`, exactly 1.6 times `radius`, and a `current` or `planned` marker has the `strokeWidth` of its outline (2 reference pixels at 720, at most half the radius). The drawn and exported ellipses have exactly these radii, so every ellipse of a deck has the same size for the same radius (a done, planned and unset marker are the same size); an outline is centered on its ellipse\'s edge and reaches half a stroke beyond it, and the cell check and the vertical rail count that half stroke. A ring that leaves the cell is reported as a `event-space` diagnostic like any marker. In the vertical arrangement a deck with a `current` event moves the rail and the text out by the ring\'s extra width so the text keeps its usual distance from the marker. Events without a status produce exactly the geometry and no extra keys they did before.\n- **Hollow means background-filled.** The outline of a hollow marker and the ring are drawn over the slide background color (in the SVG as the fill, in PPTX as a solid fill of the same color) so the connector does not cross them.\n- **Colors.** `timelineTextColor` returns `mutedText` for a planned event, lightened or darkened until it reaches 4.5:1 against the slide background (`TIMELINE_TEXT_MIN_CONTRAST`), and the normal text color otherwise. Outlines and rings are kept at 3:1 (`TIMELINE_OUTLINE_MIN_CONTRAST`); the filled marker keeps the primary color exactly.\n- **Bold label.** The weight is part of the layout, not a drawing detail: the `what` part of a `current` event is requested at weight 700, so line breaks and heights were measured bold.\n- **PPTX.** The marker is a native `ellipse` (solid primary fill for `done`, `current` and no status; background fill plus a primary line for `planned`), the ring is a second native `ellipse` (background fill plus a primary line), the bold label is a bold run, and planned text takes the muted color. The exporter records each event\'s status in the marker\'s `OPF_TIMELINE_V1` tag and the ring in a tag of its own, and the importer restores `status` from them; a deck whose marker or ring was edited away imports as ordinary text without status, like the rest of the timeline provenance. Status is also written in Markdown as a task-list prefix on the event line: `[x]` done, `[>]` current, `[ ]` planned.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. A v2 template\u2019s region grid is changed by editing the template\u2019s `columns` and `rows`. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.presentation, { fonts }).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.presentation, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.presentation with the same renderer and fonts handle before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.presentation);\nconst prepared = prepareBlockMove(editor.presentation,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{presentation, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(presentation, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
|
|
81
|
+
"markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe published Node 24 set is the registry\'s `latest` of each package (`node scripts/registry-plan.mjs`); a layout is one list of regions (FA-33, Gallery 5). Use the exact pins it prints; the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nComposed font sizes lie on PowerPoint\'s 0.01 pt grid (RR-16). PowerPoint stores a run size (`sz`) in hundredths of a point and a point is 4/3 reference pixels, so every size a fit accepts is a whole multiple of 1/75 px (`FONT_SIZE_GRID_PER_PX`), and the preview draws exactly the size the export writes. Fitting evaluates each trial size on the grid before breaking lines and placing text: trial sizes stay anchored to the unsnapped request (no drift) and round down (`snapFontSizeDown`), so a size that fit before snapping still fits; a readability floor rounds up (`snapFontSizeUp`), so `minFontSize` is never undercut; and a result that reports no overflow was measured at the size it carries. The rule covers plain, rich, list (marker, description and picture-bullet side), table, quote, code, metric, timeline, furniture and heading text, including each rich run and script. `wrapText` measures at exactly the size it is given.\n\nHeadings reserve space according to their wrapped text. Title and subtitle share the padded width of the free area, and a missing tag or subtitle leaves no gap.\n\nCover slides vertically center the combined tag/title/subtitle group in the free heading area. A cover is a slide with no body payload (no root content field, where a root `image` counts unless it carries its own `placement`, no `blocks` other than placed images, no promoted regions; empty payloads such as `blocks: []`, `text: ""`, empty lists and regions with nothing in them count as no body; whitespace-only text is still body) on a v2 template or with no layout record. Empty template regions collapse according to the record and its heading regions place the headings. The rule reads the record, never its id. The free area is the slide minus the bands of placed images, header and footer furniture, and the usual padding; a picture background reserves nothing. A wrapped heading makes the group taller and the group recenters. A group that already fills the free area is not moved. Accepted line and outline origins move with the boxes. Explicit heading `alignment` positions ink inside the box and never changes the vertical position. A flowed image is body, so image slides keep the top-aligned content origin; a placed image is not. Content slides are not affected: headings stay at the top and the body follows them. This is a reference-engine default, not a schema field.\n\n## Adaptive layout templates (OPF 0.19)\n\nA layout record is one ordered list of regions, declared once in reading order, and describes the content each body region accepts. The 30 built-in layouts are named by intent, such as `cover`, `text`, `chart`, `list`, `metrics` and `image-beside`. `layout: "auto"` explicitly asks for automatic composition; omitting `layout` does the same. See [Layout templates](layout-templates.md) for the record shape, the built-ins, region binding and overflow rules.\n\n```json\n{\n "name": "Evidence beside the recommendation",\n "slides": [{\n "layout": "two-column",\n "title": "Approve the next phase",\n "blocks": [\n { "region": "first", "text": "The pilot met its acceptance criteria." },\n { "region": "second", "text": "Fund the next two sites." }\n ]\n }]\n}\n```\n\nA block\'s `region` pin is resolved before the layout binds unpinned content by kind, role and reading order. A region\'s `flow` arranges several blocks; `max` limits how many it accepts, and `overflowRegion` receives content no region accepts. If there is no overflow region, unplaced content receives a row below the grid. An empty area collapses into a neighbouring area unless its record says `empty: "keep"`. The geometry exposes the result in `geometry.regions`, including each region\'s box, bound paths, overflow, collapse and bleed. Pagination repeats the selected layout and carries a region pin forward for a block moved to a continuation slide.\n\nThe slide\'s own `composition.mode`, `columns` and `weights` arrange the first primary flowing region. `design.contentDirection` asks flowing regions to use a row or column; `design.mirror` reverses template tracks. Effective design keys resolve per key from slide, deck, theme, layout, then engine default. Promoted regions retain their 3\xD73 vocabulary and visual reading order. The layout template does not replace content groups inside a slide; groups still keep related blocks together.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. A zone logo reference (`var:organization.logo.icon`) to an organization without a logo, missing socials, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nGenerated values are variables inside a zone\'s `text` (FA-31): `{{organization.name}}`, `{{speaker.name}}, {{speaker.title}}` and the other deck-wide built-ins resolve with `resolveVariables` before composition, and the slide-scoped `{{slide.number}}` (the displayed number), `{{slide.section}}` (the slide\'s `section`, empty when it has none) and `{{deck.slideCount}}` (the displayed slide count: `options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count) resolve in `layoutFurniture` for the slide it lays out. One `text` holds several values in the author\'s order, with `\\n` between lines (`"{{organization.name}}\\n{{slide.section}}"`, `"Page {{slide.number}} of {{deck.slideCount}}"`). An escaped `\\{{slide.number}}` draws as the literal token. Every zone `text` is one part with `field: \'text\'` and `sourcePath` at the authored text. `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields`: one `{ type: \'slideNumber\', start, end }` per substituted `{{slide.number}}` and one `{ type: \'date\', start, end, format }` for a whole current date, as half-open UTF-16 ranges into the part\'s `text`, so exporters can write native live fields while `{{deck.slideCount}}`, `{{slide.section}}` and fixed dates stay fixed text. `formatFurnitureDate()` is exported for hosts. All configured fields in one zone sit side by side in one row, in the order image, text, socials, date (see the row geometry below). An organization logo is `image: "var:organization.logo.icon"` (or any shape and organization): `layoutFurniture` resolves it for the slide\'s background and the image part keeps the reference in `reference` (see [design resolution](design-resolution.md#brand-assets-and-layout-hints)). Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\nThe slide-scoped built-ins work in body text too. Engines compose and draw the slide `resolveSlideContext(deck, index, { slideNumber, slideCount })` returns as `context.slide` (`resolveSlideVariables(slide, { slideNumber, slideCount, presentation, darkBackground })` substitutes them in the slide\'s own strings and resolves its organization logo references for the background, leaving its own header and footer to `layoutFurniture`), with the same numbers in `context.options`, so body text is measured and drawn with the real value. `paginate` measures continuation pages with the substituted values but returns the slides with the tokens kept, never splitting a page inside a token, and runs its slide-count fixed point whenever `{{deck.slideCount}}` appears in the deck.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, owner)` formats each value without network access, through the engine\'s social-platform vocabulary (`SOCIAL_PLATFORMS`, [`engine-vocabularies.json`](../spec/reference/engine-vocabularies.json)). A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. A key outside the vocabulary (the schema rejects one) is shown raw with no link, which is the Socials engine fallback. No catalog is involved: the Socials keys are an engine vocabulary. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` lays the left, center and right zones on the content inset (`composition.padding`, the body\'s own left and right edge) as three columns with a 2% gutter, and an empty zone lends its column to its neighbours: a lone `left` footer spans the content width, a `left` and a `right` zone share the free centre, and a `center` zone stays centred. The validator warns (`opf/furniture-wraps`) when a zone\'s text wraps past two lines; move sources to a caption or the speaker notes. **Zones lay out in a row (RR-71, vetoable).** A zone\'s parts sit side by side in the order image, text, socials, date (in a right-to-left deck the row runs from the right, the image at the start edge), `FURNITURE_GAP` (12 reference pixels at a 720-pixel short edge) apart, the row flush with the zone\'s left edge, centered, or flush with its right edge, and each part vertically centered on the row\'s tallest part. `\n` still breaks lines inside a text or socials part. With other parts, a text part\'s box is as wide as its longest line; a zone with a lone text part keeps the whole zone width (it wraps there, as before). When the row is wider than the zone, its text parts share what the images and gaps leave: a part narrower than an equal share keeps its width and the others wrap at the share; beside other parts an image is at most `FURNITURE_IMAGE_SHARE` (40%) of the zone\'s width, and a word that would break inside itself at the readability floor (letters or digits of a space-separated script on both sides of a soft break) is a `text-overflow` diagnostic at the part\'s text path, never a silent break. The tallest row sets the natural band height; rows start at the band\'s top edge. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. An image part (`type: \'image\'`) alone in its zone aligns like the zone\'s text: its `box` is as wide as the image\'s own proportions make it at the band height (a 36 px band at 1280 px wide), at most the zone, flush with the zone\'s left edge in the left zone, centered in the center zone and flush with its right edge in the right zone, at the same vertical position as before; consumers fit the image inside `box`. Core reads the proportions without fetching, from an embedded PNG, JPEG, GIF, WebP or SVG data URI or an `asset:` reference to one, decoding only a bounded prefix of the payload (64 KiB, 1 MiB for a JPEG whose frame header sits behind large metadata) and memoizing per source, so a multi-megabyte logo costs nothing per slide; a JPEG whose header is past 1 MiB, an SVG whose root tag is past 64 KiB, a path or a URL is unreadable is placed in a square box (vetoable), so a wide image behind a path or URL is drawn small and flush to its zone edge until it is embedded. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX (0.9.1 and later) draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates. Since PPTX 0.11.9 (RR-11), the first footer text, date and slide number that fit one accepted line become real PowerPoint `ftr`, `dt` and `sldNum` placeholders at exactly this geometry, with master/layout placeholders and `p:hf` flags (see [native header and footer](https://github.com/OpenPresentation/opf-pptx/blob/main/docs/native-header-footer.md)); header parts, organization, section, socials, images and multi-line text stay tagged shapes because PowerPoint has no object for them. No core geometry or schema changed. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Footnote areas and caption bands\n\nRR-34 (core after 0.11.4) adds two reserved regions that exist only for decks that use the fields. A slide whose runs carry `cite` or `footnote` markers gets `geometry.footnotes` (`footnote-area-v1`): a rule and the slide\'s notes in number order, directly above the footer band (or the bottom padding) with the content area\'s left edge and width, in the body family at the furniture size; the content area shrinks by exactly the area\'s height plus half the slide gap, and nothing above it moves. The area takes at most 35% of the span between the heading top and the footer band; a note that does not fit reports `text-overflow` at its source path (`references.N` or the run\'s path), which pagination treats like any other fit overflow. `slideCitations(slide, slideIndex, presentation)` is the numbering `composeSlide` uses; pass the same `presentation` and `slideIndex` to every consumer. An `image`, `chart`, `table` or `video` payload with a `caption` reserves a caption band inside its region (`item.caption`; `item.box` becomes the media box), below or above the media, at most 35% of the region; automatic grid selection scores the leaf on its media box. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Backgrounds and placed images\n\nCore 0.15 composes pictures in two ways (FA-22); [images](image-treatments.md) has the full vocabulary.\n\n- A picture background (the slide\'s `design.background`, then the deck\'s, then the theme\'s, when it is `{ type: "image", ... }` or an image-source string) is `geometry.backgroundImage`: the whole canvas as its frame, `fit` (`cover`, `contain`, `stretch`, `tile`), `focus`, `opacity`, `recolor` and the overlay box (whole picture or an edge band). It never moves content. `resolveSlideContext` passes the theme\'s background as `ComposeSlideOptions.themeBackground`.\n- Every image item carries `item.image`: the frame inside its region, `fit` (the block\'s own, else the effective `design.imageFit`, else `cover` in a layout and `contain` on a slide composed with no layout), `focus`, the mask with its DrawingML preset and SVG outline, line, opacity, recolor and overlay. `fitImage(frame, fit, aspect, focus)` is the shared fit math; core fills `item.image.picture` itself when it can read the picture\'s proportions (a data URI, or an `asset:` reference to one). A picture shown whole (`contain`) gets a frame of its own proportions at the region\'s anchor (top by default) when core can read them, so `item.image.box` is the picture and not the whole cell; a video\'s `item.box` is its 16:9 frame; in automatic composition a chart is at most 4:3 and a single row of a portrait picture beside other blocks, or of two prose blocks whose demand differs by 2.5 times, gets weighted widths.\n\nA top-level image block with `placement: { edge, size, inset }` takes a band along that slide edge: `size` of the slide width (left, right) or height (top, bottom), default 0.5, edge to edge, or inside the slide padding with `inset`. Headings, footnotes and the body compose in the free area that remains, with the usual padding; header and footer bands keep their full-width placement. Bands are taken in block order and each spans the free area the earlier ones left; a second block on a used edge flows as content (the validator reports it). Placed items are listed after the headings and before the flowed body, and their placeholder slot is not reserved in the flow. A slide whose only body is placed images is composed like a cover. A right-to-left deck mirrors `left` and `right`.\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including the current core. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title, the tag (eyebrow) and a subtitle that sits in the title area (any slide whose layout has no `subtitle` region of its own) use `titleAlignment`; every other item, including a separate `subtitle` region, body text, lists, tables and metrics, uses `contentAlignment`. The value is the effective `design` key: the slide\'s design, then the deck\'s design (or the host\'s resolved deck option), then the theme\'s design, then the layout record\'s `design`. With no explicit alignment from those layers, metrics default to `center` and other items to `left`; any resolved `contentAlignment` still overrides that fallback. A layout that sets `contentAlignment: center` therefore centers its content when no higher-priority layer sets the key. The title never inherits `contentAlignment`. On every slide, only a `contentAlignment` set on the slide\'s own design keeps the tag and the title-area subtitle apart (a deck or layout value does not), so a centred-content deck with left titles draws a left title, tag and subtitle over its centred content. Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size, `fontFamilies` and `textMeasurement`; `resolveSlideContext(deck, index, { fonts })` from the package root does the catalog resolution for one slide of a deck and returns those options, plus the slide to pass (`context.slide`, with the slide-scoped built-ins substituted). The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from the role size (28 reference pixels for prose, 32 for lists, 25 for tables) for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginate } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginate(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Right-to-left decks\n\nWhen the presentation `language` is written right to left (Arabic, Hebrew, Syriac, Thaana and the other right-to-left scripts) or the host passes `direction: \'rtl\'`, `composeSlide` mirrors the composition and reports each paragraph\'s direction; `SlideComposition.direction` is `\'rtl\'`. The first column and the `left` region are drawn at the right, banded slide images, cover logos and header/footer zones swap sides, lists put their markers at the right, tables run right to left, and `TextFit.directions` gives the direction of the paragraph each line belongs to. Alignment is logical: the authored `left` is the start edge, drawn at the right edge of a right-to-left paragraph (`physicalAlignment`). A left-to-right deck composes exactly as before. The rules and the PPTX mapping are in [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, pass the same `fonts` handle to every step (see [pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate)). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Preview polish shared by preview and export (RR-07)\n\nThree accepted spec fields used to be drawn plain; core now owns the shared tables so the SVG preview and the PPTX export draw them identically. Consumers import them from `@openpresentation/opf`; none changes geometry, and an older core simply leaves the preview and the export plain, without a new diagnostic.\n\n- **Code syntax colours.** `tokenizeCode(source, language)` returns sorted, non-overlapping token ranges (`keyword`, `string`, `number`, `comment`, `function`, `type`, `property`) over the exact `code.source`; `codeLineRuns(tokens, start, end, source?)` clips them to one accepted source line, so concatenating the runs returns the line unchanged (a tab is always its own plain run). The scanner is deterministic and dependency-free (no network, clock or locale). `code.language` resolves through aliases (`ts`, `tsx`, `py`, `sh`, `yml`, `c++`, `terraform`, and so on) to `javascript`, `typescript`, `python`, `rust`, `go`, `bash`, `json`, `yaml`, `toml`, `ini`, `html`/`xml`, `css`, `sql`, `java`, `csharp`, `kotlin`, `swift`, `ruby`, `php`, `c`/`cpp`, `hcl` and `dockerfile` (`resolveCodeLanguage`, `CODE_HIGHLIGHT_LANGUAGES`); any other language, a missing one, or source over 200,000 characters stays plain. `codeSyntaxPaletteForScheme(colorScheme)` gives one colour per kind from the deck theme (keyword = primary, string = accent, number = secondary; comment, function, type and property are fixed slate, blue, cyan and pink, rotated away from a theme colour they would collide with), each lightened until it is at least 4.5:1 on the #111827 code panel. The renderer nests coloured tspans inside each accepted segment tspan; PPTX writes the same colours as native runs in the same one-text-box-per-line shapes, so editing and import are unchanged. The language is not inferred from `code.filename`. `code.highlight` (1-based source lines and inclusive ranges) adds a band behind marked lines and dims the rest: `codeHighlightLines` resolves it against the source, `codeLineNumbers` numbers each displayed line (a wrapped line repeats its number), `codeHighlightBands` returns the runs of displayed lines to draw behind, and `codeHighlightColors(colorScheme)` gives the band and the marked/dimmed palettes, all at least 4.5:1 on what they sit on. The SVG band is one `rect` per run behind the line text; PPTX writes one native rectangle per run (`OPF code N highlight M`) after the panel and before the line text boxes, so the one-text-box-per-line shapes and the code provenance tags are unchanged.\n- **Metric trend.** A `metric.trend` (`up`, `down`, `flat`) keeps its word as the visible, editable text and gains one arrow: `metricTrendMark(layout, {background})` derives the arrow from the accepted trend line (square, 0.72 of the font size, on the baseline, 0.3 of the font size from the word; after the word for left alignment, before it for centre and right, and omitted if it would leave the field). The arrow is the DrawingML `upArrow`, `downArrow` or `rightArrow` preset at default adjustments (`metricTrendPoints` is the outline the preview draws), coloured by the metric\'s `sentiment`: positive green, negative red, neutral a neutral colour, kept at 4.5:1 or more against the slide background (`metricTrendColor`); the delta and trend text take the same colour. `layoutMetric` passes `sentiment` through as `MetricLayout.sentiment` (omitted when the metric has none) and `metricTrendMark` reads it, so the arrow direction always follows `trend` and only the colour follows `sentiment`. Absent, the sentiment is the conventional reading of the trend (up positive, down negative, flat neutral), which is the colour drawn before `sentiment` existed. A `sentiment` without a `trend` draws no arrow and colours nothing. The arrow carries the alternative text "Trend: up" (`aria-label` in SVG, `descr` in PPTX); the word and the arrow carry the direction, the colour the verdict.\n- **Pattern fills.** `PATTERN_PRESETS` lists the 54 ECMA-376 ST_PresetPatternVal names and `patternBitmap(preset)` / `patternRuns(preset)` give each as an 8 x 8 one-bit tile, one pixel per 1/96 inch, anchored at the slide\'s top-left, foreground where a bit is set (`diagStripe` still resolves to `wdUpDiag`). ECMA-376 names the presets but does not define their pixels: the tiles are measured from desktop PowerPoint (Office 365, Windows, 2026-10-01) by exporting each preset as a full-slide background to a 1280 x 720 PNG and voting every pixel into its (x mod 8, y mod 8) cell with opf-render `scripts/derive-pattern-bitmaps.mjs`. Every tile was uniform across all repeats (confidence 1.00) at one image pixel per pattern pixel, so a pattern pixel is one 1/96 inch unit, and the phase is the slide\'s top-left corner. The tool also reports any tile that later differs. PPTX already wrote every preset as native `a:pattFill`.\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from `@openpresentation/opf/composition` (the package root no longer exports engine names). It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?, sentiment?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fontFamilies`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts were included in the core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train; current pins are what `node scripts/registry-plan.mjs` prints. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` returns `bounds`, the panel the code needs at the allocated width (the insets around the metadata and the source lines, at most the cell); a composed slide draws the code panel at `bounds`, top-anchored, when the code is shorter than its cell. It uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/22 reference pixels (WI-4: the body was 18), raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the `@openpresentation/opf/composition` entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fontFamilies`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` asks for a 40-pixel body (it fits down through 28 pixels to the readability floor) wrapped in the quotation marks of the deck language (`options.language`, a BCP-47 tag: \u201C\u2026\u201D for English, \u201E\u2026\u201C for German, \xAB\u2026\xBB for French and Spanish, \u300C\u2026\u300D for Japanese, ASCII only for a language the table `QUOTE_MARKS` does not list; every mark is one UTF-16 code unit, so the body\'s source range starts at offset 1). It keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n### Quote role and photo (FA-12)\n\n`QuoteContent` also takes `role` and `photo`. The footer text is the attribution, then `role` after a line break, then `source` after an en dash (` \u2013 `); each field keeps its own source mapping (`<path>.role`). Without a `role` the footer text is unchanged. A `photo` (an `Asset` value) adds `QuoteLayout.photo`: `{path, value, box, shape}`, where `shape` is the circle mask of `imageShape(\'circle\', box)` (DrawingML `ellipse` and the same outline as an SVG path). The diameter is three times the footer font size and the gap beside it three quarters of that size, so the allocator picks the footer size (nominal or the readability floor) with the photo in the footer height and the footer text in the remaining width. The photo sits at the start edge of the inset area: the left, or the right when `options.direction` is `rtl`, where the footer text is its mirror. The text block is centered on the photo. A photo whose height cannot fit at the floor adds the `photo-fit` diagnostic reason (and `overflow`); a photo with no footer text still draws. Without a photo `layoutQuote` returns exactly what it returned before, with no `photo` key. The renderer and the PPTX exporter draw `photo` from the result and do not recompute it.\n\n## Timeline internals\n\n`layoutTimeline(value, box, options)` (`timeline-flow-v1`) places an optional name and description, then the events as a marker on a connector with each event\'s `when`, `what` and `description` as separate text parts. It tries an alternating arrangement (labels above and below a horizontal connector) and a vertical one (a marker rail down the start edge, labels beside it), at most 50 candidates in all, and keeps the first that fits or the one with the fewest diagnostics. Right-to-left decks put the vertical rail on the right edge.\n\n### Event status (FA-11)\n\n`TimelineEvent.status` is `done`, `current` or `planned`. It is a progress state that the engines draw from the deck\'s own colors without judging it; schedule health (at risk, blocked) is deliberately not a status. Layout copies the status to `TimelineLayout.markers[i].status` and to each of that event\'s text parts (`TimelineTextPart.status`), and `timelineMarkerShapes(marker, colors)` and `timelineTextColor(part, colors)` turn it into the shapes and text color that both engines draw:\n\n| Status | Marker | Text |\n| --- | --- | --- |\n| absent | A filled circle in the primary color. | Normal text color. |\n| `done` | The same filled circle, so it looks like an event without a status. | Normal text color. |\n| `current` ("we are here") | The filled circle inside a ring: a circle 1.6 times the marker\'s radius with a primary-color outline over the slide background. | The label (`what`) is bold, in the normal text color. |\n| `planned` | A hollow-looking circle of the marker\'s radius: a primary-color outline filled with the slide background. | All of the event\'s text is the muted secondary text color. |\n\n- **Geometry.** A marker\'s `radius` is the radius of its ellipse, for every status. A `current` marker also has `ring.radius`, exactly 1.6 times `radius`, and a `current` or `planned` marker has the `strokeWidth` of its outline (2 reference pixels at 720, at most half the radius). The drawn and exported ellipses have exactly these radii, so every ellipse of a deck has the same size for the same radius (a done, planned and unset marker are the same size); an outline is centered on its ellipse\'s edge and reaches half a stroke beyond it, and the cell check and the vertical rail count that half stroke. A ring that leaves the cell is reported as a `event-space` diagnostic like any marker. In the vertical arrangement a deck with a `current` event moves the rail and the text out by the ring\'s extra width so the text keeps its usual distance from the marker. Events without a status produce exactly the geometry and no extra keys they did before.\n- **Hollow means background-filled.** The outline of a hollow marker and the ring are drawn over the slide background color (in the SVG as the fill, in PPTX as a solid fill of the same color) so the connector does not cross them.\n- **Colors.** `timelineTextColor` returns `mutedText` for a planned event, lightened or darkened until it reaches 4.5:1 against the slide background (`TIMELINE_TEXT_MIN_CONTRAST`), and the normal text color otherwise. Outlines and rings are kept at 3:1 (`TIMELINE_OUTLINE_MIN_CONTRAST`); the filled marker keeps the primary color exactly.\n- **Bold label.** The weight is part of the layout, not a drawing detail: the `what` part of a `current` event is requested at weight 700, so line breaks and heights were measured bold.\n- **PPTX.** The marker is a native `ellipse` (solid primary fill for `done`, `current` and no status; background fill plus a primary line for `planned`), the ring is a second native `ellipse` (background fill plus a primary line), the bold label is a bold run, and planned text takes the muted color. The exporter records each event\'s status in the marker\'s `OPF_TIMELINE_V1` tag and the ring in a tag of its own, and the importer restores `status` from them; a deck whose marker or ring was edited away imports as ordinary text without status, like the rest of the timeline provenance. Status is also written in Markdown as a task-list prefix on the event line: `[x]` done, `[>]` current, `[ ]` planned.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. A v2 template\u2019s region grid is changed by editing the template\u2019s `columns` and `rows`. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.presentation, { fonts }).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.presentation, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.presentation with the same renderer and fonts handle before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.presentation);\nconst prepared = prepareBlockMove(editor.presentation,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{presentation, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(presentation, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
|
|
80
82
|
},
|
|
81
83
|
{
|
|
82
84
|
"slug": "ecosystem-development",
|
|
83
85
|
"file": "docs/ecosystem-development.md",
|
|
84
86
|
"title": "Local development across the packages",
|
|
85
|
-
"markdown": "# Local development across the packages\n\nUse Node 24 for the current source and published packages. All six packages live in this repository, one pnpm workspace, so there is nothing to link: `workspace:` dependencies resolve to the sibling directories, and a change that touches several packages is built and tested together.\n\n```sh\npnpm install\npnpm build # every package, in dependency order\npnpm check:changed # only what your branch changed (the check to run after each change)\npnpm test # everything; CI and the last run before a pull request\npnpm test:integration # the private tests that need several packages at once\n```\n\nFor a cheap checkout of one task (small blobless worktree, one shared pnpm store), use `node scripts/agent-worktree.mjs add <task>`.\n\nCI is `verify` (`.github/workflows/ci.yml`): one required check that runs the packages your change affects, with their dependents at the contract tier (`test:contract` of each package); `nightly.yml` runs the Windows, macOS, Node 22 and 26, packed-install and npm 12 consumer legs (the packed packages installed by npm 12 with its defaults: no install scripts, no git or remote dependencies; `pnpm test:npm12-consumer`) and the weekly `slow-checks.yml` the published-package consumers. See [the CI design](programs/monorepo/ci-design.md). Add the label `full-ci` to a pull request that touches OS- or Node-version-specific code. There is no `Depends-On`, no lock to roll and no merge queue.\n\nOn Windows, keep the checkout under a short path (esbuild and Biome exceed MAX_PATH deeper) and see the notes in `AGENTS.md`. npm and pnpm orchestration in the scripts invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of a batch shim through a shell, so paths with spaces and shell metacharacters stay literal arguments.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer package supplies the locked Playwright test dependency. Install Chromium with `pnpm --filter @openpresentation/opf-render exec playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest (one string in every workflow, `pnpm check:playwright-pin`), without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed. For full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`): it validates all generated documents and can render them with the local SVG engine.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. It also runs the offline font-switch matrix (`scripts/test-font-switch-ecosystem.mjs`, FF-09): a seeded pairwise covering array of 58 decks over the 14 gallery dimensions, plus fixed content-type, block-replacement, per-slide override, CJK-in-Latin, theme and language switches, each switched A to B and back to A. A value class is a group of catalog values that take the same path through the engines, derived from the catalogs in the script: font schemes by language family, then by licensing and preview policy (Office metric, Office visual-only, monospace, open Google); one language per script family in the array and every other catalog script in a language chain; every layout family; the eight content blocks; every distinct chart export path of the non-deprecated chart types; header/footer, background (theme, solid, gradient, pattern, image) and image treatments by kind; and the first and last record of the metadata dimensions. Dimensions that a deck can carry several times (font scheme states, layouts, blocks, charts, backgrounds, images) take several values per deck. Every state is exported and checked with the FF-08 typeface inventory, the catalog's literal theme fonts, a package structure check, a preview re-render and a re-import. It pins the office font pack with visual substitution and asserts every substitution; known engine limitations, including chart types the preview approximates or the exporter writes as bar charts, are named expected failures in the script that fail with a \"limitation resolved\" message when they go away. It runs no browser and no Office. Its report is written to `artifacts/font-switch-matrix/report.json`. The pairwise decks run on four worker threads with a fixed partition (deck i on worker i mod 4), so every host verifies each deck after the same earlier decks; `OPF_MATRIX_WORKERS=1` runs them in one thread, `OPF_MATRIX_WORKERS=n` on n workers. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n\n## Regenerating the example goldens (MR-31)\n\nThe raster golden of the examples corpus lives in one place, `packages/render/test/golden/` (a directory per baseline, one file per deck; `packages/render/test/golden/README.md` has the layout and `notes/` the review notes). The corpus is this repository's `examples/` and the renderer, core and the gallery are the checkout's own, so there is no lock selection, no core fixture and no sibling renderer to pin. A pull request that changes `examples/`, the core code that composes them, the gallery's records or the renderer's drawing moves slide hashes (or only the corpus digest, as FA-33 did: `notes/MR-31.md`); `pnpm test` and the `goldens` job of `ci.yml` fail until the baseline follows.\n\n```sh\ngh workflow run golden.yml --ref <branch> # or add the `regenerate-goldens` label to the pull request\npnpm golden:update --local # a review candidate under packages/render/artifacts/golden/, never the baseline\n```\n\n- **The workflow.** `.github/workflows/golden.yml` has two jobs. `regenerate` (read-only token) runs `pnpm golden:update` on the branch in the pinned Playwright image: it builds core, the gallery and the renderer, renders every slide, rewrites only the per-deck files whose hashes differ and uploads the patch, `diff.json` and the review sheets of the changed slides. `push` (write token, no branch code) applies the patch only when it touches `packages/render/test/golden/`, pushes one commit and dispatches `ci.yml` on it (a push made with `GITHUB_TOKEN` starts no workflow). It refuses the default branch and a fork.\n- **`pnpm golden:update`.** `scripts/golden-update.mjs`: without `--local` it refuses to rewrite the baseline anywhere but GitHub Actions on Linux in the Playwright image (the check the renderer's `golden-pinned-image.mjs` made); with `--local` it writes the review candidate only. The comparison is never changed (exact hashes, the baseline's own scale and font policy).\n- **Review.** The commit is evidence, not an approval. Open the artifact, look at the changed slides, write the reason in `notes/<ID>.md` and in the pull request, then approve. A change that moves one slide for a reason other than the PR's intent is a bug in the PR.\n- **Merge rhythm.** A merge conflict in deck files is resolved by merging `main`, taking either side, and regenerating. One pull request changes a layout and the golden of every package together (risk 23 of the monorepo plan).\n\n## Trying the packages in a site checkout\n\nThe three sites (openpresentation.org, pptx.gallery, pptx.dev) live in their own repositories and install the published packages. To browse pptx.gallery with this checkout, keep `pptx-gallery` in the same parent directory as this repository, install its dependencies, build here, run `node scripts/link-ecosystem.mjs` (it replaces the installed `@openpresentation/*` packages in the site's `node_modules` with links to this checkout without saving paths in manifests or lockfiles; reinstalling replaces the links, so rerun it) and start the site with `OPF_LOCAL_WORKSPACE=1 pnpm dev`. Layout detail pages have an interactive composition example; the flag expands Turbopack's local root to include this checkout. Build before starting the site, and stop and restart it around clean rebuilds: removing a linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\nAfter a release, the sites are bumped by `bump-sites.yml` (`release-process.md`); nothing here deploys or changes a site.\n\n## Editor host bundle and font provenance\n\nContributor details moved from the [browser integration guide](live-editor.md); published-package users can start there.\n\nFor library development, build the tarballs of the six packages from this checkout and install them into a clean consumer:\n\n```sh\npnpm build\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. A public release is the one-click workflow in [the release process](release-process.md); it packs and smoke-tests the same way before it publishes.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm vendor:gallery-editor --version X.Y.Z [--ref vX.Y.Z]` builds the host controls of the pptx.gallery site's vendored editor (`public/opf-editor`) while resolving libraries only from a fresh npm consumer; the manifest records both the example source hashes and the registry package integrities. It reads the editor examples from `packages/editor/examples` at `--ref` (default `HEAD`; the release tag `vX.Y.Z` for a release) and installs the published set `--version` names (`registryPlan({ tag })` in `scripts/registry-plan.mjs`: all six packages share one version; `--pin <package>=<X.Y.Z>` builds against a published set that is not shared) into a clean consumer under `artifacts/npm/gallery-editor-consumer`, with no sibling checkout but the site (`--site <dir>`, default `../pptx-gallery`) and no `release-plan.json`. After a stable release `bump-sites.yml` runs it in the pptx.gallery leg and commits the bundle into the bump pull request (`docs/programs/monorepo/site-workflows/README.md`).\n\nScript fonts: when the pinned editor example loads faces from `./script-fonts/` and the pinned renderer has the script pack (0.10.0 and later), the registry build also writes `script-fonts.json` and lists its hash in `manifest.json`. The manifest is the reviewable half: every `@expo-google-fonts/noto-*` package, exact version, SPDX license, license-file hash and each face's SHA-256, taken from the published renderer. The faces are binaries (63 files, 66.9 MiB), so they are never committed to the gallery repository. The gallery build copies them from its own pinned npm dependencies into the untracked `public/opf-editor/script-fonts/` directory, verifying every hash, and writes the license notices beside them; nothing is fetched from a font CDN. See `scripts/gallery-script-fonts.mjs` and the gallery's `scripts/prepare-editor-script-fonts.mjs`.\n\nBase fonts (FF-41): when the pinned editor example loads `base-fonts.json` (opf-editor 0.10.5: the example passes the faces as the renderer's `extraLazyFonts`, renderer 0.11.7 and later; 0.10.4's example used its own `examples/base-font-gate.js`), the registry build starts the editor with Roboto Regular alone in `fonts.json` (217 KB instead of 12.8 MB) and writes every other eager face (Roboto in six more styles, Roboto Mono, the Office substitutes) as a separate file named after its hash beside it, listed with its SHA-256 in `base-fonts.json` and in `manifest.json`; the editor fetches only the faces a document draws, verified, through its font gate (`scripts/gallery-base-fonts.mjs`). An older pinned example keeps every eager face in `fonts.json`. A default Roboto deck loads about 0.7 MB of fonts instead of 12.8 MB.\n\nLazy fonts: when the pinned editor example calls `ensureLazyFonts` and the pinned renderer vendors faces (Intos for the default Aptos scheme and the open families, renderer 0.11.0 and later), the registry build also writes `lazy-fonts.json` and lists its hash in `manifest.json`. It pins every vendored package (exact version, SPDX license, license-file and notice hashes) and each face SHA-256, taken from the published renderer. The faces are binaries, so they are not committed either: the gallery build copies them from its pinned `@openpresentation/opf-render` package (`fonts/<name>/`) into the untracked `public/opf-editor/fonts/` directory, verifying every hash, and the editor fetches only the families a document uses, same-origin. See `scripts/gallery-lazy-fonts.mjs` and the gallery's `scripts/prepare-editor-lazy-fonts.mjs`.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n"
|
|
87
|
+
"markdown": "# Local development across the packages\n\nUse Node 24 for the current source and published packages. All six packages live in this repository, one pnpm workspace, so there is nothing to link: `workspace:` dependencies resolve to the sibling directories, and a change that touches several packages is built and tested together.\n\n```sh\npnpm install\npnpm build # every package, in dependency order\npnpm check:changed # only what your branch changed (the check to run after each change)\npnpm test # everything; CI and the last run before a pull request\npnpm test:integration # the private tests that need several packages at once\n```\n\nFor a cheap checkout of one task (small blobless worktree, one shared pnpm store), use `node scripts/agent-worktree.mjs add <task>`.\n\nCI is `verify` (`.github/workflows/ci.yml`): one required check that runs the packages your change affects, with their dependents at the contract tier (`test:contract` of each package); `nightly.yml` runs the Windows, macOS, Node 22 and 26, packed-install and npm 12 consumer legs (the packed packages installed by npm 12 with its defaults: no install scripts, no git or remote dependencies; `pnpm test:npm12-consumer`) and the weekly `slow-checks.yml` the published-package consumers. See [the CI design](programs/monorepo/ci-design.md). Add the label `full-ci` to a pull request that touches OS- or Node-version-specific code (it starts `full-ci.yml`, which runs the nightly legs on the pull request; it is not part of `verify`). There is no `Depends-On`, no lock to roll and no merge queue.\n\nOn Windows, keep the checkout under a short path (esbuild and Biome exceed MAX_PATH deeper) and see the notes in `AGENTS.md`. npm and pnpm orchestration in the scripts invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of a batch shim through a shell, so paths with spaces and shell metacharacters stay literal arguments.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer package supplies the locked Playwright test dependency. Install Chromium with `pnpm --filter @openpresentation/opf-render exec playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest (one string in every workflow, `pnpm check:playwright-pin`), without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed. For full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`): it validates all generated documents and can render them with the local SVG engine.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. It also runs the offline font-switch matrix (`scripts/test-font-switch-ecosystem.mjs`, FF-09): a seeded pairwise covering array of 58 decks over the 14 gallery dimensions, plus fixed content-type, block-replacement, per-slide override, CJK-in-Latin, theme and language switches, each switched A to B and back to A. A value class is a group of catalog values that take the same path through the engines, derived from the catalogs in the script: font schemes by language family, then by licensing and preview policy (Office metric, Office visual-only, monospace, open Google); one language per script family in the array and every other catalog script in a language chain; every layout family; the eight content blocks; every distinct chart export path of the non-deprecated chart types; header/footer, background (theme, solid, gradient, pattern, image) and image treatments by kind; and the first and last record of the metadata dimensions. Dimensions that a deck can carry several times (font scheme states, layouts, blocks, charts, backgrounds, images) take several values per deck. Every state is exported and checked with the FF-08 typeface inventory, the catalog's literal theme fonts, a package structure check, a preview re-render and a re-import. It pins the office font pack with visual substitution and asserts every substitution; known engine limitations, including chart types the preview approximates or the exporter writes as bar charts, are named expected failures in the script that fail with a \"limitation resolved\" message when they go away. It runs no browser and no Office. Its report is written to `artifacts/font-switch-matrix/report.json`. The pairwise decks run on four worker threads with a fixed partition (deck i on worker i mod 4), so every host verifies each deck after the same earlier decks; `OPF_MATRIX_WORKERS=1` runs them in one thread, `OPF_MATRIX_WORKERS=n` on n workers. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n\n## Regenerating the example goldens (MR-31)\n\nThe raster golden of the examples corpus lives in one place, `packages/render/test/golden/` (a directory per baseline, one file per deck; `packages/render/test/golden/README.md` has the layout and `notes/` the review notes). The corpus is this repository's `examples/` and the renderer, core and the gallery are the checkout's own, so there is no lock selection, no core fixture and no sibling renderer to pin. A pull request that changes `examples/`, the core code that composes them, the gallery's records or the renderer's drawing moves slide hashes (or only the corpus digest, as FA-33 did: `notes/MR-31.md`); `pnpm test` and the `goldens` job of `ci.yml` fail until the baseline follows.\n\n```sh\ngh workflow run golden.yml --ref <branch> # or add the `regenerate-goldens` label to the pull request\npnpm golden:update --local # a review candidate under packages/render/artifacts/golden/, never the baseline\n```\n\n- **The workflow.** `.github/workflows/golden.yml` has two jobs. `regenerate` (read-only token) runs `pnpm golden:update` on the branch in the pinned Playwright image: it builds core, the gallery and the renderer, renders every slide, rewrites only the per-deck files whose hashes differ and uploads the patch, `diff.json` and the review sheets of the changed slides. `push` (write token, no branch code) applies the patch only when it touches `packages/render/test/golden/`, pushes one commit and dispatches `ci.yml` on it (a push made with `GITHUB_TOKEN` starts no workflow). It refuses the default branch and a fork.\n- **`pnpm golden:update`.** `scripts/golden-update.mjs`: without `--local` it refuses to rewrite the baseline anywhere but GitHub Actions on Linux in the Playwright image (the check the renderer's `golden-pinned-image.mjs` made); with `--local` it writes the review candidate only. The comparison is never changed (exact hashes, the baseline's own scale and font policy).\n- **Review.** The commit is evidence, not an approval. Open the artifact, look at the changed slides, write the reason in `notes/<ID>.md` and in the pull request, then approve. A change that moves one slide for a reason other than the PR's intent is a bug in the PR.\n- **Merge rhythm.** A merge conflict in deck files is resolved by merging `main`, taking either side, and regenerating. One pull request changes a layout and the golden of every package together (risk 23 of the monorepo plan).\n\n## Trying the packages in a site checkout\n\nThe three sites (openpresentation.org, pptx.gallery, pptx.dev) live in their own repositories and install the published packages. To browse pptx.gallery with this checkout, keep `pptx-gallery` in the same parent directory as this repository, install its dependencies, build here, run `node scripts/link-ecosystem.mjs` (it replaces the installed `@openpresentation/*` packages in the site's `node_modules` with links to this checkout without saving paths in manifests or lockfiles; reinstalling replaces the links, so rerun it) and start the site with `OPF_LOCAL_WORKSPACE=1 pnpm dev`. Layout detail pages have an interactive composition example; the flag expands Turbopack's local root to include this checkout. Build before starting the site, and stop and restart it around clean rebuilds: removing a linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\nAfter a release, the sites are bumped by `bump-sites.yml` (`release-process.md`); nothing here deploys or changes a site.\n\n## Editor host bundle and font provenance\n\nContributor details moved from the [browser integration guide](live-editor.md); published-package users can start there.\n\nFor library development, build the tarballs of the six packages from this checkout and install them into a clean consumer:\n\n```sh\npnpm build\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. A public release is the one-click workflow in [the release process](release-process.md); it packs and smoke-tests the same way before it publishes.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm vendor:gallery-editor --version X.Y.Z [--ref vX.Y.Z]` builds the host controls of the pptx.gallery site's vendored editor (`public/opf-editor`) while resolving libraries only from a fresh npm consumer; the manifest records both the example source hashes and the registry package integrities. It reads the editor examples from `packages/editor/examples` at `--ref` (default `HEAD`; the release tag `vX.Y.Z` for a release) and installs the published set `--version` names (`registryPlan({ tag })` in `scripts/registry-plan.mjs`: all six packages share one version; `--pin <package>=<X.Y.Z>` builds against a published set that is not shared) into a clean consumer under `artifacts/npm/gallery-editor-consumer`, with no sibling checkout but the site (`--site <dir>`, default `../pptx-gallery`) and no `release-plan.json`. After a stable release `bump-sites.yml` runs it in the pptx.gallery leg and commits the bundle into the bump pull request (`docs/programs/monorepo/site-workflows/README.md`).\n\nScript fonts: when the pinned editor example loads faces from `./script-fonts/` and the pinned renderer has the script pack (0.10.0 and later), the registry build also writes `script-fonts.json` and lists its hash in `manifest.json`. The manifest is the reviewable half: every `@expo-google-fonts/noto-*` package, exact version, SPDX license, license-file hash and each face's SHA-256, taken from the published renderer. The faces are binaries (63 files, 66.9 MiB), so they are never committed to the gallery repository. The gallery build copies them from its own pinned npm dependencies into the untracked `public/opf-editor/script-fonts/` directory, verifying every hash, and writes the license notices beside them; nothing is fetched from a font CDN. See `scripts/gallery-script-fonts.mjs` and the gallery's `scripts/prepare-editor-script-fonts.mjs`.\n\nBase fonts (FF-41): when the pinned editor example loads `base-fonts.json` (opf-editor 0.10.5: the example passes the faces as the renderer's `extraLazyFonts`, renderer 0.11.7 and later; 0.10.4's example used its own `examples/base-font-gate.js`), the registry build starts the editor with Roboto Regular alone in `fonts.json` (217 KB instead of 12.8 MB) and writes every other eager face (Roboto in six more styles, Roboto Mono, the Office substitutes) as a separate file named after its hash beside it, listed with its SHA-256 in `base-fonts.json` and in `manifest.json`; the editor fetches only the faces a document draws, verified, through its font gate (`scripts/gallery-base-fonts.mjs`). An older pinned example keeps every eager face in `fonts.json`. A default Roboto deck loads about 0.7 MB of fonts instead of 12.8 MB.\n\nLazy fonts: when the pinned editor example calls `ensureLazyFonts` and the pinned renderer vendors faces (Intos for the default Aptos scheme and the open families, renderer 0.11.0 and later), the registry build also writes `lazy-fonts.json` and lists its hash in `manifest.json`. It pins every vendored package (exact version, SPDX license, license-file and notice hashes) and each face SHA-256, taken from the published renderer. The faces are binaries, so they are not committed either: the gallery build copies them from its pinned `@openpresentation/opf-render` package (`fonts/<name>/`) into the untracked `public/opf-editor/fonts/` directory, verifying every hash, and the editor fetches only the families a document uses, same-origin. See `scripts/gallery-lazy-fonts.mjs` and the gallery's `scripts/prepare-editor-lazy-fonts.mjs`.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n"
|
|
86
88
|
},
|
|
87
89
|
{
|
|
88
90
|
"slug": "evidence-2026-09-08-windows",
|
|
@@ -124,7 +126,7 @@ var docsData = Object.freeze([
|
|
|
124
126
|
"slug": "format-card",
|
|
125
127
|
"file": "docs/format-card.md",
|
|
126
128
|
"title": "OPF format card",
|
|
127
|
-
"markdown": '# OPF format card\n\nA self-contained authoring reference for agents and humans writing `*.opf.json` documents, sized for pasting into a model\'s context. The canonical contract is the JSON Schema (`https://openpresentation.org/schema/opf/v1`, in [`spec/schemas/opf.schema.json`](../spec/schemas/opf.schema.json)); this card compresses it. Validate with `validate` from `@openpresentation/opf` or `opf validate <file>` ([validate](validate.md)).\n\n## Document shape\n\nA presentation is one JSON object; only `slides` is required.\n\n```json\n{ "name": "Minimal Deck", "slides": [{ "title": "Minimal Deck" }] }\n```\n\nOptional top-level fields, grouped:\n\n- Identity: `name`, `description`, `filename`, `author`, `organization`, `speaker`, `tags`.\n- Intent (used by AI generation): `audience`, `purpose`, `tone`, `language`, `narrative`, `takeaway`, `duration`.\n- Appearance: `design` (`theme`, `colorScheme`, `fontScheme`, `dimensions`, `background`, `variant` (from core 0.23.0: `"dark1"` or `{ background, accents }`, a map over the one color scheme), `logo` override, `watermark`, `header`, `footer`, alignment hints), `variables`; logos themselves live on `organization`.\n- Resources: `assets` (registry referenced as `asset:<id>`), `catalogs` (the records the document embeds, grouped by catalog: `default`, `custom` and named groups with a `source`).\n- Machine state: `extensions` (preserved, never rendered).\n\n## Slides and content\n\nA slide carries `title` / `subtitle` / `tag` / `notes` / `section` / `beat` / `layout` / `id` / `extensions` plus content in one of three shapes \u2014 pick the loosest that says what you mean:\n\n1. **Root payload** \u2014 content-kind fields directly on the slide. The kind is inferred from the field: `text`, `items` (list), `bullets`, `image`, `video`, `chart`, `table`, `code`, `metric`, `quote`, `timeline`. Several kinds at the root are shorthand for `blocks`.\n2. **`blocks`** \u2014 an ordered array of payloads when placement should stay engine-inferred. A block is a leaf payload or a **group**: `{ "type": "group", "blocks": [...], "composition": { ... } }`. Groups nest (hard cap 32; stay \u2264 3 in practice) and cannot mix `blocks` with leaf fields.\n3. **Promoted regions** \u2014 a 3\xD73 grid when position matters. Rows `top|middle|bottom`, columns `left|center|right`, spans with `+`, intersections with `:` \u2014 `"left"`, `"center+right"`, `"top:left"`, `"middle+bottom:center+right"`. Keys must not overlap; regions cannot mix with a root payload.\n\n`composition` (on a slide or group) arranges children: `{ "mode": "auto|grid|row|column", "columns": 1-12, "weights": [..], "gap": 0-0.1, "padding": 0-0.2, "minFontSize": 8-32, "overflow": "warn|error" }`. Weights are relative track sizes; gap/padding are fractions of the canvas short edge. Engines own placement \u2014 there is no x/y.\n\nPayload notes: chart is `{ "type": "<chart-type id>", "data": { "columns": [...], "rows": [...] } }` or `{ "data": { "dataset": "<id>" } }`; table rows may hold scalars, `TextRun[]`, or styled cells `{ "value", "style": { "fill", "color", "align", "borders", ... }, "colSpan", "rowSpan" }`; `metric`/`quote`/`code` accept string shorthand; list nesting uses `level` on items, not nested payloads.\n\n## Rich text and color references\n\n`text`, list items, bullets, and table cells accept `TextRun[]`: strings or `{ "text", "bold", "italic", "underline", "strikethrough", "color", "fontSize", "fontFamily", "link", "superscript", "subscript" }`.\n\nEvery content color field (`TextRun.color`, table cell `style.fill` / `style.color`, cell border `color`) accepts three forms:\n\n- Literal hex: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme name, resolved through the effective scheme: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`, or roles `primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`.\n- A variable reference `var:<id>` into the top-level map: `"variables": { "risk": "#B42318" }` (or `{ "type": "color", "value": "#B42318", "description": "..." }`).\n\nPrefer names and variables over hex \u2014 they survive re-theming. Unknown `var:` ids warn, never error. The styled cell and border color fields enforce the three forms at the schema level; run colors tolerate any string (unrecognized values warn and renderers fall back to the theme), so imported decks keep validating.\n\n## Ids and extensions\n\n`id` is optional on slides and on any content payload (including groups), unique document-wide. Use ids when something outside the document must address content across edits \u2014 patch edits, comments, review state. `extensions` objects at document, slide, and payload scope carry machine state; engines ignore and preserve them.\n\n## Catalog references\n\nReusable records live in catalogs; a reference is a bare id or `name:id`: `narrative`, `tone`, `purpose`, `audience`, `design.theme`, `design.colorScheme`, `design.fontScheme`, `Slide.layout`. `catalogs` holds the records a document embeds, grouped by catalog: `default` (with its `source`), `custom` (the document\'s own) and named groups (`"acme": { "source": "pkg:@acme/opf-catalog", ... }`, referenced as `acme:hero`). Resolution: `custom` \u2192 embedded `default` \u2192 the catalog the host registered for its source; nothing is fetched. Object form overrides a record per key: `{ "id": "cool-horizon", "accent1": "#0F4C81" }`. A reference that resolves nowhere warns (`opf/unresolved-reference`), never errors. `opf embed <in> <out>` embeds every record a document uses so it renders with no catalog registered. `language` (a BCP-47 tag), `Chart.type` and the socials keys are engine vocabularies, not references.\n\n## Design in three lines\n\n`design.theme` bundles scheme + fonts + background + dimensions; `design.colorScheme` / `fontScheme` override it; `slides[].design` overrides per slide; resolution is per field, most specific wins. Backgrounds accept slot names (`"light1"`) or hex. `false` suppresses inherited `watermark` / `header` / `footer`. A header or footer zone (`left` / `center` / `right`) takes `image`, `text`, `socials`, `date`, laid out side by side in that order; generated values are variables in `text`: `"{{slide.number}} / {{deck.slideCount}}"`, `"{{slide.section}}"`, `"{{organization.name}}"` (the slide-scoped `{{slide.number}}`, `{{slide.section}}`, `{{deck.slideCount}}` also work in body text). Logos live on `organization.logo` (one path, or `full` / `stacked` / `icon` / `wordmark`, each a path or `{ onLight, onDark }`); place one with `"image": "var:organization.logo.icon"`; covers draw the full logo, and `design.logo` is only an override reference or `false`.\n\n## Prefer / never\n\nPrefer: string shorthands; bare ids for default-catalog records; inference over explicit `type`; regions only when position matters; groups only when `blocks` ordering is not enough; names/`var:` over hex in color fields; `items` for lists and `bullets` only for plain talking points (never both in one block or region; see [content payloads](content-payloads.md#items-versus-bullets)).\n\nNever: loose chart or table fields directly on a slide; region keys mixed with a root payload; overlapping region keys; `blocks` mixed with leaf fields on one payload; duplicate ids; x/y or pixel geometry (it does not exist in OPF).\n\n## Two canonical slides\n\n```json\n{\n "id": "headline",\n "title": "Adoption Doubled",\n "left": { "id": "kpi", "metric": { "value": "2.1x", "label": "Adoption", "trend": "up" } },\n "center+right": { "chart": { "type": "line", "data": { "columns": ["Month", "Teams"], "rows": [["Jan", 12], ["Feb", 18]] } } }\n}\n```\n\n```json\n{\n "id": "risks",\n "title": "What Could Go Wrong",\n "composition": { "mode": "column" },\n "blocks": [\n { "items": [["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }]] },\n { "quote": { "text": "Exceptions became visible before they became escalations.", "attribution": "VP Operations" } }\n ],\n "extensions": { "authoring": { "locked": true } }\n}\n```\n'
|
|
129
|
+
"markdown": '# OPF format card\n\nA self-contained authoring reference for agents and humans writing `*.opf.json` documents, sized for pasting into a model\'s context. The canonical contract is the JSON Schema (`https://openpresentation.org/schema/opf/v1`, in [`spec/schemas/opf.schema.json`](../spec/schemas/opf.schema.json)); this card compresses it. Validate with `validate` from `@openpresentation/opf` or `opf validate <file>` ([validate](validate.md)); treat the deck as done only when `opf validate <file> --fail-on warning` passes, since an unknown layout id is only a warning. `v1` in the schema id is the pre-1.0 line, which changes across 0.x releases ([schema versioning](schema-versioning.md)).\n\n## Document shape\n\nA presentation is one JSON object; only `slides` is required.\n\n```json\n{ "name": "Minimal Deck", "slides": [{ "title": "Minimal Deck" }] }\n```\n\nOptional top-level fields, grouped:\n\n- Identity: `name`, `description`, `filename`, `author`, `organization`, `speaker`, `tags`.\n- Intent (used by AI generation): `audience`, `purpose`, `tone`, `language`, `narrative`, `takeaway`, `duration`.\n- Appearance: `design` (`theme`, `colorScheme`, `fontScheme`, `dimensions`, `background`, `variant` (from core 0.23.0: `"dark1"` or `{ background, accents }`, a map over the one color scheme), `logo` override, `watermark`, `header`, `footer`, alignment hints), `variables`; logos themselves live on `organization`.\n- Resources: `assets` (registry referenced as `asset:<id>`), `catalogs` (the records the document embeds, grouped by catalog: `default`, `custom` and named groups with a `source`).\n- Machine state: `extensions` (preserved, never rendered).\n\n## Slides and content\n\nA slide carries `title` / `subtitle` / `tag` / `notes` / `section` / `beat` / `layout` / `id` / `extensions` plus content in one of three shapes \u2014 pick the loosest that says what you mean:\n\n1. **Root payload** \u2014 content-kind fields directly on the slide. The kind is inferred from the field: `text`, `items` (list), `bullets`, `image`, `video`, `chart`, `table`, `code`, `metric`, `quote`, `timeline`. Several kinds at the root are shorthand for `blocks`.\n2. **`blocks`** \u2014 an ordered array of payloads when placement should stay engine-inferred. A block is a leaf payload or a **group**: `{ "type": "group", "blocks": [...], "composition": { ... } }`. Groups nest (hard cap 32; stay \u2264 3 in practice) and cannot mix `blocks` with leaf fields.\n3. **Promoted regions** \u2014 a 3\xD73 grid when position matters. Rows `top|middle|bottom`, columns `left|center|right`, spans with `+`, intersections with `:` \u2014 `"left"`, `"center+right"`, `"top:left"`, `"middle+bottom:center+right"`. Keys must not overlap; regions cannot mix with a root payload.\n\n`composition` (on a slide or group) arranges children: `{ "mode": "auto|grid|row|column", "columns": 1-12, "weights": [..], "gap": 0-0.1, "padding": 0-0.2, "minFontSize": 8-32, "overflow": "warn|error" }`. Weights are relative track sizes; gap/padding are fractions of the canvas short edge. Engines own placement \u2014 there is no x/y.\n\nPayload notes: chart (never the Chart.js `categories`/`series`/`labels`/`datasets` shape; the first column holds the labels, each further column is one series) is `{ "type": "<chart-type id>", "data": { "columns": [...], "rows": [...] } }` or `{ "data": { "dataset": "<id>" } }`; table rows may hold scalars, `TextRun[]`, or styled cells `{ "value", "style": { "fill", "color", "align", "borders", ... }, "colSpan", "rowSpan" }`; `metric`/`quote`/`code` accept string shorthand; list nesting uses `level` on items, not nested payloads.\n\n## Rich text and color references\n\n`text`, list items, bullets, and table cells accept `TextRun[]`: strings or `{ "text", "bold", "italic", "underline", "strikethrough", "color", "fontSize", "fontFamily", "link", "superscript", "subscript" }`.\n\nEvery content color field (`TextRun.color`, table cell `style.fill` / `style.color`, cell border `color`) accepts three forms:\n\n- Literal hex: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme name, resolved through the effective scheme: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`, or roles `primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`.\n- A variable reference `var:<id>` into the top-level map: `"variables": { "risk": "#B42318" }` (or `{ "type": "color", "value": "#B42318", "description": "..." }`).\n\nPrefer names and variables over hex \u2014 they survive re-theming. Unknown `var:` ids warn, never error. The styled cell and border color fields enforce the three forms at the schema level; run colors tolerate any string (unrecognized values warn and renderers fall back to the theme), so imported decks keep validating.\n\n## Ids and extensions\n\n`id` is optional on slides and on any content payload (including groups), unique document-wide. Use ids when something outside the document must address content across edits \u2014 patch edits, comments, review state. `extensions` objects at document, slide, and payload scope carry machine state; engines ignore and preserve them.\n\n## Catalog references\n\nReusable records live in catalogs; a reference is a bare id or `name:id`: `narrative`, `tone`, `purpose`, `audience`, `design.theme`, `design.colorScheme`, `design.fontScheme`, `Slide.layout`. `catalogs` holds the records a document embeds, grouped by catalog: `default` (with its `source`), `custom` (the document\'s own) and named groups (`"acme": { "source": "pkg:@acme/opf-catalog", ... }`, referenced as `acme:hero`). Resolution: `custom` \u2192 embedded `default` \u2192 the catalog the host registered for its source; nothing is fetched. Object form overrides a record per key: `{ "id": "cool-horizon", "accent1": "#0F4C81" }`. A reference that resolves nowhere warns (`opf/unresolved-reference`), never errors. `opf embed <in> <out>` embeds every record a document uses so it renders with no catalog registered. `language` (a BCP-47 tag), `Chart.type` and the socials keys are engine vocabularies, not references.\n\n## Design in three lines\n\n`design.theme` bundles scheme + fonts + background + dimensions; `design.colorScheme` / `fontScheme` override it; `slides[].design` overrides per slide; resolution is per field, most specific wins. Backgrounds accept slot names (`"light1"`) or hex. `false` suppresses inherited `watermark` / `header` / `footer`. A header or footer zone (`left` / `center` / `right`) takes `image`, `text`, `socials`, `date`, laid out side by side in that order; generated values are variables in `text`: `"{{slide.number}} / {{deck.slideCount}}"`, `"{{slide.section}}"`, `"{{organization.name}}"` (the slide-scoped `{{slide.number}}`, `{{slide.section}}`, `{{deck.slideCount}}` also work in body text). Logos live on `organization.logo` (one path, or `full` / `stacked` / `icon` / `wordmark`, each a path or `{ onLight, onDark }`); place one with `"image": "var:organization.logo.icon"`; covers draw the full logo, and `design.logo` is only an override reference or `false`.\n\n## Prefer / never\n\nPrefer: string shorthands; bare ids for default-catalog records; inference over explicit `type`; regions only when position matters; groups only when `blocks` ordering is not enough; names/`var:` over hex in color fields; `items` for lists and `bullets` only for plain talking points (never both in one block or region; see [content payloads](content-payloads.md#items-versus-bullets)).\n\nNever: loose chart or table fields directly on a slide; region keys mixed with a root payload; overlapping region keys; `blocks` mixed with leaf fields on one payload; duplicate ids; x/y or pixel geometry (it does not exist in OPF).\n\n## Two canonical slides\n\n```json\n{\n "id": "headline",\n "title": "Adoption Doubled",\n "left": { "id": "kpi", "metric": { "value": "2.1x", "label": "Adoption", "trend": "up" } },\n "center+right": { "chart": { "type": "line", "data": { "columns": ["Month", "Teams"], "rows": [["Jan", 12], ["Feb", 18]] } } }\n}\n```\n\n```json\n{\n "id": "risks",\n "title": "What Could Go Wrong",\n "composition": { "mode": "column" },\n "blocks": [\n { "items": [["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }]] },\n { "quote": { "text": "Exceptions became visible before they became escalations.", "attribution": "VP Operations" } }\n ],\n "extensions": { "authoring": { "locked": true } }\n}\n```\n'
|
|
128
130
|
},
|
|
129
131
|
{
|
|
130
132
|
"slug": "handoff-2026-09-08",
|
|
@@ -202,25 +204,25 @@ var docsData = Object.freeze([
|
|
|
202
204
|
"slug": "image-treatments",
|
|
203
205
|
"file": "docs/image-treatments.md",
|
|
204
206
|
"title": "Images: backgrounds, image blocks and treatments",
|
|
205
|
-
"markdown": '# Images: backgrounds, image blocks and treatments\n\nOPF 0.15 has two places for a picture, with one fit vocabulary:\n\n- A **background** is the slide\'s canvas fill. A picture background fills the whole slide behind everything and never moves content.\n- An **image block** holds every content picture. It carries fit, focus, the treatment vocabulary below, and an optional placement that bleeds it to one slide edge.\n\nopf-render draws both as SVG and opf-pptx exports them as native PowerPoint DrawingML, from the same core geometry. This page also maps each of the 15 [pptx.gallery image treatments](https://www.pptx.gallery/image-treatments) to OPF, with its support status and the reason. The reference deck [`fixtures/image-treatments.opf.json`](fixtures/image-treatments.opf.json) has one slide per gallery treatment. Core\'s `test/image-blocks.test.mjs` checks it against core 0.14\'s frozen picture geometry, and `pnpm test:image-treatments` checks it across core, SVG and PPTX.\n\n## Backgrounds\n\n```json\n{\n "design": {\n "background": {\n "type": "image",\n "src": "./images/harbour.jpg",\n "alt": "Container ships at dawn",\n "fit": "cover",\n "focus": { "x": 0.5, "y": 0.7 },\n "overlay": { "color": "dark1", "opacity": 0.4 }\n }\n }\n}\n```\n\nThe effective background is the slide\'s `design.background`, then the deck\'s, then the theme\'s. A picture background has these properties:\n\n| Property | Meaning |\n|---|---|\n| `src` | The picture: `asset:<id>`, an HTTPS URL, a data URI or a relative path. |\n| `alt` | Alternative text. With `alt` the picture is meaningful, and PPTX export draws it as a full-slide picture at the back that carries the text. Without `alt` it is decorative and exports as the native slide background. A referenced asset\'s alt text does not count. In the preview a picture with `alt` is an image with that name, and one with `alt: ""` is hidden from assistive technology. |\n| `fit` | `cover` (the default) fills the slide and crops around `focus`. `contain` shows the whole picture over the colour scheme\'s default background. `stretch` scales it to the slide. `tile` repeats it at its own size from the top-left corner. |\n| `focus` | `{ x, y }` in 0 to 1: the point of the picture that a `cover` crop keeps in view. The default is the center. |\n| `opacity` | Picture opacity. The overlay keeps its own. |\n| `recolor` | `"grayscale"`, or `{ dark, light }` for duotone, as on an image block. Recolor and opacity apply to the picture\'s pixels; the overlay is drawn above them. |\n| `overlay` | A scrim over the whole picture, or a band along one edge (`edge`, `size`; see [Overlay](#overlay)). |\n| `tone` | `dark` or `light` (from core 0.23.0): the variant the slide takes (`dark1` or `light1`) so text and lines read on the picture. Absent: the deck\'s variant, else light. |\n\nA picture or gradient background can carry `tone: "dark"` or `"light"` (from core 0.23.0). The tone picks the color map (`dark1` or `light1`, see `design.variant` in [design resolution](design-resolution.md#color-variants)) so the slide\'s text, borders and chart furniture are chosen to read on the picture. Without a tone the picture takes the deck\'s variant, else counts as light, as before.\n\nThe string shorthand `"asset:hero"` (or any source starting with `asset:`, `https://`, `data:`, `./` or `../`) is a cover picture background. Theme slots (`light1`) and hex colours keep their meaning, and any other string is invalid.\n\n`composeSlide` reports the picture as `SlideComposition.backgroundImage`: the canvas `box`, `fit`, `focus`, `opacity`, `recolor`, the overlay box, and the fit placement when core can read the picture\'s proportions. The headings and the body compose exactly as on a slide without the background. A slide with only a title over a picture background is a cover, and its title centers like any cover\'s.\n\n## Image blocks\n\n```json\n{ "image": { "src": "asset:hero", "alt": "Harbour" }, "fit": "cover", "focus": { "x": 0.3, "y": 0.5 }, "shape": "rounded", "border": { "color": "accent5", "width": 1 } }\n```\n\n`Slide.image` is source shorthand for one image block, with the effective `design.imageFit`: it takes no fit, focus, treatment or placement keys of its own (the schema rejects them on the slide). To treat or place a picture, write an image block in `blocks`. Every property below is optional, valid only on an image payload, and additive.\n\n| Property | Meaning | Native PPTX | SVG preview |\n|---|---|---|---|\n| `fit` | `cover` fills the frame and crops around `focus`. `contain` shows the whole picture. `stretch` scales it to the frame. Default: the effective `design.imageFit` (the slide\'s, the deck\'s, then the layout\'s), else `cover`. | `a:srcRect`: positive insets crop, negative insets pad | the shared placement (`fitImage`) |\n| `focus` | `{ x, y }`: the point a `cover` crop keeps in view. | `a:srcRect` | the same placement |\n| `aspectRatio` | The largest centered frame with this width/height ratio (`circle` uses 1). | frame geometry | frame geometry |\n| `shape` | `rectangle`, `rounded`, `circle` or `hexagon`. | `a:prstGeom` `rect` / `roundRect` / `ellipse` / `hexagon` with core\'s guide values | `clipPath` with core\'s outline of the same preset formula |\n| `cornerRadius` | Radius of a `rounded` frame as a share of the shorter side. Default 0.16667. | `roundRect` `adj` | circular arcs with the same radius |\n| `border` | `{ color, width }`. The line is centered on the outline, and its width is in reference pixels. | `a:ln` with `solidFill`, `algn="ctr"` and a miter join | `stroke` on the same outline, miter join |\n| `opacity` | Opacity of the picture\'s pixels only. | `a:alphaModFix` | `opacity` on the image only |\n| `recolor` | `"grayscale"`, or `{ dark, light }` for duotone. Luminance uses Rec. 601 weights on sRGB. | `a:grayscl` / `a:duotone` | `feColorMatrix` in sRGB |\n| `overlay` | A scrim in the frame\'s shape, or an edge band on a rectangle frame. | one `p:sp` directly above the picture | `path` with `fill-opacity` |\n| `placement` | `{ edge, size, inset }`: bleed the block to one slide edge (see below). | frame geometry | frame geometry |\n\nColors accept the usual `ColorRef` forms: hex, scheme slot or role, or `var:<id>`. Eight-digit hex alpha is kept as native `a:alpha` and SVG opacity. Alt text comes from the image\'s asset.\n\n`composeSlide` reports every image item\'s picture as `ComposedItem.image`: the `region` (the cell, or the placement band), the frame `box`, `fit`, `focus`, the `shape` with its DrawingML preset and SVG outline, `border`, `opacity`, `recolor`, `overlay` and `placement`. Both engines draw exactly that. The paint order is the same in both: the picture with its recolor and opacity, clipped to the shape, then the line, then the overlay.\n\n### Placement\n\n`placement: { edge: "left" | "right" | "top" | "bottom", size: 0.1\u20130.9 (default 0.5), inset: false }` gives the image a band along that edge of the slide, edge to edge, and the headings and other content compose in the rest of the slide at the normal padding. `size` is the band\'s share of the slide width (left, right) or height (top, bottom). `inset: true` puts the frame inside the slide padding of the band, like a card. Header and footer bands keep their full-width placement.\n\n- Only a top-level block (`slides.N.blocks.I`) can be placed, and each slide edge takes at most one placed image. A placed block inside a group or a promoted region, or a second block on the same edge, is an `opf/image-placement-invalid` error.\n- Bands are taken in block order. Each spans the free area left by the earlier ones.\n- Placement belongs to a top-level image block. Current v2 layout records do not have image-placeholder placement; write a block in `blocks` to place a picture rather than using the root `Slide.image` shorthand.\n- A slide whose only body is placed images is composed like a cover: its headings center in the free area beside the image.\n- In a right-to-left deck `left` is the start side, drawn at the right. Edge overlays mirror the same way.\n\nPlacement is the only mechanism that moves content aside. A background never does.\n\n## Overlay\n\n`{ color, opacity, edge?, size? }` is shared by backgrounds and image blocks. Without `edge` it covers the whole frame in the frame\'s shape. With `edge` (`top`, `bottom`, `left` or `right`) it covers a band of `size` (default 0.3) of the frame along that edge, for example a caption strip. An edge overlay on a non-rectangle image frame reports `unsupported-image-treatment` at `...overlay.edge`, and neither engine draws it. That diagnostic never fails strict layout. Text over a picture background is certified readable (`opf/text-on-image`) only through a full-frame overlay strong enough for every possible pixel; an edge band does not count.\n\nAn unchanged export imports back as the same OPF: the background and the image blocks with their treatments, plus a data URI of each embedded picture. The `OPF_IMAGE_V1` and `OPF_IMAGE_OVERLAY_V1` shape tags record the native geometry of an image block. A picture that was moved, re-cropped or recolored stays an ordinary image and reports its provenance diagnostic. An edited overlay drops only the overlay.\n\n## pptx.gallery treatments\n\n| Gallery treatment | OPF | Status | Reason or gap |\n|---|---|---|---|\n| full-bleed | background `{type: image, src, overlay: {color: dark1, opacity: 0.2}}` | supported | |\n| text-overlay | background with `overlay: {color: dark1, opacity: 0.55}` | supported | Heading color still follows the theme background. Use a dark theme or background for light text over photos. |\n| side-by-side | image block, `placement: {edge: left, size: 0.46}` | supported | |\n| caption-overlay | image block, `overlay: {color: dark1, opacity: 0.75, edge: bottom, size: 0.25}` | supported | Square corners. An edge band cannot follow a rounded mask as one native shape. The gallery\'s card sits inside the slide padding, which a background cannot do, so the picture is content below the headings. Without the inset it is a background with the same edge-band overlay. |\n| masked-shape | image block, `shape: hexagon, aspectRatio: 1.1547, placement: {edge: right, inset: true}` | supported | Masks are limited to the four presets. Arbitrary polygon and custom-geometry masks are not in the vocabulary. |\n| circular-crop | image block, `shape: circle, placement: {edge: left, size: 0.4, inset: true}` | supported | The gallery\'s small shadow is not drawn (see shadows). |\n| rounded-card | image block, `shape: rounded, cornerRadius: 0.05, border: {color: accent5, width: 1}` | supported, without shadow | The optional card shadow is unsupported (see shadows). |\n| duotone | background with `recolor: {dark: accent1, light: light1}` | supported | Native raster parity of PowerPoint\'s luminance weights is unverified (see fidelity). |\n| background-blur | background with `overlay: {color: light1, opacity: 0.4}`, plus `contentBox: true` | **unsupported: blur** | Blur is not expressible. The mapping keeps the light scrim and the sharp card, without the blur. |\n| image-strip | image block, `placement: {edge: bottom, size: 0.3}` | partial | A placed block is one picture. A strip of several images is content: image blocks in a row composition. |\n| collage-grid | 4 image blocks, `composition: {mode: grid, columns: 2}`, `imageFit: cover` | supported via content blocks | A collage is content composition, not a single-image treatment. |\n| device-frame | image block, `aspectRatio: 0.5, shape: rounded, cornerRadius: 0.12, border: {color: dark1, width: 12}, placement: {edge: right, inset: true}` | partial | The bezel is a thick line on a rounded portrait frame. Device artwork (notch, buttons, device-specific screens) is not expressible. |\n| cutout-subject | image block, `fit: contain, placement: {edge: right}`, with a transparent PNG | partial | **Background removal is unsupported.** PowerPoint\'s Remove Background produces an edited bitmap, not a declarative effect. Supply a pre-cut transparent PNG, and both engines keep its alpha. The grounding shadow is unsupported (see shadows). |\n| watermark | image block, `fit: contain, opacity: 0.1, recolor: grayscale` | supported | `design.watermark` remains a separate, preview-only feature. |\n| cinematic-crop | image block, `aspectRatio: 2.39`, with `design.background: dark1` | supported | |\n\nThe frames of full-bleed, text-overlay, background-blur, duotone, side-by-side, masked-shape, circular-crop, image-strip, device-frame, cutout-subject and collage-grid are those of core 0.14. Caption-overlay, rounded-card, watermark and cinematic-crop were 0.14 slide images behind the headings that need an inset, a mask, a line or an aspect ratio, which a background does not have. In 0.15 they are image blocks below the headings, and their treatment geometry within the frame is unchanged. The gallery snippets themselves live in pptx.gallery (FF-30).\n\n## Unsupported effects\n\nThese effects are left out of the vocabulary on purpose. A value that both engines cannot draw identically would break the preview/export contract.\n\n- **Blur** (`a:blur` inside a blip, or the Office 2010 `a14:imgEffect` artistic blur): PowerPoint\'s blur kernel and radius scale are not specified. There is also no native evidence that PowerPoint renders a blip `a:blur` on pictures. An SVG `feGaussianBlur` cannot be shown to match. background-blur is therefore unsupported.\n- **Shadows and soft edges** (`a:outerShdw`, `a:softEdge` in `a:effectLst`): these are native, but PowerPoint\'s blur kernel for them is unspecified. An SVG drop shadow would only approximate it.\n- **Background removal**: this is a bitmap edit, not an effect. Use a transparent source.\n- **Device artwork and arbitrary masks**: these would need custom geometry and composed artwork. The treatment vocabulary is limited to preset masks and a line.\n- **Tint, brightness and contrast** (`a:lum`, `a:clrChange`, `a:tint`): no gallery treatment needs them. They are left out until they have the same luminance-formula verification as grayscale and duotone.\n- **Frame treatments on backgrounds**: a background has fit, focus, opacity, recolor and an overlay (with edge bands). A masked, bordered, inset or aspect-ratio picture is an image block.\n\n## Fidelity boundary\n\nThese checks prove the shared contract: geometry, preset guides and outlines, line width and color, recolor endpoints, alpha, overlay and round trip (`pnpm test:image-treatments`, plus the tests in each engine). They do not prove PowerPoint raster parity. Office runs are root-only, and the following are still unverified in native PowerPoint:\n\n- negative `a:srcRect` insets for `contain`;\n- the luminance weights PowerPoint uses for `a:grayscl` and `a:duotone`. The preview uses Rec. 601 on sRGB, as LibreOffice does.\n- line joins on the hexagon outline.\n'
|
|
207
|
+
"markdown": '# Images: backgrounds, image blocks and treatments\n\nOPF 0.15 has two places for a picture, with one fit vocabulary:\n\n- A **background** is the slide\'s canvas fill. A picture background fills the whole slide behind everything and never moves content.\n- An **image block** holds every content picture. It carries fit, focus, the treatment vocabulary below, and an optional placement that bleeds it to one slide edge.\n\nopf-render draws both as SVG and opf-pptx exports them as native PowerPoint DrawingML, from the same core geometry. This page also maps each of the 15 [pptx.gallery image treatments](https://www.pptx.gallery/image-treatments) to OPF, with its support status and the reason. The reference deck [`fixtures/image-treatments.opf.json`](fixtures/image-treatments.opf.json) has one slide per gallery treatment. Core\'s `test/image-blocks.test.mjs` checks it against core 0.14\'s frozen picture geometry, and `pnpm test:image-treatments` checks it across core, SVG and PPTX.\n\n## Backgrounds\n\n```json\n{\n "design": {\n "background": {\n "type": "image",\n "src": "./images/harbour.jpg",\n "alt": "Container ships at dawn",\n "fit": "cover",\n "focus": { "x": 0.5, "y": 0.7 },\n "overlay": { "color": "dark1", "opacity": 0.4 }\n }\n }\n}\n```\n\nThe effective background is the slide\'s `design.background`, then the deck\'s, then the theme\'s. A picture background has these properties:\n\n| Property | Meaning |\n|---|---|\n| `src` | The picture: `asset:<id>`, an HTTPS URL, a data URI or a relative path. |\n| `alt` | Alternative text. With `alt` the picture is meaningful, and PPTX export draws it as a full-slide picture at the back that carries the text. Without `alt` it is decorative and exports as the native slide background. A referenced asset\'s alt text does not count. In the preview a picture with `alt` is an image with that name, and one with `alt: ""` is hidden from assistive technology. |\n| `fit` | `cover` (the default) fills the slide and crops around `focus`. `contain` shows the whole picture over the colour scheme\'s default background. `stretch` scales it to the slide. `tile` repeats it at its own size from the top-left corner. |\n| `focus` | `{ x, y }` in 0 to 1: the point of the picture that a `cover` crop keeps in view. The default is the center. |\n| `opacity` | Picture opacity. The overlay keeps its own. |\n| `recolor` | `"grayscale"`, or `{ dark, light }` for duotone, as on an image block. Recolor and opacity apply to the picture\'s pixels; the overlay is drawn above them. |\n| `overlay` | A scrim over the whole picture, or a band along one edge (`edge`, `size`; see [Overlay](#overlay)). |\n| `tone` | `dark` or `light` (from core 0.23.0): the variant the slide takes (`dark1` or `light1`) so text and lines read on the picture. Absent: the deck\'s variant, else light. |\n\nA picture or gradient background can carry `tone: "dark"` or `"light"` (from core 0.23.0). The tone picks the color map (`dark1` or `light1`, see `design.variant` in [design resolution](design-resolution.md#color-variants)) so the slide\'s text, borders and chart furniture are chosen to read on the picture. Without a tone the picture takes the deck\'s variant, else counts as light, as before.\n\nThe string shorthand `"asset:hero"` (or any source starting with `asset:`, `https://`, `data:`, `./` or `../`) is a cover picture background. Theme slots (`light1`) and hex colours keep their meaning, and any other string is invalid.\n\n`composeSlide` reports the picture as `SlideComposition.backgroundImage`: the canvas `box`, `fit`, `focus`, `opacity`, `recolor`, the overlay box, and the fit placement when core can read the picture\'s proportions. The headings and the body compose exactly as on a slide without the background. A slide with only a title over a picture background is a cover, and its title centers like any cover\'s.\n\n## Image blocks\n\n```json\n{ "image": { "src": "asset:hero", "alt": "Harbour" }, "fit": "cover", "focus": { "x": 0.3, "y": 0.5 }, "shape": "rounded", "border": { "color": "accent5", "width": 1 } }\n```\n\n`Slide.image` is source shorthand for one image block, with the effective `design.imageFit`: it takes no fit, focus, treatment or placement keys of its own (the schema rejects them on the slide). To treat or place a picture, write an image block in `blocks`. Every property below is optional, valid only on an image payload, and additive.\n\n| Property | Meaning | Native PPTX | SVG preview |\n|---|---|---|---|\n| `fit` | `cover` fills the frame and crops around `focus`. `contain` shows the whole picture. `stretch` scales it to the frame. Default: the effective `design.imageFit` (the slide\'s, the deck\'s, then the layout\'s), else `cover` in a layout and `contain` on a slide with no layout. A contained picture has a frame of its own proportions at the region\'s anchor (top by default). | `a:srcRect`: positive insets crop, negative insets pad | the shared placement (`fitImage`) |\n| `focus` | `{ x, y }`: the point a `cover` crop keeps in view. | `a:srcRect` | the same placement |\n| `aspectRatio` | The largest centered frame with this width/height ratio (`circle` uses 1). | frame geometry | frame geometry |\n| `shape` | `rectangle`, `rounded`, `circle` or `hexagon`. | `a:prstGeom` `rect` / `roundRect` / `ellipse` / `hexagon` with core\'s guide values | `clipPath` with core\'s outline of the same preset formula |\n| `cornerRadius` | Radius of a `rounded` frame as a share of the shorter side. Default 0.16667. | `roundRect` `adj` | circular arcs with the same radius |\n| `border` | `{ color, width }`. The line is centered on the outline, and its width is in reference pixels. | `a:ln` with `solidFill`, `algn="ctr"` and a miter join | `stroke` on the same outline, miter join |\n| `opacity` | Opacity of the picture\'s pixels only. | `a:alphaModFix` | `opacity` on the image only |\n| `recolor` | `"grayscale"`, or `{ dark, light }` for duotone. Luminance uses Rec. 601 weights on sRGB. | `a:grayscl` / `a:duotone` | `feColorMatrix` in sRGB |\n| `overlay` | A scrim in the frame\'s shape, or an edge band on a rectangle frame. | one `p:sp` directly above the picture | `path` with `fill-opacity` |\n| `placement` | `{ edge, size, inset }`: bleed the block to one slide edge (see below). | frame geometry | frame geometry |\n\nColors accept the usual `ColorRef` forms: hex, scheme slot or role, or `var:<id>`. Eight-digit hex alpha is kept as native `a:alpha` and SVG opacity. Alt text comes from the image\'s asset.\n\n`composeSlide` reports every image item\'s picture as `ComposedItem.image`: the `region` (the cell, or the placement band), the frame `box`, `fit`, `focus`, the `shape` with its DrawingML preset and SVG outline, `border`, `opacity`, `recolor`, `overlay` and `placement`. Both engines draw exactly that. The paint order is the same in both: the picture with its recolor and opacity, clipped to the shape, then the line, then the overlay.\n\n### Placement\n\n`placement: { edge: "left" | "right" | "top" | "bottom", size: 0.1\u20130.9 (default 0.5), inset: false }` gives the image a band along that edge of the slide, edge to edge, and the headings and other content compose in the rest of the slide at the normal padding. `size` is the band\'s share of the slide width (left, right) or height (top, bottom). `inset: true` puts the frame inside the slide padding of the band, like a card. Header and footer bands keep their full-width placement.\n\n- Only a top-level block (`slides.N.blocks.I`) can be placed, and each slide edge takes at most one placed image. A placed block inside a group or a promoted region, or a second block on the same edge, is an `opf/image-placement-invalid` error.\n- Bands are taken in block order. Each spans the free area left by the earlier ones.\n- Placement belongs to a top-level image block. Current v2 layout records do not have image-placeholder placement; write a block in `blocks` to place a picture rather than using the root `Slide.image` shorthand.\n- A slide whose only body is placed images is composed like a cover: its headings center in the free area beside the image.\n- In a right-to-left deck `left` is the start side, drawn at the right. Edge overlays mirror the same way.\n\nPlacement is the only mechanism that moves content aside. A background never does.\n\n## Overlay\n\n`{ color, opacity, edge?, size? }` is shared by backgrounds and image blocks. Without `edge` it covers the whole frame in the frame\'s shape. With `edge` (`top`, `bottom`, `left` or `right`) it covers a band of `size` (default 0.3) of the frame along that edge, for example a caption strip. An edge overlay on a non-rectangle image frame reports `unsupported-image-treatment` at `...overlay.edge`, and neither engine draws it. That diagnostic never fails strict layout. Text over a picture background is certified readable (`opf/text-on-image`) only through a full-frame overlay strong enough for every possible pixel; an edge band does not count.\n\nAn unchanged export imports back as the same OPF: the background and the image blocks with their treatments, plus a data URI of each embedded picture. The `OPF_IMAGE_V1` and `OPF_IMAGE_OVERLAY_V1` shape tags record the native geometry of an image block. A picture that was moved, re-cropped or recolored stays an ordinary image and reports its provenance diagnostic. An edited overlay drops only the overlay.\n\n## pptx.gallery treatments\n\n| Gallery treatment | OPF | Status | Reason or gap |\n|---|---|---|---|\n| full-bleed | background `{type: image, src, overlay: {color: dark1, opacity: 0.2}}` | supported | |\n| text-overlay | background with `overlay: {color: dark1, opacity: 0.55}` | supported | Heading color still follows the theme background. Use a dark theme or background for light text over photos. |\n| side-by-side | image block, `placement: {edge: left, size: 0.46}` | supported | |\n| caption-overlay | image block, `overlay: {color: dark1, opacity: 0.75, edge: bottom, size: 0.25}` | supported | Square corners. An edge band cannot follow a rounded mask as one native shape. The gallery\'s card sits inside the slide padding, which a background cannot do, so the picture is content below the headings. Without the inset it is a background with the same edge-band overlay. |\n| masked-shape | image block, `shape: hexagon, aspectRatio: 1.1547, placement: {edge: right, inset: true}` | supported | Masks are limited to the four presets. Arbitrary polygon and custom-geometry masks are not in the vocabulary. |\n| circular-crop | image block, `shape: circle, placement: {edge: left, size: 0.4, inset: true}` | supported | The gallery\'s small shadow is not drawn (see shadows). |\n| rounded-card | image block, `shape: rounded, cornerRadius: 0.05, border: {color: accent5, width: 1}` | supported, without shadow | The optional card shadow is unsupported (see shadows). |\n| duotone | background with `recolor: {dark: accent1, light: light1}` | supported | Native raster parity of PowerPoint\'s luminance weights is unverified (see fidelity). |\n| background-blur | background with `overlay: {color: light1, opacity: 0.4}`, plus `contentBox: true` | **unsupported: blur** | Blur is not expressible. The mapping keeps the light scrim and the sharp card, without the blur. |\n| image-strip | image block, `placement: {edge: bottom, size: 0.3}` | partial | A placed block is one picture. A strip of several images is content: image blocks in a row composition. |\n| collage-grid | 4 image blocks, `composition: {mode: grid, columns: 2}`, `imageFit: cover` | supported via content blocks | A collage is content composition, not a single-image treatment. |\n| device-frame | image block, `aspectRatio: 0.5, shape: rounded, cornerRadius: 0.12, border: {color: dark1, width: 12}, placement: {edge: right, inset: true}` | partial | The bezel is a thick line on a rounded portrait frame. Device artwork (notch, buttons, device-specific screens) is not expressible. |\n| cutout-subject | image block, `fit: contain, placement: {edge: right}`, with a transparent PNG | partial | **Background removal is unsupported.** PowerPoint\'s Remove Background produces an edited bitmap, not a declarative effect. Supply a pre-cut transparent PNG, and both engines keep its alpha. The grounding shadow is unsupported (see shadows). |\n| watermark | image block, `fit: contain, opacity: 0.1, recolor: grayscale` | supported | `design.watermark` remains a separate, preview-only feature. |\n| cinematic-crop | image block, `aspectRatio: 2.39`, with `design.background: dark1` | supported | |\n\nThe frames of full-bleed, text-overlay, background-blur, duotone, side-by-side, masked-shape, circular-crop, image-strip, device-frame, cutout-subject and collage-grid are those of core 0.14. Caption-overlay, rounded-card, watermark and cinematic-crop were 0.14 slide images behind the headings that need an inset, a mask, a line or an aspect ratio, which a background does not have. In 0.15 they are image blocks below the headings, and their treatment geometry within the frame is unchanged. The gallery snippets themselves live in pptx.gallery (FF-30).\n\n## Unsupported effects\n\nThese effects are left out of the vocabulary on purpose. A value that both engines cannot draw identically would break the preview/export contract.\n\n- **Blur** (`a:blur` inside a blip, or the Office 2010 `a14:imgEffect` artistic blur): PowerPoint\'s blur kernel and radius scale are not specified. There is also no native evidence that PowerPoint renders a blip `a:blur` on pictures. An SVG `feGaussianBlur` cannot be shown to match. background-blur is therefore unsupported.\n- **Shadows and soft edges** (`a:outerShdw`, `a:softEdge` in `a:effectLst`): these are native, but PowerPoint\'s blur kernel for them is unspecified. An SVG drop shadow would only approximate it.\n- **Background removal**: this is a bitmap edit, not an effect. Use a transparent source.\n- **Device artwork and arbitrary masks**: these would need custom geometry and composed artwork. The treatment vocabulary is limited to preset masks and a line.\n- **Tint, brightness and contrast** (`a:lum`, `a:clrChange`, `a:tint`): no gallery treatment needs them. They are left out until they have the same luminance-formula verification as grayscale and duotone.\n- **Frame treatments on backgrounds**: a background has fit, focus, opacity, recolor and an overlay (with edge bands). A masked, bordered, inset or aspect-ratio picture is an image block.\n\n## Fidelity boundary\n\nThese checks prove the shared contract: geometry, preset guides and outlines, line width and color, recolor endpoints, alpha, overlay and round trip (`pnpm test:image-treatments`, plus the tests in each engine). They do not prove PowerPoint raster parity. Office runs are root-only, and the following are still unverified in native PowerPoint:\n\n- negative `a:srcRect` insets for `contain`;\n- the luminance weights PowerPoint uses for `a:grayscl` and `a:duotone`. The preview uses Rec. 601 on sRGB, as LibreOffice does.\n- line joins on the hexagon outline.\n'
|
|
206
208
|
},
|
|
207
209
|
{
|
|
208
210
|
"slug": "layout-templates",
|
|
209
211
|
"file": "docs/layout-templates.md",
|
|
210
212
|
"title": "Layout templates",
|
|
211
|
-
"markdown": '# Layout templates\n\nA layout is **one ordered list of regions, declared once, in reading order.** Each region says how much of the slide it\ntakes (`size`), which content it accepts and how several blocks share it. The list has a `direction`: `row` puts the\nregions side by side, left to right; `column` stacks them top to bottom. For two dimensions, an entry of the list can be\na **split**: a nested list that runs the other way. That is the whole format. There is no grid of cell names to keep in\nstep with a table of sizes and a table of rules.\n\nA layout record (`https://openpresentation.org/schema/opf-layout/v2`) is an ordinary JSON object. The built-in layouts\nand a deck\'s own layouts are records of this one schema; a custom layout is embedded under `catalogs.custom.layouts` (or\na named catalog group) and referenced by id like any other. This page is the reference for what core implements.\n\n> The 28 built-in templates come from the matching `@openpresentation/gallery` package. Core and Gallery 5 use the region\n> lists documented below; Gallery 4 and earlier used a grid of area names (`areas`). Use coordinated core and Gallery\n> versions. V1 layout records, including `placeholders` and placeholder groups, remain unsupported; there are no aliases or\n> migration API.\n\n## A record\n\nA story on the left and the evidence beside it, a third of the width:\n\n```json\n{\n "$schema": "https://openpresentation.org/schema/opf-layout/v2",\n "id": "story-and-evidence",\n "name": "Story and evidence",\n "direction": "row",\n "anchor": "middle",\n "regions": [\n { "name": "main", "size": 2, "accepts": ["text"] },\n { "name": "aside", "size": 1, "accepts": ["metric"] }\n ]\n}\n```\n\nRead it left to right, because the record\'s `direction` is `row`: `main` first, then `aside`, and `main` is twice as wide.\n`anchor: "middle"` is written once and applies to both regions. A block targets a region by name (`"region": "aside"`),\nand a slide\'s blocks otherwise bind to regions by kind, role and the order of the list.\n\nA layout with a title and a row of two regions, the most common shape of the built-ins, nests one split:\n\n```json\n{\n "name": "Two column",\n "regions": [\n { "name": "title" },\n { "direction": "row", "regions": [\n { "name": "first", "accepts": ["text", "list"] },\n { "name": "second", "accepts": ["text", "list"] }\n ] }\n ]\n}\n```\n\nThe record\'s `direction` is omitted, so it is `column`: the title on top, then one entry, a split whose own `direction` is\n`row`. A split has no name; it is a place to put regions the other way. Splits nest up to three levels, so a record can\ndescribe any layout made of full-width and full-height cuts (a guillotine partition), which is every built-in layout.\n\n`hero` nests the same way, with sizes: a title, a row of a lead and a picture twice as tall as the supporting row, then\nthe supporting content.\n\n```json\n{\n "name": "Hero",\n "regions": [\n { "name": "title" },\n { "direction": "row", "size": 2, "regions": [\n { "name": "lead", "accepts": ["text", "quote", "metric"], "flow": "none" },\n { "name": "media", "accepts": ["image", "video"], "role": "media", "flow": "none" }\n ] },\n { "name": "support", "size": 1, "accepts": ["text", "list", "metric", "group"], "role": "supporting", "flow": "grid" }\n ]\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `direction` | `row` (side by side, left to right in reading order) or `column` (stacked, top to bottom). Default `column`. |\n| `regions` | 1 to 12 entries in reading order, each a region or a split. |\n| `anchor` | Vertical position of the content of every body region under the record (or split) that does not set its own. Inherited by body regions only, never by `title` or `subtitle`. |\n| `overflowRegion` | Where content no region accepts goes. Omitted: the first `primary` region whose flow is not `none`. |\n| `design` | The layout\'s design defaults (below). |\n| `composition` | Only `gap`, `padding`, `minFontSize` and `overflow`, with their 0.18 meaning. |\n\nAn entry of `regions` is one of three things:\n\n- a **region**: `{ "name": "...", "size"?, ...rules }`, the rules below;\n- a **heading region**: `{ "name": "title" }` or `{ "name": "subtitle" }`, which take only a `size`;\n- a **split**: `{ "direction": "row" | "column", "regions": [...], "size"?, "anchor"? }`, a nested list.\n\n`size` is a number above 0 and up to 100 (default 1). Siblings share their parent\'s length in proportion, separated by the\ngap, so `2` and `1` are two thirds and one third. `"auto"` is the height the content needs, at most half the content box,\nand an `auto` region with no content takes no space; it is allowed only on an entry of a `column` (a `row` has no height\nto ask for), and elsewhere it is the validate finding `opf/layout-template`.\n\n`title` and `subtitle` are reserved heading regions: `title` holds the tag, title and subtitle stacked, and a `subtitle`\nregion takes the subtitle out of it (a `subtitle` needs a `title`). Their `size` defaults to `auto` inside a `column` and to\n1 inside a `row`, so a cover that puts the title beside a picture is `{ "direction": "row", "regions": [{ "name": "title" },\n{ "name": "media", ... }] }`. A layout with no `title` region gets an implicit `auto` row on top for a slide that has\nheadings. Region names are unique across the whole record and may not be `tag`, `auto` or a promoted-region word (`left`,\n`center`, `right`, `top`, `middle`, `bottom`).\n\n### Regions\n\n| Field | Values | Default |\n| --- | --- | --- |\n| `name` | lowercase kebab-case, unique in the record; a block\'s `region` names it | required |\n| `size` | a number above 0 and up to 100, or `auto` (columns only) | 1 |\n| `accepts` | `text`, `list`, `image`, `video`, `chart`, `table`, `code`, `metric`, `quote`, `timeline`, `group` | every body kind and `group` |\n| `role` | `primary`, `secondary`, `media`, `supporting` | `primary` |\n| `flow` | `none`, `grid`, `column`, `auto` | `auto` |\n| `max` | 1 to 12 (`none` requires 1) | 1 for `none`, else 6 |\n| `bleed` | boolean: outer edges on the content box reach the slide edge; never a card | `false` |\n| `listColumns` | `"auto"` (a list that does not fit its cell flows into up to three columns before it shrinks) or `1` | `1` |\n| `anchor` | `top`, `middle`, `bottom` | inherited from the nearest split or the record, else `top` |\n| `empty` | `collapse`, `keep` | `collapse` |\n\n### Choosing an anchor\n\nA block\'s `region` selects a named region; that region\'s `anchor` controls vertical placement inside it. Region names such\nas `main` and `aside` have no special positioning behavior. Declare the names in the layout\'s region list. Write `anchor`\nonce on the record (or on a split) when every region under it shares it: body regions inherit it, a region\'s own `anchor`\nwins, and the heading regions never inherit it.\n\nUse middle anchoring for a compact statement, quotation, headline metric or short explanation beside a visual. Keep\nsubstantial text, lists, charts and tables top-aligned so readers can compare common starting positions. The title\nremains in its own heading region. Anchoring does not enlarge short text to fill unused space.\n\nThe measured-content implementation supports text/list leaves and uncaptioned quote/metric leaves. A quote keeps\nits body, attribution and photo together. A metric keeps its value and metadata together without reducing the\nnominal font size merely because its placement box becomes shorter. Grid and horizontal rows share an origin\nbased on their tallest measured content, so differing description lengths do not stagger headline numbers.\nContent that does not fit retains its allocation and overflow diagnostics. Nested groups, captioned quotes/metrics,\ncharts, tables and explicit slide composition overrides keep their existing allocation behavior; an anchor is not\na promise to measure arbitrary content.\n\n### Validation\n\nThe schema checks types, enums and ranges; an embedded record that fails it is `opf/catalog-record`. `validate` adds:\n\n| Rule | Severity | Raised for |\n| --- | --- | --- |\n| `opf/layout-template` | error | A `regions` list that is not 1 to 12 entries; an entry that is neither a region nor a split; a bad `direction` or `size`; `auto` outside a column; splits nested more than three levels (the record\'s own list is level 1); regions that compile to a grid over 12 x 12. |\n| `opf/layout-region` | error | A name used twice; a reserved region name; a heading region with content rules; a `subtitle` without a `title`; a region that accepts no kind; `flow: none` with `max` other than 1; an `overflowRegion` that is not a body region. |\n\n`composeSlide` refuses such a record (`OPFLayoutTemplateError`). `layoutTemplate(record)` parses, checks and compiles a record\nand `layoutTemplateIssues(record)` lists the problems, both on `@openpresentation/opf/composition`.\n\n### How a record becomes a grid\n\nEvery region list is a guillotine partition: each split cuts its cell across, all the way. So the engine compiles the tree\ninto the grid it has always composed with, and composition, the renderer, the PPTX export and the editor read that grid:\n\n1. Each split divides its cell among its entries in proportion to their `size`; `auto` entries take the height their\n content needs.\n2. The cuts of all splits along one axis are collected into the grid\'s tracks (rows and columns), and a region fills the\n rectangle of tracks it covers.\n3. `layoutTemplate(record)` returns that grid (`rows` as numbers or `auto`, `columns`, a cell grid of names, the areas with\n their spans) and the regions in reading order with every default and inherited `anchor` applied.\n\nNested sizes are relative to the entry\'s own parent, so a split of `{ "size": 2 }` beside a region of `{ "size": 1 }` is\ntwice as large, whatever its children\'s sizes. Because the grid is shared, the gaps between regions are the grid\'s gaps: a\nrow of two regions inside a column puts one gap between the two columns, as the cell grid does. The 28 built-in layouts\ncompile to exactly the grids they had in Gallery 4, so nothing they draw moved.\n\n## Binding content to regions\n\nSlide content stays flat. `bindRegions(slide, record)` (pure, on `/composition`) binds the slide\'s root nodes to regions,\nin source order; placed images (`placement`) take their band and never occupy a region, and a content group is one node of\nkind `group`.\n\n1. **Pins.** A block with `region: "<name>"` goes to that region when it exists, accepts the block\'s kind and has room.\n Otherwise the pin is ignored (`opf/region-unknown`, `opf/region-kind`, `opf/region-full`, warnings) and the block\n joins step 2.\n2. **Everything else.** Of the regions that accept the block and have room, the lowest role rank wins (pictures:\n media, primary, secondary, supporting; every other kind: primary, secondary, supporting, media), then the first in\n reading order (the order of the `regions` list, depth first).\n3. **No candidate.** The block goes to the overflow region whatever its `accepts` while it has room, and beyond `max`\n it is drawn there anyway with `opf/layout-unplaced` (`paginate` moves it to a continuation slide). A layout with no\n overflow region draws it in an implicit row below the grid.\n\nAn empty region collapses into a neighbour that shares the full length of one of its edges (start side, end side, below,\nabove; a body region before a heading region). Headings are centred vertically in their region unless every row it spans is\n`auto`, so a `cover` or a title-only slide centres its title. A slide with promoted regions (`left`, `top:left`, ...)\ncomposes them as in 0.18.\n\nIn the Markdown dialect, `<!-- block: region=metrics -->` pins a block; a promoted-region key keeps its 0.18 meaning.\n\n## Flow\n\n| `flow` | Blocks are arranged |\n| --- | --- |\n| `none` | One block fills the region. |\n| `grid` | By count: 1 fills; 2, 3 in a row; 4 as 2 x 2; 5 as 3 + 2; 6 as 3 x 2; 7 to 12 in four columns. Rows and columns swap in a portrait region. A short last row keeps the column width and is centred. |\n| `column` | Stacked, equal heights. |\n| `auto` | `grid` unless a block is long: more than 6 lines (code: 12, table: 6 rows) in its grid cell at the starting size, counted with core\'s estimate, never a host font. |\n\n`design.contentDirection` makes every flowing region one row or one column. A slide\'s own `composition.mode`, `columns` and\n`weights` arrange its first primary flowing region with the 0.18 modes, above `contentDirection`. A list in a region\nwith `listColumns: "auto"` (the lone list of the design, and, since the RR-81 review, any list of the region that does not\nfit its own cell), or any list payload with `columns: "auto"`, tries 1, 2 then 3 columns at the starting size (a column at\nleast a quarter of the slide wide); `columns: 2` or `3` fixes the count.\n\n### Tracks\n\nColumns share the content width in proportion. Rows are allocated so that no row with content is squeezed to nothing:\n\n1. A heading `auto` row takes what its headings need, at most half the content box.\n2. The numeric rows keep at least what their content needs at the starting size, up to half of the space under the\n headings.\n3. The other `auto` rows take what they need (at most half the content box each; the implicit row below the grid, which\n holds what no region took, has no such cap) from what is left, and shrink together in proportion when it is not enough.\n An empty region with `empty: "keep"` holds two lines of body text in an `auto` row (plus the card insets on a slide\n that draws cards, as a filled region does).\n4. The numeric rows share the rest in proportion to their sizes.\n\nComposed content never crosses a heading or another region and never leaves its region\'s box. What still does not fit\nuses the design\'s tools in order: text fits down to `minFontSize`, lists flow into columns where the region allows, blocks\nbeyond the room go to the overflow region and, through `paginate`, to a continuation slide; what remains is the\n`opf/text-overflow` or `opf/layout-unplaced` diagnostic.\n\nPagination continues on a new slide with the same layout: blocks beyond a region\'s `max` move first, in source order, and\ncarry a `region` pin to the region they came from. A block that no region of the layout accepts, on a layout with no\noverflow region, stays below the grid on every page and never drives a split.\n\n## Design keys\n\nPer key: the slide\'s `design`, the deck\'s `design`, the theme record\'s `design` (new in 0.19: `titleAlignment`,\n`contentAlignment`, `contentBox`, `contentDirection`, `imageFit`, `listBullet`), the layout\'s `design`, then the engine\ndefault. `design.mirror: true` draws a template mirrored (each row\'s cells and the column sizes reversed); a right-to-left\ndeck mirrors on top. `layout: "auto"` is automatic composition: reserved, never a catalog id, and `opf format` drops it.\n\n## Composition output\n\nFor a slide with a template layout, `composeSlide` adds (see the declarations on `/composition`):\n\n| Field | Meaning |\n| --- | --- |\n| `regions` | Every region in reading order: `name`, `box` (after collapse, mirroring and bleed), `accepts`, `role`, `flow`, `max`, `anchor`, `arrangement` (`grid`, `row`, `column`, or `composition`), `content` (root block paths bound within `max`), `overflow` (paths drawn beyond it), `collapsed`, `bleed` and, on a bled region, `areaBox` (its cell before bleed). It replaces `slots` for templates. |\n| `headingAreas` | The `title` (and `subtitle`) areas with their boxes, `collapsed` or `implicit`. On a template without a `subtitle` area the title area carries `parts: { title, subtitle }`: it is split where the stacked heading group puts the subtitle, so `title` runs from the area\'s top to the bottom of the title (the tag above it included) and `subtitle` from the subtitle\'s top (half a gap under the title when the slide has none) to the area\'s bottom. PowerPoint\'s `ctrTitle` and `subTitle` placeholders use these boxes. |\n| `items[].region` | The region an item was drawn in, for tagging shapes. |\n| `items[].listColumns` | A list in columns: one `{ box, start, end, text }` per column at one shared size; numbering continues. The item\'s `text` is the whole list (every entry at its column\'s position), so a consumer that ignores `listColumns` still draws every item. Entry paths are the payload\'s: `<list>.<n>` for a string item, `<list>.<n>.text` for an object. |\n\n`composeLayoutAreas(record, options)` gives the areas of the empty layout: where a PowerPoint slide layout puts a placeholder\nper area. It is `composeSlide`\'s own geometry for a slide with a one-line title and subtitle and every region kept, so it\ntakes `composeSlide`\'s options (`width`, `height`, `presentation` for the deck\'s header and footer, `direction`, ...) plus\n`mirror`, `placements` (placed-image bands to reserve, `{ edge, size, inset }`) and `bleed` (default `true`: a bled region\nreaches the slide edge as on a slide; `false` gives its cell inside the content box). Its title area carries the same\n`parts` as a slide\'s. `bindRegions`, `layoutTemplate`, `regionAccepts` and `gridFlowShape` expose the rest of the model.\n\n## No migration from the 0.18 ids\n\n0.19 breaks layouts: there is no migration path. A layout id of the 0.18 default catalog that 0.19 does not have is an\nunknown layout like any other (`opf/unresolved-reference`: the slide composes automatically and a strict export fails).\nSix 0.18 ids are also 0.19 built-ins and resolve to them: `agenda`, `comparison`, `dashboard`, `faq`, `timeline` and\n`two-column`. The release notes list the 28 built-ins.\n'
|
|
213
|
+
"markdown": '# Layout templates\n\nA layout is **one ordered list of regions, declared once, in reading order.** Each region says how much of the slide it\ntakes (`size`), which content it accepts and how several blocks share it. The list has a `direction`: `row` puts the\nregions side by side, left to right; `column` stacks them top to bottom. For two dimensions, an entry of the list can be\na **split**: a nested list that runs the other way. That is the whole format. There is no grid of cell names to keep in\nstep with a table of sizes and a table of rules.\n\nA layout record (`https://openpresentation.org/schema/opf-layout/v2`) is an ordinary JSON object. The built-in layouts\nand a deck\'s own layouts are records of this one schema; a custom layout is embedded under `catalogs.custom.layouts` (or\na named catalog group) and referenced by id like any other. This page is the reference for what core implements.\n\n> The 30 built-in templates come from the matching `@openpresentation/gallery` package. Core and Gallery 5 use the region\n> lists documented below; Gallery 4 and earlier used a grid of area names (`areas`). Use coordinated core and Gallery\n> versions. V1 layout records, including `placeholders` and placeholder groups, remain unsupported; there are no aliases or\n> migration API.\n\n## A record\n\nA story on the left and the evidence beside it, a third of the width:\n\n```json\n{\n "$schema": "https://openpresentation.org/schema/opf-layout/v2",\n "id": "story-and-evidence",\n "name": "Story and evidence",\n "direction": "row",\n "anchor": "middle",\n "regions": [\n { "name": "main", "size": 2, "accepts": ["text"] },\n { "name": "aside", "size": 1, "accepts": ["metric"] }\n ]\n}\n```\n\nRead it left to right, because the record\'s `direction` is `row`: `main` first, then `aside`, and `main` is twice as wide.\n`anchor: "middle"` is written once and applies to both regions. A block targets a region by name (`"region": "aside"`),\nand a slide\'s blocks otherwise bind to regions by kind, role and the order of the list.\n\nA layout with a title and a row of two regions, the most common shape of the built-ins, nests one split:\n\n```json\n{\n "name": "Two column",\n "regions": [\n { "name": "title" },\n { "direction": "row", "regions": [\n { "name": "first", "accepts": ["text", "list"] },\n { "name": "second", "accepts": ["text", "list"] }\n ] }\n ]\n}\n```\n\nThe record\'s `direction` is omitted, so it is `column`: the title on top, then one entry, a split whose own `direction` is\n`row`. A split has no name; it is a place to put regions the other way. Splits nest up to three levels, so a record can\ndescribe any layout made of full-width and full-height cuts (a guillotine partition), which is every built-in layout.\n\n`hero` nests the same way, with sizes: a title, a row of a lead and a picture twice as tall as the supporting row, then\nthe supporting content.\n\n```json\n{\n "name": "Hero",\n "regions": [\n { "name": "title" },\n { "direction": "row", "size": 2, "regions": [\n { "name": "lead", "accepts": ["text", "quote", "metric"], "flow": "none" },\n { "name": "media", "accepts": ["image", "video"], "role": "media", "flow": "none" }\n ] },\n { "name": "support", "size": 1, "accepts": ["text", "list", "metric", "group"], "role": "supporting", "flow": "grid" }\n ]\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `direction` | `row` (side by side, left to right in reading order) or `column` (stacked, top to bottom). Default `column`. |\n| `regions` | 1 to 12 entries in reading order, each a region or a split. |\n| `anchor` | Vertical position of the content of every body region under the record (or split) that does not set its own. Inherited by body regions only, never by `title` or `subtitle`. Exception: on a column record whose entries are all `auto`, it places the whole stack (see Choosing an anchor). |\n| `overflowRegion` | Where content no region accepts goes. Omitted: the first `primary` region whose flow is not `none`. |\n| `design` | The layout\'s design defaults (below). |\n| `composition` | Only `gap`, `padding`, `minFontSize` and `overflow`, with their 0.18 meaning. |\n\nAn entry of `regions` is one of three things:\n\n- a **region**: `{ "name": "...", "size"?, ...rules }`, the rules below;\n- a **heading region**: `{ "name": "title" }` or `{ "name": "subtitle" }`, which take only a `size`;\n- a **split**: `{ "direction": "row" | "column", "regions": [...], "size"?, "anchor"? }`, a nested list.\n\n`size` is a number above 0 and up to 100 (default 1). Siblings share their parent\'s length in proportion, separated by the\ngap, so `2` and `1` are two thirds and one third. `"auto"` is the height the content needs, up to the space the headings and\nthe numeric rows\' content leave (see Tracks), and an `auto` region with no content takes no space; it is allowed only on an entry of a `column` (a `row` has no height\nto ask for), and elsewhere it is the validate finding `opf/layout-template`.\n\n`title` and `subtitle` are reserved heading regions: `title` holds the tag, title and subtitle stacked, and a `subtitle`\nregion takes the subtitle out of it (a `subtitle` needs a `title`). Their `size` defaults to `auto` inside a `column` and to\n1 inside a `row`, so a cover that puts the title beside a picture is `{ "direction": "row", "regions": [{ "name": "title" },\n{ "name": "media", ... }] }`. A layout with no `title` region gets an implicit `auto` row on top for a slide that has\nheadings. Region names are unique across the whole record and may not be `tag`, `auto` or a promoted-region word (`left`,\n`center`, `right`, `top`, `middle`, `bottom`).\n\n### Regions\n\n| Field | Values | Default |\n| --- | --- | --- |\n| `name` | lowercase kebab-case, unique in the record; a block\'s `region` names it | required |\n| `size` | a number above 0 and up to 100, or `auto` (columns only) | 1 |\n| `accepts` | `text`, `list`, `image`, `video`, `chart`, `table`, `code`, `metric`, `quote`, `timeline`, `group` | every body kind and `group` |\n| `role` | `primary`, `secondary`, `media`, `supporting` | `primary` |\n| `flow` | `none`, `grid`, `column`, `auto` | `auto` |\n| `max` | 1 to 12 (`none` requires 1) | 1 for `none`, else 6 |\n| `bleed` | boolean: outer edges on the content box reach the slide edge; never a card | `false` |\n| `listColumns` | `"auto"` (a list that does not fit its cell flows into up to three columns before it shrinks) or `1` | `1` |\n| `anchor` | `top`, `middle`, `bottom` | inherited from the nearest split or the record, else `top` |\n| `empty` | `collapse`, `keep` | `collapse` |\n\n### Choosing an anchor\n\nA block\'s `region` selects a named region; that region\'s `anchor` controls vertical placement inside it. Region names such\nas `main` and `aside` have no special positioning behavior. Declare the names in the layout\'s region list. Write `anchor`\nonce on the record (or on a split) when every region under it shares it: body regions inherit it, a region\'s own `anchor`\nwins, and the heading regions never inherit it.\n\nUse middle anchoring for a compact statement, quotation, headline metric or short explanation beside a visual. Keep\nsubstantial text, lists, charts and tables top-aligned so readers can compare common starting positions. A region\'s\nanchor never moves the title: it stays in its own heading region. Anchoring does not enlarge short text to fill unused space.\n\n#### Placing the whole group\n\nA single-message slide (a statement, a closing, a quotation) reads best when the title and what belongs to it sit together\nin the middle of the slide. A column has no numeric row to hold the leftover height when **every entry of the record\'s\n`regions` list is sized `auto`** (`title`, `subtitle` and the body regions alike), so the record\'s `anchor` then places\nthe whole stack, headings included, in the content box\'s leftover height: `middle` centres it, `bottom` sits it at the foot,\n`top` (the default) starts it at the top. Empty optional regions collapse and take no space, so a title-only slide centres\nthe title alone.\n\n```json\n{ "id": "closing", "direction": "column", "anchor": "middle",\n "regions": [ { "name": "title" }, { "name": "details", "size": "auto", "accepts": ["text", "list", "image"], "flow": "auto" } ] }\n```\n\nThe rule applies only to the record\'s own `anchor` on a column record with all-`auto` entries (an `auto` split counts as an\n`auto` entry). A record with any numeric entry, a row record, and a split\'s own `anchor` keep the behaviour above, so no\nexisting record changes until it opts in. `LayoutTemplate.stack` reports the resolved anchor.\n\nThe measured-content implementation supports text/list leaves and uncaptioned quote/metric leaves. A quote keeps\nits body, attribution and photo together. A metric keeps its value and metadata together without reducing the\nnominal font size merely because its placement box becomes shorter. Grid and horizontal rows share an origin\nbased on their tallest measured content, so differing description lengths do not stagger headline numbers.\nContent that does not fit retains its allocation and overflow diagnostics. Nested groups, captioned quotes/metrics,\ncharts, tables and explicit slide composition overrides keep their existing allocation behavior; an anchor is not\na promise to measure arbitrary content.\n\nPictures shown whole and videos follow the anchor too (WI-3). A picture with `fit: "contain"` (a layout\'s `imageFit`, or the\nblock\'s own) takes a frame of its own proportions, as wide as its cell allows and centered across, and sits at the region\'s\nanchor down the cell, so a landscape picture beside text starts on the text\'s first line instead of floating in the middle of\na tall cell. A video draws in a 16:9 frame, the largest that fits its cell, placed the same way. A frame the block shapes with\n`aspectRatio` or a circle stays centered, and `cover` and `stretch` fill the cell. `image-beside` and `logos` write\n`anchor: "middle"` on their media regions to keep their centered look.\n\n### Validation\n\nThe schema checks types, enums and ranges; an embedded record that fails it is `opf/catalog-record`. `validate` adds:\n\n| Rule | Severity | Raised for |\n| --- | --- | --- |\n| `opf/layout-template` | error | A `regions` list that is not 1 to 12 entries; an entry that is neither a region nor a split; a bad `direction` or `size`; `auto` outside a column; splits nested more than three levels (the record\'s own list is level 1); regions that compile to a grid over 12 x 12. |\n| `opf/layout-region` | error | A name used twice; a reserved region name; a heading region with content rules; a `subtitle` without a `title`; a region that accepts no kind; `flow: none` with `max` other than 1; an `overflowRegion` that is not a body region. |\n\n`composeSlide` refuses such a record (`OPFLayoutTemplateError`). `layoutTemplate(record)` parses, checks and compiles a record\nand `layoutTemplateIssues(record)` lists the problems, both on `@openpresentation/opf/composition`.\n\n### How a record becomes a grid\n\nEvery region list is a guillotine partition: each split cuts its cell across, all the way. So the engine compiles the tree\ninto the grid it has always composed with, and composition, the renderer, the PPTX export and the editor read that grid:\n\n1. Each split divides its cell among its entries in proportion to their `size`; `auto` entries take the height their\n content needs.\n2. The cuts of all splits along one axis are collected into the grid\'s tracks (rows and columns), and a region fills the\n rectangle of tracks it covers.\n3. `layoutTemplate(record)` returns that grid (`rows` as numbers or `auto`, `columns`, a cell grid of names, the areas with\n their spans) and the regions in reading order with every default and inherited `anchor` applied.\n\nNested sizes are relative to the entry\'s own parent, so a split of `{ "size": 2 }` beside a region of `{ "size": 1 }` is\ntwice as large, whatever its children\'s sizes. Because the grid is shared, the gaps between regions are the grid\'s gaps: a\nrow of two regions inside a column puts one gap between the two columns, as the cell grid does. The 30 built-in layouts\ncompile to exactly the grids they had in Gallery 4, so nothing they draw moved.\n\n## Binding content to regions\n\nSlide content stays flat. `bindRegions(slide, record)` (pure, on `/composition`) binds the slide\'s root nodes to regions,\nin source order; placed images (`placement`) take their band and never occupy a region, and a content group is one node of\nkind `group`.\n\n1. **Pins.** A block with `region: "<name>"` goes to that region when it exists, accepts the block\'s kind and has room.\n Otherwise the pin is ignored (`opf/region-unknown`, `opf/region-kind`, `opf/region-full`, warnings) and the block\n joins step 2.\n2. **Everything else.** Of the regions that accept the block and have room, the lowest role rank wins (pictures:\n media, primary, secondary, supporting; every other kind: primary, secondary, supporting, media), then the first in\n reading order (the order of the `regions` list, depth first).\n3. **No candidate.** The block goes to the overflow region whatever its `accepts` while it has room, and beyond `max`\n it is drawn there anyway with `opf/layout-unplaced` (`paginate` moves it to a continuation slide). A layout with no\n overflow region draws it in an implicit row below the grid.\n\nAn empty region collapses into a neighbour that shares the full length of one of its edges (start side, end side, below,\nabove; a body region before a heading region). Headings are centred vertically in their region unless every row it spans is\n`auto`, so a `cover` or a title-only slide centres its title. A slide with promoted regions (`left`, `top:left`, ...)\ncomposes them as in 0.18.\n\nIn the Markdown dialect, `<!-- block: region=metrics -->` pins a block; a promoted-region key keeps its 0.18 meaning.\n\n## Flow\n\n| `flow` | Blocks are arranged |\n| --- | --- |\n| `none` | One block fills the region. |\n| `grid` | By count: 1 fills; 2, 3 in a row; 4 as 2 x 2; 5 as 3 + 2; 6 as 3 x 2; 7 to 12 in four columns. Rows and columns swap in a portrait region. A short last row keeps the column width and is centred. |\n| `column` | Stacked at the heights the blocks need, one gap apart, and placed by the region\'s `anchor`. Charts, pictures, tables, timelines and videos take what the measured blocks (text, lists, quotes, metrics, code) leave. Blocks that do not fit share the region: each gives up the same fraction of what it can give up above its height at the minimum font size. A group\'s blocks in one column stack the same way. |\n| `auto` | `grid` unless a block is long: more than 6 lines (code: 12, table: 6 rows) in its grid cell at the starting size (28 px for prose), counted with core\'s estimate, never a host font. In an `auto` region a paragraph that wraps takes a reading measure of about 70 average characters of its size (never at the cost of size; it follows the content alignment) and a lone block of up to two lines uses the 32 px lead size. |\n\n`design.contentDirection` makes every flowing region one row or one column. A slide\'s own `composition.mode`, `columns` and\n`weights` arrange its first primary flowing region with the 0.18 modes, above `contentDirection`. A list in a region\nwith `listColumns: "auto"` (the lone list of the design, and, since the RR-81 review, any list of the region that does not\nfit its own cell), or any list payload with `columns: "auto"`, tries 1, 2 then 3 columns at the starting size (a column at\nleast a quarter of the slide wide); `columns: 2` or `3` fixes the count.\n\n### Tracks\n\nColumns share the content width in proportion. Rows are allocated so that no row with content is squeezed to nothing:\n\n1. A heading `auto` row takes what its headings need, at most half the content box.\n2. The numeric rows keep at least what their content needs at the starting size, up to half of the space under the\n headings.\n3. The other `auto` rows take what they need from what is left (the implicit row below the grid holds what no region\n took). When that is not enough they shrink together: each gives up the same fraction of what it can give up above its\n content\'s height at the minimum font size, so a one-line note keeps nearly its need beside a dense row, and no row\n overflows while those heights fit.\n An empty region with `empty: "keep"` holds two lines of body text in an `auto` row (plus the card insets on a slide\n that draws cards, as a filled region does).\n4. The numeric rows share the rest in proportion to their sizes.\n\nParagraphs and lists of one stacked column that do not fit at their role sizes shrink together to the largest one size that fits, and read at that one size.\n\nBlocks drawn side by side as peers (regions of one role in the same rows, such as the columns of `two-column` or\n`comparison`, and the blocks of one row of a `grid` or `row` flow) share one size per kind: their prose, and their lists,\nare refitted at the smallest size any of them fitted at.\n\nComposed content never crosses a heading or another region and never leaves its region\'s box. What still does not fit\nuses the design\'s tools in order: text fits down to `minFontSize`, lists flow into columns where the region allows, blocks\nbeyond the room go to the overflow region and, through `paginate`, to a continuation slide; what remains is the\n`opf/text-overflow` or `opf/layout-unplaced` diagnostic.\n\nPagination continues on a new slide with the same layout: blocks beyond a region\'s `max` move first, in source order, and\ncarry a `region` pin to the region they came from. A block that no region of the layout accepts, on a layout with no\noverflow region, stays below the grid on every page and never drives a split.\n\n## Design keys\n\nPer key: the slide\'s `design`, the deck\'s `design`, the theme record\'s `design` (new in 0.19: `titleAlignment`,\n`contentAlignment`, `contentBox`, `contentDirection`, `imageFit`, `listBullet`), the layout\'s `design`, then the engine\ndefault. `design.mirror: true` draws a template mirrored (each row\'s cells and the column sizes reversed); a right-to-left\ndeck mirrors on top. `layout: "auto"` is automatic composition: reserved, never a catalog id, and `opf format` drops it.\n\n## Composition output\n\nFor a slide with a template layout, `composeSlide` adds (see the declarations on `/composition`):\n\n| Field | Meaning |\n| --- | --- |\n| `regions` | Every region in reading order: `name`, `box` (after collapse, mirroring and bleed), `accepts`, `role`, `flow`, `max`, `anchor`, `arrangement` (`grid`, `row`, `column`, or `composition`), `content` (root block paths bound within `max`), `overflow` (paths drawn beyond it), `collapsed`, `bleed` and, on a bled region, `areaBox` (its cell before bleed). It replaces `slots` for templates. |\n| `headingAreas` | The `title` (and `subtitle`) areas with their boxes, `collapsed` or `implicit`. On a template without a `subtitle` area the title area carries `parts: { title, subtitle }`: it is split where the stacked heading group puts the subtitle, so `title` runs from the area\'s top to the bottom of the title (the tag above it included) and `subtitle` from the subtitle\'s top (half a gap under the title when the slide has none) to the area\'s bottom. PowerPoint\'s `ctrTitle` and `subTitle` placeholders use these boxes. |\n| `items[].region` | The region an item was drawn in, for tagging shapes. |\n| `items[].listColumns` | A list in columns: one `{ box, start, end, text }` per column at one shared size; numbering continues. The item\'s `text` is the whole list (every entry at its column\'s position), so a consumer that ignores `listColumns` still draws every item. Entry paths are the payload\'s: `<list>.<n>` for a string item, `<list>.<n>.text` for an object. |\n\n`composeLayoutAreas(record, options)` gives the areas of the empty layout: where a PowerPoint slide layout puts a placeholder\nper area. It is `composeSlide`\'s own geometry for a slide with a one-line title and subtitle and every region kept, so it\ntakes `composeSlide`\'s options (`width`, `height`, `presentation` for the deck\'s header and footer, `direction`, ...) plus\n`mirror`, `placements` (placed-image bands to reserve, `{ edge, size, inset }`) and `bleed` (default `true`: a bled region\nreaches the slide edge as on a slide; `false` gives its cell inside the content box). Its title area carries the same\n`parts` as a slide\'s. `bindRegions`, `layoutTemplate`, `regionAccepts` and `gridFlowShape` expose the rest of the model.\n\n## No migration from the 0.18 ids\n\n0.19 breaks layouts: there is no migration path. A layout id of the 0.18 default catalog that 0.19 does not have is an\nunknown layout like any other (`opf/unresolved-reference`: the slide composes automatically and a strict export fails).\nSix 0.18 ids are also 0.19 built-ins and resolve to them: `agenda`, `comparison`, `dashboard`, `faq`, `timeline` and\n`two-column`. The release notes list the 30 built-ins.\n'
|
|
212
214
|
},
|
|
213
215
|
{
|
|
214
216
|
"slug": "live-editor",
|
|
215
217
|
"file": "docs/live-editor.md",
|
|
216
218
|
"title": "Browser preview and live editing",
|
|
217
|
-
"markdown": "# Browser preview and live editing\n\nEditor 0.26.1 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n## Install the published packages\n\nUse Node 24 with Core, editor, PPTX, renderer and Gallery 0.26.1. This is the current published setup; the [quickstart](quickstart.md) is the shared command-line first run:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.26.1 @openpresentation/opf-render@0.26.1 @openpresentation/opf-editor@0.26.1 @openpresentation/opf-pptx@0.26.1 @openpresentation/gallery@0.26.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.20.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nThe editor exports in the browser (SVG through the renderer's browser entries, PPTX through opf-pptx). To export a saved presentation on a server, call `convert(\"deck.opf.json\", \"deck.pdf\")` (or `convert(presentation, { format: \"pdf\" | \"png\" | \"svg\" | \"pptx\" })` for the bytes) from `@openpresentation/opf` in Node, which draws with the same engines and the bundled fonts; `open(\"deck.pptx\")` reads a PowerPoint file back into a presentation for the editor ([OPF files in Node](node.md)).\n\nFor source builds and the Gallery host bundle, see [ecosystem development](ecosystem-development.md#editor-host-bundle-and-font-provenance).\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { saveDocument } from '@openpresentation/opf-editor';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-browser';\nimport { gallery } from '@openpresentation/gallery';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadFonts({ faces: [\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n] });\n\nconst container = document.querySelector('#slide');\nif (!(container instanceof HTMLElement)) throw new Error('Missing slide container.');\nconst canvas = createCanvasEditor(container, {\n presentation: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n catalogs: [gallery], // core ships no records: register the catalog the theme and font scheme above come from\n fonts,\n onCommit: ({ editor }) => {\n const { document: updatedOPF, unresolved } = saveDocument(editor);\n // Host owns storage. Surface unresolved catalog references before saving.\n if (unresolved.length) console.warn(unresolved);\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadFonts` accepts explicit font-file URLs or `Uint8Array` data (`faces`). It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces; the handle's `pending` and `ensure` load the script and vendored faces a document needs, and the canvas waits for them before it renders. Cross-origin font URLs need CORS access. Load fonts once and share the handle between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass the handle as `{ fonts }` to `toSvg` (`toSvg(deck, n, ...)` is slide n, `toSvg(deck, ...)` every slide); the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { toSvg } from '@openpresentation/opf-render/svg';\nconst svg = toSvg(canvas.editor.presentation, 1, { fonts, catalogs: canvas.editor.catalogs }); // slide 1: slides count from 1\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `toPng` and `toPdf`; those functions are not browser APIs.\n\n## Save, undo and host state\n\nBefore saving, exporting, importing or navigating, call `canvas.commit()`. A `false` result means the active draft could not be committed: keep it visible, show the error and stop that action. `canvas.editor.presentation` is the committed document, not necessarily the text currently being edited. A source editor must preserve invalid raw text separately instead of silently replacing it with the last valid preview.\n\n```js\nfunction documentForDownload() {\n if (!canvas.commit()) throw new Error('Finish or cancel the current edit first.');\n const result = saveDocument(canvas.editor);\n if (result.unresolved.length) throw new Error('Resolve catalog references before sharing.');\n return result.document; // Referenced catalog records are embedded; the session is unchanged.\n}\nconst unsubscribe = canvas.editor.subscribe(({ type, snapshot }) => {\n // Runs for patch, undo, redo, restore and catalog changes, not just canvas commits.\n console.log(type, snapshot.canUndo, snapshot.canRedo, snapshot.validation);\n});\n// Host teardown: unsubscribe(); canvas.destroy(); fonts.dispose();\n```\n\nUndo and redo belong to that same session (`canvas.editor.undo()` / `redo()`). Subscribe when synchronizing another control or a save indicator; `onCommit` alone does not cover external edits, undo or restore. Dispose subscriptions and canvases on unmount, and dispose a shared fonts handle only after its last consumer is gone.\n\nLocal persistence is optional. [`createPersistence` from `/persistence`](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/README.md#autosave-and-restore-rr-22) supports restore offers and explicit `flush()`. Commit the canvas yourself before an explicit save or flush; persistence stores the session, not an invalid raw source draft. Show unavailable-storage errors and retain a download path. Call `markSaved()` only after a real host save or OPF download; a preview or PowerPoint export does not preserve the complete OPF source. Destroy the persistence controller on teardown. It is not a server or collaboration service.\n\n## Export results and diagnostics\n\nIn a browser, `toSvg` returns SVG strings and `toPptx` returns bytes; pass the same `fonts` and `catalogs`, surface `onDiagnostic` notifications and catch rejected conversions. When importing with `fromPptx(bytes, { signals: true })`, inspect both `presentation` and `signals` before replacing the session. Native conversion can change unsupported features. In Node, Core `convert` instead returns `{ files, findings }`; [the Node guide](node.md) explains findings, errors and file publication. Do not treat a successful conversion as a claim of complete native fidelity.\n\n## Exact package references\n\n- [Editor session types](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/index.d.ts), [canvas lifecycle](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/canvas.d.ts), [portable saving](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/catalogs.d.ts).\n- [Renderer entries and optional peers](https://github.com/OpenPresentation/opf-render/blob/opf-render-v0.20.2/README.md#install-what-to-add-for-what-rr-63).\n- [PowerPoint export/import types](https://github.com/OpenPresentation/opf-pptx/blob/opf-pptx-v0.20.3/src/index.d.ts).\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. A timeline event's `status` (`done`, `current`, `planned`) is a menu, not free text. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `presentation`, plus `slideIndex`, `fonts` (the renderer's fonts handle), `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(presentation)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should embed the catalog records and assets they use. Inserting slides copies their records with core `copySlides`: groups match by `source`, an identical record is reused, a differing `custom` record is renamed `<id>-2` and a differing catalog revision moves into `custom` as `<id>-2`, so the inserted slides look the same and the existing slides stay intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.presentation, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.presentation, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.presentation before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.presentation }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst presentation = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `presentation`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.presentation, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.presentation, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.presentation with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{presentation, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
|
|
219
|
+
"markdown": "# Browser preview and live editing\n\nEditor 0.26.1 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n\n## The whole editor in one call\n\n`@openpresentation/opf-editor/workspace` mounts everything the sections below build by hand (the canvas, the source pane, the slide navigator and thumbnails, Present, Export, the sorter, the outline, Layout Studio, Review, Find and autosave) over one session and one fonts handle: `createOpfEditor(element, { document, catalogs, fonts })`. `@openpresentation/opf-editor/react` has `<OpfEditor>` and the hooks `useOpfEditor`, `useOpfDocument`, `useOpfSelection` and `useOpfCursor` for React 18 and 19. The chrome reads `--opf-ui-*` tokens that a `theme` option sets. The source pane tells the host where the caret is (`cursor` event: pointer, id path, selection), reveals a field without taking focus (`sourcePane.revealPath`) and draws the host's own marks (`sourcePane.setDecorations`). See [Embedding the editor](../packages/editor/docs/embedding.md) and [Theming the editor chrome](../packages/editor/docs/theming.md).\n\n## Install the published packages\n\nUse Node 24 with Core, editor, PPTX, renderer and Gallery 0.26.1. This is the current published setup; the [quickstart](quickstart.md) is the shared command-line first run:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.26.1 @openpresentation/opf-render@0.26.1 @openpresentation/opf-editor@0.26.1 @openpresentation/opf-pptx@0.26.1 @openpresentation/gallery@0.26.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.20.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nThe editor exports in the browser (SVG through the renderer's browser entries, PPTX through opf-pptx). To export a saved presentation on a server, call `convert(\"deck.opf.json\", \"deck.pdf\")` (or `convert(presentation, { format: \"pdf\" | \"png\" | \"svg\" | \"pptx\" })` for the bytes) from `@openpresentation/opf` in Node, which draws with the same engines and the bundled fonts; `open(\"deck.pptx\")` reads a PowerPoint file back into a presentation for the editor ([OPF files in Node](node.md)).\n\nFor source builds and the Gallery host bundle, see [ecosystem development](ecosystem-development.md#editor-host-bundle-and-font-provenance).\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { saveDocument } from '@openpresentation/opf-editor';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-browser';\nimport { gallery } from '@openpresentation/gallery';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadFonts({ faces: [\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n] });\n\nconst container = document.querySelector('#slide');\nif (!(container instanceof HTMLElement)) throw new Error('Missing slide container.');\nconst canvas = createCanvasEditor(container, {\n presentation: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n catalogs: [gallery], // core ships no records: register the catalog the theme and font scheme above come from\n fonts,\n onCommit: ({ editor }) => {\n const { document: updatedOPF, unresolved } = saveDocument(editor);\n // Host owns storage. Surface unresolved catalog references before saving.\n if (unresolved.length) console.warn(unresolved);\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadFonts` accepts explicit font-file URLs or `Uint8Array` data (`faces`). It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces; the handle's `pending` and `ensure` load the script and vendored faces a document needs, and the canvas waits for them before it renders. Cross-origin font URLs need CORS access. Load fonts once and share the handle between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass the handle as `{ fonts }` to `toSvg` (`toSvg(deck, n, ...)` is slide n, `toSvg(deck, ...)` every slide); the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { toSvg } from '@openpresentation/opf-render/svg';\nconst svg = toSvg(canvas.editor.presentation, 1, { fonts, catalogs: canvas.editor.catalogs }); // slide 1: slides count from 1\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `toPng` and `toPdf`; those functions are not browser APIs.\n\n## Save, undo and host state\n\nBefore saving, exporting, importing or navigating, call `canvas.commit()`. A `false` result means the active draft could not be committed: keep it visible, show the error and stop that action. `canvas.editor.presentation` is the committed document, not necessarily the text currently being edited. A source editor must preserve invalid raw text separately instead of silently replacing it with the last valid preview.\n\n```js\nfunction documentForDownload() {\n if (!canvas.commit()) throw new Error('Finish or cancel the current edit first.');\n const result = saveDocument(canvas.editor);\n if (result.unresolved.length) throw new Error('Resolve catalog references before sharing.');\n return result.document; // Referenced catalog records are embedded; the session is unchanged.\n}\nconst unsubscribe = canvas.editor.subscribe(({ type, snapshot }) => {\n // Runs for patch, undo, redo, restore and catalog changes, not just canvas commits.\n console.log(type, snapshot.canUndo, snapshot.canRedo, snapshot.validation);\n});\n// Host teardown: unsubscribe(); canvas.destroy(); fonts.dispose();\n```\n\nUndo and redo belong to that same session (`canvas.editor.undo()` / `redo()`). Subscribe when synchronizing another control or a save indicator; `onCommit` alone does not cover external edits, undo or restore. Dispose subscriptions and canvases on unmount, and dispose a shared fonts handle only after its last consumer is gone.\n\nLocal persistence is optional. [`createPersistence` from `/persistence`](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/README.md#autosave-and-restore-rr-22) supports restore offers and explicit `flush()`. Commit the canvas yourself before an explicit save or flush; persistence stores the session, not an invalid raw source draft. Show unavailable-storage errors and retain a download path. Call `markSaved()` only after a real host save or OPF download; a preview or PowerPoint export does not preserve the complete OPF source. Destroy the persistence controller on teardown. It is not a server or collaboration service.\n\n## Export results and diagnostics\n\nIn a browser, `toSvg` returns SVG strings and `toPptx` returns bytes; pass the same `fonts` and `catalogs`, surface `onDiagnostic` notifications and catch rejected conversions. When importing with `fromPptx(bytes, { signals: true })`, inspect both `presentation` and `signals` before replacing the session. Native conversion can change unsupported features. In Node, Core `convert` instead returns `{ files, findings }`; [the Node guide](node.md) explains findings, errors and file publication. Do not treat a successful conversion as a claim of complete native fidelity.\n\n## Exact package references\n\n- [Editor session types](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/index.d.ts), [canvas lifecycle](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/canvas.d.ts), [portable saving](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/catalogs.d.ts).\n- [Renderer entries and optional peers](https://github.com/OpenPresentation/opf-render/blob/opf-render-v0.20.2/README.md#install-what-to-add-for-what-rr-63).\n- [PowerPoint export/import types](https://github.com/OpenPresentation/opf-pptx/blob/opf-pptx-v0.20.3/src/index.d.ts).\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. A timeline event's `status` (`done`, `current`, `planned`) is a menu, not free text. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `presentation`, plus `slideIndex`, `fonts` (the renderer's fonts handle), `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(presentation)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should embed the catalog records and assets they use. Inserting slides copies their records with core `copySlides`: groups match by `source`, an identical record is reused, a differing `custom` record is renamed `<id>-2` and a differing catalog revision moves into `custom` as `<id>-2`, so the inserted slides look the same and the existing slides stay intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.presentation, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.presentation, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.presentation before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.presentation }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst presentation = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `presentation`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.presentation, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.presentation, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.presentation with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{presentation, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
|
|
218
220
|
},
|
|
219
221
|
{
|
|
220
222
|
"slug": "llm-authoring",
|
|
221
223
|
"file": "docs/llm-authoring.md",
|
|
222
224
|
"title": "Authoring OPF with an LLM",
|
|
223
|
-
"markdown": '# Authoring OPF with an LLM\n\nWrite a complete JSON document with `name` and `slides`. Put visible words in slide fields, not presentation metadata. Use `*.opf.json` filenames and stable, unique slide `id` values when a deck will be revised repeatedly.\n\n```json\n{\n "name": "Launch decision",\n "slides": [{\n "id": "recommendation",\n "title": "Launch to the pilot group first",\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n { "items": ["Validate onboarding", "Measure activation", "Fix the largest drop-off"] },\n { "metric": { "value": 200, "label": "Pilot customers" } }\n ],\n "notes": "Confirm the rollout owner and checkpoint date."\n }]\n}\n```\n\nChoose one content structure per slide:\n\n- Root payloads for a simple slide: `text`, `items`, `image`, `chart`, `table`, `code`, `metric`, `quote`, or `timeline`. Mark a roadmap\'s progress with `status` on the timeline events (`done`, `current`, `planned`); the engines draw it, so do not add color or schedule-health words to fake it.\n- `blocks` for a sequence that should reflow. Set `composition` only when an arrangement matters. Omit it to let the engine choose.\n- Promoted regions such as `left`, `center+right`, `top`, and `bottom` for spatially meaningful content. Regions must not overlap. Do not mix regions with root payloads or blocks.\n\nReference records by id from the pptx.gallery catalog (`gallery` of `@openpresentation/gallery`) or a catalog the host registers (`name:id` for a named group), and put the deck\'s own records in `catalogs.custom`. Embed what a deck uses before saving it (`embed`, `opf embed`), so it renders the same with no catalog registered. Do not invent an unresolvable layout or assume a network lookup will happen.\n\nA testimonial quote is `{ "quote": { "text": "...", "attribution": "Priya Raman", "role": "VP Operations, Acme", "photo": { "src": "asset:priya", "alt": "Priya Raman" } } }`: the attribution holds the name, `role` the title and organization, and `photo` a circular headshot that needs alt text.\n\nPick `bullets` for plain talking points and `items` for a list whose entries have the same shape or a supporting `description` (see [content payloads](content-payloads.md#items-versus-bullets)). Mark an inline code span with `{ "text": "pnpm install", "code": true }`, a stretch in another language with `lang`, lines of a code block with `code.highlight` (`[3, [5, 7]]`), and stamp a deck with `design.watermark` `{ "text": "DRAFT", "opacity": 0.1 }`.\n\nA headline or quote may carry inline formatting: `title`, `subtitle`, `tag` and `quote.text` accept a string or `TextRun[]`, for example `"title": ["Revenue grew ", { "text": "28%", "color": "accent1" }]` to color the word that carries the claim, or a `cite` marker on the claim. Use a plain string unless a run needs its own style.\n\nTables use `{ "columns": ["Category", "Value"], "rows": [["A", 10]] }`, with an optional `alt` sentence for the takeaway of a large table (`""` marks it decorative). Charts put a `type` and the same tabular structure inside `chart.data`, plus an `alt` sentence that says what the data shows. For an amount beside a rate, a `combo` chart draws columns with line series (`line`, default the last series) and can put a line on a secondary axis (`secondaryAxis`); see [chart-options.md](chart-options.md#combo-charts). Images use a source string or `{ "src": "...", "alt": "..." }`; use the top-level `assets` registry and `asset:<id>` references for reuse. The local renderer does not fetch remote sources.\n\n## Deck facts: built-in variables\n\nDo not retype the speaker, organization or deck name into slide text. Put them in the root `speaker`, `organization`, `name`, `description` and `author` once and reference them: `{{speaker.name}}`, `{{speaker.title}}`, `{{organization.name}}`, `{{organization.tagline}}`, `{{deck.name}}` inside any string, and `var:speaker.photo` as a whole image field. Logos go on the organization (`"logo": "./assets/acme-logo.svg"`, or `{ "full", "stacked", "icon", "wordmark" }` with `{ "onLight", "onDark" }` files) and are placed with `var:organization.logo.icon` (or `.wordmark`, `.stacked`, the bare full logo, `var:organization.<id>.logo` for a partner); covers draw the full logo by themselves. `{{speakers}}` joins every speaker name, and `speaker.<id>.<field>` / `organization.<id>.<field>` address one entry by id. Nothing about a speaker is drawn unless you reference it (a footer `text` of `{{speaker.name}}, {{speaker.title}}` is a reference too); the organization\'s tagline, legal name, domain, email and phone are likewise only available this way. A built-in whose source field is missing resolves to nothing and warns; an unknown path is a validation error. Speaker ids and organization ids must each be unique and `Speaker.organizationId` must name an organization. See [templates and variables](templates-and-variables.md#built-in-variables).\n\n## Revision loop\n\n1. Validate with `validate` (or `opf validate`). Fix error findings at their returned JSON paths, then read the warnings: references that resolve nowhere (`opf/unresolved-reference`), missing alt text, low contrast, overflow and placeholder text each have a rule id and a suggested fix ([validate](validate.md)).\n2. Render with `onDiagnostic` and inspect `text-overflow` / `small-cell` paths. Shorten text, reduce the number of blocks, change composition, or explicitly split the slide. Revalidate after edits.\n3. Use `composition.overflow: "error"` for a strict text-layout gate. It does not certify chart readability, font availability, or exact PowerPoint rendering.\n4. Inspect the actual preview and exported PPTX. Geometry is shared; font substitution and specialized objects can still differ. Previews draw an open look-alike where the license-restricted font cannot be bundled (metric-compatible where one exists, for example Carlito for Calibri; Intos for Aptos), but the exported PPTX keeps the font name the user selected.\n5. Apply focused JSON Patch edits through the editor session and retain undo history. Resolve stable slide IDs to current array indices before constructing patches; indices can change when slides are inserted or moved.\n\nPreserve factual content, sources, notes, and asset descriptions during layout repair. A fit diagnostic is a request to revise the slide; it is not permission to silently drop the end of a paragraph.\n\nSee [dynamic composition](dynamic-composition.md), [content payloads](content-payloads.md), and [design precedence](design-resolution.md).\n\nUse nested `blocks` to keep related content together. Put `composition` on the group to arrange its children, for example a column of evidence inside a row of sections. Read `composeSlide().groups` for group bounds and `items[].path` for precise leaf edits. Groups inherit readability constraints; splitting content into more levels does not make text smaller.\n\nUse `paginate(deck)` when a draft exceeds readable space. Review the returned ordinary OPF slides and source mappings before export. Pagination preserves source text exactly; it does not summarize or rewrite it. An atomic item that cannot fit produces a diagnostic for a targeted edit.\n'
|
|
225
|
+
"markdown": '# Authoring OPF with an LLM\n\nWrite a complete JSON document with `name` and `slides`. Put visible words in slide fields, not presentation metadata. Use `*.opf.json` filenames and stable, unique slide `id` values when a deck will be revised repeatedly.\n\n```json\n{\n "name": "Launch decision",\n "slides": [{\n "id": "recommendation",\n "title": "Launch to the pilot group first",\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n { "items": ["Validate onboarding", "Measure activation", "Fix the largest drop-off"] },\n { "metric": { "value": 200, "label": "Pilot customers" } }\n ],\n "notes": "Confirm the rollout owner and checkpoint date."\n }]\n}\n```\n\nChoose one content structure per slide:\n\n- Root payloads for a simple slide: `text`, `items`, `image`, `chart`, `table`, `code`, `metric`, `quote`, or `timeline`. Mark a roadmap\'s progress with `status` on the timeline events (`done`, `current`, `planned`); the engines draw it, so do not add color or schedule-health words to fake it.\n- `blocks` for a sequence that should reflow. Set `composition` only when an arrangement matters. Omit it to let the engine choose.\n- Promoted regions such as `left`, `center+right`, `top`, and `bottom` for spatially meaningful content. Regions must not overlap. Do not mix regions with root payloads or blocks.\n\nReference records by id from the pptx.gallery catalog (`gallery` of `@openpresentation/gallery`) or a catalog the host registers (`name:id` for a named group), and put the deck\'s own records in `catalogs.custom`. Embed what a deck uses before saving it (`embed`, `opf embed`), so it renders the same with no catalog registered. Do not invent an unresolvable layout or assume a network lookup will happen.\n\nA testimonial quote is `{ "quote": { "text": "...", "attribution": "Priya Raman", "role": "VP Operations, Acme", "photo": { "src": "asset:priya", "alt": "Priya Raman" } } }`: the attribution holds the name, `role` the title and organization, and `photo` a circular headshot that needs alt text.\n\nPick `bullets` for plain talking points and `items` for a list whose entries have the same shape or a supporting `description` (see [content payloads](content-payloads.md#items-versus-bullets)). Mark an inline code span with `{ "text": "pnpm install", "code": true }`, a stretch in another language with `lang`, lines of a code block with `code.highlight` (`[3, [5, 7]]`), and stamp a deck with `design.watermark` `{ "text": "DRAFT", "opacity": 0.1 }`.\n\nA headline or quote may carry inline formatting: `title`, `subtitle`, `tag` and `quote.text` accept a string or `TextRun[]`, for example `"title": ["Revenue grew ", { "text": "28%", "color": "accent1" }]` to color the word that carries the claim, or a `cite` marker on the claim. Use a plain string unless a run needs its own style.\n\nTables use `{ "columns": ["Category", "Value"], "rows": [["A", 10]] }`, with an optional `alt` sentence for the takeaway of a large table (`""` marks it decorative). Charts put a `type` and the same tabular structure inside `chart.data`, plus an `alt` sentence that says what the data shows. For an amount beside a rate, a `combo` chart draws columns with line series (`line`, default the last series) and can put a line on a secondary axis (`secondaryAxis`); see [chart-options.md](chart-options.md#combo-charts). Images use a source string or `{ "src": "...", "alt": "..." }`; use the top-level `assets` registry and `asset:<id>` references for reuse. The local renderer does not fetch remote sources.\n\n## Deck facts: built-in variables\n\nDo not retype the speaker, organization or deck name into slide text. Put them in the root `speaker`, `organization`, `name`, `description` and `author` once and reference them: `{{speaker.name}}`, `{{speaker.title}}`, `{{organization.name}}`, `{{organization.tagline}}`, `{{deck.name}}` inside any string, and `var:speaker.photo` as a whole image field. Logos go on the organization (`"logo": "./assets/acme-logo.svg"`, or `{ "full", "stacked", "icon", "wordmark" }` with `{ "onLight", "onDark" }` files) and are placed with `var:organization.logo.icon` (or `.wordmark`, `.stacked`, the bare full logo, `var:organization.<id>.logo` for a partner); covers draw the full logo by themselves. `{{speakers}}` joins every speaker name, and `speaker.<id>.<field>` / `organization.<id>.<field>` address one entry by id. Nothing about a speaker is drawn unless you reference it (a footer `text` of `{{speaker.name}}, {{speaker.title}}` is a reference too); the organization\'s tagline, legal name, domain, email and phone are likewise only available this way. A built-in whose source field is missing resolves to nothing and warns; an unknown path is a validation error. Speaker ids and organization ids must each be unique and `Speaker.organizationId` must name an organization. See [templates and variables](templates-and-variables.md#built-in-variables).\n\n## Revision loop\n\n1. Validate with `validate` (or `opf validate`); a deck is done only when `opf validate deck.opf.json --fail-on warning` passes, because an unknown layout id is only a warning. Fix error findings at their returned JSON paths, then read the warnings: references that resolve nowhere (`opf/unresolved-reference`), missing alt text, low contrast, overflow and placeholder text each have a rule id and a suggested fix ([validate](validate.md)).\n2. Render with `onDiagnostic` and inspect `text-overflow` / `small-cell` paths. Shorten text, reduce the number of blocks, change composition, or explicitly split the slide. Revalidate after edits.\n3. Use `composition.overflow: "error"` for a strict text-layout gate. It does not certify chart readability, font availability, or exact PowerPoint rendering.\n4. Inspect the actual preview and exported PPTX. Geometry is shared; font substitution and specialized objects can still differ. Previews draw an open look-alike where the license-restricted font cannot be bundled (metric-compatible where one exists, for example Carlito for Calibri; Intos for Aptos), but the exported PPTX keeps the font name the user selected.\n5. Apply focused JSON Patch edits through the editor session and retain undo history. Resolve stable slide IDs to current array indices before constructing patches; indices can change when slides are inserted or moved.\n\nPreserve factual content, sources, notes, and asset descriptions during layout repair. A fit diagnostic is a request to revise the slide; it is not permission to silently drop the end of a paragraph.\n\nSee [dynamic composition](dynamic-composition.md), [content payloads](content-payloads.md), and [design precedence](design-resolution.md).\n\nUse nested `blocks` to keep related content together. Put `composition` on the group to arrange its children, for example a column of evidence inside a row of sections. Read `composeSlide().groups` for group bounds and `items[].path` for precise leaf edits. Groups inherit readability constraints; splitting content into more levels does not make text smaller.\n\nUse `paginate(deck)` when a draft exceeds readable space. Review the returned ordinary OPF slides and source mappings before export. Pagination preserves source text exactly; it does not summarize or rewrite it. An atomic item that cannot fit produces a diagnostic for a targeted edit.\n'
|
|
224
226
|
},
|
|
225
227
|
{
|
|
226
228
|
"slug": "markdown",
|
|
@@ -228,6 +230,12 @@ var docsData = Object.freeze([
|
|
|
228
230
|
"title": "Markdown and outlines",
|
|
229
231
|
"markdown": '# Markdown and outlines\n\n`@openpresentation/opf/markdown` (RR-30) converts between OPF and a small, documented Markdown dialect, in both directions, with no renderer, fonts, network or model: the same input always gives the same output. The CLI exposes it through `opf convert` (`deck.md` or `deck.opf.md` to `deck.opf.json` and back; `opf from-md` and `opf to-md` before 0.18), and every command that reads or writes a deck understands a file ending `.opf.md` ([Markdown decks in every command](#markdown-decks-in-every-command)); `parse` and `stringify` read and write a deck in JSON, YAML or Markdown with one call, and `open`, `save` and `convert` of `@openpresentation/opf` do it for files in Node.\n\n- **JSON stays canonical.** The schema, the interchange form and every package describe the JSON. A `.opf.md` file is an authoring serialization of the same data, as a `.opf.yaml` file is ([OPF as YAML](yaml.md)).\n- **Write a deck as text.** YAML front matter holds the deck, `---` separates slides, `#` is the title, `##` the subtitle, and lists, quotes, tables, images, code, charts, metrics, timelines and speaker notes have their own syntax.\n- **Read a deck as text.** `toMarkdown` writes any valid OPF document as that dialect. What the dialect has no syntax for (a design, nested groups, a styled table cell) is embedded as YAML in a fenced block, so nothing is lost; or it is left out and reported, on request.\n- **Round trip.** The Markdown the writer produces converts back to the same deck and to itself, byte for byte. Markdown written by hand converts to a deck whose canonical Markdown is the same text, apart from layout the dialect treats as equivalent (the examples in `examples/markdown/` are canonical).\n- **Editors read it too.** `scanMarkdown` of `@openpresentation/opf/source` lists the regions of a text with the converter\'s own line rules (front matter, slides, `---` separators, options comments, fences with their kind, speaker notes), and `locate` and `pathAt` of the same module turn a JSON Pointer into a range of the Markdown (a slide\'s title, a list item, the `layout=` token of an options comment) and a caret into a path. The vocabulary (`SLIDE_OPTION_KEYS`, `BLOCK_OPTION_KEYS`, `FENCE_KINDS`, `METRIC_KEYS`, `SPAN_FLAGS`, `SPAN_VALUES`) is exported from the same module.\n- **Errors have places.** [Validation results](finding-schema-reference.md) carry the source offset, length, line and column of the Markdown that caused them, including OPF validation errors mapped back to the Markdown that produced the field.\n\n```js\nimport { fromMarkdown, toMarkdown } from "@openpresentation/opf/markdown";\n\nconst { presentation, valid, findings } = fromMarkdown(markdown);\nconst { markdown: text, report } = toMarkdown(presentation);\n```\n\n```sh\nopf convert deck.md deck.opf.json\nopf convert deck.opf.json deck.opf.md\nopf validate deck.opf.md\nopf convert deck.opf.md slides/deck.svg\n```\n\n## An example\n\n````md\n---\nname: Q3 Business Review\nlanguage: en-US\n---\n\n<!-- slide: id=cover layout=cover section=Overview -->\n# Q3 Business Review\n\n## Operations and growth\n\nNote: Two minutes on the agenda, then straight into the numbers.\n\n---\n\n# Revenue grew every quarter\n\n```chart column\nQuarter,Revenue\nQ1,12\nQ2,18\n```\n\n> We would rather spend a week on capacity than a month on an outage.\n> \u2014 Priya Raman, Head of Platform\n````\n\nconverts to\n\n```json\n{\n "name": "Q3 Business Review",\n "language": "en-US",\n "slides": [\n { "id": "cover", "layout": "cover", "section": "Overview", "title": "Q3 Business Review", "subtitle": "Operations and growth", "notes": "Two minutes on the agenda, then straight into the numbers." },\n {\n "title": "Revenue grew every quarter",\n "blocks": [\n { "chart": { "type": "column", "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] } } },\n { "quote": { "text": "We would rather spend a week on capacity than a month on an outage.", "attribution": "Priya Raman, Head of Platform" } }\n ]\n }\n ]\n}\n```\n\n[`examples/markdown/quarterly-review.opf.md`](../examples/markdown/quarterly-review.opf.md) uses every block kind. [`examples/markdown/outline.md`](../examples/markdown/outline.md) is a plain outline read with `split: "headings"`.\n\n## The dialect\n\n### Document\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Deck | YAML between a first line `---` and a closing `---` (or `...`) | Every deck property except `slides`: `name`, `description`, `language`, `design`, `variables`, `assets`, `extensions`, ... in the order written. JSON-compatible YAML only: no anchors, tags or duplicate keys. `slides` is not allowed |\n| Slides | Lines of three or more hyphens, outside fences, HTML comments and speaker notes\' fences | One slide per segment. Empty segments are skipped (a warning when between two slides); `<!-- slide -->` keeps an empty slide |\n| Outline | `split: "auto"` (the default) or `"headings"` (`--split headings`) | With no `---` line and two or more `# ` headings outside fences, comments and notes, each `# ` heading starts a slide, so a bare outline needs no `---`. `"headings"` cuts at every `# ` heading, even inside speaker notes; `"rules"` (`--split rules`) cuts only at `---`, so a second `# ` is a second title |\n\nA deck without front matter starts with its first slide, so a file never begins with `---` unless it opens the front matter. Line endings may be LF, CRLF or CR and a leading BOM is ignored. The converter adds nothing the Markdown does not say: no `$schema`, no `name` (pass `defaults: { name }` or `--title` for a fallback that the front matter overrides).\n\n### Slide\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Options | `<!-- slide: id=cover layout=cover section="Part 1" tag=NEW hidden beat=a beat=b type=chart -->` | `id`, `layout`, `section`, `tag`, `hidden` (`hidden=false` too), `beat` (repeat for several), `type`. Values are bare words or JSON strings. At most one per slide, anywhere in it; written first |\n| Title | `# Title` | `title`: a string, or `TextRun[]` when the line has [inline formatting](#inline-text). One per slide; closing `#` characters are dropped |\n| Subtitle | `## Subtitle` | `subtitle`, a string or `TextRun[]` like the title. One per slide |\n| Deeper headings | `### Text` | A bold paragraph, with a `heading-demoted` warning |\n| Notes | `Note: ...` or `Notes: ...` at the start of a line | `notes`: plain text, every following line of the slide verbatim (blank lines, `#`, `-` and fences included), ending at the next `---` |\n| Content | The blocks below, top to bottom | One block with no `id`/`type`/`region`: a root field (`text`, `items`, `chart`, ...). Otherwise `blocks` |\n| Embedded | A fenced `opf-slide` block of YAML | Extra slide fields merged in (`composition`, `design`, `extensions`, ...). A field set both here and in Markdown is an error |\n\n### Blocks\n\nBlocks are separated by blank lines; a fence, heading, quote, table, image line or comment also ends a paragraph or list without one. A comment `<!-- block: ... -->` directly before a block gives it options:\n\n| Option | Meaning |\n| --- | --- |\n| `id=...`, `type=...` | The block\'s `id` and explicit `type`. Such a block is always an entry in `blocks` |\n| `as=bullets` | Store a list as `bullets` instead of `items` |\n| `as=video` | Store an image line as `video` |\n| `region=top:left` | Put the block in that promoted region of the slide (`left`, `center+right`, `top+middle`, `middle+bottom:left+center+right`, ...) instead of the content |\n\n| Markdown | OPF field | Notes |\n| --- | --- | --- |\n| A paragraph | `text` | A soft line break is a space; a backslash at the end of a line is a hard break (`\\n`). Inline formatting becomes `TextRun[]`, otherwise a string |\n| `- item` (`*` and `+` too) | `items` | Nested by deeper indentation (two spaces when written) into `level`. An indented line ` : text` after an item is its `description`. A different marker character starts a new list (the writer alternates `-` and `*` for lists in a row). Numbered markers (`1.`) are read as bullets with a `numbered-list` warning: the dialect has no syntax for the `numbering` field of [numbered lists](numbered-lists.md), so numbers and styles are not kept (add `numbering` in an `opf-slide` or `opf-block` fence) |\n| `> text` and trailing dash lines | `quote` | `> \u2014 Name, Title` is the `attribution`, a second dash line the `source`. The rules (`\u2014`, `\u2013`, `--`, `~`, `-`; one short line; not a dashed list) are those of [content conversions](conversions.md), as is the code fence rule. A quote with a `role` or a `photo` has no Markdown form (a dash line holds a name and a title in one string, and a quote has no image syntax): it is written as an `opf-block`, which reads back exactly. Put `Name, Title` in the attribution line for a plain Markdown quote |\n| ```` ```lang title="file" ```` | `code` | `{ source, language, filename }`, or just the string when there is neither. Backtick or tilde fences, longer fences for code that contains fences |\n| `\\| a \\| b \\|` rows with a `\\| --- \\| --- \\|` row | `table` | The first row is `columns`; an all-empty first row means none. Cells are text with inline formatting, as in the [data import](data-import.md) (`007` and `2024` stay text); an empty cell is `null`. `\\|` is a pipe, `<br>` a line break. Alignment colons are ignored. Short and long rows warn. A table with an `alt` has no pipe-table syntax for it, so it is written as an `opf-block` and reads back exactly |\n| `` | `image` | `{ src, alt, title }`, or the string when there is only a source. `<src with spaces>` in angle brackets |\n| ```` ```metric ```` | `metric` | `key: value` lines: `value`, `label`, `description`, `unit`, `delta`, `trend`, `sentiment`. A plain decimal `value` or `delta` is a number; `"..."` is a JSON string; `unit: %` and `delta: +3 pts` are plain text |\n| ```` ```chart column ```` | `chart` | The word after `chart` is the chart `type`; an optional `alt="..."` after it is the chart\'s text alternative (`alt=""` marks it decorative; a quoted JSON string, as on the timeline fence). The body is CSV with a header row (the first column is the category labels and always text; in the other columns an unquoted decimal is a number, `true` and `false` are booleans, an empty field is `null` and a quoted field is text) or a JSON object holding `columns` and `rows` |\n| ```` ```timeline name="Roadmap" ```` | `timeline` | One event per line: `when \u2014 what` (spaced em dash), or the date forms of the conversions (`2026 Q1: Pilot`, `Jan - Kickoff`), or just `what`. A task-list prefix sets the event\'s `status`: `[x]` done, `[>]` current ("we are here"), `[ ]` planned. An indented line is the event\'s `description`. `name` and `description` go on the fence line |\n| ```` ```opf-block ```` | any block | A YAML `ContentPayload`: `id`, `type`, any content field, nested `blocks` groups |\n\n### Inline text\n\n| Syntax | Run |\n| --- | --- |\n| `**bold**`, `__bold__` | `bold` |\n| `*italic*`, `_italic_` | `italic` (`_` only at word edges: `snake_case` is text) |\n| `~~strike~~` | `strikethrough` |\n| `` `code` ``, ``` ``a ` b`` ``` | `code` (a backtick fence of any length; one space is trimmed from each side when both sides have one; the text is literal and other formatting wraps the span) |\n| `<u>`, `<sup>`, `<sub>` | `underline`, `superscript`, `subscript` |\n| `[text](url)`, `<https://...>` | `link` (a link title is ignored) |\n| `[text]{color=#B42318 size=24 font="Open Sans"}` | `color` (a hex colour, scheme slot or `var:name`), `fontSize`, `fontFamily`, `lang` (a BCP-47 tag); `bold italic underline strike sup sub` work inside the braces too |\n| `\\*` and any backslash before ASCII punctuation | The character itself |\n\nFlanking follows CommonMark, so `2*(3+4)*5` and `a * b` are text. A backtick that has no matching fence, or that is escaped with a backslash, is text. There is no raw HTML beyond the tags above and `<br>`, no entities (`&` is literal) and no setext headings or indented code. The title, the subtitle and a quote\'s text keep inline formatting (a string when nothing is formatted, `TextRun[]` otherwise); a quote with formatted text and no attribution stays `{ text: [...] }`, because the string shorthand is for plain text. A quote\'s attribution and source are plain strings and drop formatting with a `formatting-dropped` warning; backticks in them stay literal characters. `cite` and `footnote` have no inline syntax: a heading or quote that carries one is written in an `opf-slide` fence. Image alt text, code, metric values and timeline lines are read as written, without inline formatting.\n\n## Writing OPF as Markdown\n\n`toMarkdown(presentation, { unsupported })` first validates the presentation (`OPFMarkdownError`, `code: "invalid-document"`, when it is not valid OPF). Every part is written in the dialect and **read back before it is kept**: a part whose text could not be read back as the same value (a table cell that is not text, a title that ends in ` #`, a title or quote run with `cite` or `footnote`, notes with a `---` line, formatting next to punctuation that CommonMark cannot open) takes the fallback instead of being written wrongly.\n\n| `unsupported` | Behaviour |\n| --- | --- |\n| `"embed"` (default) | The part is written as YAML: a block in an `opf-block` fence, slide fields (and any region that is not one plain block) in an `opf-slide` fence. The result converts back to the same deck |\n| `"drop"` | The part is left out and listed in `report.loss`. For Markdown to read, not to convert back |\n\nThe report is `{ lossless, native, embedded, loss }`: `native` is true when the whole document is plain dialect syntax, `embedded` lists the JSON Pointer and reason of each YAML part, `loss` is empty unless `unsupported` is `drop`. The 126 example decks need YAML for fewer than 5% of their slides (36 of 805): table cells that are numbers, booleans or styled, video assets with a `description`, nested groups, `href` runs.\n\nThe writer\'s canonical form: front matter, then slides joined by a blank line, `---` and a blank line; the options comment directly above what follows it; one blank line between blocks; two lists in a row alternate `-` and `*` so they stay two blocks. A round trip may collapse equivalent forms:\n\n- `blocks` with one plain block becomes the root field; a root with several content fields becomes `blocks` in key order.\n- Shorthand and object forms collapse to the shorter one: a metric with only `value`, a code object with only `source`, an image with only `src`, a quote with only `text`, a timeline array with no name, a list item with level 0 and no description.\n- Adjacent runs with the same formatting merge and `false` flags drop.\n\n## Findings\n\n`fromMarkdown(markdown, options)` never throws for malformed content. It returns the deck with the [validate](validate.md) report of it, `{ presentation, valid, findings, counts, schemaValid, checks }` (plus `template` and `unfilledVariables` for a deck with content variables), as `fromYaml` does; `presentation` is a best effort when `valid` is false and `schemaValid` is `null` when the check did not run (`validate: false`, or no slides). Each finding is a [Finding](finding-schema-reference.md) (`ruleId`, `severity`, `category` `format`, `path`, `scope`, `message`, `help`) plus `location` (`offset`, `length` in UTF-16 units, one-based `line` and `column`). Rule ids starting `markdown/` come from the Markdown; ids starting `opf/` are the [`validate`](validate.md) findings of the converted deck, located by the Markdown of the field they name. Options: `split` (`"rules"` or `"headings"`), `defaults`, `validate` (`true`, the default, checks `format` and `references`; a `ValidateOptions` object such as `{ only: ["content"] }` picks other rules; `false` skips the check).\n\nErrors: `front-matter`, `front-matter-not-mapping`, `front-matter-unterminated`, `front-matter-slides`, `no-slides`, `options-syntax`, `options-unknown-key`, `options-value`, `options-duplicate`, `options-orphan`, `options-trailing`, `options-embedded`, `comment-unterminated`, `duplicate-title`, `duplicate-subtitle`, `empty-heading`, `empty-quote`, `image-source`, `fence-unterminated`, `code-fence`, `chart-type`, `chart-attributes`, `chart-data`, `metric-block`, `timeline-attributes`, `timeline-description`, `timeline-events`, `opf-slide`, `opf-slide-not-mapping`, `opf-block`, `opf-block-not-mapping`, `slide-property-conflict`, `span-attributes`. Warnings: `front-matter-comments`, `empty-slide`, `heading-demoted`, `numbered-list`, `table-ragged`, `chart-ragged`, `formatting-dropped`.\n\nThe YAML of the front matter and of `opf-slide` and `opf-block` fences is read as `yaml/syntax` is (a tab, a quote or bracket never closed and the like are named in plain words, at the place that matters). `markdown/options-unknown-key` and `markdown/chart-type` carry `suggestions` (the near-miss option names, the chart types), which an editor offers as `Rename to layout` or `Insert column`. `describeFinding(finding)` (`@openpresentation/opf/source`) turns every finding into `{ message, next }`, and `explain(ruleId)` says what a rule checks and why.\n\n## Command line\n\n```text\nopf convert <deck.md|deck.opf.md|-> <output.opf.json|output.opf.yaml|-> [--from md] [--to json|yaml] [--split <auto|rules|headings>] [--title <text>] [--force] [--fail-on <level>]\nopf convert <deck.opf.json|-> <output.opf.md|-> [--to md] [--drop-unsupported] [--force] [--fail-on <level>]\n```\n\nA plain `.md` input is read as OPF Markdown (only on `opf convert`\'s input side; a Markdown output is named `.opf.md`). Output `-` writes stdout (with `--to`) and the JSON report goes to stderr. Exit codes follow the other commands: 0 success, 1 a deck that is not valid OPF, an output conflict or a `--fail-on` failure, 2 usage, a Markdown syntax error (with the located `findings`), or I/O. Nothing is written on a failure. A part the dialect has no syntax for is embedded and counted as a warning (`markdown/embedded`; `markdown/dropped` with `--drop-unsupported`), so `--fail-on warning` keeps a deck inside the plain dialect. `--split` and `--title` apply to a Markdown input and `--drop-unsupported` to a Markdown output; anywhere else they are usage errors. `opf from-md` and `opf to-md` were removed in 0.18.\n\n## Markdown decks in every command\n\nEvery command that reads a deck reads Markdown, through one reader, as it reads [YAML](yaml.md#yaml-everywhere):\n\n1. A file whose name ends `.opf.md` (in any case) is a Markdown deck. **Only `.opf.md` counts**: a plain `.md` file is never taken for a deck, so a README or notes file is never converted by accident (a plain `.md` needs `--from md`, or convert it with `opf convert notes.md deck.opf.json`, or name it `.opf.md`).\n2. Anything else (stdin and every other name) is JSON when the text starts with `{` or `[`; otherwise `--from md` (`markdown` is an alias; or `--from yaml`) says what it is, and without it the command exits 2 and asks for it. `--from` wins over a name.\n3. The Markdown is converted with the CLI\'s registered catalogs (the default catalog), so the references check of `validate` and `convert` resolves `name:id` references as it does for JSON. A Markdown syntax or conversion error exits **2**, like invalid JSON, and the message carries the position: `Invalid Markdown in deck.opf.md at line 1, column 1: The front matter starting on line 1 is not closed. [markdown/front-matter-unterminated]`, with the located `findings` and `code: "invalid-markdown"` in the JSON error report. `opf validate` instead reports it as a `markdown/<rule>` finding and exits 1. A deck that is valid Markdown but not valid OPF is reported by the command as it is for JSON, and `validate` and `convert` locate every finding in the Markdown (`deck.opf.md:3:1 error opf/schema ...`). A JSON Patch is JSON or YAML, never Markdown.\n\nA deck is written as Markdown when the output name ends `.opf.md` or `--to md` (or `--to markdown`) is given; `--to json` and `--to yaml` force the others. When the output is stdout or has another name, the format of the deck that was read is used: a Markdown deck is edited, merged, filled, paginated and embedded back to Markdown, in place (`-i`) or to stdout. `create` has no deck input and defaults to JSON. In `fill` and `ingest`, `--data-format csv|tsv|json` names the data format and `--to` the output.\n\n```sh\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md # canonical Markdown: toMarkdown(fromMarkdown(text))\nopf format deck.opf.md --check # exit 1 if the file would change\nopf convert deck.opf.json deck.opf.md # convert; - --to md for stdout\nopf paginate deck.opf.md deck.paginated.opf.md\nopf validate - --from md < deck.txt\n```\n\n`opf format` rewrites a Markdown deck to the canonical form the writer produces, so the layout the dialect treats as equivalent (`*` and `-` bullets, extra blank lines, spacing in front matter) is normalized. Anything the dialect has no syntax for is carried in an `opf-slide` or `opf-block` fence, so a command that rewrites a deck can move content into a fence that was not there: for example an `edit` that adds `design` to a slide. The CLI prints `warning: <file> has N more opf-slide/opf-block fences than before: ...` on stderr when that happens (it does not fail, nothing is lost, and a deck that already had its fences does not warn). The `<!-- -->` comments of the source are not preserved beyond the slide and block options the dialect reads.\n\nIn code, `parse(text, { format, filename, catalogs })` and `stringify(presentation, { format | filename })` (the root and `@openpresentation/opf/deck`) read and write the three forms, and in Node `@openpresentation/opf` reads and writes the files (`open`, `save`, `convert("deck.opf.json", "deck.opf.md")`): see [Reading and writing a deck in any form](validate.md#reading-a-deck-in-any-form).\n\n## Decisions and limits\n\nThese choices are the defaults of the module and can be changed.\n\n- **A dialect, not all of Markdown.** The block and inline syntax is the subset above with CommonMark\'s rules where they overlap. Setext headings, indented code, nested block quotes, raw HTML, reference links, footnotes and task lists are not part of it. Text the writer produces is escaped so it reads back as text.\n- **One YAML reader.** Front matter and the `opf-slide` and `opf-block` fences follow the same rules as a whole deck written as YAML ([OPF as YAML](yaml.md)) and use the `yaml` package (ISC, no dependencies, YAML 1.2 core schema: `2026-10-01` and `yes` stay strings). Metric blocks, timelines and chart CSV have small readers of their own because hand-written values such as `unit: %` are not valid YAML.\n- **Slide options live in HTML comments**, so any other Markdown viewer shows the slide content and ignores them.\n- **The pptx.dev Markdown view** targets the pre-v1 `elements` model: it starts a slide at each `##` heading and writes `{id=...}` attribute lines. This module keeps its front matter and its escape-hatch fences (`opf-slide`, `opf-block`) and replaces the rest, because the v1 slide is a title, a subtitle and content that maps one-to-one onto `#`, `##` and blocks.\n- **Templates.** A [template](templates-and-variables.md) is an ordinary deck here: `template: true` and `variables` are front matter, and placeholders such as `{{client}}` and `var:logo` fields are plain text. Convert, then fill with `opf fill`.\n- **Not yet in the dialect.** `numbering` on lists ([numbered lists](numbered-lists.md): the schema and the renderers have it, but the dialect does not read `1.` markers as numbering; they become bullets with a warning, and a `numbering` field in a deck is written in an `opf-slide` or `opf-block` fence and round trips), footnotes, citations and captions (RR-34), video with a description, and per-slide `design`, `composition` and nested groups (all of these embed as YAML and round trip).\n'
|
|
230
232
|
},
|
|
233
|
+
{
|
|
234
|
+
"slug": "mcp",
|
|
235
|
+
"file": "docs/mcp.md",
|
|
236
|
+
"title": "OPF MCP server",
|
|
237
|
+
"markdown": '# OPF MCP server\n\n`@openpresentation/mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server for OPF, so an agent or editor that speaks MCP can learn the format, look things up and check a deck without shelling out and parsing text. It teaches OPF (documentation, schemas, the default catalog, example decks and the six agent skills, all as resources, with a `start_here` tool that gives a path for a goal), and it runs the checks the CLI runs (`validate_deck`, `convert`).\n\nIt keeps no state, reads no file (everything it serves is bundled in the package, and tools work on the text you pass in), fetches nothing and has no network access. It runs two ways: locally over stdio (this page), and hosted on an edge runtime (the same server definition over Streamable HTTP; a hosted URL at `openpresentation.org/mcp` is coming). The local server is the only one that can measure layout with the real fonts, when `@openpresentation/opf-render` is installed.\n\n## Set it up\n\nAny MCP client that starts a local server takes a command and arguments:\n\n```json\n{\n "mcpServers": {\n "opf": { "command": "npx", "args": ["-y", "@openpresentation/mcp"] }\n }\n}\n```\n\nPin a version (`@openpresentation/mcp@0.27.0`) to make the setup repeatable. Node 22 or later is required.\n\n| Client | How |\n| --- | --- |\n| Claude Code | `claude mcp add opf -- npx -y @openpresentation/mcp` (add `--scope project` to write `.mcp.json` for the team) |\n| Cursor | Add the JSON above to `.cursor/mcp.json` in the project, or to `~/.cursor/mcp.json` |\n| Claude Desktop | Add the JSON above to `claude_desktop_config.json` |\n| Any other client | Run `npx -y @openpresentation/mcp` as a stdio server (installed, the binary is `opf-mcp`) |\n\nCheck it with the official inspector: `npx @modelcontextprotocol/inspector npx -y @openpresentation/mcp`.\n\n`opf mcp` of the CLI is a convenience: it runs this server when `@openpresentation/mcp` is installed beside the CLI or in the working directory, and otherwise prints `npx -y @openpresentation/mcp` and exits with 2. The CLI itself carries no MCP code.\n\n## Tools\n\nEvery tool takes JSON arguments and returns structured JSON (`structuredContent`, with the same JSON as text for clients that read only text). A failure comes back with `isError: true` and a text report `{ ok: false, code, error, ... }` (no structured content), with a stable `code` (`unknown-id`, `invalid-document`, ...) and, where it helps, a suggestion ("Did you mean two-column?"). All tools are read-only.\n\n| Tool | Arguments | Returns |\n| --- | --- | --- |\n| `start_here` | `goal?` (what you are trying to do, in your own words) | `{ goal, summary, steps[], docs[], skills[], examples[], layouts[], themes[], validate }`: a short ordered path. Which documentation pages and skill to read, two or three example decks, layouts and themes from the catalog, and the validate step (`validate_deck` with `failOn: "warning"`). With no goal, the general learning path. Everything it names is a resource URI or a tool to call next. |\n| `validate_deck` | `document` (a JSON object, or deck text), `format?` (`json`, `yaml`, `markdown`; default JSON), `failOn?` (`error`, `warning`, `info`; default `error`) | `{ ok, valid, schemaValid, failOn, format, counts, findings[], checks, hint? }`. The findings are the ones `opf validate` reports (rule id, severity, category, JSON Pointer `path`, message, `help`, `fixes`, and a `location` with line and column for JSON, YAML and Markdown text). `ok` is false when a finding is at or above `failOn`. It does not look for local image files (it reads none). |\n| `get_schema` | `name?` (a schema such as `presentation` or `layout`, or a `$defs` name such as `Chart`), `pointer?` (a JSON Pointer into the schema, or a `$defs` name) | `{ name, pointer, id, schema, opfVersion }`, like `opf schema`. `$ref`s stay as written. A whole schema names the `@openpresentation/opf` release it came from in its `$comment`; a part is as it is. |\n| `search_catalog` | `kind` (`layouts`, `themes`, `color-schemes`, `font-schemes`, `narratives`, `audiences`, `purposes`, `tones`, `chart-types`, `languages`, `social-platforms`, `examples`), `query?`, `limit?` | `{ kind, total, returned, records[] }`: short records (id, name, summary, description, tags), best match first. |\n| `get_catalog_record` | `kind`, `id` | `{ kind, id, uri, record }`: the record in full, like `opf catalog <kind> <id>`. |\n| `convert` | `document`, `from?`, `to` (`json`, `yaml`, `markdown`) | `{ ok, from, to, text, findings, counts }`, like `opf convert`. The deck must be valid; otherwise nothing is converted and the findings are returned. Text formats only: PDF, PPTX, PNG and SVG need the local `opf convert`. |\n| `search_docs` | `query`, `limit?` | `{ total, results[] }`: ranked pages and skills, each with `uri`, `title`, the `heading` the snippet sits under, a `snippet` and a `score`. |\n| `list_examples` | `category?`, `query?` | `{ total, categories, examples[] }`: slug, name, category, slide count and `uri` for each bundled example deck. |\n\n## Resources\n\nResources have stable URIs. The server lists them (`resources/list`) and also offers URI templates (`resources/templates/list`) so a client can read a URI it builds itself.\n\n| URI | What it is |\n| --- | --- |\n| `opf://docs/index` | Every documentation page and skill, with its URI |\n| `opf://docs/whats-new` | The most recent release notes: the last five versions of the changelog of `@openpresentation/opf` (the format, the validators and the engine), newest first, as text |\n| `opf://docs/<slug>` | A documentation page as Markdown: `format-card` (the whole authoring surface on one page, sized for pasting into a model\'s context), `how-opf-works`, `schema-reference`, `validate`, `cli`, and the other pages meant for people who use OPF. Handoff, evidence, status and release notes are not served. |\n| `opf://schema/<name>` | A JSON Schema (`opf://schema/presentation`) |\n| `opf://schema/<name>#<pointer or $defs name>` | Part of a schema: `opf://schema/presentation#Chart`, `opf://schema/presentation#/properties/slides`. `opf://schema/Chart` also works. |\n| `opf://examples/<slug>` | A valid example deck as JSON |\n| `opf://catalog/<kind>/<id>` | A record of the default catalog (`@openpresentation/gallery`): `opf://catalog/layouts/two-column` |\n| `opf://skills/<name>` | A skill entrypoint (`opf-author`, `opf-layout`, `opf-presets`, `opf-edit`, `opf-export`, `opf-inspect`) |\n| `opf://skills/<name>/<path>` | A reference or asset of a skill: `opf://skills/opf-author/references/content.md` |\n\nSkill scripts and agent manifests are not served; the server runs no code from them.\n\n## Prompts\n\n| Prompt | Arguments | Use |\n| --- | --- | --- |\n| `learn_opf` | none | Orient an agent that has not used OPF; includes the format card |\n| `author_deck` | `brief`, `audience?`, `slides?` | Write a deck from a brief, then validate and fix it; includes the format card and the `opf-author` skill |\n| `fix_validation_findings` | `document`, `format?` | Repair a deck that does not validate with the smallest change; includes the `opf-inspect` skill |\n| `choose_layout` | `content`, `intent?` | Pick a layout from the content a slide carries; includes the `opf-layout` skill |\n\n## A typical session\n\n1. The agent calls `start_here { goal: "a board update with a revenue chart" }` (or reads `opf://docs/format-card`, or runs the `learn_opf` prompt).\n2. It reads what the path names, finds layouts with `search_catalog { kind: "layouts", query: "two columns" }` and reads one with `get_catalog_record`.\n3. It writes the deck and calls `validate_deck` with `failOn: "warning"`; each finding names a rule, a JSON Pointer and a hint. It fixes the findings and repeats until `ok` is true. A field it is unsure of comes from `get_schema { name: "Chart" }`.\n4. It uses `convert` to hand the deck over as YAML or Markdown. Rendering and PPTX export are outside this server: the local `opf convert deck.opf.json deck.pdf` (or `.pptx`) does them.\n\nValidation checks the file. It does not render the deck, so it is not proof of how the deck looks.\n\n## Host it\n\nThe package is a library as well as a command. Its entries:\n\n| Entry | What it is |\n| --- | --- |\n| `@openpresentation/mcp` | `createOpfServer(options)`: the server with the bundled docs, examples, skills and release notes. Edge-safe. |\n| `@openpresentation/mcp/server` | `createOpfMcpServer(options)`: the same server with no bundled data; you pass `sources`. Edge-safe, and small. |\n| `@openpresentation/mcp/stdio` | `runStdio()`: the Node stdio wrapper that `opf-mcp` and `opf mcp` run. Uses Node. |\n| `opf-mcp` (bin) | The stdio server; `npx -y @openpresentation/mcp` runs it. |\n\nThe two library entries import no Node builtin and read no file; documentation, examples, skills and release notes are bundled data; validation uses the precompiled validators (`@openpresentation/opf/precompiled`), so they run where code generation from strings is refused, such as Cloudflare Workers (the tests bundle them the way Wrangler does and run them in workerd):\n\n```ts\nimport { createOpfServer } from "@openpresentation/mcp";\nimport { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";\n\nexport default {\n async fetch(request: Request): Promise<Response> {\n const server = createOpfServer({ version: "0.27.0" });\n const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });\n await server.connect(transport);\n return transport.handleRequest(request);\n },\n};\n```\n\n`createOpfServer({ version, opfVersion?, sources?, checkText? })` returns an SDK `McpServer`. `sources` replaces any of the bundled `docs`, `examples`, `skills` and `whatsNew`. `checkText` replaces the check behind `validate_deck`; `opf-mcp` passes the one that measures layout with the fonts `opf convert` draws with. Without it, layout is estimated and the report\'s `hint` says so.\n\nThe bundled data is most of the size of a Worker bundle (about 12 MB minified, 5.6 MB gzipped, of which the example decks are 7 MB and the documentation 1.4 MB). A host that must stay small imports `createOpfMcpServer` from `@openpresentation/mcp/server`, which bundles none of it (about 3.5 MB minified, 0.7 MB gzipped), and takes `sources: { docs, examples, skills, whatsNew? }` from wherever it keeps them (KV, R2, static assets): `docs` is `[{ slug, title, markdown }]`, `examples` is `[{ slug, category, file, deck }]`, `skills` maps a skill name to `{ "SKILL.md": text, ... }`. `defaultSources()` and `DOC_SLUGS` (the pages served) are exported from `@openpresentation/mcp`.\n\n## Limits\n\n- Text formats only for `convert`. Rendering and PowerPoint need the local `opf` command, which can write files.\n- `validate_deck` reads no files, so a local image path in a deck is not looked up (the CLI warns with `cli/asset-missing`). Pass images as data URIs or assets.\n- Layout is measured with real fonts only under `opf-mcp` with `@openpresentation/opf-render` installed; `checks.layout` says `measured` or `estimated`.\n'
|
|
238
|
+
},
|
|
231
239
|
{
|
|
232
240
|
"slug": "native-content-release-2026-09-09",
|
|
233
241
|
"file": "docs/native-content-release-2026-09-09.md",
|
|
@@ -250,13 +258,13 @@ var docsData = Object.freeze([
|
|
|
250
258
|
"slug": "node",
|
|
251
259
|
"file": "docs/node.md",
|
|
252
260
|
"title": "OPF files in Node: `@openpresentation/opf`",
|
|
253
|
-
"markdown": '# OPF files in Node: `@openpresentation/opf`\n\n`@openpresentation/opf` is one import for every runtime. In Node (and Bun and Deno) it is core plus the file API: it converts,\nopens and saves OPF files, and draws them as PDF, PNG, SVG and PPTX through the optional engines. In a browser or a worker the\nsame import gives the browser-safe build, with the same names, where only the file functions refuse.\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf");\nconst deck = await opf.open("deck.opf.md");\ndeck.slides.push({ title: "Q4" });\nawait opf.save(deck, "deck.opf.md");\nconst { files } = await opf.convert(deck, { format: "pptx" }); // bytes, nothing written\n```\n\nThe namespace is all of core (`validate`, `parse`, `stringify`, `paginate`, `parseSlideSelection`, the types and the error\nclasses), so one import covers an application. The pptx.gallery catalog is the package `@openpresentation/gallery` (`gallery`, a\ndependency of core, RR-78); in Node, `open`, `save` and `convert` register it when no `catalogs` are passed. A gallery that targets\nanother catalog record schema than core reads (`CATALOG_SCHEMA`) is refused with `gallery-schema-mismatch`.\n\n## The CLI\'s verbs, with the CLI\'s defaults\n\nOPF 0.18 gives the Node build the verbs of the `opf` command, so a script calls them instead of spawning `opf`:\n\n| Function | Command | In Node, when `catalogs` is omitted |\n| --- | --- | --- |\n| `validate(deck, options?)` | `opf validate` | the gallery is registered, so `opf validate deck` and `validate(deck)` report the same findings |\n| `stats(deck, options?)` | `opf stats` | the gallery |\n| `paginate(deck, options?)` | `opf paginate` | the gallery. Pass `fonts` (opf-render\'s `loadFonts()`) for measured page breaks; without it the result says `layout: "estimated"`. `opf paginate` measures with the office pack, prepared once per process, as `convert` does for an export with `paginate: true` |\n| `embed(deck, options?)` | `opf embed` | the gallery |\n| `edit(deck, patch, options?)` | `opf edit` | the gallery for the check. Applies a JSON Patch whole or not at all (`OPFPatchError`), checks the result (`OPFValidationError`; `validate: false` skips it) and returns `{ presentation, findings, inverse }` |\n| `fill(template, data?, options?)` | `opf fill` | no catalog: `data` is CSV, TSV or JSON text, a record or a list of records; returns `{ decks, complete, unfilled, diagnostics }`, and `presentation` with `combine: true`. `fillRecords(text)` reads the records alone |\n| `diff(a, b)`, `merge(base, ours, theirs)` | `opf diff`, `opf merge` | no catalog |\n\nAn explicit `catalogs: []` registers none. The browser build exports the same functions and registers no catalog (FA-21): a\nbrowser host passes `{ catalogs: [gallery] }` itself (`import { gallery } from "@openpresentation/gallery"`). Only `open`, `save` and `convert` are Node only.\n\nA shorter name is available as an npm alias, if you want one: `npm i opf@npm:@openpresentation/opf` installs the same package\nas `opf`, so `import * as opf from "opf"` works too. The docs keep the full name.\n\n## One import for every runtime\n\nOPF 0.18 removed the `@openpresentation/opf/node` subpath of 0.17: import `@openpresentation/opf` instead, with the same names\n(OPF 0.19 removed `@openpresentation/opf/catalog` and `defaultCatalog`, with no alias: import `gallery` from\n`@openpresentation/gallery`).\nThe package\'s conditional exports pick the build:\n\n| Condition | Build | `open`, `save`, `convert` |\n| --- | --- | --- |\n| `bun`, `deno`, `node` | the full build (`dist/index.js`) | work on files, and `convert` also returns bytes |\n| `workerd` (Cloudflare), `worker`, `browser`, `default` | the browser-safe build (`dist/browser.js`) | reject with `OPFApiError` code `node-only` |\n\n- **One type surface.** Both builds share `dist/index.d.ts` (the `types` condition, first in the map), so TypeScript sees the\n same declarations under `NodeNext` and `Bundler` resolution; the file functions are documented there as Node only. The\n declarations need no Node types.\n- **The browser-safe build** imports no Node builtin, no file system and neither engine (`scripts/check-browser-safe.mjs`\n bundles it for the browser and worker conditions). Its `node-only` messages name the alternative: `parse(text, { filename })`\n for `open`, `stringify(deck, { filename })` for `save`, and for `convert` the deck forms through `parse` and `stringify` and\n drawing through opf-render\'s `/export-browser` entry. The in-memory `convert(deck, { format })` is Node only in 0.18 too; a\n later release adds a browser implementation through `/export-browser`.\n- **The Node build** loads its file engine on the first file call, so importing the root costs no more than core.\n- **Bundlers** pick the browser build for a browser target and the full build for a Node target. A bundler that sets neither\n condition gets `default`, the browser-safe build: for a Node bundle, add the `node` condition (Rollup\'s node-resolve\n `exportConditions: ["node"]`, esbuild\'s `platform: "node"`).\n\n## The stack\n\nOPF is six packages. **Core** (`@openpresentation/opf`) is the format and its API: schemas, types, `validate`, `parse` and\n`stringify`, composition and pagination, and in Node the files. **The gallery** (`@openpresentation/gallery`) is the pptx.gallery\ncatalog, a dependency of core released on its own version line. **The CLI** (`@openpresentation/cli`) is the `opf` command;\nits `convert` command runs core\'s Node engine (through `@openpresentation/opf/internal/engine`,\nwhich the package exports under the `node` condition for the CLI only; it is not an application API). **opf-render**,\n**opf-pptx** and **opf-editor** are the engines: drawing (SVG, PNG, PDF), PowerPoint (export and import) and the editor.\n\n## Install\n\n```sh\nnpm install @openpresentation/opf @openpresentation/opf-render @openpresentation/opf-pptx \\\n @resvg/resvg-js sharp pdf-lib \\\n @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 \\\n @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 \\\n @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2\n```\n\n`@openpresentation/opf-render` and `@openpresentation/opf-pptx` are optional peer dependencies of core: npm does not install them\nfor you, and an application that only reads, validates or converts between deck forms needs neither. Core loads them the\nfirst time a call needs them, from core\'s own install location and then from the working directory; a missing one throws\n`peer-not-installed` with the install command. What each output needs is listed in [the CLI reference](cli.md#install): PNG\nneeds `@resvg/resvg-js` and `sharp`, raster PDF `pdf-lib`, pictures in a PDF `sharp`, every format the office font pack, and\n`.pptx` input or output `@openpresentation/opf-pptx`.\n\n## `convert(input, output, options?)`\n\n```ts\nawait opf.convert("deck.opf.md", "deck.pdf");\nawait opf.convert("deck.opf.md", "deck.pptx");\nawait opf.convert("deck.opf.md", "slides/deck.png"); // one PNG per slide: slides/deck-1.png, -2, ...\nawait opf.convert("deck.opf.md", "slides/slide-{n}.png"); // always numbered: slides/slide-1.png, slide-2.png, ...\nawait opf.convert("deck.pptx", "deck.opf.yaml"); // import\nawait opf.convert("deck.opf.json", "deck.opf.md"); // change the deck form\nawait opf.convert("deck.opf.md", "deck.png", { slides: "1-3", scale: 2 });\nawait opf.convert("deck.opf.md", "slides.zip", { format: "svg" });\n```\n\n`input` is a file path, a presentation object or the bytes of a `.pptx` file (`Uint8Array`). The formats come from the names:\n\n| Input | Read as |\n| --- | --- |\n| `.pptx`, PPTX bytes | imported through opf-pptx |\n| `.opf.md` | a Markdown deck |\n| `.yaml`, `.yml` | a YAML deck |\n| `.json` | a JSON deck |\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide, named after the output: `slides/deck.png` gives `slides/deck-1.png`, `-2`, ... (the numbers are the deck\'s slide numbers, padded with zeros to the width of the largest number written: `deck-01.png` from ten slides, `deck-001.png` from a hundred; `slides: "2,5"` gives `deck-2.png` and `deck-5.png`). When one slide is selected (or the deck has one), it is written to the output name itself, as `opf convert deck.opf.json slide.png --slides 2` does. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide, and `{n}` is the slide number padded by the same rule. `{n}` is the only placeholder: any other `{key}`, and `{n}` in a `.pdf`, `.pptx`, `.zip` or deck-form output, rejects with `invalid-option`. |\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered; the archive\'s own name takes no `{n}`). The slides are PNG unless `format: "svg"`. |\n| `.opf.md`, `.yaml`, `.yml`, `.json` | the deck in that form |\n\nAny other extension, and a plain `.md` file, rejects with `invalid-option`, which names the supported extensions. A `format`\nthat disagrees with the output\'s extension is refused too; it is only needed for a `.zip`.\n\n**Without an output path**, pass the format in the options: `convert(input, { format })` writes nothing and returns the files\nwith their names and bytes. `name` sets the base name of the files; without it they follow the deck: its `filename`, else its\nslugified `name`, else the input file\'s stem (`Q4-Review.pdf`, `Q4-Review-1.png`; padded like the files above, `Q4-Review-01.png` from ten slides, `deck.opf.yaml`). `zip: true` returns one archive of\nthe PNG or SVG slides; with an output path a `.zip` name makes the archive instead.\n\n**Options** are the flags of `opf convert` in camel case: `slides` (`3`, `"1,3-5"` or `[1, 3]`, read by `parseSlideSelection`), `includeHidden`, `paginate`, `scale`\n(0.1 to 8, PNG and raster PDF), `raster` (`true` draws a PDF as a picture per page; vector text is the default), `text` (an SVG\ncarries `"fonts"`, the default, which embeds the faces the slide uses; `"system"`, none, for a page that has the fonts; or\n`"paths"`, glyph outlines, so the file needs no font), `charts` (`"auto"`, `"native"` or `"picture"`), `provenance` and `images`\n(`"compatible"` or `"preserve"`) for PPTX, `date` (`YYYY-MM-DD` for date fields; nothing reads a clock), `catalogs` (the default\ncatalog when omitted), `fonts` (a prepared `loadFonts()` handle of `@openpresentation/opf-render/fonts-node`, or a folder or a list\nof folders of `.ttf`/`.otf` files), `assetDir`, plus `signals` (a `.pptx` input: also return the importer\'s per-shape\nsignals), and for output without a path `zip` and `name`, and `overwrite`. An option that does not apply to the pair rejects with\n`invalid-option`: `scale` for a deck output, `slides` for a PPTX, `raster` for anything but a PDF, `text` for anything but an SVG,\n`zip` or `name` with an output path. The 0.17 names (`pdfMode`, `svgFonts`, `chartex`, `imageFormat`, `fontDirs`, `filename`) were\nrenamed in 0.18 and are refused with the new name.\n\n**Writing.** Inputs are checked and all results are produced and staged before destination files are published. A preflight,\nrender or staging failure publishes no destination files, though parent folders may have been created. Each final file is\npublished atomically from a temporary sibling, so no partial file has the final name. Multiple files are not one transaction:\na later publication I/O failure can leave earlier complete outputs in place. Keep that possibility in your recovery logic.\nExisting regular outputs are replaced by default, as `save` and `fs.writeFile` do: running the same `convert` again rebuilds\nthe file. `overwrite: false` checks for existing outputs during preflight and reports `output-exists`, the rule the `opf`\ncommands apply without `--force`. A symlink, a directory or another non-regular file is refused (`output-not-file`). The result\nis `{ files: [{ name, path, type, bytes, ... }], findings }`, with the slide number, id and size of each picture and the pages of a PDF.\n\n**Images.** When `input` is a path, a deck\'s relative image paths resolve against the input file\'s folder, from any working\ndirectory; `assetDir` replaces that folder. Only image files inside it are read, and URLs are never fetched.\n\n**Chaining.** A `.pptx` input is imported, checked and then exported or written. `findings` holds both steps: the importer\'s\ndiagnostics carry the `import/` rule prefix, the format and references check `opf/`, and the engines `render/`, `pptx/`,\n`pdf/`, `fonts/` and `cli/`.\n\n## `open(pathOrBytes, options?)`\n\nReturns the presentation itself. A `.opf.md`, `.yaml`/`.yml` or `.json` file is read in the form its extension names and\nchecked for format and references; a `.pptx` path or its bytes are imported. An invalid deck throws `OPFValidationError`,\nwhose findings are located by line and column in the file; a PowerPoint file that cannot be imported throws `OPFImportError`.\nWarnings are not returned: `validate` reports them, and `convert("deck.pptx", "deck.opf.yaml")` returns what an import could\nnot keep. `catalogs` (default: the default catalog) is where references resolve.\n\n## `save(deck, path, options?)`\n\nWrites the deck in the form the file name names (`.opf.md`, `.yaml`/`.yml` or `.json`), atomically, creating folders and\nreplacing the file. It checks format and references first and refuses an invalid deck with `OPFValidationError`; `validate:\nfalse` writes work in progress, as an editor\'s autosave does (the YAML and Markdown writers still refuse a document that is\nnot well-formed OPF). `schemaComment: true` starts a YAML file with the editor modeline. Returns `{ path, format }`.\n\n## Errors\n\nEvery error has a `code`; branch on it, never on the message.\n\n| Class | Codes |\n| --- | --- |\n| `OPFApiError` (the base class) | `node-only` (the browser-safe build), `invalid-option`, `input-not-found`, `input-unreadable`, `invalid-presentation` (a deck written as a deck), `output-exists`, `output-not-file`, `output-unwritable` |\n| `OPFExportError` | the export step: `peer-not-installed` (the message carries the install command; `details` the package and range), `peer-too-old`, `peer-load-failed`, `invalid-option`, `invalid-presentation`, `no-slides`, `all-slides-hidden`, `export-failed`, a font code |\n| `OPFImportError` | the import step and `open` of a PowerPoint file: `peer-not-installed`, `peer-too-old`, `peer-load-failed`, `import-failed`, `invalid-presentation` |\n| `OPFValidationError` | `open` and `save` of an invalid deck; also thrown by core\'s `parse` and `assertValid` |\n\n`findings` on an error holds the located findings of the failed step.\n\n## Fonts, determinism and the network\n\nDrawing uses the renderer\'s bundled open font pack (the office pack with visual substitution) plus the files in the `fonts` folders, never\nsystem fonts. The pack is prepared once per process and reused by every call that passes no `fonts` (with `fonts` folders, once per\nlist of files); a deck that draws a script beyond Latin, Greek and Cyrillic gets a handle with its script\nfaces. Concurrent calls share the one preparation and draw one at a time on it. The output is the same bytes as with a handle\nof the caller\'s own. Nothing is fetched and no clock is read: the same input, options and installed versions give the same\nbytes on every machine.\n\n## Reading and writing text without files\n\nCore reads and writes deck text in every runtime: `parse(text, { filename?, format?, catalogs? })` returns the\npresentation and throws `OPFValidationError` on syntax, schema or reference errors (findings located by line and column);\n`stringify(deck, { format? | filename? })` returns text; `validate(text)` returns the full report, warnings included.\n\n## Slide selections\n\n`parseSlideSelection(selection, total, label?)` (both builds) turns a selection into slide numbers, one-based, ascending and\nwithout repeats: `3`, `[1, 3]`, `"1,3-5"`, `"2-"` (to the end) and `"-3"` (from the start). It throws `OPFApiError`\n`invalid-option` for a malformed selection, a number that is not a whole number from 1, a reversed range or a slide past the\nend, and `no-slides` when `total` is 0. `convert` and the opf CLI\'s `--slides` read selections with it, and an engine can share it.\n'
|
|
261
|
+
"markdown": '# OPF files in Node: `@openpresentation/opf`\n\n`@openpresentation/opf` is one import for every runtime. In Node (and Bun and Deno) it is core plus the file API: it converts,\nopens and saves OPF files, and draws them as PDF, PNG, SVG and PPTX through the optional engines. In a browser or a worker the\nsame import gives the browser-safe build, with the same names, where only the file functions refuse.\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf");\nconst deck = await opf.open("deck.opf.md");\ndeck.slides.push({ title: "Q4" });\nawait opf.save(deck, "deck.opf.md");\nconst { files } = await opf.convert(deck, { format: "pptx" }); // bytes, nothing written\n```\n\nThe namespace is all of core (`validate`, `parse`, `stringify`, `paginate`, `parseSlideSelection`, the types and the error\nclasses), so one import covers an application. The pptx.gallery catalog is the package `@openpresentation/gallery` (`gallery`, a\ndependency of core, RR-78); in Node, `open`, `save` and `convert` register it when no `catalogs` are passed. A gallery that targets\nanother catalog record schema than core reads (`CATALOG_SCHEMA`) is refused with `gallery-schema-mismatch`.\n\n## The CLI\'s verbs, with the CLI\'s defaults\n\nOPF 0.18 gives the Node build the verbs of the `opf` command, so a script calls them instead of spawning `opf`:\n\n| Function | Command | In Node, when `catalogs` is omitted |\n| --- | --- | --- |\n| `validate(deck, options?)` | `opf validate` | the gallery is registered, so `opf validate deck` and `validate(deck)` report the same findings |\n| `stats(deck, options?)` | `opf stats` | the gallery |\n| `paginate(deck, options?)` | `opf paginate` | the gallery. Pass `fonts` (opf-render\'s `loadFonts()`) for measured page breaks; without it the result says `layout: "estimated"`. `opf paginate` measures with the office pack, prepared once per process, as `convert` does for an export with `paginate: true` |\n| `embed(deck, options?)` | `opf embed` | the gallery |\n| `edit(deck, patch, options?)` | `opf edit` | the gallery for the check. Applies a JSON Patch whole or not at all (`OPFPatchError`), checks the result (`OPFValidationError`; `validate: false` skips it) and returns `{ presentation, findings, inverse }` |\n| `fill(template, data?, options?)` | `opf fill` | no catalog: `data` is CSV, TSV or JSON text, a record or a list of records; returns `{ decks, complete, unfilled, diagnostics }`, and `presentation` with `combine: true`. `fillRecords(text)` reads the records alone |\n| `diff(a, b)`, `merge(base, ours, theirs)` | `opf diff`, `opf merge` | no catalog |\n\nAn explicit `catalogs: []` registers none. The browser build exports the same functions and registers no catalog (FA-21): a\nbrowser host passes `{ catalogs: [gallery] }` itself (`import { gallery } from "@openpresentation/gallery"`). Only `open`, `save` and `convert` are Node only.\n\nA shorter name is available as an npm alias, if you want one: `npm i opf@npm:@openpresentation/opf` installs the same package\nas `opf`, so `import * as opf from "opf"` works too. The docs keep the full name.\n\n## One import for every runtime\n\nOPF 0.18 removed the `@openpresentation/opf/node` subpath of 0.17: import `@openpresentation/opf` instead, with the same names\n(OPF 0.19 removed `@openpresentation/opf/catalog` and `defaultCatalog`, with no alias: import `gallery` from\n`@openpresentation/gallery`).\nThe package\'s conditional exports pick the build:\n\n| Condition | Build | `open`, `save`, `convert` |\n| --- | --- | --- |\n| `bun`, `deno`, `node` | the full build (`dist/index.js`) | work on files, and `convert` also returns bytes |\n| `workerd` (Cloudflare), `worker`, `browser`, `default` | the browser-safe build (`dist/browser.js`) | reject with `OPFApiError` code `node-only` |\n\nThe condition also decides how `validate` runs the JSON Schemas; see [Edge runtimes](#edge-runtimes-validators-that-never-compile-code).\n\n- **One type surface.** Both builds share `dist/index.d.ts` (the `types` condition, first in the map), so TypeScript sees the\n same declarations under `NodeNext` and `Bundler` resolution; the file functions are documented there as Node only. The\n declarations need no Node types.\n- **The browser-safe build** imports no Node builtin, no file system and neither engine (`scripts/check-browser-safe.mjs`\n bundles it for the browser and worker conditions). Its `node-only` messages name the alternative: `parse(text, { filename })`\n for `open`, `stringify(deck, { filename })` for `save`, and for `convert` the deck forms through `parse` and `stringify` and\n drawing through opf-render\'s `/export-browser` entry. The in-memory `convert(deck, { format })` is Node only in 0.18 too; a\n later release adds a browser implementation through `/export-browser`.\n- **The Node build** loads its file engine on the first file call, so importing the root costs no more than core.\n- **Bundlers** pick the browser build for a browser target and the full build for a Node target. A bundler that sets neither\n condition gets `default`, the browser-safe build: for a Node bundle, add the `node` condition (Rollup\'s node-resolve\n `exportConditions: ["node"]`, esbuild\'s `platform: "node"`).\n\n## Edge runtimes: validators that never compile code\n\n`validate` checks a document against the JSON Schemas with Ajv, and Ajv compiles each schema to JavaScript with `new Function` the\nfirst time it is used. Cloudflare Workers, Vercel Edge and a page under a Content-Security-Policy without `unsafe-eval` refuse\ncode generation from strings (Workers: "Code generation from strings disallowed for this context", while a request is handled), so\non those hosts the package ships the same validators compiled at build time (Ajv\'s standalone code generation, `scripts/validators-codegen.mjs`).\nThey give the same verdicts and the same findings: `test/precompiled-validators.test.mjs` compares them with Ajv on every example deck,\non catalog records and on random mutants of both, and on every `oneOf` and `anyOf` branch of the schemas.\n\nThe choice is made by the package\'s export conditions, through the internal `#schema-engine` import (package.json `imports`), so a\nhost needs no alias, no build step and no option:\n\n| Condition | Validators |\n| --- | --- |\n| `workerd` (Cloudflare Workers), `worker`, `edge-light` (Vercel Edge) | precompiled; no code is generated at runtime |\n| `node`, `bun`, `deno`, `browser`, `default` | Ajv, compiled on the first `validate` of the process |\n\nWrangler and OpenNext set `workerd`, `worker` and `browser` when they bundle a Worker, and the Edge runtime of Next.js sets `edge-light`,\nso a Worker needs nothing from you. Two cases do not get them by condition. Code that runs on a worker but was bundled for Node (a\nNext.js route handler that Turbopack or webpack resolved with the `node` condition before OpenNext packaged it, for one), and a browser\npage under a strict Content-Security-Policy. Import the precompiled validators once, before the first `validate`:\n\n```ts\nimport "@openpresentation/opf/precompiled"; // installs the build-time validators for this process; it exports nothing\n```\n\nThe price is size: the precompiled validators add about 78 KB gzipped (1.4 MB minified, mostly generated code) to the browser-safe\nbuild, which is why the browser keeps Ajv by default. In the package they are one minified chunk of `dist/` (1.5 MB, 121 KB gzipped;\nthe schemas are shared with the rest of the package, not copied), which adds about 125 KB to the tarball. On the first `validate` they save the time Ajv spends compiling the schemas\n(the first `validate` took about 100 ms with Ajv and about 10 ms precompiled in Node). A schema you compile yourself with Ajv is not covered: the precompiled build validates the OPF schemas that\nship in the package.\n\nThe `workerd` CI lane (`packages/javascript/test/workerd.test.mjs`, in the core job of `ci.yml`) bundles the package the way Wrangler\ndoes, runs it in the real `workerd` runtime from the `workerd` npm package (no Cloudflare account, no login), validates the example\ndecks, and requires the findings to equal Node\'s. A control in the same file bundles a Worker without the worker conditions and\nrequires workerd to refuse it, so the lane fails if the default validators go back to runtime compilation. Fonts, rasterizing and the\nin-memory `convert` are not part of this lane yet (CE-02, CE-03 in `docs/programs/cloud-editor/`).\n\n## The stack\n\nOPF is six packages. **Core** (`@openpresentation/opf`) is the format and its API: schemas, types, `validate`, `parse` and\n`stringify`, composition and pagination, and in Node the files. **The gallery** (`@openpresentation/gallery`) is the pptx.gallery\ncatalog, a dependency of core released on its own version line. **The CLI** (`@openpresentation/cli`) is the `opf` command;\nits `convert` command runs core\'s Node engine (through `@openpresentation/opf/internal/engine`,\nwhich the package exports under the `node` condition for the CLI only; it is not an application API). **opf-render**,\n**opf-pptx** and **opf-editor** are the engines: drawing (SVG, PNG, PDF), PowerPoint (export and import) and the editor.\n\n## Install\n\n```sh\nnpm install @openpresentation/opf @openpresentation/opf-render @openpresentation/opf-pptx \\\n @resvg/resvg-js sharp pdf-lib \\\n @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 \\\n @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 \\\n @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2\n```\n\n`@openpresentation/opf-render` and `@openpresentation/opf-pptx` are optional peer dependencies of core: npm does not install them\nfor you, and an application that only reads, validates or converts between deck forms needs neither. Core loads them the\nfirst time a call needs them, from core\'s own install location and then from the working directory; a missing one throws\n`peer-not-installed` with the install command. What each output needs is listed in [the CLI reference](cli.md#install): PNG\nneeds `@resvg/resvg-js` and `sharp`, raster PDF `pdf-lib`, pictures in a PDF `sharp`, every format the office font pack, and\n`.pptx` input or output `@openpresentation/opf-pptx`.\n\n## `convert(input, output, options?)`\n\n```ts\nawait opf.convert("deck.opf.md", "deck.pdf");\nawait opf.convert("deck.opf.md", "deck.pptx");\nawait opf.convert("deck.opf.md", "slides/deck.png"); // one PNG per slide: slides/deck-1.png, -2, ...\nawait opf.convert("deck.opf.md", "slides/slide-{n}.png"); // always numbered: slides/slide-1.png, slide-2.png, ...\nawait opf.convert("deck.pptx", "deck.opf.yaml"); // import\nawait opf.convert("deck.opf.json", "deck.opf.md"); // change the deck form\nawait opf.convert("deck.opf.md", "deck.png", { slides: "1-3", scale: 2 });\nawait opf.convert("deck.opf.md", "slides.zip", { format: "svg" });\n```\n\n`input` is a file path, a presentation object or the bytes of a `.pptx` file (`Uint8Array`). The formats come from the names:\n\n| Input | Read as |\n| --- | --- |\n| `.pptx`, PPTX bytes | imported through opf-pptx |\n| `.opf.md` | a Markdown deck |\n| `.yaml`, `.yml` | a YAML deck |\n| `.json` | a JSON deck |\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide, named after the output: `slides/deck.png` gives `slides/deck-1.png`, `-2`, ... (the numbers are the deck\'s slide numbers, padded with zeros to the width of the largest number written: `deck-01.png` from ten slides, `deck-001.png` from a hundred; `slides: "2,5"` gives `deck-2.png` and `deck-5.png`). When one slide is selected (or the deck has one), it is written to the output name itself, as `opf convert deck.opf.json slide.png --slides 2` does. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide, and `{n}` is the slide number padded by the same rule. `{n}` is the only placeholder: any other `{key}`, and `{n}` in a `.pdf`, `.pptx`, `.zip` or deck-form output, rejects with `invalid-option`. |\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered; the archive\'s own name takes no `{n}`). The slides are PNG unless `format: "svg"`. |\n| `.opf.md`, `.yaml`, `.yml`, `.json` | the deck in that form |\n\nAny other extension, and a plain `.md` file, rejects with `invalid-option`, which names the supported extensions. A `format`\nthat disagrees with the output\'s extension is refused too; it is only needed for a `.zip`.\n\n**Without an output path**, pass the format in the options: `convert(input, { format })` writes nothing and returns the files\nwith their names and bytes. `name` sets the base name of the files; without it they follow the deck: its `filename`, else its\nslugified `name`, else the input file\'s stem (`Q4-Review.pdf`, `Q4-Review-1.png`; padded like the files above, `Q4-Review-01.png` from ten slides, `deck.opf.yaml`). `zip: true` returns one archive of\nthe PNG or SVG slides; with an output path a `.zip` name makes the archive instead.\n\n**Options** are the flags of `opf convert` in camel case: `slides` (`3`, `"1,3-5"` or `[1, 3]`, read by `parseSlideSelection`), `includeHidden`, `paginate`, `scale`\n(0.1 to 8, PNG and raster PDF), `raster` (`true` draws a PDF as a picture per page; vector text is the default), `text` (an SVG\ncarries `"fonts"`, the default, which embeds the faces the slide uses; `"system"`, none, for a page that has the fonts; or\n`"paths"`, glyph outlines, so the file needs no font), `charts` (`"auto"`, `"native"` or `"picture"`), `provenance` and `images`\n(`"compatible"` or `"preserve"`) for PPTX, `date` (`YYYY-MM-DD` for date fields; nothing reads a clock), `catalogs` (the default\ncatalog when omitted), `fonts` (a prepared `loadFonts()` handle of `@openpresentation/opf-render/fonts-node`, or a folder or a list\nof folders of `.ttf`/`.otf` files), `assetDir`, plus `signals` (a `.pptx` input: also return the importer\'s per-shape\nsignals), and for output without a path `zip` and `name`, and `overwrite`. An option that does not apply to the pair rejects with\n`invalid-option`: `scale` for a deck output, `slides` for a PPTX, `raster` for anything but a PDF, `text` for anything but an SVG,\n`zip` or `name` with an output path. The 0.17 names (`pdfMode`, `svgFonts`, `chartex`, `imageFormat`, `fontDirs`, `filename`) were\nrenamed in 0.18 and are refused with the new name.\n\n**Writing.** Inputs are checked and all results are produced and staged before destination files are published. A preflight,\nrender or staging failure publishes no destination files, though parent folders may have been created. Each final file is\npublished atomically from a temporary sibling, so no partial file has the final name. Multiple files are not one transaction:\na later publication I/O failure can leave earlier complete outputs in place. Keep that possibility in your recovery logic.\nExisting regular outputs are replaced by default, as `save` and `fs.writeFile` do: running the same `convert` again rebuilds\nthe file. `overwrite: false` checks for existing outputs during preflight and reports `output-exists`, the rule the `opf`\ncommands apply without `--force`. A symlink, a directory or another non-regular file is refused (`output-not-file`). The result\nis `{ files: [{ name, path, type, bytes, ... }], findings }`, with the slide number, id and size of each picture and the pages of a PDF.\n\n**Images.** When `input` is a path, a deck\'s relative image paths resolve against the input file\'s folder, from any working\ndirectory; `assetDir` replaces that folder. Only image files inside it are read, and URLs are never fetched.\n\n**Chaining.** A `.pptx` input is imported, checked and then exported or written. `findings` holds both steps: the importer\'s\ndiagnostics carry the `import/` rule prefix, the format and references check `opf/`, and the engines `render/`, `pptx/`,\n`pdf/`, `fonts/` and `cli/`.\n\n## `open(pathOrBytes, options?)`\n\nReturns the presentation itself. A `.opf.md`, `.yaml`/`.yml` or `.json` file is read in the form its extension names and\nchecked for format and references; a `.pptx` path or its bytes are imported. An invalid deck throws `OPFValidationError`,\nwhose findings are located by line and column in the file; a PowerPoint file that cannot be imported throws `OPFImportError`.\nWarnings are not returned: `validate` reports them, and `convert("deck.pptx", "deck.opf.yaml")` returns what an import could\nnot keep. `catalogs` (default: the default catalog) is where references resolve.\n\n## `save(deck, path, options?)`\n\nWrites the deck in the form the file name names (`.opf.md`, `.yaml`/`.yml` or `.json`), atomically, creating folders and\nreplacing the file. It checks format and references first and refuses an invalid deck with `OPFValidationError`; `validate:\nfalse` writes work in progress, as an editor\'s autosave does (the YAML and Markdown writers still refuse a document that is\nnot well-formed OPF). `schemaComment: true` starts a YAML file with the editor modeline. Returns `{ path, format }`.\n\n## Errors\n\nEvery error has a `code`; branch on it, never on the message.\n\n| Class | Codes |\n| --- | --- |\n| `OPFApiError` (the base class) | `node-only` (the browser-safe build), `invalid-option`, `input-not-found`, `input-unreadable`, `invalid-presentation` (a deck written as a deck), `output-exists`, `output-not-file`, `output-unwritable` |\n| `OPFExportError` | the export step: `peer-not-installed` (the message carries the install command; `details` the package and range), `peer-too-old`, `peer-load-failed`, `invalid-option`, `invalid-presentation`, `no-slides`, `all-slides-hidden`, `export-failed`, a font code |\n| `OPFImportError` | the import step and `open` of a PowerPoint file: `peer-not-installed`, `peer-too-old`, `peer-load-failed`, `import-failed`, `invalid-presentation` |\n| `OPFValidationError` | `open` and `save` of an invalid deck; also thrown by core\'s `parse` and `assertValid` |\n\n`findings` on an error holds the located findings of the failed step.\n\n## Fonts, determinism and the network\n\nDrawing uses the renderer\'s bundled open font pack (the office pack with visual substitution) plus the files in the `fonts` folders, never\nsystem fonts. The pack is prepared once per process and reused by every call that passes no `fonts` (with `fonts` folders, once per\nlist of files); a deck that draws a script beyond Latin, Greek and Cyrillic gets a handle with its script\nfaces. Concurrent calls share the one preparation and draw one at a time on it. The output is the same bytes as with a handle\nof the caller\'s own. Nothing is fetched and no clock is read: the same input, options and installed versions give the same\nbytes on every machine.\n\n## Reading and writing text without files\n\nCore reads and writes deck text in every runtime: `parse(text, { filename?, format?, catalogs? })` returns the\npresentation and throws `OPFValidationError` on syntax, schema or reference errors (findings located by line and column);\n`stringify(deck, { format? | filename? })` returns text; `validate(text)` returns the full report, warnings included.\n\n## Slide selections\n\n`parseSlideSelection(selection, total, label?)` (both builds) turns a selection into slide numbers, one-based, ascending and\nwithout repeats: `3`, `[1, 3]`, `"1,3-5"`, `"2-"` (to the end) and `"-3"` (from the start). It throws `OPFApiError`\n`invalid-option` for a malformed selection, a number that is not a whole number from 1, a reversed range or a slide past the\nend, and `no-slides` when `total` is 0. `convert` and the opf CLI\'s `--slides` read selections with it, and an engine can share it.\n'
|
|
254
262
|
},
|
|
255
263
|
{
|
|
256
264
|
"slug": "numbered-lists",
|
|
257
265
|
"file": "docs/numbered-lists.md",
|
|
258
266
|
"title": "Numbered lists",
|
|
259
|
-
"markdown": '# Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets. The preview draws the numbers at the bullet geometry core composes, and the PowerPoint export writes native auto-numbers (`a:buAutoNum`), so a numbered list is a real numbered list in PowerPoint and the two agree. Without `numbering` nothing changes: a deck that does not use the field composes, renders and exports exactly as before.\n\n```json\n{\n "title": "Rollout",\n "items": [\n "Freeze the schema",\n { "text": "Migrate the data", "level": 1 },\n { "text": "Verify the counts", "level": 1 },\n "Switch traffic",\n "Retire the old service"\n ],\n "numbering": ["arabic", { "style": "alpha-lower", "suffix": "paren" }]\n}\n```\n\ndraws `1.`, `a)`, `b)`, `2.`, `3.`. The fixture [numbered-lists.opf.json](fixtures/numbered-lists.opf.json) covers every style, start numbers, per-level styles and nesting.\n\n## The field\n\n`numbering` is optional on a slide root payload and on a content payload (a block or a promoted region). It is valid only beside `items` or `bullets`; on any other payload, or on a group of `blocks`, validation reports an error. A payload that has both `items` and `bullets` numbers both lists.\n\n```text\nnumbering: NumberingStyle | Numbering | (NumberingStyle | Numbering)[]\nNumberingStyle = "arabic" | "roman-upper" | "roman-lower" | "alpha-upper" | "alpha-lower"\nNumbering = { style?: NumberingStyle (default "arabic"),\n start?: integer 1..32767 (default 1),\n suffix?: "period" | "paren" | "paren-both" (default "period") }\n```\n\n- A style name is shorthand for `{ "style": name }`. A style name or an object applies to every list level; an array has one entry per level (index = `level`) and its last entry repeats for deeper levels (at most 9 entries, the depth a native paragraph can carry).\n- Styles draw `1.`, `I.`, `i.`, `A.`, `a.`; the suffix draws `1.` (`period`), `1)` (`paren`) or `(1)` (`paren-both`).\n- `start` is the first number counted at that level. It is limited to 32767, the largest value `a:buAutoNum@startAt` holds; validation reports a list that would count past it.\n- An object item (`ListItem` or `BulletItem`) may carry its own `start`: the count restarts at that number for that entry, and the entries after it continue from it. Pagination uses this to keep the numbers of the whole list on continuation pages (see below); it is also the way to author "continue from 5" in a second list. `start` on an entry without `numbering` on the payload has no effect and validation warns.\n\n## Counting\n\nCounting follows PowerPoint, level by level:\n\n- Consecutive entries of one level count up from that level\'s `start`.\n- An entry of a shallower level restarts every deeper level.\n- Deeper entries between two entries of one level do not interrupt that level (`1.`, `a)`, `2.` and not `1.`, `a)`, `1.`).\n- The first entry of a level counts from that level\'s `start`, wherever it first appears.\n\nAlphabetic numbering past 26 repeats the letter as PowerPoint does (`z`, `aa`, `bb`, ..., `zz`, `aaa`). Roman numerals stop at 3999: a larger value is drawn in arabic (PowerPoint draws it that way too, and the export writes it as an arabic auto-number) and composition reports `numbering-adapted`; the notice never fails a strict (`composition.overflow: "error"`) slide.\n\n## Geometry (shared by the preview and the export)\n\nCore composes the marker, the preview draws it and the exporter writes it; none of them measures the number again.\n\n- `ListEntryLayout.marker.text` is the formatted number (`iv.`), at the marker position of the bullet it replaces: left edge at the entry\'s level offset, baseline of the entry\'s first line. `marker.number` carries the counted value, the style actually drawn, the suffix and an `adapted` flag, and `marker.width` is the measured advance.\n- The marker\'s `style` is the list style with the first run\'s weight and slant. PowerPoint draws an auto-number with the character formatting of the paragraph\'s first run, so an entry that starts with a bold run draws a bold number; the marker is measured the same way.\n- The hanging indent is one value for the whole list: the larger of 1.1 em (the bullet indent) and the widest marker in the list plus 0.3 em. Wide markers (`viii.`, `10.`, `(iv)`) therefore never reach their text, and every level steps by that same indent. An unnumbered list keeps 1.1 em.\n- A numbered list draws numbers even when `design.listBullet` is `image`;
|
|
267
|
+
"markdown": '# Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets. The preview draws the numbers at the bullet geometry core composes, and the PowerPoint export writes native auto-numbers (`a:buAutoNum`), so a numbered list is a real numbered list in PowerPoint and the two agree. Without `numbering` nothing changes: a deck that does not use the field composes, renders and exports exactly as before.\n\n```json\n{\n "title": "Rollout",\n "items": [\n "Freeze the schema",\n { "text": "Migrate the data", "level": 1 },\n { "text": "Verify the counts", "level": 1 },\n "Switch traffic",\n "Retire the old service"\n ],\n "numbering": ["arabic", { "style": "alpha-lower", "suffix": "paren" }]\n}\n```\n\ndraws `1.`, `a)`, `b)`, `2.`, `3.`. The fixture [numbered-lists.opf.json](fixtures/numbered-lists.opf.json) covers every style, start numbers, per-level styles and nesting.\n\n## The field\n\n`numbering` is optional on a slide root payload and on a content payload (a block or a promoted region). It is valid only beside `items` or `bullets`; on any other payload, or on a group of `blocks`, validation reports an error. A payload that has both `items` and `bullets` numbers both lists.\n\n```text\nnumbering: NumberingStyle | Numbering | (NumberingStyle | Numbering)[]\nNumberingStyle = "arabic" | "roman-upper" | "roman-lower" | "alpha-upper" | "alpha-lower"\nNumbering = { style?: NumberingStyle (default "arabic"),\n start?: integer 1..32767 (default 1),\n suffix?: "period" | "paren" | "paren-both" (default "period") }\n```\n\n- A style name is shorthand for `{ "style": name }`. A style name or an object applies to every list level; an array has one entry per level (index = `level`) and its last entry repeats for deeper levels (at most 9 entries, the depth a native paragraph can carry).\n- Styles draw `1.`, `I.`, `i.`, `A.`, `a.`; the suffix draws `1.` (`period`), `1)` (`paren`) or `(1)` (`paren-both`).\n- `start` is the first number counted at that level. It is limited to 32767, the largest value `a:buAutoNum@startAt` holds; validation reports a list that would count past it.\n- An object item (`ListItem` or `BulletItem`) may carry its own `start`: the count restarts at that number for that entry, and the entries after it continue from it. Pagination uses this to keep the numbers of the whole list on continuation pages (see below); it is also the way to author "continue from 5" in a second list. `start` on an entry without `numbering` on the payload has no effect and validation warns.\n\n## Counting\n\nCounting follows PowerPoint, level by level:\n\n- Consecutive entries of one level count up from that level\'s `start`.\n- An entry of a shallower level restarts every deeper level.\n- Deeper entries between two entries of one level do not interrupt that level (`1.`, `a)`, `2.` and not `1.`, `a)`, `1.`).\n- The first entry of a level counts from that level\'s `start`, wherever it first appears.\n\nAlphabetic numbering past 26 repeats the letter as PowerPoint does (`z`, `aa`, `bb`, ..., `zz`, `aaa`). Roman numerals stop at 3999: a larger value is drawn in arabic (PowerPoint draws it that way too, and the export writes it as an arabic auto-number) and composition reports `numbering-adapted`; the notice never fails a strict (`composition.overflow: "error"`) slide.\n\n## Geometry (shared by the preview and the export)\n\nCore composes the marker, the preview draws it and the exporter writes it; none of them measures the number again.\n\n- `ListEntryLayout.marker.text` is the formatted number (`iv.`), at the marker position of the bullet it replaces: left edge at the entry\'s level offset, baseline of the entry\'s first line. `marker.number` carries the counted value, the style actually drawn, the suffix and an `adapted` flag, and `marker.width` is the measured advance.\n- The marker\'s `style` is the list style with the first run\'s weight and slant. PowerPoint draws an auto-number with the character formatting of the paragraph\'s first run, so an entry that starts with a bold run draws a bold number; the marker is measured the same way.\n- The hanging indent is one value for the whole list: the larger of 1.1 em (the bullet indent) and the widest marker in the list plus 0.3 em. Wide markers (`viii.`, `10.`, `(iv)`) therefore never reach their text, and every level steps by that same indent. An unnumbered list keeps 1.1 em.\n- A numbered list draws numbers even when `design.listBullet` is `image` or `none`; those values are for bulleted lists.\n\n`formatListNumber(value, style, suffix)`, `listNumbers(items, numbering)`, `resolveNumbering` and `numberingAtLevel` are exported (`@openpresentation/opf/composition`) so hosts, the exporter and tests compute the same numbers.\n\n## PowerPoint\n\nThe exporter writes, per entry marker line, `a:buAutoNum` with the scheme that matches the style and suffix and `startAt` set to the counted number whenever it is not 1, with `marL` and `indent` from the hanging indent above:\n\n| style | `period` | `paren` | `paren-both` |\n| --- | --- | --- | --- |\n| `arabic` | `arabicPeriod` | `arabicParenR` | `arabicParenBoth` |\n| `roman-upper` | `romanUcPeriod` | `romanUcParenR` | `romanUcParenBoth` |\n| `roman-lower` | `romanLcPeriod` | `romanLcParenR` | `romanLcParenBoth` |\n| `alpha-upper` | `alphaUcPeriod` | `alphaUcParenR` | `alphaUcParenBoth` |\n| `alpha-lower` | `alphaLcPeriod` | `alphaLcParenR` | `alphaLcParenBoth` |\n\nImport maps `a:buAutoNum` back to `numbering` (`type` to style and suffix, `startAt` to `start`, per level to an array, uniform to a single object or a style name) and a list whose counted numbers differ from the plain count to per-entry `start`. A scheme OPF has no equivalent for (for example the East Asian, Hebrew or Arabic numbering schemes) imports as `arabic` with a specific diagnostic.\n\n## Pagination\n\n`paginateSlide` splits a numbered list between whole entries and keeps the numbers of the whole list: an entry on a continuation page whose number would differ from its number in the whole list carries `start`, so page two of a fifteen item list starts at 9 and a nested list continues its letters. Pages of an unnumbered list are unchanged.\n\n## Not covered\n\nVetoable boundaries of this design: no `numbering` default in `design` (set it on the payload); no per-entry style (only per-level); no RTL-specific number placement (the list code places markers at the left edge for every direction); the East Asian, Hebrew and Arabic native numbering schemes; and numbering of descriptions or of table rows. Table cells and text paragraphs have no list structure to number.\n'
|
|
260
268
|
},
|
|
261
269
|
{
|
|
262
270
|
"slug": "open-ecosystem",
|
|
@@ -268,7 +276,7 @@ var docsData = Object.freeze([
|
|
|
268
276
|
"slug": "patch-diff-merge-format",
|
|
269
277
|
"file": "docs/patch-diff-merge-format.md",
|
|
270
278
|
"title": "Patch, diff, merge and format",
|
|
271
|
-
"markdown": '# Patch, diff, merge and format\n\nStatus: in core 0.12.0 (RR-31) as the `@openpresentation/opf/patch`, `/diff` and `/format` entrypoints; the CLI commands `opf diff`, `opf merge` and `opf format` are in CLI 0.10.0 and later, not in CLI 0.9.2 (which has `opf edit`, JSON Patch). Core 0.11.4 and earlier lack the entrypoints. Check `opf --help` and the installed package exports before relying on them.\n\nFour deterministic, local pieces share one implementation: no network, no model call, no clock, no randomness. The same inputs always give the same output.\n\n| Piece | Core entrypoint | CLI |\n| --- | --- | --- |\n| JSON Patch (RFC 6902) with inverse patches | `@openpresentation/opf/patch` | `opf edit` |\n| Semantic diff of two documents | `@openpresentation/opf/diff` | `opf diff` |\n| Three-way merge with conflict objects | `@openpresentation/opf/diff` | `opf merge` |\n| Canonical key order and layout | `@openpresentation/opf/format` | `opf format` |\n\n## One patch module\n\n`opf edit`, the editor session (`@openpresentation/opf-editor`, undo/redo) and the diff output all use `@openpresentation/opf/patch`. There is no second implementation.\n\n```ts\nimport { applyPatch, applyPatchWithInverse, invertPatch, OPFPatchError } from "@openpresentation/opf/patch";\n\nconst next = applyPatch(presentation, [\n { op: "test", path: "/slides/0/id", value: "intro" },\n { op: "replace", path: "/slides/0/title", value: "Welcome" },\n { op: "move", from: "/slides/3", path: "/slides/1" },\n]);\n// applyPatch clones: `presentation` is never changed, and any failure applies nothing.\n\nconst { presentation: after, inverse } = applyPatchWithInverse(presentation, patch);\napplyPatch(after, inverse); // deep-equals `presentation`: this is the undo patch\n```\n\n- All six RFC 6902 operations: `add`, `remove`, `replace`, `move`, `copy`, `test`. Unknown members of an operation are ignored (RFC 6902 section 4); unknown `op` values are errors.\n- Pointers are strict RFC 6901. `-` is accepted only as the final token of an `add`, `move` target or `copy` target. Array indexes are canonical decimal (`01`, `-1` and `1.0` are errors). `parsePointer`, `formatPointer`, `escapePointerToken`, `pointerFromPath` (a pointer, an OPF dotted path such as `slides.0.title`, or a segment array) and `splitPointer` give every caller the same path handling; build pointers with `formatPointer` rather than joining unescaped keys.\n- Errors are `OPFPatchError` with a stable `code` (`patch-test-failed`, `patch-path-missing`, `patch-parent-missing`, `invalid-array-index`, `invalid-json-pointer`, `invalid-patch`, `invalid-patch-operation`, `unsupported-patch-operation`, `patch-invalid-move`, `patch-root-remove`, `patch-invalid-document`), the failing operation\'s `index` and `path`, and a message that starts with `Operation <n>:`.\n- `test` fails on a missing path, and compares by JSON value: member order is irrelevant, `0` equals `-0`, arrays compare in order. `move` onto itself is a no-op; moving a value into its own descendant is an error; removing the document root is an error. A `__proto__` key is data and never reaches a prototype.\n- Optional schema validation of the result: `applyPatch(presentation, patch, { validate: true })` (or a custom validator; add `strict: true` to reject warnings) throws `OPFPatchValidationError` with the full validation report when the final document is invalid. Intermediate states are not validated, so a patch may pass through an invalid state. `opf edit` validates the saved document itself, as before.\n- Inverse patches: `applyPatchWithInverse` and `invertPatch` return operations that restore the input exactly, in reverse order, with concrete array indexes (the inverse of an `add` at `-` removes the real index). `test` has no inverse. The editor stores these in its undo stack.\n\n## Diff\n\n```ts\nimport { diff, formatDiffReport } from "@openpresentation/opf/diff";\n\nconst result = diff(a, b);\nresult.equal; // true when nothing differs\nresult.patch; // RFC 6902 patch: applyPatch(a, result.patch) deep-equals b\nresult.changes; // typed, categorised changes\nresult.slides; // how each slide was matched, and its status\nresult.summary; // counts by type, category and slide status\nformatDiffReport(result); // plain-text report\n```\n\n**Matching.** Arrays are matched element by element, in this order, and the same rule applies to slides, blocks, list items, table rows and every other array:\n\n1. a shared string `id`;\n2. identical content (an order-preserving match first, then anywhere, so a moved slide with no `id` is still found);\n3. content similarity for objects and arrays: the share of scalar leaves with the same path and value, with partial credit for text that shares words. The default threshold is 0.5 (`threshold` option); two elements that both carry an `id` and differ in it must reach 0.8, so an unrelated added slide is not mistaken for a renamed one;\n4. leftover primitives between matched neighbours pair by position (an edited bullet is one `replace`).\n\nTies break by index, so the result never depends on iteration order or object key order. Arrays too large for the similarity pass (over 250,000 element pairs) fall back to id and equality matching.\n\n**Moves.** Matched elements that are out of order are found with a longest in-order chain, so moving one slide is one `move` operation and one `moved` change, not a remove and an add.\n\n**Changes.** Each change has a `type` (`added`, `removed`, `changed`, `moved`), a `category` (`slide`, `block`, `field`, `design`, `metadata`, `assets`, `catalogs`, `variables`, `narrative`, `extensions`), a JSON Pointer in A (`aPath`) and/or B (`bPath`), the `before` and `after` values, and the `slide` it belongs to (`id`, `title`, `aIndex`, `bIndex`). Root keys other than `slides`, `design`, `assets`, `catalogs`, `variables`, `narrative` and `extensions` are `metadata`; a slide\'s own `design` is `design`; adding, removing or moving a `blocks` element or a region payload (`left`, `center+right`, `top:left`, ...) is `block`; everything else inside a slide is `field`.\n\n**The patch** applies removals (highest index first), then moves, then insertions in ascending order, then edits inside matched elements at their final positions, so every path is valid when its operation runs.\n\n```text\n$ opf diff before.opf.json after.opf.json\n7 changes (slides: 1 added, 1 removed, 1 moved, 2 modified; metadata: 1; design: 1)\n\nMetadata\n ~ name "Deck" -> "Renamed"\n\nDesign\n ~ design.theme "bold" -> "minimal"\n\nSlides\n ~ slide #1 "End" (id end)\n + blocks[1] chart\n ~ slide #2 "Welcome" (id intro) (moved from #1)\n ~ title "Intro" -> "Welcome"\n + slide #3 "New" (id new)\n - slide #2 "Old" (id old)\n```\n\nCLI: `opf diff <a|-> <b|-> [--format text|json|patch] [--exit-code] [--threshold <0-1>]`. `--format json` prints the report `{command, ok, input, outputs, findings, counts, equal, summary, slides, changes, patch}` (CLI 0.18; `{equal, summary, slides, changes, patch}` before); `--format patch` prints only the patch (feed it to `opf edit --patch`). Exit 0 unless `--exit-code` is given and the documents differ (then 1); usage and I/O errors exit 2. `-` reads one side from stdin.\n\n## Merge\n\n```ts\nimport { merge } from "@openpresentation/opf/diff";\n\nconst { merged, conflicts, clean, applied } = merge(base, ours, theirs, { prefer: "ours" });\n```\n\nChanges in different places merge automatically; the same change on both sides is applied once. Arrays merge element by element with the diff matcher:\n\n- insertions from both sides are kept (ours first where both inserted at the same position); an identical insertion, or the same `id` inserted twice, appears once (same `id` with different content merges field by field);\n- a deletion of an untouched element wins; a deletion against an edit is a conflict;\n- a move on one side is applied on top of the other side\'s edits; the same move on both sides agrees; different moves of the same element conflict.\n\n**Conflicts are never silent.** Each is an object with `kind` (`modify-modify`, `modify-delete`, `delete-modify`, `add-add`, `move-move`), `path` (JSON Pointer in the merged document), `base`, `ours` and `theirs` values (absent when that side deleted the value; `deletedBy` names the deleting side), `resolution` (which side the merged document took), the `slide` it is inside, and a `message`. The merged document always holds one side\'s value at a conflict (`prefer`, default `ours`), and the conflict keeps the other side\'s value, so neither side is dropped without a record. `clean` is `true` only when there are no conflicts. `applied` counts the places taken from only one side or identical on both.\n\nThe merge result is not schema-validated by the library; `opf merge` validates it before writing, because two valid documents can merge into an invalid one (for example duplicate ids).\n\nCLI: `opf merge <base> <ours> <theirs> [output|-] [-i] [--force] [--prefer ours|theirs] [--report <file>] [--threshold <0-1>] [--fail-on <level>]`. With conflicts and no `--prefer`, nothing is written, the conflict report goes to stderr as JSON and the exit code is 1. With `--prefer`, the chosen side\'s value is written, the report lists every conflict, and the exit code is 0. `--report <file>` saves the `{clean, conflicts, applied}` summary. `-i` (`--in-place`) rewrites the *ours* file (like `git merge-file`) with the same guard as `opf edit`. The usual output rules apply: stdout without an output, `--force` to replace a file.\n\n## Format\n\n```ts\nimport { format, sortPresentationKeys, isFormatted } from "@openpresentation/opf/format";\n\nconst text = format(sourceTextOrDocument);\n```\n\n- **Key order** follows the property declaration order of the OPF schema at every depth (through `$ref`, `oneOf`, `anyOf`, `allOf` and `then`/`else`): `$schema`, `name`, `description`, ..., `design`, `variables`, `narrative`, `slides`, `assets`, `catalogs`, `extensions` at the root, `id`, `layout`, `title`, ... in a slide. Keys the schema does not declare (extensions, catalog records, custom colours, unknown fields) follow the declared ones in their original relative order, so formatting never reorders user data that has no canonical order. Array order and every value are unchanged.\n- **Layout** is two-space indentation (`indent` 0 to 8), LF line endings (`eol: "crlf"` for Windows checkouts that use `core.autocrlf`), no BOM, one trailing newline. Formatting is idempotent.\n- It does not validate: any JSON document can be formatted, and formatting never changes validity.\n\nCLI: `opf format <file|-> [output|-]` or `opf format <file>... (-i | --check)`, with `[--indent <0-8>] [--eol lf|crlf|preserve]`. One file with no output prints the canonical text to stdout; an output must name the same form (`opf convert` changes forms). `--check` reports `checked`, `formatted` and `unformatted` and exits 1 if any file would change (use it in CI); `-i` rewrites only changed files atomically; several files need `--check` or `-i`. `--eol preserve` keeps each file\'s own line endings.\n\n## What this does not do\n\n- It is structural, not visual: a diff of two decks says nothing about whether either composes, overflows or exports well.\n- Merge treats text as a value: two edits to different words of the same string are a conflict, not a text merge.\n- Similarity matching is a heuristic with a documented threshold. When it matters, give slides stable ids; ids are always trusted over content.\n'
|
|
279
|
+
"markdown": '# Patch, diff, merge and format\n\nStatus: in core 0.12.0 (RR-31) as the `@openpresentation/opf/patch`, `/diff` and `/format` entrypoints; the CLI commands `opf diff`, `opf merge` and `opf format` are in CLI 0.10.0 and later, not in CLI 0.9.2 (which has `opf edit`, JSON Patch). Core 0.11.4 and earlier lack the entrypoints. Check `opf --help` and the installed package exports before relying on them.\n\nFour deterministic, local pieces share one implementation: no network, no model call, no clock, no randomness. The same inputs always give the same output.\n\n| Piece | Core entrypoint | CLI |\n| --- | --- | --- |\n| JSON Patch (RFC 6902) with inverse patches | `@openpresentation/opf/patch` | `opf edit` |\n| Semantic diff of two documents | `@openpresentation/opf/diff` | `opf diff` |\n| Three-way merge with conflict objects | `@openpresentation/opf/diff` | `opf merge` |\n| Canonical key order and layout | `@openpresentation/opf/format` | `opf format` |\n| Ids, id-addressed paths, document hash, patch scope | `@openpresentation/opf/ids` (the patch parts also on `/patch`) | |\n\n## One patch module\n\n`opf edit`, the editor session (`@openpresentation/opf-editor`, undo/redo) and the diff output all use `@openpresentation/opf/patch`. There is no second implementation.\n\n```ts\nimport { applyPatch, applyPatchWithInverse, invertPatch, OPFPatchError } from "@openpresentation/opf/patch";\n\nconst next = applyPatch(presentation, [\n { op: "test", path: "/slides/0/id", value: "intro" },\n { op: "replace", path: "/slides/0/title", value: "Welcome" },\n { op: "move", from: "/slides/3", path: "/slides/1" },\n]);\n// applyPatch clones: `presentation` is never changed, and any failure applies nothing.\n\nconst { presentation: after, inverse } = applyPatchWithInverse(presentation, patch);\napplyPatch(after, inverse); // deep-equals `presentation`: this is the undo patch\n```\n\n- All six RFC 6902 operations: `add`, `remove`, `replace`, `move`, `copy`, `test`. Unknown members of an operation are ignored (RFC 6902 section 4); unknown `op` values are errors.\n- Pointers are strict RFC 6901. `-` is accepted only as the final token of an `add`, `move` target or `copy` target. Array indexes are canonical decimal (`01`, `-1` and `1.0` are errors). `parsePointer`, `formatPointer`, `escapePointerToken`, `pointerFromPath` (a pointer, an OPF dotted path such as `slides.0.title`, or a segment array) and `splitPointer` give every caller the same path handling; build pointers with `formatPointer` rather than joining unescaped keys.\n- Errors are `OPFPatchError` with a stable `code` (`patch-test-failed`, `patch-path-missing`, `patch-parent-missing`, `invalid-array-index`, `invalid-json-pointer`, `invalid-patch`, `invalid-patch-operation`, `unsupported-patch-operation`, `patch-invalid-move`, `patch-root-remove`, `patch-invalid-document`, `patch-id-not-found`, `patch-id-ambiguous`, `invalid-expect-hash`, `patch-hash-mismatch`), the failing operation\'s `index` and `path`, and a message that starts with `Operation <n>:`.\n- `test` fails on a missing path, and compares by JSON value: member order is irrelevant, `0` equals `-0`, arrays compare in order. `move` onto itself is a no-op; moving a value into its own descendant is an error; removing the document root is an error. A `__proto__` key is data and never reaches a prototype.\n- Optional schema validation of the result: `applyPatch(presentation, patch, { validate: true })` (or a custom validator; add `strict: true` to reject warnings) throws `OPFPatchValidationError` with the full validation report when the final document is invalid. Intermediate states are not validated, so a patch may pass through an invalid state. `opf edit` validates the saved document itself, as before.\n- Inverse patches: `applyPatchWithInverse` and `invertPatch` return operations that restore the input exactly, in reverse order, with concrete array indexes (the inverse of an `add` at `-` removes the real index). `test` has no inverse. The editor stores these in its undo stack.\n\n## Ids, id-addressed paths, hashes and patch scope\n\nIndexes move: `/slides/3/blocks/1` is another block once a slide is inserted above it. Anything that edits with other people or other programs (remote patches, comments, review, an agent\'s proposal) needs paths that name the thing, and a way to say "only if nothing changed". Slides and content payloads (blocks, groups, regions) already have an optional `id`, unique among slide and payload ids; these pieces build on it. They live in `@openpresentation/opf/ids` and on the package root (the pointer, hash and `expectHash` parts are also on `/patch`); all of them are deterministic, offline and browser-safe.\n\n### `assignIds`\n\n```ts\nimport { assignIds } from "@openpresentation/opf/ids";\n\nconst { document, patch, assigned } = assignIds(presentation);\n// document: a copy where every slide and payload has an id\n// patch: the same change as `add` operations (`/slides/0/id`, ...), for an editor to apply as one undoable edit\n// assigned: [{ pointer: "/slides/0", kind: "slide", id: "slide-1" }, ...]\n```\n\nMissing ids are filled in document order (slides, then each slide\'s blocks, nested groups and promoted regions), as `slide-1`, `slide-2`, ... and `block-1`, `block-2`, ..., skipping ids already in use, so the same document always gets the same ids and the result is valid against the schema. An existing id is never changed. Pass `generate({ kind, pointer, node, parentId })` to choose the ids yourself (a UUID, a slug of the title); it must return a non-empty string no slide or payload uses, or `assignIds` throws `OPFIdError` (`invalid-id`, `duplicate-id`). The id is added as the node\'s last member; `format` puts it in canonical order. List items, table rows and runs have no id.\n\n### Id-addressed paths\n\nIn an array, the pointer token `@intro` is the element whose `id` is `intro`: `/slides/@kpis/blocks/@note/text`. It is a valid RFC 6901 pointer, so every JSON Pointer tool can carry it; it resolves in OPF\'s own `readPointer`, `getAtPointer`, `applyPatch` (every operation, `from` and `path`) and the editor\'s `get`, `set` and `applyPatch`.\n\n```ts\nimport { applyPatch, resolveIdPointer, toIdPointer, resolveIdPatch, toIdPatch } from "@openpresentation/opf/patch";\n\nresolveIdPointer(doc, "/slides/@kpis/blocks/@note/text"); // "/slides/1/blocks/1/text"\ntoIdPointer(doc, "/slides/1/blocks/1/text"); // "/slides/@kpis/blocks/@note/text"\napplyPatch(doc, [{ op: "replace", path: "/slides/@kpis/blocks/@note/text", value: "Edited" }]);\ntoIdPatch(doc, patch); // every index that names an element with an id, operation by operation\nresolveIdPatch(doc, patch); // the reverse: every id token as the index it has when its operation runs\n```\n\n- **Why `@`.** It is legal in a pointer token and in a URI fragment (`#/slides/@intro`) without percent-encoding; `{id:intro}` would need `%7B` and `%7D` there. Arrays have no other use for a token that is not an index, and the token is read as an id only where the value is an array, so an object key such as an extension\'s `@context` is still a plain key. An id that begins with `@` is written `@@id`; `/` and `~` in an id escape as in any pointer (`~1`, `~0`).\n- **Resolution.** `resolveIdPointer` throws `patch-id-not-found` for an id no element has and `patch-id-ambiguous` for an id several elements of the array share (an invalid document; `readPointer` reports it as not found). A path that does not exist is not an error: only the id tokens are resolved. `toIdPointer` writes an id token only where the element\'s `id` is a non-empty string no other element of the array has; other indexes stay.\n- **`move` and insertion.** An id as the last token of `add`, `copy` or `move` inserts before that element (an index does the same). A `move` resolves its destination after the value has left, as RFC 6902 reads an index. The `patch` and `inverse` that `applyPatchWithInverse` returns keep the paths as written and in index form respectively: an inverse never depends on ids.\n- **`toIdPatch`** writes each operation against the document it applies to, so a host that syncs by id sends a patch that still means the same thing after the receiver has reordered. Where an operation inserts, the last token stays an index (or `-`): there is no element yet to name; its ancestors are named by the ids they have before the insertion.\n\n### `documentHash` and `expectHash`\n\n```ts\nimport { documentHash, applyPatch, OPFPatchHashError } from "@openpresentation/opf/patch";\n\nconst hash = documentHash(doc); // "sha256:" + 64 hex digits\napplyPatch(doc, patch, { expectHash: hash }); // throws OPFPatchHashError (code patch-hash-mismatch) when doc is not the document hashed; nothing is applied\n```\n\nThe hash is SHA-256 over the document\'s canonical JSON: the members of every object sorted by UTF-16 code unit at every depth (RFC 8785), no whitespace. Two documents hash alike exactly when `jsonEqual` calls them equal: key order, a JSON, YAML or Markdown round trip, and formatting do not matter; any content change, including array order, does. (`format` is not the canonical form here: it keeps keys the schema does not declare in their written order, which is right for a file and wrong for an identity.) The digest is pure JavaScript and synchronous, with no Node import and no Web Crypto, so it is the same in Node, browsers and edge runtimes and can be checked inside the synchronous `applyPatch`. `expectHash` also takes the bare 64-digit hex; a value that is not a hash throws `invalid-expect-hash`. `edit(deck, patch, { expectHash })` takes it too. A server that holds the document answers a client\'s patch with `expectHash` as optimistic concurrency, without writing its own etag code. This is not the CLI\'s `--expect-sha256`, which is the digest of a file\'s bytes.\n\n### `touches`\n\n```ts\nimport { touches } from "@openpresentation/opf/ids";\n\nconst scope = touches(patch, document);\nscope.writes; // [{ operation: 1, op: "replace", pointer: "/slides/1/title", idPointer: "/slides/@kpis/title", kind: "slide", ids: ["kpis"], slideIds: ["kpis"] }]\nscope.reads; // `test`, and the source of `copy`\nscope.ids, scope.slideIds, scope.kinds;\n```\n\nWhat a patch reads and writes, for conflict detection and permission checks. With the `document` the patch was made for, the patch is walked operation by operation, so every `pointer` is the index path as it is when its operation runs, every `idPointer` is its id form, and a `remove`, `replace` or `move` of a slide or group lists the ids inside it. Without a document, the pointers are as written and `ids` holds the id tokens and the ids of the values the operations insert. A patch that does not apply throws the error `applyPatch` throws. `kind` is the part of the deck a location belongs to: `document`, `slide-order` (the `slides` array, or a slide added, removed or moved), `slide`, `block` (a payload or the `blocks` array that holds it), `design`, `assets`, `catalogs` or `metadata`.\n\nTwo patches made against the same document conflict when a write of one has the same `idPointer` as, or sits inside or above, a location the other reads or writes, or the other destroys what is in its `ids`. A `move` changes where an element sits, not what is inside it: do not count the descendants of a `move` location. A permission check reads `kind`, `slideIds` and `ids` of `writes` (moves included). What counts as a conflict and who may touch what stays with the host; the library only reports the scope.\n\nThe editor reports the same patches by id (`createEditorSession(doc, { idPaths: true })`), accepts `expectHash` on `applyPatch` and assigns ids as one edit (`session.assignIds()`); see the editor README.\n\n## Diff\n\n```ts\nimport { diff, formatDiffReport } from "@openpresentation/opf/diff";\n\nconst result = diff(a, b);\nresult.equal; // true when nothing differs\nresult.patch; // RFC 6902 patch: applyPatch(a, result.patch) deep-equals b\nresult.changes; // typed, categorised changes\nresult.slides; // how each slide was matched, and its status\nresult.summary; // counts by type, category and slide status\nformatDiffReport(result); // plain-text report\n```\n\n**Matching.** Arrays are matched element by element, in this order, and the same rule applies to slides, blocks, list items, table rows and every other array:\n\n1. a shared string `id`;\n2. identical content (an order-preserving match first, then anywhere, so a moved slide with no `id` is still found);\n3. content similarity for objects and arrays: the share of scalar leaves with the same path and value, with partial credit for text that shares words. The default threshold is 0.5 (`threshold` option); two elements that both carry an `id` and differ in it must reach 0.8, so an unrelated added slide is not mistaken for a renamed one;\n4. leftover primitives between matched neighbours pair by position (an edited bullet is one `replace`).\n\nTies break by index, so the result never depends on iteration order or object key order. Arrays too large for the similarity pass (over 250,000 element pairs) fall back to id and equality matching.\n\n**Moves.** Matched elements that are out of order are found with a longest in-order chain, so moving one slide is one `move` operation and one `moved` change, not a remove and an add.\n\n**Changes.** Each change has a `type` (`added`, `removed`, `changed`, `moved`), a `category` (`slide`, `block`, `field`, `design`, `metadata`, `assets`, `catalogs`, `variables`, `narrative`, `extensions`), a JSON Pointer in A (`aPath`) and/or B (`bPath`), the `before` and `after` values, and the `slide` it belongs to (`id`, `title`, `aIndex`, `bIndex`). Root keys other than `slides`, `design`, `assets`, `catalogs`, `variables`, `narrative` and `extensions` are `metadata`; a slide\'s own `design` is `design`; adding, removing or moving a `blocks` element or a region payload (`left`, `center+right`, `top:left`, ...) is `block`; everything else inside a slide is `field`.\n\n**The patch** applies removals (highest index first), then moves, then insertions in ascending order, then edits inside matched elements at their final positions, so every path is valid when its operation runs.\n\n```text\n$ opf diff before.opf.json after.opf.json\n7 changes (slides: 1 added, 1 removed, 1 moved, 2 modified; metadata: 1; design: 1)\n\nMetadata\n ~ name "Deck" -> "Renamed"\n\nDesign\n ~ design.theme "bold" -> "minimal"\n\nSlides\n ~ slide #1 "End" (id end)\n + blocks[1] chart\n ~ slide #2 "Welcome" (id intro) (moved from #1)\n ~ title "Intro" -> "Welcome"\n + slide #3 "New" (id new)\n - slide #2 "Old" (id old)\n```\n\nCLI: `opf diff <a|-> <b|-> [--format text|json|patch] [--exit-code] [--threshold <0-1>]`. `--format json` prints the report `{command, ok, input, outputs, findings, counts, equal, summary, slides, changes, patch}` (CLI 0.18; `{equal, summary, slides, changes, patch}` before); `--format patch` prints only the patch (feed it to `opf edit --patch`). Exit 0 unless `--exit-code` is given and the documents differ (then 1); usage and I/O errors exit 2. `-` reads one side from stdin.\n\n## Merge\n\n```ts\nimport { merge } from "@openpresentation/opf/diff";\n\nconst { merged, conflicts, clean, applied } = merge(base, ours, theirs, { prefer: "ours" });\n```\n\nChanges in different places merge automatically; the same change on both sides is applied once. Arrays merge element by element with the diff matcher:\n\n- insertions from both sides are kept (ours first where both inserted at the same position); an identical insertion, or the same `id` inserted twice, appears once (same `id` with different content merges field by field);\n- a deletion of an untouched element wins; a deletion against an edit is a conflict;\n- a move on one side is applied on top of the other side\'s edits; the same move on both sides agrees; different moves of the same element conflict.\n\n**Conflicts are never silent.** Each is an object with `kind` (`modify-modify`, `modify-delete`, `delete-modify`, `add-add`, `move-move`), `path` (JSON Pointer in the merged document), `base`, `ours` and `theirs` values (absent when that side deleted the value; `deletedBy` names the deleting side), `resolution` (which side the merged document took), the `slide` it is inside, and a `message`. The merged document always holds one side\'s value at a conflict (`prefer`, default `ours`), and the conflict keeps the other side\'s value, so neither side is dropped without a record. `clean` is `true` only when there are no conflicts. `applied` counts the places taken from only one side or identical on both.\n\nThe merge result is not schema-validated by the library; `opf merge` validates it before writing, because two valid documents can merge into an invalid one (for example duplicate ids).\n\nCLI: `opf merge <base> <ours> <theirs> [output|-] [-i] [--force] [--prefer ours|theirs] [--report <file>] [--threshold <0-1>] [--fail-on <level>]`. With conflicts and no `--prefer`, nothing is written, the conflict report goes to stderr as JSON and the exit code is 1. With `--prefer`, the chosen side\'s value is written, the report lists every conflict, and the exit code is 0. `--report <file>` saves the `{clean, conflicts, applied}` summary. `-i` (`--in-place`) rewrites the *ours* file (like `git merge-file`) with the same guard as `opf edit`. The usual output rules apply: stdout without an output, `--force` to replace a file.\n\n## Format\n\n```ts\nimport { format, sortPresentationKeys, isFormatted } from "@openpresentation/opf/format";\n\nconst text = format(sourceTextOrDocument);\n```\n\n- **Key order** follows the property declaration order of the OPF schema at every depth (through `$ref`, `oneOf`, `anyOf`, `allOf` and `then`/`else`): `$schema`, `name`, `description`, ..., `design`, `variables`, `narrative`, `slides`, `assets`, `catalogs`, `extensions` at the root, `id`, `layout`, `title`, ... in a slide. Keys the schema does not declare (extensions, catalog records, custom colours, unknown fields) follow the declared ones in their original relative order, so formatting never reorders user data that has no canonical order. Array order and every value are unchanged.\n- **Layout** is two-space indentation (`indent` 0 to 8), LF line endings (`eol: "crlf"` for Windows checkouts that use `core.autocrlf`), no BOM, one trailing newline. Formatting is idempotent.\n- It does not validate: any JSON document can be formatted, and formatting never changes validity.\n\nCLI: `opf format <file|-> [output|-]` or `opf format <file>... (-i | --check)`, with `[--indent <0-8>] [--eol lf|crlf|preserve]`. One file with no output prints the canonical text to stdout; an output must name the same form (`opf convert` changes forms). `--check` reports `checked`, `formatted` and `unformatted` and exits 1 if any file would change (use it in CI); `-i` rewrites only changed files atomically; several files need `--check` or `-i`. `--eol preserve` keeps each file\'s own line endings.\n\n## What this does not do\n\n- It is structural, not visual: a diff of two decks says nothing about whether either composes, overflows or exports well.\n- Merge treats text as a value: two edits to different words of the same string are a conflict, not a text merge.\n- Similarity matching is a heuristic with a documented threshold. When it matters, give slides stable ids (`assignIds`); ids are always trusted over content.\n- `touches` and the hash do not transform or rebase a patch over another: that is a later item of the cloud-editor program. They give a host what it needs to detect that a rebase is required.\n'
|
|
272
280
|
},
|
|
273
281
|
{
|
|
274
282
|
"slug": "pr-backlog-2026-09-09",
|
|
@@ -286,7 +294,7 @@ var docsData = Object.freeze([
|
|
|
286
294
|
"slug": "release-process",
|
|
287
295
|
"file": "docs/release-process.md",
|
|
288
296
|
"title": "OPF Release Process",
|
|
289
|
-
"markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe packages declare the open-ended `engines.node` `>=22` (RR-20, tested on Node 22, 24 and 26 by the `node-range`\nCI legs, which run in the nightly workflow, before every release and on pull requests labelled `full-ci`); a closed range such as `24.x` makes npm on any other Node silently install an older release, and\n`pnpm check:engines-range` fails on one, including in a lockfile that pins a release made after the fix.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the six published packages of this repository:\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf) (core, `packages/javascript`),\n`@openpresentation/cli`, `@openpresentation/opf-render`, `@openpresentation/opf-pptx`, `@openpresentation/opf-editor` and\n`@openpresentation/gallery`. They are released together, at one version, with one tag.\n\n## Release: one click\n\n> Status: the workflows and `scripts/release.mjs` are in place and rehearsed with dry runs. The first real release through them\n> is the canary `0.26.0-rc.0` (MR-41), then 0.26.0 (MR-44). Releases up to 0.25.x were made from the four old repositories with\n> a per-package train; that process is retired and not described here.\n\n**One version, one tag, one workflow.** All six packages carry the same version (`pnpm check:versions`); a release is the tag\n`vX.Y.Z` on a commit of `main`, published by `.github/workflows/npm-publish.yml` in dependency order: gallery, core, render,\nPPTX, editor, CLI. In 0.x a breaking change is a minor release and everything else a patch. Prereleases (`0.26.0-rc.0`) go to\nthe dist-tag `next`, releases to `latest`; both are passed explicitly to `npm publish --tag` for every package.\n\n### Run a release\n\nActions, **Release**, **Run workflow** on `main`:\n\n| Input | Meaning |\n| --- | --- |\n| `version` | `X.Y.Z`, `X.Y.Z-rc.N`, or `patch` / `minor` of the shared line (the highest `latest` of core, CLI, render, PPTX and editor) |\n| `dry-run` | Default **true**: rehearse everything up to `npm publish --dry-run`; nothing is committed, tagged or published. Set false for the real release |\n| `skip-gate` | A dry run only: skip the slow gate |\n\nRun it with `dry-run` true first when the change touches the release machinery; a real release of ordinary changes needs no\nrehearsal. The jobs:\n\n1. **plan**: a real release runs on `main`, and the check `verify` is green for its head (one API call, no waiting).\n2. **gate** (parallel with prepare): `nightly.yml` called on that commit: Windows, macOS, Node 22 and 26, the packed-install,\n registry and npm 12 consumers. A red leg stops the release before anything is committed or tagged.\n3. **prepare** (parallel with gate): `node scripts/release.mjs prepare <version>` in the runner: sets the six versions, core's\n peer ranges and `PEER_RANGES` (`packages/javascript/src/node/peers.ts`), assembles every `changes/` fragment into its package\n changelog (a package without a fragment gets \"No package changes; released with the set.\"), applies the gallery stability gate\n and the version rules below, refreshes the lockfile, and uploads the result as a patch. A **prerelease** (`X.Y.Z-rc.N`) only sets the versions: it leaves every changelog and `changes/` alone, and its GitHub release notes are assembled from the fragments (`release.mjs notes`), so the stable release that follows carries every note. A refused version fails here, in two\n minutes.\n4. **commit and tag** (real release, after gate and prepare): with the GitHub App's token (a bypass actor on the `main` ruleset)\n applies the patch as one commit `release vX.Y.Z` on exactly the commit the gate tested, pushes it to `main`, then pushes the\n tag `vX.Y.Z`. If `main` moved during the run, or the tag exists, it stops. A tag pushed with the App's token starts\n `npm-publish.yml` (a tag pushed with `GITHUB_TOKEN` would not).\n5. **npm-publish.yml** on the tag: `plan` (the tag is on `main`), `build` (one install and build, `pnpm pack` of the six,\n `scripts/stamp-githead.mjs` adds `gitHead` to each tarball because `pnpm pack` omits it, a smoke test installs the six\n tarballs together, renders the quickstart deck and runs the CLI), `publish` (the only job with `id-token: write`; the\n pre-publish gate, then `npm publish <tarball> --access public --provenance --tag <tag>` six times, printing npm's own output; a version already on npm is\n skipped. Once npm accepted an upload the package is published: if the registry does not serve it yet it is logged as\n **pending**, which is not a failure, and the job goes on), `verify` (below: it first waits for the registry), `release` (the GitHub release, notes grouped by package) and `report` (a failure opens the issue\n \"Release vX.Y.Z failed\", labeled `release-failed`).\n6. **follow the publish run** (the Release run waits for it, so it is green only when the whole release is).\n\nTime target: about 20 minutes from the click to six verified packages (measured in MR-44).\n\n### Version rules\n\n- `prepare` refuses a version that is not above the shared line's previous release, and one already on npm for any package\n (a published version is never reused; fix forward).\n- **The gallery stability rule** (`scripts/gallery-stability.mjs`, `pnpm check:stability`; see\n [default-catalog.md](default-catalog.md)): a declared drawing change (a `drawing` entry in a gallery fragment), a removed record\n id or a `CATALOG_SCHEMA` change needs a **minor** release; `prepare` refuses a patch. New records and unchanged drawings go in\n any release. The baseline is the gallery's `latest` dist-tag, never its highest version.\n- **The publish gate** (`release.mjs check-publish`, run by `publish`): each package's version is above its own registry\n `latest`, except the gallery, which is compared with the shared line, because its `latest` was 5.0.0 before it joined the\n shared 0.x line (D16). A stable version needs `latest`, a prerelease must not take it. 5.0.0 and the older gallery versions\n stay published and are never deprecated; because they are above the shared line, every gallery publish names its tag explicitly.\n- The shared line cannot use 1.0.0 to 5.0.0 (taken by the gallery): the first stable major for all six is 6.0.0.\n\n### Trusted publishing\n\nAll six packages have the trusted publisher: GitHub Actions, organization `OpenPresentation`, repository `opf`, workflow filename\n`npm-publish.yml`, no environment (owner steps O1 in `docs/programs/monorepo/owner-steps.md`). npm checks the entry against\nthe **top-level** workflow of the run, not against a reusable workflow that contains `npm publish`, so `npm-publish.yml` is the\nworkflow that starts on the tag and `release.yml` only pushes the tag. `publish` refuses to publish from any other top-level\nworkflow. A publish fails with `E404` or `E403 ... not a trusted publisher` when an entry is wrong: fix the entry and re-run\nthe workflow on the tag. There is no `NPM_TOKEN`, and no local publish: the provenance gate applies to every release.\n\n### Retry and failure\n\nA failed `npm-publish.yml` run is fixed and run again on the same tag: Actions, **Publish OPF npm packages**, **Run workflow**,\nref `vX.Y.Z`, `dry-run` false (or re-run the failed run). Versions already on npm are skipped, so it continues where it stopped.\nA partial publish is safe: the `^X.Y.Z` floors point at packages that exist, because the order is dependency order. A real\ndefect in a published version is fixed forward with the next patch and a new tag; tags are never moved. If `prepare` or the gate\nfails, nothing was committed: fix the cause and run the release again.\n\n### Fallback: a release pull request\n\nIf no App token is available, or a person prefers review: on a branch run `pnpm release:prepare X.Y.Z`\n(`node scripts/release.mjs prepare X.Y.Z`; it needs `pnpm build` first for the stability gate), open the pull request, merge it\non green, then on `main` run `git tag vX.Y.Z && git push origin vX.Y.Z`. The publish workflow is the same.\n\n### Verify\n\nThe registry serves an accepted upload some time after `npm publish` exits 0: 1 to 7 minutes for v0.26.0-rc.0 and about 20 for core\nin v0.26.9. That lag is waited out in the `verify` job, never failed in `publish` (a lagging package used to fail the publish job\nand skip verify and the GitHub release; a manual `gh run rerun --failed` was the fix). The `verify` job runs, in order:\n\n1. `node scripts/release.mjs await-registry X.Y.Z --wait 60 --poll 15 --max-poll 120`: for each package still missing, requests\n the version's own registry document (`/@scope%2fname/X.Y.Z`) with `Cache-Control: no-cache` and a unique query string, so\n neither the npm CLI cache nor a CDN's cached 404 answers. The first look is immediate; the interval starts at 15 s, grows by\n half each round up to 120 s, and each round logs which packages are still missing and for how long. A served package is not\n asked again. Only when the 60 minute budget is spent does it fail, with the staged-package guidance (see Troubleshooting). A\n lookup that errors (HTTP 5xx) counts as not served yet.\n2. `node scripts/release.mjs verify X.Y.Z --tag <dist-tag> --commit <release commit> --wait 10 --poll 30`, which waits only for the\n attestation bundle and the scratch install (a few more minutes), and then the smoke test against the registry\n (`release.mjs smoke X.Y.Z --registry`).\n\nFor all six packages `verify` checks the registry manifest,\n`gitHead` and the provenance source commit equal to the release commit, the provenance workflow `npm-publish.yml` on\n`refs/tags/vX.Y.Z`, the subject digest, the dist-tag, that no manifest carries a `workspace:` range and every sibling range is\n`^X.Y.Z`, `npm audit signatures`, and that each package changelog has the version's section. If the budget is spent, re-run the\nfailed jobs (`gh run rerun --failed`): `publish` is not repeated, because it succeeded. By hand:\n\n```sh\nnode scripts/release.mjs verify X.Y.Z --tag latest # exit 0 when all six verify\nnpm view @openpresentation/opf@X.Y.Z gitHead dist.attestations.url\nnpm view @openpresentation/gallery dist-tags --json # latest is X.Y.Z; 5.0.0 stays published\n```\n\n### `scripts/release.mjs`\n\n| Command | Does |\n| --- | --- |\n| `prepare <version> [--dry-run] [--no-stability] [--previous V]` | the prepare step above (`--dry-run` writes nothing) |\n| `pack <version> --commit <sha> --out <dir>` | `pnpm pack` of the six, `gitHead` stamped, `release-manifest.json` with digests |\n| `smoke <version> (--dir <dir> \\| --registry)` | install the six together, render, resolve, run the CLI |\n| `check-publish <version> [--tag t]` | the pre-publish gate |\n| `publish <version> --dir <dir> [--tag t] [--dry-run] [--wait minutes] [--poll seconds] [--expect-staged]` | the gate, then `npm publish` in dependency order: npm's output is printed; after all six uploads each package gets one registry look (`--wait`, default 0, polls longer) and one not served yet is logged **pending**, never a failure; a staged result fails unless `--expect-staged` |\n| `await-registry <version> [--wait minutes] [--poll seconds] [--max-poll seconds]` | waits (default 60 min, backoff 15 s to 120 s, cache-busting) until the registry serves all six; fails only when the budget is spent |\n| `verify <version> [--tag t] [--commit sha] [--wait minutes]` | the registry and provenance check (a prerelease has no changelog section to check) |\n| `notes <version> [--out file]` | the GitHub release body |\n| `order` | the publish order |\n\n### After a release: the sites\n\nAfter a **stable** release, `release.yml` calls `.github/workflows/bump-sites.yml` (a prerelease never does). For each of the\nthree sites (`Data-Advantage/openpresentation-site` and `Data-Advantage/pptx-gallery` on `main`, `Data-Advantage/pptx-dev` on\n`master`) it opens one pull request, \"bump to vX.Y.Z\", from the branch `bot/opf-vX.Y.Z`. What a bump touches is the site's own\nbusiness, so each site owns one script, `pnpm opf:bump <X.Y.Z> [--opf <checkout>]` (`scripts/opf-bump.mjs` of the site), that sets its\n`@openpresentation/*` pins, refreshes its lockfile, regenerates every file it derives from the release (a release history from the\nregistry and these release notes, a pin mirror, preview fonts, chart previews, the vendored editor bundle, a homepage manifest) and runs\nthe checks of its own CI that read them; `scripts/bump-site.mjs` decides the label and calls it (a site without one gets only the\ngeneric pins and lockfile). A bump needs no human edit to be green; one that does (a release that changes behavior or removes a\ndocument the site links) fails the site's CI and stays open. A patch bump is labeled\n`auto-merge`; a minor or major bump (a breaking change in 0.x) is labeled `needs-review` and stays open. The workflow only opens\nand labels the pull request and never merges it: the site's own CI merges an `auto-merge` pull request after its\n`build-and-smoke` job passes, using the GitHub App's token so that the merge starts the site's deploy workflows (D19; the sites\nare private repositories in a Free-plan organization and have no rulesets or required checks). A bump that cannot be made opens\nan issue in this repository (`bump-sites-failed`) and never fails the release. To run it again, or for a release made by the pull-request\nfallback or from a tag, use Actions, **Bump sites**, **Run workflow** with the version. The App, its secrets and the site-side\nworkflows are owner steps O4 and O8 (`docs/programs/monorepo/owner-steps.md`); the workflow files for the sites are in\n`docs/programs/monorepo/site-workflows/`.\n\n### Who may release\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29 (`AGENTS.md`). A release is made only through\n`release.yml` (or the pull-request fallback) and `npm-publish.yml`, only when one is required and only with every gate green;\nagents never publish from a laptop, never skip the gate and never pass a different `--tag` to make a check pass. Dry runs are\nfree at any time.\n\n## Geometry-moving core releases\n\nCore composition changes that move geometry make the preview and the PPTX export drift when the renderer and the exporter resolve\ndifferent core versions. This cannot happen between the packages of this repository: all six are released at one version, and\ntheir runtime dependencies and peers on each other are `workspace:^`, published as `^X.Y.Z`, so the floor of every sibling is the\nrelease itself. Do not hand-edit those ranges, and do not release one package alone.\n\n## Renderer font-count gate (RR-90)\n\nThe installed-consumer check (`scripts/test-packed-ecosystem.mjs`) and `test:gallery-lazy-fonts` compare the number of vendored faces a renderer lists as lazy fonts with `scripts/lazy-font-counts.mjs`. A renderer release whose fonts did not change needs no new entry. The gate hashes the renderer package's font set, which is 143 files: `dist/font-manifest.js` and every file under `fonts/` (faces, licenses, `PROVENANCE.json`, `NOTICE.md`). An unlisted version, stable or a prerelease (`X.Y.Z-rc.N`), passes when that hash is in `REVIEWED_FONT_SETS` and its measured count equals the count recorded there.\n\nIt fails closed, with a message that says what changed, when:\n\n- the font files differ from every reviewed set (the message prints the new hashes): review what changed, then add the set to `REVIEWED_FONT_SETS` and the version's count to `LAZY_FONT_COUNTS`;\n- the font files are a reviewed set but the count differs: find out why the count moved, then add an entry;\n\nA prerelease is held to the same rule as the stable version: its label neither admits nor refuses it, its font files and count do.\n\nTo review a set, pack the release (`npm pack @openpresentation/opf-render@<version>`), run `hashFontSet` from `scripts/lazy-font-counts.mjs` over the unpacked `package/` folder, and record the result with the versions you checked. Every existing `LAZY_FONT_COUNTS` entry stays and keeps its own rule.\n\n## Troubleshooting\n\n- **The tag or version check fails** (`npm-publish.yml`, job `plan`): the tag does not point at a commit of `main`, or the version\n is not well formed. Inspect the exact tag. Never move or re-push a release tag; fix a real error on `main` and release the\n next version. A transient failure is retried on the same unchanged tag.\n- **`E404`, `E403` or `ENEEDAUTH` from `npm publish`**: the trusted publisher of that package is missing or wrong. It must be\n organization `OpenPresentation`, repository `opf`, workflow filename `npm-publish.yml`, no environment, and the `publish` job\n needs `id-token: write`. Fix the entry in npm (owner step O1) and re-run the workflow on the tag.\n- **`verify` fails at \"Wait for the registry\" with \"npm accepted X (exit 0) but the registry still does not serve it after 60\n min\"**, or `publish` fails with \"npm staged X\": npm returned success (or staged the upload) but the version is not installable.\n Ordinary registry lag never gets here: it is logged as pending in `publish` and waited out in `verify`. Open the package on\n npmjs.com, Staged Packages tab, or run `npm stage list <name>` as a maintainer: a staged version must be approved with 2FA\n (`npm stage approve <stage-id>`; trusted-publishing OIDC tokens cannot approve). Otherwise check npm's status page and, once\n the version is served, re-run the failed jobs of the run (`gh run rerun --failed`); the packages already served are skipped.\n- **A version is already on npm**: `publish` skips it; do not try to publish it again. Verify the package and continue.\n- **The gate is red**: the failing leg of `nightly.yml` is in the Release run. Fix it on `main` and release again; nothing was\n committed. A flaky leg is quarantined (`pnpm check:quarantine`), never skipped for a release.\n- **`release.yml` stopped with \"main moved\"**: a commit landed on `main` after the run started, and the gate had tested the\n earlier one. Run the release again.\n- **A nightly failure**: the `nightly-red` issue lists the failing legs and the run; it never blocks a pull request, and the\n release gate refuses to publish on the same failure.\n"
|
|
297
|
+
"markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe packages declare the open-ended `engines.node` `>=22` (RR-20, tested on Node 22, 24 and 26 by the `node-range`\nCI legs, which run in the nightly workflow, before every release and on pull requests labelled `full-ci`); a closed range such as `24.x` makes npm on any other Node silently install an older release, and\n`pnpm check:engines-range` fails on one, including in a lockfile that pins a release made after the fix.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the seven published packages of this repository:\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf) (core, `packages/javascript`),\n`@openpresentation/cli`, `@openpresentation/opf-render`, `@openpresentation/opf-pptx`, `@openpresentation/opf-editor`,\n`@openpresentation/gallery` and `@openpresentation/mcp` (the MCP server, first published with 0.27.0; its first publish needs the\ntrusted-publisher entry of `docs/programs/monorepo/owner-steps.md`, O1b). They are released together, at one version, with one tag.\n\n## Release: one click\n\n> Status: the workflows and `scripts/release.mjs` are in place and rehearsed with dry runs. The first real release through them\n> is the canary `0.26.0-rc.0` (MR-41), then 0.26.0 (MR-44). Releases up to 0.25.x were made from the four old repositories with\n> a per-package train; that process is retired and not described here.\n\n**One version, one tag, one workflow.** All seven packages carry the same version (`pnpm check:versions`); a release is the tag\n`vX.Y.Z` on a commit of `main`, published by `.github/workflows/npm-publish.yml` in dependency order: gallery, core, render,\nPPTX, editor, CLI, MCP. In 0.x a breaking change is a minor release and everything else a patch. Prereleases (`0.26.0-rc.0`) go to\nthe dist-tag `next`, releases to `latest`; both are passed explicitly to `npm publish --tag` for every package.\n\n### Run a release\n\nActions, **Release**, **Run workflow** on `main`:\n\n| Input | Meaning |\n| --- | --- |\n| `version` | `X.Y.Z`, `X.Y.Z-rc.N`, or `patch` / `minor` of the shared line (the highest `latest` of core, CLI, render, PPTX and editor) |\n| `dry-run` | Default **true**: rehearse everything up to `npm publish --dry-run`; nothing is committed, tagged or published. Set false for the real release |\n| `skip-gate` | A dry run only: skip the slow gate |\n\nRun it with `dry-run` true first when the change touches the release machinery; a real release of ordinary changes needs no\nrehearsal. The jobs:\n\n1. **plan**: a real release runs on `main`, and the check `verify` is green for its head (one API call, no waiting).\n2. **gate** (parallel with prepare): `nightly.yml` called on that commit: Windows, macOS, Node 22 and 26, the packed-install,\n registry and npm 12 consumers. A red leg stops the release before anything is committed or tagged.\n3. **prepare** (parallel with gate): `node scripts/release.mjs prepare <version>` in the runner: sets the seven versions, core's\n peer ranges and `PEER_RANGES` (`packages/javascript/src/node/peers.ts`), assembles every `changes/` fragment into its package\n changelog (a package without a fragment gets \"No package changes; released with the set.\"), applies the gallery stability gate\n and the version rules below, refreshes the lockfile, and uploads the result as a patch. A **prerelease** (`X.Y.Z-rc.N`) only sets the versions: it leaves every changelog and `changes/` alone, and its GitHub release notes are assembled from the fragments (`release.mjs notes`), so the stable release that follows carries every note. A refused version fails here, in two\n minutes.\n4. **commit and tag** (real release, after gate and prepare): with the GitHub App's token (a bypass actor on the `main` ruleset)\n applies the patch as one commit `release vX.Y.Z` on exactly the commit the gate tested, pushes it to `main`, then pushes the\n tag `vX.Y.Z`. If `main` moved during the run, or the tag exists, it stops. A tag pushed with the App's token starts\n `npm-publish.yml` (a tag pushed with `GITHUB_TOKEN` would not).\n5. **npm-publish.yml** on the tag: `plan` (the tag is on `main`), `build` (one install and build, `pnpm pack` of the seven,\n `scripts/stamp-githead.mjs` adds `gitHead` to each tarball because `pnpm pack` omits it, a smoke test installs the seven\n tarballs together, renders the quickstart deck and runs the CLI), `publish` (the only job with `id-token: write`; the\n pre-publish gate, then `npm publish <tarball> --access public --provenance --tag <tag>` seven times, printing npm's own output; a version already on npm is\n skipped. Once npm accepted an upload the package is published: if the registry does not serve it yet it is logged as\n **pending**, which is not a failure, and the job goes on), `verify` (below: it first waits for the registry), `release` (the GitHub release, notes grouped by package) and `report` (a failure opens the issue\n \"Release vX.Y.Z failed\", labeled `release-failed`).\n6. **follow the publish run** (the Release run waits for it, so it is green only when the whole release is).\n\nTime target: about 20 minutes from the click to seven verified packages (measured in MR-44).\n\n### Version rules\n\n- `prepare` refuses a version that is not above the shared line's previous release, and one already on npm for any package\n (a published version is never reused; fix forward).\n- **The gallery stability rule** (`scripts/gallery-stability.mjs`, `pnpm check:stability`; see\n [default-catalog.md](default-catalog.md)): a declared drawing change (a `drawing` entry in a gallery fragment), a removed record\n id or a `CATALOG_SCHEMA` change needs a **minor** release; `prepare` refuses a patch. New records and unchanged drawings go in\n any release. The baseline is the gallery's `latest` dist-tag, never its highest version.\n- **The publish gate** (`release.mjs check-publish`, run by `publish`): each package's version is above its own registry\n `latest`, except the gallery, which is compared with the shared line, because its `latest` was 5.0.0 before it joined the\n shared 0.x line (D16). A stable version needs `latest`, a prerelease must not take it. 5.0.0 and the older gallery versions\n stay published and are never deprecated; because they are above the shared line, every gallery publish names its tag explicitly.\n- The shared line cannot use 1.0.0 to 5.0.0 (taken by the gallery): the first stable major for all seven is 6.0.0.\n\n### Trusted publishing\n\nAll seven packages have the trusted publisher: GitHub Actions, organization `OpenPresentation`, repository `opf`, workflow filename\n`npm-publish.yml`, no environment (owner steps O1 in `docs/programs/monorepo/owner-steps.md`). npm checks the entry against\nthe **top-level** workflow of the run, not against a reusable workflow that contains `npm publish`, so `npm-publish.yml` is the\nworkflow that starts on the tag and `release.yml` only pushes the tag. `publish` refuses to publish from any other top-level\nworkflow. A publish fails with `E404` or `E403 ... not a trusted publisher` when an entry is wrong: fix the entry and re-run\nthe workflow on the tag. There is no `NPM_TOKEN`, and no local publish: the provenance gate applies to every release.\n\n### Retry and failure\n\nA failed `npm-publish.yml` run is fixed and run again on the same tag: Actions, **Publish OPF npm packages**, **Run workflow**,\nref `vX.Y.Z`, `dry-run` false (or re-run the failed run). Versions already on npm are skipped, so it continues where it stopped.\nA partial publish is safe: the `^X.Y.Z` floors point at packages that exist, because the order is dependency order. A real\ndefect in a published version is fixed forward with the next patch and a new tag; tags are never moved. If `prepare` or the gate\nfails, nothing was committed: fix the cause and run the release again.\n\n### Fallback: a release pull request\n\nIf no App token is available, or a person prefers review: on a branch run `pnpm release:prepare X.Y.Z`\n(`node scripts/release.mjs prepare X.Y.Z`; it needs `pnpm build` first for the stability gate), open the pull request, merge it\non green, then on `main` run `git tag vX.Y.Z && git push origin vX.Y.Z`. The publish workflow is the same.\n\n### Verify\n\nThe registry serves an accepted upload some time after `npm publish` exits 0: 1 to 7 minutes for v0.26.0-rc.0 and about 20 for core\nin v0.26.9. That lag is waited out in the `verify` job, never failed in `publish` (a lagging package used to fail the publish job\nand skip verify and the GitHub release; a manual `gh run rerun --failed` was the fix). The `verify` job runs, in order:\n\n1. `node scripts/release.mjs await-registry X.Y.Z --wait 60 --poll 15 --max-poll 120`: for each package still missing, requests\n the version's own registry document (`/@scope%2fname/X.Y.Z`) with `Cache-Control: no-cache` and a unique query string, so\n neither the npm CLI cache nor a CDN's cached 404 answers. The first look is immediate; the interval starts at 15 s, grows by\n half each round up to 120 s, and each round logs which packages are still missing and for how long. A served package is not\n asked again. Only when the 60 minute budget is spent does it fail, with the staged-package guidance (see Troubleshooting). A\n lookup that errors (HTTP 5xx) counts as not served yet.\n2. `node scripts/release.mjs verify X.Y.Z --tag <dist-tag> --commit <release commit> --wait 10 --poll 30`, which waits only for the\n attestation bundle and the scratch install (a few more minutes), and then the smoke test against the registry\n (`release.mjs smoke X.Y.Z --registry`).\n\nFor all seven packages `verify` checks the registry manifest,\n`gitHead` and the provenance source commit equal to the release commit, the provenance workflow `npm-publish.yml` on\n`refs/tags/vX.Y.Z`, the subject digest, the dist-tag, that no manifest carries a `workspace:` range and every sibling range is\n`^X.Y.Z`, `npm audit signatures`, and that each package changelog has the version's section. If the budget is spent, re-run the\nfailed jobs (`gh run rerun --failed`): `publish` is not repeated, because it succeeded. By hand:\n\n```sh\nnode scripts/release.mjs verify X.Y.Z --tag latest # exit 0 when all seven verify\nnpm view @openpresentation/opf@X.Y.Z gitHead dist.attestations.url\nnpm view @openpresentation/gallery dist-tags --json # latest is X.Y.Z; 5.0.0 stays published\n```\n\n### `scripts/release.mjs`\n\n| Command | Does |\n| --- | --- |\n| `prepare <version> [--dry-run] [--no-stability] [--previous V]` | the prepare step above (`--dry-run` writes nothing) |\n| `pack <version> --commit <sha> --out <dir>` | `pnpm pack` of the seven, `gitHead` stamped, `release-manifest.json` with digests |\n| `smoke <version> (--dir <dir> \\| --registry)` | install the seven together, render, resolve, run the CLI |\n| `check-publish <version> [--tag t]` | the pre-publish gate |\n| `publish <version> --dir <dir> [--tag t] [--dry-run] [--wait minutes] [--poll seconds] [--expect-staged]` | the gate, then `npm publish` in dependency order: npm's output is printed; after all six uploads each package gets one registry look (`--wait`, default 0, polls longer) and one not served yet is logged **pending**, never a failure; a staged result fails unless `--expect-staged` |\n| `await-registry <version> [--wait minutes] [--poll seconds] [--max-poll seconds]` | waits (default 60 min, backoff 15 s to 120 s, cache-busting) until the registry serves all six; fails only when the budget is spent |\n| `verify <version> [--tag t] [--commit sha] [--wait minutes]` | the registry and provenance check (a prerelease has no changelog section to check) |\n| `notes <version> [--out file]` | the GitHub release body |\n| `order` | the publish order |\n\n### After a release: the sites\n\nAfter a **stable** release, `release.yml` calls `.github/workflows/bump-sites.yml` (a prerelease never does). For each of the\nthree sites (`Data-Advantage/openpresentation-site` and `Data-Advantage/pptx-gallery` on `main`, `Data-Advantage/pptx-dev` on\n`master`) it opens one pull request, \"bump to vX.Y.Z\", from the branch `bot/opf-vX.Y.Z`. What a bump touches is the site's own\nbusiness, so each site owns one script, `pnpm opf:bump <X.Y.Z> [--opf <checkout>]` (`scripts/opf-bump.mjs` of the site), that sets its\n`@openpresentation/*` pins, refreshes its lockfile, regenerates every file it derives from the release (a release history from the\nregistry and these release notes, a pin mirror, preview fonts, chart previews, the vendored editor bundle, a homepage manifest) and runs\nthe checks of its own CI that read them; `scripts/bump-site.mjs` decides the label and calls it (a site without one gets only the\ngeneric pins and lockfile). A bump needs no human edit to be green; one that does (a release that changes behavior or removes a\ndocument the site links) fails the site's CI and stays open. A patch bump is labeled\n`auto-merge`; a minor or major bump (a breaking change in 0.x) is labeled `needs-review` and stays open. The workflow only opens\nand labels the pull request and never merges it: the site's own CI merges an `auto-merge` pull request after its\n`build-and-smoke` job passes, using the GitHub App's token so that the merge starts the site's deploy workflows (D19; the sites\nare private repositories in a Free-plan organization and have no rulesets or required checks). A bump that cannot be made opens\nan issue in this repository (`bump-sites-failed`) and never fails the release. To run it again, or for a release made by the pull-request\nfallback or from a tag, use Actions, **Bump sites**, **Run workflow** with the version. The App, its secrets and the site-side\nworkflows are owner steps O4 and O8 (`docs/programs/monorepo/owner-steps.md`); the workflow files for the sites are in\n`docs/programs/monorepo/site-workflows/`.\n\n### Who may release\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29 (`AGENTS.md`). A release is made only through\n`release.yml` (or the pull-request fallback) and `npm-publish.yml`, only when one is required and only with every gate green;\nagents never publish from a laptop, never skip the gate and never pass a different `--tag` to make a check pass. Dry runs are\nfree at any time.\n\n## Geometry-moving core releases\n\nCore composition changes that move geometry make the preview and the PPTX export drift when the renderer and the exporter resolve\ndifferent core versions. This cannot happen between the packages of this repository: all seven are released at one version, and\ntheir runtime dependencies and peers on each other are `workspace:^`, published as `^X.Y.Z`, so the floor of every sibling is the\nrelease itself. Do not hand-edit those ranges, and do not release one package alone.\n\n## Renderer font-count gate (RR-90)\n\nThe installed-consumer check (`scripts/test-packed-ecosystem.mjs`) and `test:gallery-lazy-fonts` compare the number of vendored faces a renderer lists as lazy fonts with `scripts/lazy-font-counts.mjs`. A renderer release whose fonts did not change needs no new entry. The gate hashes the renderer package's font set, which is 143 files: `dist/font-manifest.js` and every file under `fonts/` (faces, licenses, `PROVENANCE.json`, `NOTICE.md`). An unlisted version, stable or a prerelease (`X.Y.Z-rc.N`), passes when that hash is in `REVIEWED_FONT_SETS` and its measured count equals the count recorded there.\n\nIt fails closed, with a message that says what changed, when:\n\n- the font files differ from every reviewed set (the message prints the new hashes): review what changed, then add the set to `REVIEWED_FONT_SETS` and the version's count to `LAZY_FONT_COUNTS`;\n- the font files are a reviewed set but the count differs: find out why the count moved, then add an entry;\n\nA prerelease is held to the same rule as the stable version: its label neither admits nor refuses it, its font files and count do.\n\nTo review a set, pack the release (`npm pack @openpresentation/opf-render@<version>`), run `hashFontSet` from `scripts/lazy-font-counts.mjs` over the unpacked `package/` folder, and record the result with the versions you checked. Every existing `LAZY_FONT_COUNTS` entry stays and keeps its own rule.\n\n## Troubleshooting\n\n- **The tag or version check fails** (`npm-publish.yml`, job `plan`): the tag does not point at a commit of `main`, or the version\n is not well formed. Inspect the exact tag. Never move or re-push a release tag; fix a real error on `main` and release the\n next version. A transient failure is retried on the same unchanged tag.\n- **`E404`, `E403` or `ENEEDAUTH` from `npm publish`**: the trusted publisher of that package is missing or wrong. It must be\n organization `OpenPresentation`, repository `opf`, workflow filename `npm-publish.yml`, no environment, and the `publish` job\n needs `id-token: write`. Fix the entry in npm (owner step O1) and re-run the workflow on the tag.\n- **`verify` fails at \"Wait for the registry\" with \"npm accepted X (exit 0) but the registry still does not serve it after 60\n min\"**, or `publish` fails with \"npm staged X\": npm returned success (or staged the upload) but the version is not installable.\n Ordinary registry lag never gets here: it is logged as pending in `publish` and waited out in `verify`. Open the package on\n npmjs.com, Staged Packages tab, or run `npm stage list <name>` as a maintainer: a staged version must be approved with 2FA\n (`npm stage approve <stage-id>`; trusted-publishing OIDC tokens cannot approve). Otherwise check npm's status page and, once\n the version is served, re-run the failed jobs of the run (`gh run rerun --failed`); the packages already served are skipped.\n- **A version is already on npm**: `publish` skips it; do not try to publish it again. Verify the package and continue.\n- **The gate is red**: the failing leg of `nightly.yml` is in the Release run. Fix it on `main` and release again; nothing was\n committed. A flaky leg is quarantined (`pnpm check:quarantine`), never skipped for a release.\n- **`release.yml` stopped with \"main moved\"**: a commit landed on `main` after the run started, and the gate had tested the\n earlier one. Run the release again.\n- **A nightly failure**: the `nightly-red` issue lists the failing legs and the run; it never blocks a pull request, and the\n release gate refuses to publish on the same failure.\n"
|
|
290
298
|
},
|
|
291
299
|
{
|
|
292
300
|
"slug": "rich-text",
|
|
@@ -298,7 +306,13 @@ var docsData = Object.freeze([
|
|
|
298
306
|
"slug": "schema-reference",
|
|
299
307
|
"file": "docs/schema-reference.md",
|
|
300
308
|
"title": "OPF Presentation Schema Reference",
|
|
301
|
-
"markdown": "# OPF Presentation Schema Reference\n\nThis reference documents the author-facing shape of a complete `*.opf.json` presentation document. It summarizes the canonical schema in `spec/schemas/opf.schema.json`; the schema remains the source of truth for validators.\n\n## Document Contract\n\n- Schema id: `https://openpresentation.org/schema/opf/v1`\n- Required top-level fields: `slides`\n- Additional top-level fields: not allowed\n\n## Top-Level Fields\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | no | `const:\"https://openpresentation.org/schema/opf/v1\"` | Optional OPF schema version. When omitted, validators and engines should assume the latest supported OPF schema. |\n| `name` | no | `string` | Display name of the presentation for GUI/TUI lists, library/search indexing, OS-level metadata, and default export filenames. This is deck identity, not slide content. Use slides[].title and slides[].subtitle for text... |\n| `description` | no | `string` | Free-form prose describing what this presentation is about. Used by agents and humans as a deck-level summary; complements purpose (the goal) and narrative (the structured storyline). Round-trips to OOXML 'docProps/co... |\n| `filename` | no | `string` | Optional base filename for exports (without extension). The opf CLI (convert) and the editor strip a trailing .pptx, .pdf, .png, or .svg (case-insensitive) and append the target format's extension. When omitted, they... |\n| `organization` | no | `oneOf:ref:Organization / array<ref:Organization>` | Organization associated with the presentation, usually the presenting company. Array form supports hosts, partners, clients, and sponsors. The primary organization (Organization.role 'primary', else the first item) su... |\n| `speaker` | no | `oneOf:ref:Speaker / array<ref:Speaker>` | Person presenting the deck. Array form supports panels and multi-speaker decks. The first speaker is the primary speaker: the built-in variables 'speaker.<field>' read it, 'speakers' lists every name, and 'speaker.<id... |\n| `author` | no | `oneOf:string / array<string>` | Optional credit for the person who authored or contributed to the deck, distinct from speaker. Array form supports multiple contributors. Round-trips to OOXML 'docProps/core.xml' as '<dc:creator>' (semicolon-joined wh... |\n| `audience` | no | `oneOf:string / ref:Audience / array<oneOf:string / ref:Audience>` | Intended audiences for the presentation. Accepts any of: - A single string: an audiences reference ('executive', or 'acme:board' for a record of the catalogs.acme group), or free-form text ('Series B investors'). A st... |\n| `purpose` | no | `oneOf:string / ref:Purpose` | Primary goal of the presentation. Accepts either: - A string: a purposes reference ('decide', or 'acme:fundraise' for a record of the catalogs.acme group), or a free-form goal ('Raise a Series B round of $30M'). A str... |\n| `language` | no | `oneOf:ref:LanguageTag / ref:Language` | Language of the presentation content, an engine vocabulary: a BCP-47 language tag, validated directly with no catalog lookup. Accepts either: - A BCP-47 tag ('en-US', 'en-GB', 'ja', 'zh-Hans', 'ar-SA'). - An inline La... |\n| `tone` | no | `oneOf:ref:CatalogReference / ref:Tone` | Desired tone for the presentation. Accepts either: - A tones reference: a bare id ('formal') or 'name:id' for a record of a named catalogs group. - An inline Tone object for custom tone metadata or overrides on a refe... |\n| `takeaway` | no | `oneOf:string / array<string>` | Audience-facing takeaway the presentation should leave behind. Array form supports multiple takeaways. Deck-level intent used by AI to seed and pressure-test slide content. |\n| `duration` | no | `integer` | Target presentation duration, as an integer number of minutes. The opf-render presenter view counts the elapsed time against it, and core validate warns when it lies outside the resolved narrative's 'duration' range.... |\n| `tags` | no | `array<string>` | Free-form labels used for categorization, search, and filtering. Lowercase kebab-case is recommended for consistency across a deck library. |\n| `design` | no | `ref:Design` | Optional design system covering theme, color scheme, font scheme, dimensions, background, logo, watermark, header, and footer applied to the deck. When omitted, engines use their default design configuration. |\n| `variables` | no | `ref:Variables` | Optional named variables: deck colors referenced as 'var:<id>' (the original use), and typed content variables (text, number, date, image, url, list) referenced inline as '{{<id>}}' or whole as 'var:<id>'. Variables a... |\n| `template` | no | `boolean` | Marks this document as a template: an incomplete OPF file. A template declares variables (top-level 'variables') and references them from content, and may leave required variables unfilled; validation then reports the... |\n| `narrative` | no | `ref:CatalogReference` | The deck's narrative plan, by reference: a bare narratives id ('classic-story') or 'name:id' for a record of a named catalogs group ('acme:founder-pitch'). The deck holds only this pointer; the plan (the arc, its beat... |\n| `slides` | yes | `array<ref:Slide>` | Ordered array of slides that make up the presentation. |\n| `references` | no | `array<ref:Reference>` | Sources that text runs cite with 'cite'. Ids are unique. A cited reference is listed in the footnote area of every slide that cites it, with a marker number assigned per deck in order of first use; a reference no run... |\n| `datasets` | no | `ref:Datasets` | Optional shared data tables, keyed by id. A chart ('chart.data': { \"dataset\": \"<id>\" }) or a table ('table': { \"dataset\": \"<id>\" }) references one instead of holding its own copy; engines inline the reference before c... |\n| `assets` | no | `ref:Assets` | Optional reusable asset registry for images, data files, videos, documents, fonts, and other resources referenced elsewhere in the deck via 'asset:<id>' strings. |\n| `catalogs` | no | `ref:Catalogs` | The catalog records this document embeds, grouped by the catalog they came from: 'default' (the catalog bare ids come from), 'custom' (records the document defines itself) and any number of named catalogs ('acme', ref... |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows; ignored by the engine but preserved across read/write round-trips. |\n\n## Object And Type Reference\n\n### Assets\n\n- Type: `object`\n- Required fields: none\n- Purpose: Reusable asset registry for resources used by slides, charts, metadata, and design. Keys are stable asset ids referenced elsewhere as 'asset:<id>'. Each asset can be a source string or an object with src plus optional metadata.\n\n_No named properties._\n\n\n### Asset\n\n- Type: `oneOf:string / object`\n- Required fields: none\n- Purpose: Reusable or inline resource. A string is shorthand for { \"src\": value }. Source strings accept 'asset:<id>' references, HTTPS URLs, data URIs, relative paths resolved against the OPF file location, or local filesystem paths. Use object form when metadata such as alt text, title or mediaType matters.\n\n_No named properties._\n\n\n### Audience\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline audience metadata for the presentation. Use 'id' to reference an audiences record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline audience.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional audiences reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience. |\n| `description` | no | `string` | Longer prose describing the audience and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, advise, or decide. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on focused attention for a single presentation, in minutes. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids that work well for this audience, for picker UIs. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: tone ids that work well for this audience, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Purpose\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline purpose metadata for the presentation. Use 'id' to reference a purposes record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline purpose.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional purposes reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids that work well for this purpose, for picker UIs. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: tone ids that work well for this purpose, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Language\n\n- Type: `object`\n- Required fields: `bcp47`\n- Purpose: Inline language metadata for the presentation: a BCP-47 'bcp47' tag plus overrides of what engines know about it. Languages are an engine vocabulary, not catalog records, so there is no 'id'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | no | `string` | Human-readable language name. |\n| `bcp47` | yes | `ref:LanguageTag` | BCP-47 language tag used for locale-aware rendering, proofing, and accessibility metadata. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Without it, en... |\n| `code` | no | `string` | ISO 639-3 or 639-2 language code carried for engines that prefer ISO codes. |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') for this language's script slot when targeting PowerPoint output. Its major/minor families fill the language's script slot (eastAsian or complexScript, chosen by 'script'... |\n| `googleFontScheme` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') used in place of 'fontScheme' when resolving script fonts for Google Slides output. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Tone\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline tone metadata for the presentation. Use 'id' to reference a tones record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline tone.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional tones reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids this tone pairs well with, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Organization\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: An organization associated with the presentation, typically the presenting company, but also hosts, partners, clients, or sponsors. The primary organization (role 'primary', else the first one) supplies the cover and section logo (unless design.logo overrides it) and the 'socials' header/footer field (its socials). Every field is also a built-in variable ('{{organization.name}}', 'organization.<id>.<field>'), and every logo a slide-scoped image ('var:organization.logo.icon', 'var:organization...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier for the organization, used to reference it from Speaker.organizationId and to address it in built-in variables as 'organization.<id>.<field>'. Must be unique among organizations (a duplicate is a val... |\n| `name` | yes | `string` | Display name. The built-in variable 'organization.name' (primary organization) or 'organization.<id>.name' carries it into any string, such as a header or footer text of '{{organization.name}}'. |\n| `legalName` | no | `string` | Optional legal entity name when it differs from the display name. Never drawn automatically; available as the built-in variable 'organization.legalName'. |\n| `logo` | no | `oneOf:ref:Asset / ref:OrganizationLogo` | The organization's logo. A path, URL or Asset is one logo for every shape; an object gives up to four shapes (full, stacked, icon, wordmark), each a path or Asset, or { onLight, onDark } for the two backgrounds. A pat... |\n| `domain` | no | `string` | Bare internet domain for the organization. Never drawn automatically and not used to look up assets; available as the built-in variable 'organization.domain'. |\n| `email` | no | `string` | General contact email for the organization. Never drawn automatically; available as the built-in variable 'organization.email'. |\n| `phone` | no | `string` | Main contact phone number for the organization. E.164 format is recommended. Never drawn automatically; available as the built-in variable 'organization.phone'. |\n| `tagline` | no | `string` | Short tagline. Never drawn automatically (cover slides do not show it); available as the built-in variable 'organization.tagline', for example in a footer 'text' of '{{organization.tagline}}'. |\n| `role` | no | `enum:primary \\| partner \\| client \\| sponsor \\| host` | Role of the organization relative to the presentation. Only 'primary' has behavior: it selects the primary organization (cover and section logo, the 'socials' header/footer field, and the 'organization.<field>' built-... |\n| `socials` | no | `ref:Socials` | Optional social media handles or URLs for the organization. The primary organization's socials render in header/footer zones that set socials: true; otherwise they are authoring metadata. |\n\n\n### Speaker\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A person presenting the deck. A speaker is drawn only through built-in variables ('{{speaker.name}}' inside any string, a header or footer text included, and 'var:speaker.photo' as a whole image field). No layout, cover or bio slide places a speaker on its own. See docs/templates-and-variables.md.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier for the speaker. Must be unique among speakers (a duplicate is a validation error). Addresses this speaker in built-in variables as 'speaker.<id>.<field>', such as '{{speaker.alice.title}}'. |\n| `name` | yes | `string` | Display name. Built-in variable 'speaker.name' (first speaker) or 'speaker.<id>.name'; every name is listed by 'speakers'. |\n| `title` | no | `string` | Role or title. Built-in variable 'speaker.title' (or 'speaker.<id>.title'), for example in a footer text of '{{speaker.name}}, {{speaker.title}}'. |\n| `photo` | no | `ref:Asset` | Source for the speaker's headshot image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are JPG or PNG; SVG is not appropr... |\n| `email` | no | `string` | Contact email. Never drawn automatically; available as the built-in variable 'speaker.email'. |\n| `phone` | no | `string` | Contact phone number for the speaker. E.164 format is recommended. Never drawn automatically; available as the built-in variable 'speaker.phone'. |\n| `bio` | no | `string` | Short biographical paragraph. Never drawn automatically; available as the built-in variable 'speaker.bio', for a bio or 'about the speaker' slide you write. |\n| `organizationId` | no | `string` | Reference to an Organization.id in organization; it must name an existing organization (a validation error otherwise). Attribution metadata for hosts: no engine draws it or changes which organization is primary becaus... |\n| `socials` | no | `ref:Socials` | Optional social media handles or URLs for the speaker. Authoring metadata only: nothing draws speaker socials and there is no built-in variable for them (the 'socials' header/footer field draws the primary organizatio... |\n\n\n### Socials\n\n- Type: `object`\n- Required fields: none\n- Purpose: Social media handles or URLs, keyed by platform. The keys are an engine vocabulary validated directly (no catalog lookup): engines link a handle with the platform's URL pattern and handle prefix (spec/reference/engine-vocabularies.json). Each value is a string, either a full URL or a platform handle (e.g., '@acme'). Brand colours and icons are catalog display metadata for authoring UIs; engines render the profile URL, not icons or brand colours. Used both for organization and speaker records.\n\n_No named properties._\n\n\n### Design\n\n- Type: `object`\n- Required fields: none\n- Purpose: Visual design system applied to the presentation; individual slides may override fields via Slide.design.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `theme` | no | `ref:CatalogReference` | Theme for the deck: a themes reference, a bare id ('minimal') or 'name:id' ('acme:brand'). The theme supplies the default colour scheme, font scheme, background and dimensions; design.colorScheme, design.fontScheme, d... |\n| `colorScheme` | no | `oneOf:string / ref:ColorScheme` | Color scheme for the presentation. Accepts two forms: - A colorSchemes reference: a bare id ('cool-horizon') or 'name:id' ('acme:ocean'). - Object form: a ColorScheme with an optional 'id' reference as the base plus s... |\n| `fontScheme` | no | `oneOf:string / ref:FontScheme` | Font scheme for heading, body, accent, and code text. Accepts two forms: - A fontSchemes reference: a bare id ('aptos') or 'name:id' ('acme:inter'). - Object form: a FontScheme with an optional 'id' reference as the b... |\n| `dimensions` | no | `oneOf:ref:DimensionPreset / ref:Dimensions` | Slide dimensions and aspect ratio. String shorthand such as 'widescreen' is equivalent to { preset: 'widescreen' }. |\n| `background` | no | `oneOf:ref:BackgroundShortcut / ref:Background` | Default slide background applied across the deck unless overridden on a slide (slides.N.design.background); the resolved theme's background is the fallback. A background is the only canvas fill and never moves content... |\n| `variant` | no | `oneOf:ref:ThemeBackgroundSlot / ref:ColorVariant` | Optional color variant, as a background slot ('light1', 'light2', 'dark1', 'dark2'; the same as { background: slot }) or the object form, which is needed only for an accent order: which slot of the color scheme is the... |\n| `logo` | no | `oneOf:string / const:false` | Which logo covers, section dividers and picture bullets draw. Logos live on the organization (Organization.logo); without this field covers and sections draw the primary organization's full logo and picture bullets it... |\n| `watermark` | no | `oneOf:const:false / ref:Asset / ref:Watermark` | Optional decorative watermark applied across slides: an image or a text stamp, in the fixed frame and at the opacity described on Watermark. Use false to suppress an inherited watermark in slide-level design; a string... |\n| `header` | no | `oneOf:const:false / ref:HeaderFooter` | Repeated header furniture rendered outside the main slide content. Use false to suppress an inherited header. |\n| `footer` | no | `oneOf:const:false / ref:HeaderFooter` | Repeated footer furniture rendered outside the main slide content. Use false to suppress an inherited footer. |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Default horizontal alignment for the title heading area (the title and the tag) in resolved layouts. Effective value: the slide's design, then the deck's design, then the theme's design.titleAlignment, then the layout... |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Default horizontal alignment for body/content regions in resolved layouts. Effective value: the slide's design, then the deck's design, then the theme's design.contentAlignment, then the layout record's design.content... |\n| `contentBox` | no | `boolean` | Whether body/content regions are rendered inside a visible card or surface. Effective value: the slide's design, then the deck's design, then the theme's design.contentBox, then the layout record's design.contentBox,... |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. Effective value: the slide's design, then the deck's design, then the theme's design.contentDirection, then the layout re... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content. Effective value: slide design, then deck design, then the layout record's design.chartPrimary. It applies to automatic slides only; a template layout says t... |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of image blocks (and Slide.image) that set no fit of their own: 'cover' fills the frame and crops what overflows around the block's focus, 'contain' shows the whole picture centered in the frame, 'stretch'... |\n| `listBullet` | no | `enum:character \\| image` | Marker style for items and bullets lists. Effective value: the slide's design, then the deck's design, then the theme's design.listBullet, then the layout record's design.listBullet, then 'character'. 'character' (the... |\n| `mirror` | no | `boolean` | Draw the slide's template layout mirrored (OPF 0.19): each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Bin... |\n\n\n### ColorScheme\n\n- Type: `object`\n- Required fields: none\n- Purpose: Color palette used by the design system. Every slot and role is a hex color ('#RGB', '#RRGGBB' or '#RRGGBBAA'); a color-scheme name or a 'var:<id>' reference is not valid here, because those resolve through the scheme. The slot fields (accent1-accent6, dark1, dark2, light1, light2, hyperlink, followedHyperlink) mirror color-scheme.schema.json (https://openpresentation.org/schema/opf-color-scheme/v1) so library records and inline OPF overrides are interchangeable on those fields. Two parallel...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Colour-scheme reference (a bare id or 'name:id') resolved as the base; slot and role overrides on the surrounding ColorScheme object take precedence over the resolved scheme. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. Link runs with no color of their own are drawn underlined in it, in the preview and in PowerPoint, unless it has under 4.5:1 contrast against the slide background, then in the slide... |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `primary` | no | `ref:HexColor` | Abstract role: primary brand color (hex). The engine maps this onto an OOXML accent slot when serializing. |\n| `secondary` | no | `ref:HexColor` | Abstract role: secondary brand color (hex). |\n| `accent` | no | `ref:HexColor` | Abstract role: accent color used for highlights and emphasis (hex). |\n| `background` | no | `ref:HexColor` | Abstract role: default slide background color (hex), used when the design names no single-color background (and for gradient and picture backgrounds). Overrides light1 as that default. |\n| `surface` | no | `ref:HexColor` | Abstract role: color for elevated surfaces such as cards and panels (hex). Overrides the default of light2 (dark2 on a dark slide). |\n| `text` | no | `ref:HexColor` | Abstract role: primary body text color (hex). Overrides dark1 on a light slide only; a dark slide always uses light1. |\n| `textSecondary` | no | `ref:HexColor` | Abstract role: secondary or muted text color used for captions and supporting copy (hex). Overrides the default of dark2 (light2 on a dark slide). |\n\n\n### FontScheme\n\n- Type: `object`\n- Required fields: none\n- Purpose: Typography selections used by the design system. The pair fields (major, minor) and refinement fields (type, app, languageFamily) mirror font-scheme.schema.json (https://openpresentation.org/schema/opf-font-scheme/v1) so library records and inline OPF overrides are interchangeable on those fields. Two parallel models are supported and may be mixed: - OOXML pairs (major, minor) - heading and body family names that round-trip directly to PowerPoint majorFont/minorFont entries. - Abstract roles...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') resolved as the base; field overrides on the surrounding FontScheme object take precedence over the resolved scheme. |\n| `major` | no | `string` | Heading (major) font family mirrors the OOXML majorFont entry. Pairs with 'minor'. |\n| `minor` | no | `string` | Body (minor) font family mirrors the OOXML minorFont entry. Pairs with 'major'. |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `heading` | no | `string` | Abstract role: font family name used for slide titles and headings. Maps onto the OOXML major slot when serializing. |\n| `body` | no | `string` | Abstract role: font family name used for body copy. Maps onto the OOXML minor slot when serializing. |\n| `accent` | no | `string` | Abstract role: font family name used for accent text. When set, the slide tag (eyebrow) and the quote body use this family instead of the body and heading families; nothing else changes. resolveFontFamilies() returns... |\n| `code` | no | `string` | Abstract role: monospaced font family name used for code blocks and for inline code runs (TextRun.code). No direct OOXML slot. Resolution: this override, then the resolved font-scheme record's 'code' (for example Cons... |\n\n\n### SlideDesign\n\n- Type: `allOf:ref:Design + schema`\n- Required fields: none\n- Purpose: A slide's design: every Design field except dimensions. A PPTX has one slide size, so the size is set once, on the deck's design.dimensions (or its theme), and a slide's design cannot set it. A slide-level theme whose resolved dimensions differ from the deck's is a validate warning (opf/slide-theme-dimensions); exporting such a deck to PPTX fails with mixed-slide-dimensions.\n\n_No named properties._\n\n\n### DimensionPreset\n\n- Type: `enum:16:9 | 4:3 | 16:10 | 1:1 | 4:5 | 9:16 | letter | a4 | widescreen | standard`\n- Required fields: none\n- Purpose: Named preset; sets aspect ratio and physical size. 16:9 ('widescreen'): 13.333 x 7.5 in, PowerPoint's Widescreen. 4:3 ('standard'): 10 x 7.5 in. 16:10: 10 x 6.25 in. letter (11 x 8.5 in) and a4 (11.69 x 8.27 in) are paper sizes, not PowerPoint's Letter Paper or A4 Paper. 1:1, 4:5 and 9:16 are 7.5 in on the short edge.\n\n_No named properties._\n\n\n### Dimensions\n\n- Type: `object`\n- Required fields: none\n- Purpose: Slide dimensions: a preset, or custom inches that override it. PowerPoint's Letter Paper (10 x 7.5 in), A4 Paper (10.8333 x 7.5 in) and On-screen Show (16:9) (10 x 5.625 in) are not OPF presets; use widthInches and heightInches. docs/slide-sizes.md compares every preset.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `preset` | no | `ref:DimensionPreset` | |\n| `widthInches` | no | `number` | Custom slide width in inches; overrides the preset width when provided. |\n| `heightInches` | no | `number` | Custom slide height in inches; overrides the preset height when provided. |\n\n\n### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n\n### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color shorthand accepted by selected string fields.\n\n_No named properties._\n\n\n### ColorRef\n\n- Type: `anyOf:ref:HexColor / enum:accent1 | accent2 | accent3 | accent4 | accent5 | accent6 | dark1 | dark2 | light1 | light2 | hyperlink | followedHyperlink | primary | secondary | accent | background | surface | surfaceAlt | text | textSecondary / string`\n- Required fields: none\n- Purpose: A color value or reference, enforced on styled table cell fill and text colors, cell border colors, solid background colors, gradient stop colors and pattern foreground and background colors. Three forms: - Literal hex: '#RGB', '#RRGGBB', or '#RRGGBBAA'. - Color-scheme name, resolved through the effective color scheme after design resolution: an OOXML slot ('accent1'-'accent6', 'dark1', 'dark2', 'light1', 'light2', 'hyperlink', 'followedHyperlink') or an abstract role ('primary', 'secondary',...\n\n_No named properties._\n\n\n### Variables\n\n- Type: `object`\n- Required fields: none\n- Purpose: Named variables, keyed by stable kebab-case id. A variable is a typed, named value the deck declares once and uses in many places: a color ('var:<id>' in color fields, the original use), or content that fills a template (text, number, date, image, url, list). Content is referenced inline as '{{<id>}}' inside any string, or whole as 'var:<id>' in a field of the matching type; '\\{{' writes a literal '{{'. A hex string is shorthand for a color variable. A variable with no 'value' is unfilled: ex...\n\n_No named properties._\n\n\n### Variable\n\n- Type: `oneOf:ref:HexColor / ref:ColorVariable / ref:TextVariable / ref:NumberVariable / ref:DateVariable / ref:ImageVariable / ref:UrlVariable / ref:ListVariable`\n- Required fields: none\n- Purpose: A single named variable: a hex string (shorthand for a color variable) or an object whose 'type' is color, text, number, date, image, url or list. 'value' is the current value and is optional: a variable with no value is unfilled, which a template allows and a normal deck does not. 'example' only illustrates the slot (fill forms, template previews) and never reaches output.\n\n_No named properties._\n\n\n### ColorVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A named color. Content color fields reference it as 'var:<id>'. Colors resolve at render time through the ordinary color-reference path, so a color variable keeps working as before.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"color\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `ref:HexColor` | Hex color this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `ref:HexColor` | Illustrative color shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### TextVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: Text content. Plain string or rich TextRun[]. Insert it inline as '{{<id>}}' inside any string (rich text is flattened to plain text there), or reference it whole as 'var:<id>' in a field that accepts string or TextRun[] (the rich runs are kept).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"text\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `oneOf:string / array<ref:TextRun>` | Text this variable resolves to: a plain string, or TextRun[] for rich text. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `oneOf:string / array<ref:TextRun>` | Illustrative text shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### NumberVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A number. '{{<id>}}' inserts it as text using 'format'; 'var:<id>' as a whole field supplies the number itself (chart values, font sizes).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"number\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `number` | Number this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `number` | Illustrative number shown in fill forms and used when a template is previewed with examples. Never written to output. |\n| `format` | no | `ref:NumberFormat` | Display pattern used by '{{<id>}}' (the shared NumberFormat syntax). A literal prefix, a numeric part of '#', '0', ',' and '.', and a literal suffix. '0' pads digits, '#' is optional, ',' groups thousands, digits afte... |\n\n\n### NumberFormat\n\n- Type: `string`\n- Required fields: none\n- Purpose: Number display pattern, shared by NumberVariable.format, data column formats (DataColumn.format) and table cell formats (StyledTableCell.format). An optional literal prefix, a numeric part of '#', '0', ',' and '.', and an optional literal suffix. '0' pads digits, '#' is optional, ',' groups thousands, digits after '.' fix the decimals ('0' required, '#' optional), and a '%' in the prefix or suffix multiplies the value by 100. English separators only. A format applies only to number values; st...\n\n_No named properties._\n\n\n### DateVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A calendar date as an ISO YYYY-MM-DD string. No time zone and no clock are involved. '{{<id>}}' inserts it formatted with 'format'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"date\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `string` | ISO calendar date (YYYY-MM-DD) this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `string` | Illustrative ISO date shown in fill forms and used when a template is previewed with examples. Never written to output. |\n| `format` | no | `string` | Date display pattern, the same LDML-style tokens as header/footer dateFormat: yyyy, yy, MMMM, MMM, MM, M, dd, d, EEEE, EEE and quoted literals. English names. Default 'MMMM d, yyyy'. |\n\n\n### ImageVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: An image source: any Asset (an 'asset:<id>' reference, HTTPS URL, data URI, relative or local path, or an object with src, alt and metadata). Reference it whole as 'var:<id>' in an image, asset or src field; '{{<id>}}' inserts the source string.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"image\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `ref:Asset` | Image source this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `ref:Asset` | Illustrative image source shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### UrlVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A link target (http, https, mailto or tel). Use it as '{{<id>}}' inside a link string or reference it whole as 'var:<id>' in a link field.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"url\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `string` | Link target this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `string` | Illustrative link shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### ListVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A list of strings, for bullets and list items. A whole-string array element 'var:<id>' splices every entry into the array in place; a whole field 'var:<id>' becomes the array; '{{<id>}}' joins the entries with ', ' (or the separator after a pipe: '{{<id>|; }}').\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"list\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `array<string>` | Entries this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `array<string>` | Illustrative entries shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### BackgroundShortcut\n\n- Type: `oneOf:ref:ThemeBackgroundSlot / ref:HexColor / ref:ImageSource`\n- Required fields: none\n- Purpose: String shorthand for a background. Theme slots ('light1', 'light2', 'dark1', 'dark2') are equivalent to { type: 'theme', slot: value }; hex colors are equivalent to { type: 'solid', color: value }; an image source (starting with 'asset:', 'https://', 'data:', './' or '../') is equivalent to { type: 'image', src: value }, a cover image. Any other string is invalid.\n\n_No named properties._\n\n\n### ImageSource\n\n- Type: `string`\n- Required fields: none\n- Purpose: An image source in a string shorthand: an 'asset:<id>' reference, an HTTPS URL, a data URI, or a relative path starting with './' or '../' (resolved against the OPF file location).\n\n_No named properties._\n\n\n### ColorVariant\n\n- Type: `object`\n- Required fields: `background`\n- Purpose: A variant of the deck's single color scheme, as a mapping over its twelve slots (PowerPoint's clrMap): 'background' is the slot the slide background and the role bg1 take, and with it the other roles: light1 maps bg1 = light1, tx1 = dark1, bg2 = light2, tx2 = dark2; dark1 maps bg1 = dark1, tx1 = light1, bg2 = dark2, tx2 = light2; light2 and dark2 make that slot both the background and the alternate surface (light2: bg1 = bg2 = light2, tx1 = dark1, tx2 = dark2; dark2: bg1 = bg2 = dark2, tx1 =...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `background` | yes | `ref:ThemeBackgroundSlot` | The scheme slot that is the background (bg1) of the variant. |\n| `accents` | no | `array<enum:accent1 \\| accent2 \\| accent3 \\| accent4 \\| accent5 \\| accent6>` | Optional accent order: a permutation of accent1 to accent6. Entry i is the scheme slot drawn as accent i + 1, so primary, secondary and accent (the first three) and the chart series colors follow it. Default: the sche... |\n\n\n### Background\n\n- Type: `oneOf:ref:ThemeBackground / ref:SolidBackground / ref:GradientBackground / ref:ImageBackground / ref:PatternBackground`\n- Required fields: none\n- Purpose: Background fill applied to slides. Theme backgrounds preserve PowerPoint's color-scheme background choice; other variants represent fixed background fills.\n\n_No named properties._\n\n\n### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n\n### SolidBackground\n\n- Type: `object`\n- Required fields: `type`, `color`\n- Purpose: Fixed solid slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"solid\"` | Fixed solid background fill. |\n| `color` | yes | `ref:ColorRef` | Fixed solid fill color: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef, resolved against the effective color scheme and the deck variables). Use { type: 'theme', slot: ...... |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### GradientBackground\n\n- Type: `object`\n- Required fields: `type`, `gradient`\n- Purpose: Fixed gradient slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `tone` | no | `enum:dark \\| light` | Whether the background is dark or light, which picks the color map (dark1 or light1) so text, lines and neutrals read on it. Without a tone a picture or gradient takes the deck's variant, else counts as light. |\n| `type` | yes | `const:\"gradient\"` | Fixed gradient background fill. |\n| `gradient` | yes | `object` | Gradient fill definition. |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### ImageBackground\n\n- Type: `object`\n- Required fields: `type`, `src`\n- Purpose: Picture background: the image fills the whole canvas behind everything and never moves content (to push content aside, use an image block with placement). The string shorthand 'asset:x' (or any image source) is { type: 'image', src: 'asset:x' }, a cover image. Paint order: the colour scheme's default slide background, the picture, then the overlay, then furniture and content.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `tone` | no | `enum:dark \\| light` | Whether the background is dark or light, which picks the color map (dark1 or light1) so text, lines and neutrals read on it. Without a tone a picture or gradient takes the deck's variant, else counts as light. |\n| `type` | yes | `const:\"image\"` | Picture background. |\n| `src` | yes | `string` | Source for the background image: an 'asset:<id>' reference, HTTPS URL, data URI, or a relative path resolved against the OPF file location. |\n| `alt` | no | `string` | Alternative text. A background with alt is meaningful: PowerPoint export draws it as a full-slide picture at the back that carries the text, so screen readers see it. Without alt it is decorative and exports as the na... |\n| `fit` | no | `enum:cover \\| contain \\| stretch \\| tile` | How the picture fills the canvas: 'cover' fills it and crops what overflows around focus; 'contain' shows the whole picture centered over the colour scheme's default background; 'stretch' scales it to the canvas exact... |\n| `focus` | no | `ref:ImageFocus` | Point of the picture to keep in view when 'cover' crops it. Ignored by the other fits. |\n| `opacity` | no | `number` | Picture opacity from 0 (fully transparent) to 1 (fully opaque); the overlay keeps its own opacity. Default 1. |\n| `recolor` | no | `ref:ImageRecolor` | Grayscale or duotone treatment of the picture's pixels, as on an image block: recolor and opacity apply to the pixels, then the overlay is drawn above. |\n| `overlay` | no | `ref:Overlay` | Scrim over the whole picture or a band along one edge (edge, size), beneath furniture and content, for example to keep a title readable over a busy photo. |\n\n\n### ImageFocus\n\n- Type: `object`\n- Required fields: `x`, `y`\n- Purpose: A point of a picture as fractions of its width and height, from the top-left corner. A cover fit centers this point in the frame as far as the picture still covers the frame, so it always stays in view: { x: 0.5, y: 0.5 } (the default) is a center crop, { x: 0, y: 0 } keeps the top-left corner.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `x` | yes | `number` | Horizontal position, 0 (left edge) to 1 (right edge). |\n| `y` | yes | `number` | Vertical position, 0 (top edge) to 1 (bottom edge). |\n\n\n### Overlay\n\n- Type: `object`\n- Required fields: `color`, `opacity`\n- Purpose: Solid scrim over a picture: the whole frame in the frame's shape, or a band along one edge. Shared by image backgrounds and image blocks. Drawn directly above its picture and beneath headings and content; PowerPoint export writes one native shape above the picture.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Overlay fill color: a hex color (eight-digit hex keeps its alpha), a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `opacity` | yes | `number` | Overlay fill opacity. |\n| `edge` | no | `enum:top \\| bottom \\| left \\| right` | Cover only a band along this edge of the frame, for example a caption strip. Backgrounds and rectangle image frames only; on another shape the overlay is not drawn and composition reports unsupported-image-treatment.... |\n| `size` | no | `number` | Band share of the frame for an edge overlay. Default 0.3. Ignored without edge. |\n\n\n### ImagePlacement\n\n- Type: `object`\n- Required fields: `edge`\n- Purpose: Bleeds an image block to one edge of the slide: the image takes a band along that edge, edge to edge, and the headings and other content compose in the rest of the slide at the normal padding. Only top-level blocks (slides.N.blocks.I) can be placed, at most one per edge. Template layout regions do not supply image placement; set it on the image block itself. Bands are taken in block order, each spanning the free area left by earlier ones. Placement is the only mechanism that moves content asi...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `edge` | yes | `enum:left \\| right \\| top \\| bottom` | The slide edge the image bleeds to. |\n| `size` | no | `number` | Share of the slide width (left, right) or height (top, bottom) given to the band. Default 0.5. |\n| `inset` | no | `boolean` | Draw the frame inside the slide padding on every side of the band, like a card, instead of edge to edge. Default false. |\n\n\n### ImageBorder\n\n- Type: `object`\n- Required fields: `color`, `width`\n- Purpose: Solid line along the image frame's shape, centered on the outline with a miter join, like a native picture line. A thick dark border on a rounded portrait frame gives a device bezel.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Line color: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `width` | yes | `number` | Line width in reference pixels at a 720-pixel short edge. 0 removes the line. |\n\n\n### ImageRecolor\n\n- Type: `oneOf:const:\"grayscale\" / object`\n- Required fields: none\n- Purpose: Color treatment for the image pixels. Luminance uses Rec. 601 weights (0.299, 0.587, 0.114) on sRGB values.\n\n_No named properties._\n\n\n### PatternBackground\n\n- Type: `object`\n- Required fields: `type`, `pattern`\n- Purpose: Fixed pattern slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"pattern\"` | Fixed pattern background fill. |\n| `pattern` | yes | `object` | Pattern fill definition. |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### OrganizationLogo\n\n- Type: `object`\n- Required fields: none\n- Purpose: An organization's logo in up to four shapes. Each shape is a path or Asset, or { onLight, onDark } for the two backgrounds. A missing shape falls back to full, and full to the first defined of wordmark, stacked and icon; within a shape a missing onLight or onDark uses the other. Shapes are placed with slide-scoped references such as 'var:organization.logo.icon'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `full` | no | `ref:LogoSource` | The full logo (lockup): mark and name side by side. Drawn on cover and section slides, and placed by 'var:organization.logo'. |\n| `stacked` | no | `ref:LogoSource` | The stacked lockup: mark above the name, for portrait or square slots. Placed by 'var:organization.logo.stacked'. |\n| `icon` | no | `ref:LogoSource` | The mark or symbol without the name, for tight spaces such as header and footer zones and picture bullets. Placed by 'var:organization.logo.icon'. |\n| `wordmark` | no | `ref:LogoSource` | The name set in the brand's lettering, without the mark. Placed by 'var:organization.logo.wordmark'. |\n\n\n### LogoSource\n\n- Type: `oneOf:ref:Asset / object`\n- Required fields: none\n- Purpose: One logo shape: a path or Asset for every background, or { onLight, onDark }. onLight is drawn on light backgrounds (usually dark artwork) and onDark on dark backgrounds (usually light artwork), by each slide's background; a missing one uses the other.\n\n_No named properties._\n\n\n### Watermark\n\n- Type: `oneOf:schema / schema`\n- Required fields: `opacity`\n- Purpose: Decorative watermark: an image ('src') or a text stamp ('text'), exactly one of the two, with its opacity. An image is drawn once per slide, contained and centered in a fixed frame (the middle 40% of the slide width and height, from 30% to 70% on each axis). A text watermark is one line of text in the heading font and the theme text color at the given opacity, centered on the slide and rotated 30 degrees counterclockwise (rising to the right), sized to span at most 70% of the slide width and...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `src` | no | `string` | Source for the watermark image: an 'asset:<id>' reference, HTTPS URL, data URI, relative path or local path, as for Asset.src. Exactly one of src and text is set. |\n| `text` | no | `string` | Text for a text watermark, such as DRAFT or CONFIDENTIAL; line breaks are drawn as spaces. Exactly one of src and text is set. |\n| `opacity` | yes | `number` | Watermark opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### HeaderFooter\n\n- Type: `object`\n- Required fields: none\n- Purpose: Repeated header or footer content split into left, center, and right zones. Header/footer content is slide furniture, separate from the main slide content payloads.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `left` | no | `ref:HeaderFooterItem` | Left-aligned header/footer content. |\n| `center` | no | `ref:HeaderFooterItem` | Centered header/footer content. |\n| `right` | no | `ref:HeaderFooterItem` | Right-aligned header/footer content. |\n\n\n### HeaderFooterItem\n\n- Type: `object`\n- Required fields: none\n- Purpose: One header/footer zone. Every configured field renders, side by side in one row in the order image, text, socials, date (mirrored in a right-to-left deck), aligned to the zone's edge (left, center or right) and vertically centered on each other; a line break inside text or socials ('\\n') stacks lines within that part. When the parts are wider than the zone their text wraps, and a row that cannot fit even so is reported as overflow. Generated values such as the slide number, the slide count, t...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `text` | no | `string` | Text rendered in this zone. Variables resolve inside it like any string: deck-wide ones ('{{organization.name}}', '{{speaker.name}}, {{speaker.title}}', '{{customer}}') before composition, and the slide-scoped built-i... |\n| `image` | no | `ref:Asset` | Image rendered first in this zone's row, such as a logo, partner mark, certification badge, or icon: a path, URL or Asset, or an organization logo reference ('var:organization.logo.icon', 'var:organization.<id>.logo.w... |\n| `date` | no | `oneOf:boolean / string` | true renders the current date: the renderer or exporter must be given an explicit ISO date by its host (core never reads a clock). PPTX export writes a native date field only for a supported dateFormat whose complete... |\n| `dateFormat` | no | `string` | Date pattern for date. Tokens: yyyy (2026), yy (26), MMMM (April), MMM (Apr), MM (04), M (4), dd (09), d (9), EEEE (Thursday), EEE (Thu). Text in single quotes and other non-letter characters are literal. Month and we... |\n| `socials` | no | `boolean` | Whether to render the primary organization's social profiles from organization.socials, one line per platform in key order. A handle is formatted through the platform's socialPlatforms record (companyUrlPattern, else... |\n\n\n### Slide\n\n- Type: `object`\n- Required fields: none\n- Purpose: A single slide. Content can be authored as a full-slide root payload, or inside promoted named region keys such as 'left', 'center+right', and 'top:left'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `string` | Optional stable identifier for the slide within the document. Use when another system needs to reference a slide across edits, comments, generation state, exports, or narrative tooling. Slide order is defined by the s... |\n| `type` | no | `enum:text \\| list \\| image \\| chart \\| table \\| video \\| code \\| metric \\| quote \\| timeline` | Optional full-slide content kind. When omitted, engines infer the kind from root payload fields. |\n| `beat` | no | `oneOf:string / array<string>` | Optional reference to one or more beats of the deck's narrative (each value is the id of a beat in the narrative record that the root 'narrative' resolves to). A single string declares the slide's primary beat; an arr... |\n| `layout` | no | `ref:CatalogReference` | Optional layout: a layouts reference, a bare id ('pillars') or 'name:id' ('acme:hero'), or 'auto'. When omitted or 'auto', the slide is composed with no layout record: engines arrange title, subtitle, tag and content... |\n| `title` | no | `oneOf:string / array<ref:TextRun>` | Slide-level title content. A v2 layout renders this in its title heading area, or an implicit heading row when that area is absent. Use a string for plain text or TextRun[] for inline rich text (a colored word, bold,... |\n| `subtitle` | no | `oneOf:string / array<ref:TextRun>` | Slide-level subtitle or supporting line. A v2 layout renders this in its subtitle area when present, otherwise in the title heading group. Use a string for plain text or TextRun[] for inline rich text (a colored word,... |\n| `tag` | no | `oneOf:string / array<ref:TextRun>` | Small slide-level label or badge. A v2 layout renders this before the title in its title heading group; tag is not a separate layout area. Use a string for plain text or TextRun[] for inline rich text (a colored word,... |\n| `text` | no | `oneOf:string / array<ref:TextRun>` | Full-slide text payload. Use a string for plain text or TextRun[] for inline rich text. TextRun items may be plain strings or formatted run objects. |\n| `items` | no | `array<ref:ListItem>` | Full-slide structured list payload. Presence of this field infers type 'list'. Entries are strings, TextRun[] or objects with `text`, optional `description` (supporting detail under the entry), `level` and `start`. Us... |\n| `bullets` | no | `array<ref:BulletItem>` | Full-slide prose bullet payload: short talking points, each standing alone. Presence of this field infers type 'text', so it may sit beside `text` in one payload. Entries are strings, TextRun[] or objects with `text`,... |\n| `numbering` | no | `ref:NumberingSpec` | Number the full-slide `items` or `bullets` instead of bulleting them. A style name (arabic, roman-upper, roman-lower, alpha-upper, alpha-lower) or a Numbering object applies to every list level; an array gives one ent... |\n| `image` | no | `ref:Asset` | Shorthand for one image block: the picture composes as body content, like { type: 'image', image: value }, with the effective design.imageFit. A v2 layout binds it to a compatible body region; a region's bleed setting... |\n| `video` | no | `ref:Asset` | Full-slide video source. Presence of this field infers type 'video'. Engines do not play video: the preview draws a placeholder (a play badge with the asset's title or source as its caption), and PPTX export draws the... |\n| `chart` | no | `ref:Chart` | Full-slide chart payload. Presence of this field infers type 'chart'. |\n| `table` | no | `ref:Table` | Full-slide table payload. Presence of this field infers type 'table'. |\n| `code` | no | `oneOf:string / ref:Code` | Full-slide code payload. A string is shorthand for { \"source\": value }; object form carries optional syntax language and filename metadata. |\n| `metric` | no | `oneOf:string / number / ref:Metric` | Full-slide metric payload. A string or number is shorthand for { \"value\": value }; object form carries optional label, description, unit, delta, trend, and sentiment metadata. Numeric values remain numbers; renderers... |\n| `quote` | no | `oneOf:string / ref:Quote` | Full-slide quote payload. A string is shorthand for { \"text\": value }; object form carries optional attribution and source metadata. Presence of this field infers type 'quote'. |\n| `timeline` | no | `ref:Timeline` | Full-slide timeline payload. An array is shorthand for { \"events\": value }; object form carries optional name and description metadata. Presence of this field infers type 'timeline'. |\n| `caption` | no | `ref:Caption` | Caption for the slide's root image, chart, table or video payload. Valid only when the slide root holds exactly one of those payloads. |\n| `blocks` | no | `array<ref:ContentPayload>` | Layout-agnostic content blocks rendered together as a composed payload when exact placement is unspecified. At slide root, multiple content payload kinds with no explicit type, blocks, or regions are accepted as short... |\n| `design` | no | `ref:SlideDesign` | Slide-level design applied on top of the deck-wide design. |\n| `left` | no | `ref:ContentPayload` | |\n| `center` | no | `ref:ContentPayload` | |\n| `right` | no | `ref:ContentPayload` | |\n| `left+center` | no | `ref:ContentPayload` | |\n| `center+right` | no | `ref:ContentPayload` | |\n| `left+center+right` | no | `ref:ContentPayload` | |\n| `top` | no | `ref:ContentPayload` | |\n| `middle` | no | `ref:ContentPayload` | |\n| `bottom` | no | `ref:ContentPayload` | |\n| `top+middle` | no | `ref:ContentPayload` | |\n| `middle+bottom` | no | `ref:ContentPayload` | |\n| `top+middle+bottom` | no | `ref:ContentPayload` | |\n| `top:left` | no | `ref:ContentPayload` | |\n| `top:center` | no | `ref:ContentPayload` | |\n| `top:right` | no | `ref:ContentPayload` | |\n| `top:left+center` | no | `ref:ContentPayload` | |\n| `top:center+right` | no | `ref:ContentPayload` | |\n| `top:left+center+right` | no | `ref:ContentPayload` | |\n| `middle:left` | no | `ref:ContentPayload` | |\n| `middle:center` | no | `ref:ContentPayload` | |\n| `middle:right` | no | `ref:ContentPayload` | |\n| `middle:left+center` | no | `ref:ContentPayload` | |\n| `middle:center+right` | no | `ref:ContentPayload` | |\n| `middle:left+center+right` | no | `ref:ContentPayload` | |\n| `bottom:left` | no | `ref:ContentPayload` | |\n| `bottom:center` | no | `ref:ContentPayload` | |\n| `bottom:right` | no | `ref:ContentPayload` | |\n| `bottom:left+center` | no | `ref:ContentPayload` | |\n| `bottom:center+right` | no | `ref:ContentPayload` | |\n| `bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `top+middle:left` | no | `ref:ContentPayload` | |\n| `top+middle:center` | no | `ref:ContentPayload` | |\n| `top+middle:right` | no | `ref:ContentPayload` | |\n| `top+middle:left+center` | no | `ref:ContentPayload` | |\n| `top+middle:center+right` | no | `ref:ContentPayload` | |\n| `top+middle:left+center+right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left` | no | `ref:ContentPayload` | |\n| `middle+bottom:center` | no | `ref:ContentPayload` | |\n| `middle+bottom:right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left+center` | no | `ref:ContentPayload` | |\n| `middle+bottom:center+right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:center` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left+center` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:center+right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `notes` | no | `string` | Speaker notes shown in presenter view. |\n| `section` | no | `string` | PowerPoint-style slide section label. Consecutive slides with the same value belong to the same section in presenter view, outlines, and PowerPoint section-aware exports. Also the slide-scoped built-in variable '{{sli... |\n| `hidden` | no | `boolean` | Whether the slide is hidden from the presented sequence. The player skips it, the PPTX export writes it as a hidden slide, and per-slide image and PDF output skips it unless the caller asks to include hidden slides (o... |\n| `composition` | no | `ref:Composition` | |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows at slide scope; ignored by the engine but preserved across read/write round-trips. Use for review state, generation provenance, or authoring conventions such as { \"authoring... |\n\n\n### ContentPayload\n\n- Type: `allOf:schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema`\n- Required fields: none\n- Purpose: A content leaf or recursively composed group. A group contains blocks and optional composition; it cannot mix blocks with leaf payload fields. Groups may nest up to 32 levels.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `string` | Optional stable identifier for this payload, unique among slide and payload ids in the document. Use when another system needs to address the payload across edits patch-style agent edits, comments, review state, or ge... |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows at payload scope; ignored by the engine but preserved across read/write round-trips. |\n| `region` | no | `string` | Pin a root block (slides.N.blocks.I) to a region of the slide's template layout by name (OPF 0.19). The block goes to that region when the region exists, accepts the block's kind and has room; otherwise the pin is ign... |\n| `type` | no | `enum:text \\| list \\| image \\| chart \\| table \\| video \\| code \\| metric \\| quote \\| timeline \\| group` | Optional content kind. When omitted, engines infer the kind from the fields present. |\n| `text` | no | `oneOf:string / array<ref:TextRun>` | Text payload. Use a string for plain text or TextRun[] for inline rich text. TextRun items may be plain strings or formatted run objects. |\n| `items` | no | `array<ref:ListItem>` | Structured list payload (type 'list'). Each item is either a plain string, a TextRun[] rich text sequence, or a ListItem object with optional `description`. List nesting uses item.level rather than nested content payl... |\n| `bullets` | no | `array<ref:BulletItem>` | Prose bullet payload: short talking points, each standing alone. Presence of this field infers type 'text', so it may sit beside `text` in one payload. Entries have no `description`; use `items` for entries with suppo... |\n| `numbering` | no | `ref:NumberingSpec` | Number the payload's `items` or `bullets` instead of bulleting them. A style name (arabic, roman-upper, roman-lower, alpha-upper, alpha-lower) or a Numbering object applies to every list level; an array gives one entr... |\n| `columns` | no | `oneOf:const:\"auto\" / integer` | Lay the payload's `items` or `bullets` out in columns (OPF 0.19). 'auto' tries 1, 2 then 3 equal columns at the starting text size and uses the first count at which the list fits, as long as a column stays at least a... |\n| `image` | no | `ref:Asset` | Source for an image item. |\n| `fit` | no | `enum:cover \\| contain \\| stretch` | Image payloads only. How the picture fills its frame: 'cover' fills it and crops what overflows around focus; 'contain' shows the whole picture centered; 'stretch' scales it to the frame exactly. Default: the effectiv... |\n| `focus` | no | `ref:ImageFocus` | Image payloads only. Point of the picture to keep in view when 'cover' crops it. Ignored by the other fits. |\n| `aspectRatio` | no | `number` | Image payloads only. Width-to-height ratio of the frame: the frame becomes the largest centered box with this ratio inside the block's region, for example 2.39 for a cinematic letterbox or 0.5 for a phone-shaped frame... |\n| `shape` | no | `enum:rectangle \\| rounded \\| circle \\| hexagon` | Image payloads only. Mask applied to the frame, exported as the picture's native preset geometry (rect, roundRect, ellipse, hexagon). 'circle' makes the frame square. Default 'rectangle'. |\n| `cornerRadius` | no | `number` | Image payloads only. Corner radius for shape 'rounded' as a fraction of the frame's shorter side. Default 0.16667, PowerPoint's roundRect default. |\n| `border` | no | `ref:ImageBorder` | Image payloads only. Line along the frame's shape. |\n| `opacity` | no | `number` | Image payloads only. Picture opacity from 0 to 1, exported as a native alphaModFix. The border and overlay keep their own opacity. Default 1. |\n| `recolor` | no | `ref:ImageRecolor` | Image payloads only. Grayscale or duotone treatment of the picture's pixels. |\n| `overlay` | no | `ref:Overlay` | Image payloads only. Scrim over the picture in the frame's shape, or a band along one edge of a rectangle frame. |\n| `placement` | no | `ref:ImagePlacement` | Image payloads only, and only on a top-level block (slides.N.blocks.I), at most one per edge. Bleeds the image to one slide edge; the headings and the other blocks compose in the rest of the slide. |\n| `video` | no | `ref:Asset` | Source for a video item. Engines do not play video: the preview draws a placeholder (a play badge with the asset's title or source as its caption), and PPTX export draws the same placeholder as native shapes linked to... |\n| `chart` | no | `ref:Chart` | Chart payload. Presence of this field infers type 'chart'. |\n| `table` | no | `ref:Table` | Table payload. Presence of this field infers type 'table'. |\n| `code` | no | `oneOf:string / ref:Code` | Code payload. A string is shorthand for { \"source\": value }; object form carries optional syntax language and filename metadata. |\n| `metric` | no | `oneOf:string / number / ref:Metric` | Metric payload. A string or number is shorthand for { \"value\": value }; object form carries optional label, description, unit, delta, trend, and sentiment metadata. Numeric values remain numbers; renderers format them... |\n| `quote` | no | `oneOf:string / ref:Quote` | Quote payload. A string is shorthand for { \"text\": value }; object form carries optional attribution, role, photo and source metadata. |\n| `timeline` | no | `ref:Timeline` | Timeline payload ordered by narrative or chronology. |\n| `caption` | no | `ref:Caption` | Caption for an image, chart, table or video payload, composed inside the block's region (below the media by default). Invalid on other payload kinds and on groups. |\n| `blocks` | no | `array<ref:ContentPayload>` | Ordered children of a group. Each child is a leaf or another group. |\n| `composition` | no | `ref:Composition` | Arrangement within this group. Only minFontSize and overflow inherit from the parent; strict overflow cannot be weakened. |\n\n\n### Quote\n\n- Type: `object`\n- Required fields: `text`\n- Purpose: Quote content with optional attribution metadata. Use 'text' for the quoted text, 'attribution' for the credited person or organization, 'role' for that person's title and organization, 'photo' for their headshot and 'source' for a citation or URL. A string value in a quote field is shorthand for { \"text\": value }.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `text` | yes | `oneOf:string / array<ref:TextRun>` | Quoted text: a string, or TextRun[] for inline rich text (a colored phrase, bold, a link, or a citation or footnote marker). The engine draws the surrounding quotation marks. |\n| `attribution` | no | `string` | Person or organization credited for the quote. Drawn after the quote text, in the muted text color. |\n| `role` | no | `string` | Title and organization of the attributed person. Drawn on its own line under the attribution; without an attribution it stands alone. It does not replace the attribution, so the person's name belongs in 'attribution'. |\n| `photo` | no | `ref:Asset` | Headshot of the attributed person, drawn as a circle beside the attribution and role lines (on the logical start side: left in a left-to-right deck, right in a right-to-left deck). Give it alt text (validate's opf/mis... |\n| `source` | no | `string` | Optional quote source, citation, or URL. Follows the attribution and role after ' - '. |\n\n\n### Code\n\n- Type: `object`\n- Required fields: `source`\n- Purpose: Code content with optional rendering metadata. Use 'source' for the code text, 'language' for syntax highlighting, 'filename' when the rendered block should show a file label, and 'highlight' to emphasize lines. A string value in a code field is shorthand for { \"source\": value }.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `source` | yes | `string` | Source code text to display. |\n| `language` | no | `string` | Language identifier used for syntax highlighting. |\n| `filename` | no | `string` | Optional file label shown with the code block. |\n| `highlight` | no | `array<oneOf:integer / array<integer>>` | Source lines to emphasize, 1-based: each entry is a line number or an inclusive [start, end] range, so [3, [5, 7]] marks lines 3 and 5 to 7. Lines are counted by line break in 'source', so a line that wraps stays one... |\n\n\n### Metric\n\n- Type: `object`\n- Required fields: `value`\n- Purpose: Metric content with optional display metadata. Use 'value' for the primary value, 'label' for the metric name, 'description' for supporting context, 'unit' for a suffix/currency marker, 'delta' for change, 'trend' for direction, and 'sentiment' for whether the change is good news. A string or number value in a metric field is shorthand for { \"value\": value }; numeric values remain numbers and are formatted by renderers.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `oneOf:string / number` | Primary metric value. |\n| `label` | no | `string` | Metric label. |\n| `description` | no | `string` | Optional supporting context for the metric. |\n| `unit` | no | `string` | Metric unit, suffix, or currency marker. |\n| `delta` | no | `oneOf:string / number` | Metric change value. |\n| `trend` | no | `enum:up \\| down \\| flat` | Metric trend direction. The preview and the PowerPoint export draw an arrow beside the trend word that always points the way the trend does (up, down or flat) and colour the arrow, the trend word and the delta text by... |\n| `sentiment` | no | `enum:positive \\| negative \\| neutral` | Whether the change is good news. Positive draws the trend arrow and the trend and delta text in green, negative in red and neutral in the neutral text colour, each kept at 4.5:1 contrast or more against the slide back... |\n\n\n### Timeline\n\n- Type: `oneOf:array<ref:TimelineEvent> / object`\n- Required fields: none\n- Purpose: Timeline content. An array is shorthand for { \"events\": value }; object form carries optional name and description metadata.\n\n_No named properties._\n\n\n### TimelineEvent\n\n- Type: `object`\n- Required fields: `what`\n- Purpose: A single event inside a timeline content payload.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `when` | no | `string` | Event time, date, or sequence label. Use ISO-like values when possible, but human labels are allowed for quarters, eras, and relative milestones. |\n| `what` | yes | `string` | Short event label. |\n| `description` | no | `string` | Optional event detail. |\n| `status` | no | `enum:done \\| current \\| planned` | Progress of the event, drawn from the deck's own colors by both the SVG renderer and the PPTX exporter. Absent means no status and the event is drawn as a plain filled marker with normal text, which is also how 'done'... |\n\n\n### ListItem\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A flat item inside a list. Strings cover the common case, TextRun[] supports inline rich text without an object wrapper, and object form adds description and nesting depth without creating nested slide content payloads.\n\n_No named properties._\n\n\n### BulletItem\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A flat bullet item. Strings cover the common case, TextRun[] supports inline rich text without an object wrapper, and object form adds nesting depth without list-item descriptions.\n\n_No named properties._\n\n\n### NumberingStyle\n\n- Type: `enum:arabic | roman-upper | roman-lower | alpha-upper | alpha-lower`\n- Required fields: none\n- Purpose: A list number style: 1, 2, 3; I, II, III; i, ii, iii; A, B, C; a, b, c. Alphabetic numbering past 26 repeats the letter as PowerPoint does (aa, bb, cc). Roman numerals stop at 3999; larger values are drawn in arabic with a numbering-adapted diagnostic.\n\n_No named properties._\n\n\n### Numbering\n\n- Type: `object`\n- Required fields: none\n- Purpose: Numbering of one list level.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `style` | no | `ref:NumberingStyle` | Number style. Default arabic. |\n| `start` | no | `integer` | First number counted at this level. Default 1. Native PowerPoint accepts 1 to 32767. |\n| `suffix` | no | `enum:period \\| paren \\| paren-both` | Text after the number: period (1.), paren (1)) or paren-both ((1)). Default period. |\n\n\n### NumberingSpec\n\n- Type: `oneOf:ref:NumberingStyle / ref:Numbering / array<oneOf:ref:NumberingStyle / ref:Numbering>`\n- Required fields: none\n- Purpose: The value of a numbering field: a style name or Numbering object for every level, or an array with one entry per level (at most 9, the native depth).\n\n_No named properties._\n\n\n### TextRun\n\n- Type: `oneOf:string / object`\n- Required fields: none\n- Purpose: A contiguous run of text. Strings cover unformatted spans; object form adds character formatting.\n\n_No named properties._\n\n\n### Caption\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A caption for an image, chart, table or video payload. A string or TextRun[] is the caption text placed below the media; object form adds the position and alignment.\n\n_No named properties._\n\n\n### Reference\n\n- Type: `object`\n- Required fields: `id`, `text`\n- Purpose: A source that runs cite with 'cite'. Cited references are listed in the footnote area of the slides that cite them, numbered per deck in order of first use; referencesSlide() builds an ordinary list slide of them.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Identifier runs cite. Unique within the references list. |\n| `text` | yes | `oneOf:string / array<ref:TextRun>` | The reference as it is listed: a string or TextRun[] for inline rich text. |\n| `url` | no | `string` | Optional link for the reference; a references slide links its entry to it. |\n\n\n### Chart\n\n- Type: `object`\n- Required fields: `type`, `data`\n- Purpose: Chart content. The chart object keeps chart-specific fields together so slides and regions do not expose loose chart fields.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `enum:100pct-stacked-area \\| 100pct-stacked-bar \\| 100pct-stacked-column \\| area \\| bar \\| box-and-whisker \\| column \\| combo \\| doughnut \\| filled-radar \\| funnel \\| histogram \\| line \\| line-with-markers \\| pareto \\| pie \\| radar \\| radar-with-markers \\| scatter \\| stacked-area \\| stacked-bar \\| stacked-column \\| stacked-line \\| stacked-line-with-markers \\| treemap \\| waterfall \\| world` | Chart type, an engine vocabulary validated directly (no catalog lookup): the chart types Aspose.Slides officially supports, plus 'combo' (clustered columns with line series, see 'line' and 'secondaryAxis'). Each engin... |\n| `data` | yes | `oneOf:ref:ChartData / ref:DatasetRef` | Chart data. Inline data uses a tabular columns/rows shape; renderers convert rows to chart series internally. A DatasetRef ({ \"dataset\": \"<id>\" }) plots a top-level dataset instead. Data from a spreadsheet or file is... |\n| `mapping` | no | `ref:ChartMapping` | Optional series mapping by column name: which column is the category, which is the X column of a scatter chart, and which columns are plotted, in order. Absent keeps the positional rule (first column the category, the... |\n| `line` | no | `array<string>` | Combo charts only (type 'combo'): the plotted series, by column name, that are drawn as lines with markers; every other plotted series is drawn as clustered columns. Absent: the last plotted series is the line. A comb... |\n| `secondaryAxis` | no | `array<string>` | Combo charts only: line series, by column name, plotted against a secondary value axis at the right of the plot. That axis has its own scale, and its tick labels use the number format of the first secondary series' co... |\n| `axisTitles` | no | `ref:ChartAxisTitles` | Optional axis titles. Absent keeps today's untitled axes. Supported on the chart types that have a category/value (or X/Y) axis pair (column, bar, line, area, scatter, and the histogram, pareto, waterfall and box-and-... |\n| `legend` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Optional legend position. 'none' hides the legend. Absent keeps today's behaviour exactly (a legend at the right of multi-series charts and of pie and doughnut charts, none for single-series charts). A named position... |\n| `dataLabels` | no | `oneOf:boolean / ref:ChartDataLabels` | Optional data labels. true shows value labels at each type's default position, false (or absent) shows none, which is today's behaviour. Use the object form for the label content, position and separator. |\n| `alt` | no | `string` | Text alternative for the chart: what the data shows (the point and the key numbers), not 'a chart' or 'bar chart'. The preview exposes it as the chart's accessible name (role img with aria-label) and the PowerPoint ex... |\n| `colors` | no | `array<ref:ColorRef>` | Optional series colors, by position: entry i replaces palette color i, the color of series i (the i-th plotted value column), of slice or category i on a pie, doughnut, treemap or funnel chart, and of the i-th color a... |\n| `highlight` | no | `ref:ChartHighlight` | Optional emphasis: the series and/or categories that carry the message. The engines draw the highlighted marks in the deck's primary (accent) color and every other mark in a muted neutral derived from the theme (a lig... |\n\n\n### ChartAxisTitles\n\n- Type: `object`\n- Required fields: none\n- Purpose: Titles for the axes of a chart. 'category' is the axis that carries the row labels (the horizontal axis of a column or line chart, the vertical axis of a bar chart, the X axis of a scatter chart); 'value' is the other axis; 'secondary' is the secondary value axis of a combo chart.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `category` | no | `string` | Title of the category (X) axis. |\n| `value` | no | `string` | Title of the value (Y) axis. On a combo chart, the primary (left) value axis. |\n| `secondary` | no | `string` | Combo charts only: title of the secondary value axis at the right of the plot, drawn rotated like the primary value axis title. Dropped with a 'chart-option-adapted' diagnostic on any other chart type and on a combo c... |\n\n\n### ChartHighlight\n\n- Type: `object`\n- Required fields: none\n- Purpose: Which series and categories a chart emphasizes. Names are matched exactly: series by data column name (after any 'fields' selection; a series is a plotted value column), categories by row label (the first column, or mapping.category), as text. A category label shared by several rows highlights each of them.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `series` | no | `array<string>` | Series (value columns) to highlight: all marks of each named series. A name that is not a column is a 'chart-highlight-unknown-name' error; a column that is not plotted as a series (the category or X column, or one ma... |\n| `categories` | no | `array<string>` | Categories (row labels) to highlight: the marks of each named category (a column or bar of every series, a line's points, a pie or doughnut slice). A label no row has is a 'chart-highlight-unknown-name' error. |\n\n\n### ChartDataLabels\n\n- Type: `object`\n- Required fields: none\n- Purpose: Data label settings. A label shows the selected content parts in the fixed order category, value, percent, joined by the separator.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `content` | no | `array<enum:value \\| percent \\| category>` | Which parts a label shows. Defaults to ['value']. 'percent' is the share of the total and exists only on pie and doughnut charts; 'category' shows the category name (the X value on a scatter chart). A part a chart typ... |\n| `position` | no | `enum:auto \\| center \\| inside-end \\| inside-base \\| outside-end \\| above \\| below \\| left \\| right` | Where a label sits relative to its mark. 'auto' (the default) is the type's default: outside-end for clustered columns and bars, pie slices and the histogram, pareto and waterfall constructs; center for stacked column... |\n| `separator` | no | `string` | Text between the parts of a label that shows more than one. Defaults to ', '. |\n\n\n### Table\n\n- Type: `object`\n- Required fields: none\n- Purpose: Table content, inline or dataset-backed. An inline table has 'rows' (required) and optional 'columns'. A dataset-backed table has 'dataset' (required) and optional 'fields', and no 'rows' or 'columns': it takes its headers, rows and column formats from the dataset (per-cell styles need an inline table).\n- Conditional requirement: `dataset`, or otherwise `rows`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `columns` | no | `array<oneOf:string / array<ref:TextRun> / ref:StyledTableCell / ref:DataColumn / null>` | Optional column labels. Labels may be strings, rich runs, styled cell objects or DataColumn objects ({ \"name\", \"format\" }). A header's 'format' (DataColumn or StyledTableCell) is the column's number format; a body cel... |\n| `rows` | no | `array<array<ref:TableCell>>` | Two-dimensional table row data; each row aligns by index with columns when columns are supplied. |\n| `dataset` | no | `ref:DatasetId` | Id of a top-level dataset that supplies this table's headers, rows and column formats. Excludes 'rows' and 'columns'. An unknown id is a 'dataset-unknown' error. |\n| `fields` | no | `ref:DatasetFields` | Dataset tables only: the dataset columns to show, by name and in order. Absent shows every column. An unknown name is a 'dataset-field-unknown' error. |\n| `alt` | no | `string` | Text alternative for the table: what it shows (the point and the key numbers), not 'a table'. The preview exposes it as the table's accessible name (role group with aria-label; the cells stay readable) and the PowerPo... |\n\n\n### ChartData\n\n- Type: `object`\n- Required fields: `columns`, `rows`\n- Purpose: Inline tabular data driving a chart. The first column usually supplies category/x-axis labels; subsequent columns are plotted measures unless 'chart.mapping' or a chart type maps them differently. Value cells are numbers; a string is read only in strict decimal syntax ('12', '-3.5', '1e6'), and anything else ('12%', '$5', '(5)', '1,234') is a gap and a 'chart-value-not-numeric' warning.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `columns` | yes | `array<oneOf:string / ref:DataColumn>` | Ordered column labels for the chart data table. A label is a string or a DataColumn ({ \"name\", \"format\" }) whose number format the data labels, value axis and exported workbook use. |\n| `rows` | yes | `array<array<ref:ChartDataCell>>` | Tabular chart rows. Each row aligns by index with columns. |\n| `source` | no | `ref:DataSourceRef` | Optional provenance: where this inline data came from. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. |\n\n\n### DataColumn\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: A named data column with an optional number format. Anywhere a chart or dataset column is a string it may be a DataColumn; the string form is { \"name\": value } with no format. A table column header may also be a DataColumn.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `string` | Column label, used as the series name, the table header and the name 'fields' and 'chart.mapping' address. |\n| `format` | no | `ref:NumberFormat` | Number format of the column's number values (data labels, value axis, table cells, exported workbook). Absent: the General form. |\n\n\n### DataSourceRef\n\n- Type: `object`\n- Required fields: `src`\n- Purpose: Provenance of inline data or a dataset: the file, sheet and range it came from and when. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. Re-import the data to update it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `src` | yes | `string` | Where the data came from: an 'asset:<id>' reference, a URL, or a relative or local path. |\n| `sheet` | no | `string` | Sheet or table name inside a spreadsheet source. |\n| `range` | no | `string` | A1-style range inside a spreadsheet source. |\n| `fields` | no | `array<string>` | The source fields the data was taken from, in order. |\n| `retrieved` | no | `anyOf:schema / schema` | When the data was taken from the source: an ISO 8601 date or date-time. |\n| `description` | no | `string` | Free-form note about the source. |\n\n\n### DatasetId\n\n- Type: `string`\n- Required fields: none\n- Purpose: Id of an entry in the top-level datasets map (the assets id pattern).\n\n_No named properties._\n\n\n### DatasetFields\n\n- Type: `array<string>`\n- Required fields: none\n- Purpose: Dataset column names to use, in order, each at most once. Selects and orders the dataset's columns; an unknown name is a 'dataset-field-unknown' error.\n\n_No named properties._\n\n\n### Datasets\n\n- Type: `object`\n- Required fields: none\n- Purpose: Shared data tables keyed by id (the assets id pattern). Charts reference one with 'chart.data': { \"dataset\": \"<id>\" } and tables with 'table': { \"dataset\": \"<id>\" }.\n\n_No named properties._\n\n\n### Dataset\n\n- Type: `object`\n- Required fields: `columns`, `rows`\n- Purpose: One shared data table: named columns (strings or DataColumn objects with a number format) and rows of scalar cells. Column names are unique ('data-column-duplicate' error). Rich text and styling belong in an inline table.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `title` | no | `string` | Human name of the dataset, shown by editors. |\n| `description` | no | `string` | What the dataset holds. |\n| `columns` | yes | `array<oneOf:string / ref:DataColumn>` | Ordered, uniquely named columns. |\n| `rows` | yes | `array<array<ref:ChartDataCell>>` | Rows of scalar cells (string, number, boolean, null); each row aligns by index with columns. |\n| `source` | no | `ref:DataSourceRef` | Optional provenance. Engines never read, fetch or refresh it. |\n\n\n### DatasetRef\n\n- Type: `object`\n- Required fields: `dataset`\n- Purpose: Chart data taken from a top-level dataset. Engines inline it before plotting; 'chart.mapping' names columns after the 'fields' selection.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `dataset` | yes | `ref:DatasetId` | Id of the dataset. An unknown id is a 'dataset-unknown' error. |\n| `fields` | no | `ref:DatasetFields` | The dataset columns to use, by name and in order. Absent uses every column. |\n\n\n### ChartMapping\n\n- Type: `object`\n- Required fields: none\n- Purpose: Series mapping by column name (after any 'fields' selection). Mapping only selects and orders columns; pie, doughnut and other single-series constructs still plot one series. An unknown name is a 'chart-mapping-unknown-column' error; a series that repeats the category or X column, and an X column on a chart type without an X axis, are dropped with a 'chart-mapping-adapted' warning.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `category` | no | `string` | The label (category) column. Default: the first column. |\n| `x` | no | `string` | The X column of an XY (scatter) chart. Default: the second column (the first, when the category is the second column), and only when there are three or more columns: with two, the second column is the one series, plot... |\n| `series` | no | `array<string>` | The plotted columns, in order. Default: every column that is not the category or the X column. |\n\n\n### ChartDataCell\n\n- Type: `oneOf:string / number / boolean / null`\n- Required fields: none\n- Purpose: A cell in inline chart data.\n\n_No named properties._\n\n\n### TableCell\n\n- Type: `oneOf:ref:TableCellValue / ref:StyledTableCell`\n- Required fields: none\n- Purpose: A scalar, rich-run array, or styled/spanning cell object. Existing scalar and rich forms remain valid.\n\n_No named properties._\n\n\n### TableCellValue\n\n- Type: `oneOf:string / number / boolean / null / array<ref:TextRun>`\n- Required fields: none\n- Purpose: A scalar table value or canonical rich text runs, without cell decoration or geometry.\n\n_No named properties._\n\n\n### StyledTableCell\n\n- Type: `object`\n- Required fields: `value`\n- Purpose: A cell with explicit visual style or merged geometry. Its position remains its array column index; use null placeholders for every covered grid position.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `ref:TableCellValue` | Editable cell content; styling and spans do not change its scalar type or rich runs. |\n| `style` | no | `ref:TableCellStyle` | |\n| `colSpan` | no | `integer` | Number of grid columns covered, starting at this cell. Covered positions must contain null. Default 1. |\n| `rowSpan` | no | `integer` | Number of grid rows covered, starting at this cell. Covered positions must contain null. Header cells cannot span into body rows. Default 1. |\n| `format` | no | `ref:NumberFormat` | Number format. On a body cell it formats that cell's number value (and wins over the column's format); on a header cell (in 'columns') it is the column's format, the same as a DataColumn header. Strings, booleans, nul... |\n\n\n### TableCellStyle\n\n- Type: `object`\n- Required fields: none\n- Purpose: Cell appearance. Sizes use reference pixels at a 720-pixel canvas short edge and scale with the slide.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `fill` | no | `ref:ColorRef` | Cell background: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. Eight-digit hex colors include alpha; #00000000 is transparent. |\n| `color` | no | `ref:ColorRef` | Default text color, overridden by individual rich run colors. Accepts a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `bold` | no | `boolean` | Bold cell text. Runs that set their own bold keep it; a number cell stays bold and still uses the cell's or column's number format. |\n| `align` | no | `enum:left \\| center \\| right` | Horizontal text alignment inside the cell. |\n| `verticalAlign` | no | `enum:top \\| middle \\| bottom` | Vertical alignment inside the padded cell box. |\n| `padding` | no | `ref:TableCellPadding` | |\n| `borders` | no | `object` | Independent cell edges. Omitted edges retain the table theme border; width 0 removes an edge. |\n\n\n### TableCellPadding\n\n- Type: `object`\n- Required fields: none\n- Purpose: Text insets in reference pixels. Defaults: top 8, right 10, bottom 4, left 10.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `top` | no | `number` | |\n| `right` | no | `number` | |\n| `bottom` | no | `number` | |\n| `left` | no | `number` | |\n\n\n### TableCellBorder\n\n- Type: `object`\n- Required fields: `color`, `width`\n- Purpose: One explicit cell border.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Border color: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. Eight-digit hex colors include alpha; #00000000 is transparent. |\n| `width` | yes | `number` | Border width in reference pixels; 0 removes this edge. |\n| `dash` | no | `enum:solid \\| dash \\| dot` | Default solid. |\n\n\n### Catalogs\n\n- Type: `object`\n- Required fields: none\n- Purpose: The catalog records a document embeds, grouped by the catalog they came from. Each property is a group; inside a group, records are keyed by kind and then by id. - 'default': the catalog bare ids come from. It needs a 'source' when it holds records. Omitted: bare ids fall back to the host's default catalog. false: no catalog fallback, so every bare id must be embedded (under 'custom' or 'default'). - 'custom': the records the document defines itself. It has no source. - any other name ('acme'...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `default` | no | `oneOf:allOf:ref:CatalogGroup + schema / const:false` | The catalog bare ids come from: its source and the records of it this document embeds. false turns off the catalog fallback for bare ids. |\n| `custom` | no | `allOf:ref:CatalogGroup + schema` | Records the document defines itself, referenced by bare id. Checked first for every bare id. Forking a catalog record copies it here under a new id, so the change of ownership is visible. |\n\n\n### CatalogGroup\n\n- Type: `object`\n- Required fields: none\n- Purpose: One catalog group: the catalog's source and the records of it the document embeds, by kind and then by id.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `source` | no | `ref:CatalogSource` | |\n| `layouts` | no | `ref:CatalogRecords` | Layout records keyed by id, each with the fields of https://openpresentation.org/schema/opf-layout/v2. Referenced from Slide.layout. |\n| `themes` | no | `ref:CatalogRecords` | Theme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-theme/v1. Referenced from design.theme. |\n| `colorSchemes` | no | `ref:CatalogRecords` | Colour-scheme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-color-scheme/v1. Referenced from design.colorScheme and a theme's colorScheme. |\n| `fontSchemes` | no | `ref:CatalogRecords` | Font-scheme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-font-scheme/v1. Referenced from design.fontScheme, a theme's fontScheme and a language's font schemes. |\n| `narratives` | no | `ref:CatalogRecords` | Narrative records keyed by id, each with the fields of https://openpresentation.org/schema/opf-narrative/v1. Referenced from narrative. |\n| `audiences` | no | `ref:CatalogRecords` | Audience records keyed by id, each with the fields of https://openpresentation.org/schema/opf-audience/v1. Referenced from audience. |\n| `purposes` | no | `ref:CatalogRecords` | Purpose records keyed by id, each with the fields of https://openpresentation.org/schema/opf-purpose/v1. Referenced from purpose. |\n| `tones` | no | `ref:CatalogRecords` | Tone records keyed by id, each with the fields of https://openpresentation.org/schema/opf-tone/v1. Referenced from tone. |\n\n\n### CatalogSource\n\n- Type: `string`\n- Required fields: none\n- Purpose: Identity of a catalog: an HTTPS URL ('https://www.pptx.gallery') or a package reference ('pkg:@acme/opf-catalog'). It names the catalog; engines never fetch or install it. A host registers the catalog's records under this exact string, and copying slides between documents matches groups by it.\n\n_No named properties._\n\n\n### CatalogRecords\n\n- Type: `object`\n- Required fields: none\n- Purpose: Embedded records of one kind, keyed by id. The key is the record's id; an embedded record carries no '$schema', 'id', origin or digest. Catalog display metadata ('x-*' members) is stripped when a record is embedded.\n\n_No named properties._\n\n\n### CatalogReference\n\n- Type: `string`\n- Required fields: none\n- Purpose: A content reference: a bare id ('two-column') or 'name:id' ('acme:hero'), where the prefix names a group of 'catalogs'. URLs and 'pkg:' strings are not references; a named catalog group replaces them. See Catalogs for the resolution order.\n\n_No named properties._\n\n\n### LanguageTag\n\n- Type: `string`\n- Required fields: none\n- Purpose: A BCP-47 language tag ('en-US', 'ja', 'zh-Hans', 'sr-Latn-ME'), an engine vocabulary. Use 'en-GB' for UK English; 'en-UK' is not a valid region.\n\n_No named properties._\n\n\n### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: Automatic slide and content-group arrangement. Nested content groups arrange their children independently, inheriting only minFontSize and overflow. Explicit promoted regions retain their positions. A layout defines its grid as one ordered list of regions; its composition supports gap, padding, minFontSize and overflow rather than mode, columns or weights.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | no | `enum:auto \\| grid \\| row \\| column` | auto chooses a grid from available space and content; grid uses columns; row and column use one horizontal or vertical track. |\n| `columns` | no | `integer` | Column count for grid. In auto mode this caps the number of columns. |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `weights` | no | `array<number>` | Relative track sizes: columns for row/grid/auto, rows for column. Omitted tracks have weight 1; extra weights are ignored. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n"
|
|
309
|
+
"markdown": "# OPF Presentation Schema Reference\n\nThis reference documents the author-facing shape of a complete `*.opf.json` presentation document. It summarizes the canonical schema in `spec/schemas/opf.schema.json`; the schema remains the source of truth for validators.\n\n## Document Contract\n\n- Schema id: `https://openpresentation.org/schema/opf/v1`\n- Required top-level fields: `slides`\n- Additional top-level fields: not allowed\n\n## Top-Level Fields\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | no | `const:\"https://openpresentation.org/schema/opf/v1\"` | Optional OPF schema version. When omitted, validators and engines should assume the latest supported OPF schema. |\n| `name` | no | `string` | Display name of the presentation for GUI/TUI lists, library/search indexing, OS-level metadata, and default export filenames. This is deck identity, not slide content. Use slides[].title and slides[].subtitle for text... |\n| `description` | no | `string` | Free-form prose describing what this presentation is about. Used by agents and humans as a deck-level summary; complements purpose (the goal) and narrative (the structured storyline). Round-trips to OOXML 'docProps/co... |\n| `filename` | no | `string` | Optional base filename for exports (without extension). The opf CLI (convert) and the editor strip a trailing .pptx, .pdf, .png, or .svg (case-insensitive) and append the target format's extension. When omitted, they... |\n| `organization` | no | `oneOf:ref:Organization / array<ref:Organization>` | Organization associated with the presentation, usually the presenting company. Array form supports hosts, partners, clients, and sponsors. The primary organization (Organization.role 'primary', else the first item) su... |\n| `speaker` | no | `oneOf:ref:Speaker / array<ref:Speaker>` | Person presenting the deck. Array form supports panels and multi-speaker decks. The first speaker is the primary speaker: the built-in variables 'speaker.<field>' read it, 'speakers' lists every name, and 'speaker.<id... |\n| `author` | no | `oneOf:string / array<string>` | Optional credit for the person who authored or contributed to the deck, distinct from speaker. Array form supports multiple contributors. Round-trips to OOXML 'docProps/core.xml' as '<dc:creator>' (semicolon-joined wh... |\n| `audience` | no | `oneOf:string / ref:Audience / array<oneOf:string / ref:Audience>` | Intended audiences for the presentation. Accepts any of: - A single string: an audiences reference ('executive', or 'acme:board' for a record of the catalogs.acme group), or free-form text ('Series B investors'). A st... |\n| `purpose` | no | `oneOf:string / ref:Purpose` | Primary goal of the presentation. Accepts either: - A string: a purposes reference ('decide', or 'acme:fundraise' for a record of the catalogs.acme group), or a free-form goal ('Raise a Series B round of $30M'). A str... |\n| `language` | no | `oneOf:ref:LanguageTag / ref:Language` | Language of the presentation content, an engine vocabulary: a BCP-47 language tag, validated directly with no catalog lookup. Accepts either: - A BCP-47 tag ('en-US', 'en-GB', 'ja', 'zh-Hans', 'ar-SA'). - An inline La... |\n| `tone` | no | `oneOf:ref:CatalogReference / ref:Tone` | Desired tone for the presentation. Accepts either: - A tones reference: a bare id ('formal') or 'name:id' for a record of a named catalogs group. - An inline Tone object for custom tone metadata or overrides on a refe... |\n| `takeaway` | no | `oneOf:string / array<string>` | Audience-facing takeaway the presentation should leave behind. Array form supports multiple takeaways. Deck-level intent used by AI to seed and pressure-test slide content. |\n| `duration` | no | `integer` | Target presentation duration, as an integer number of minutes. The opf-render presenter view counts the elapsed time against it, and core validate warns when it lies outside the resolved narrative's 'duration' range.... |\n| `tags` | no | `array<string>` | Free-form labels used for categorization, search, and filtering. Lowercase kebab-case is recommended for consistency across a deck library. |\n| `design` | no | `ref:Design` | Optional design system covering theme, color scheme, font scheme, dimensions, background, logo, watermark, header, and footer applied to the deck. When omitted, engines use their default design configuration. |\n| `variables` | no | `ref:Variables` | Optional named variables: deck colors referenced as 'var:<id>' (the original use), and typed content variables (text, number, date, image, url, list) referenced inline as '{{<id>}}' or whole as 'var:<id>'. Variables a... |\n| `template` | no | `boolean` | Marks this document as a template: an incomplete OPF file. A template declares variables (top-level 'variables') and references them from content, and may leave required variables unfilled; validation then reports the... |\n| `narrative` | no | `ref:CatalogReference` | The deck's narrative plan, by reference: a bare narratives id ('classic-story') or 'name:id' for a record of a named catalogs group ('acme:founder-pitch'). The deck holds only this pointer; the plan (the arc, its beat... |\n| `slides` | yes | `array<ref:Slide>` | Ordered array of slides that make up the presentation. |\n| `references` | no | `array<ref:Reference>` | Sources that text runs cite with 'cite'. Ids are unique. A cited reference is listed in the footnote area of every slide that cites it, with a marker number assigned per deck in order of first use; a reference no run... |\n| `datasets` | no | `ref:Datasets` | Optional shared data tables, keyed by id. A chart ('chart.data': { \"dataset\": \"<id>\" }) or a table ('table': { \"dataset\": \"<id>\" }) references one instead of holding its own copy; engines inline the reference before c... |\n| `assets` | no | `ref:Assets` | Optional reusable asset registry for images, data files, videos, documents, fonts, and other resources referenced elsewhere in the deck via 'asset:<id>' strings. |\n| `catalogs` | no | `ref:Catalogs` | The catalog records this document embeds, grouped by the catalog they came from: 'default' (the catalog bare ids come from), 'custom' (records the document defines itself) and any number of named catalogs ('acme', ref... |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows; ignored by the engine but preserved across read/write round-trips. |\n\n## Object And Type Reference\n\n### Assets\n\n- Type: `object`\n- Required fields: none\n- Purpose: Reusable asset registry for resources used by slides, charts, metadata, and design. Keys are stable asset ids referenced elsewhere as 'asset:<id>'. Each asset can be a source string or an object with src plus optional metadata.\n\n_No named properties._\n\n\n### Asset\n\n- Type: `oneOf:string / object`\n- Required fields: none\n- Purpose: Reusable or inline resource. A string is shorthand for { \"src\": value }. Source strings accept 'asset:<id>' references, HTTPS URLs, data URIs, relative paths resolved against the OPF file location, or local filesystem paths. Use object form when metadata such as alt text, title or mediaType matters.\n\n_No named properties._\n\n\n### Audience\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline audience metadata for the presentation. Use 'id' to reference an audiences record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline audience.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional audiences reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience. |\n| `description` | no | `string` | Longer prose describing the audience and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, advise, or decide. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on focused attention for a single presentation, in minutes. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids that work well for this audience, for picker UIs. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: tone ids that work well for this audience, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Purpose\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline purpose metadata for the presentation. Use 'id' to reference a purposes record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline purpose.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional purposes reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids that work well for this purpose, for picker UIs. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: tone ids that work well for this purpose, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Language\n\n- Type: `object`\n- Required fields: `bcp47`\n- Purpose: Inline language metadata for the presentation: a BCP-47 'bcp47' tag plus overrides of what engines know about it. Languages are an engine vocabulary, not catalog records, so there is no 'id'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | no | `string` | Human-readable language name. |\n| `bcp47` | yes | `ref:LanguageTag` | BCP-47 language tag used for locale-aware rendering, proofing, and accessibility metadata. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Without it, en... |\n| `code` | no | `string` | ISO 639-3 or 639-2 language code carried for engines that prefer ISO codes. |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') for this language's script slot when targeting PowerPoint output. Its major/minor families fill the language's script slot (eastAsian or complexScript, chosen by 'script'... |\n| `googleFontScheme` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') used in place of 'fontScheme' when resolving script fonts for Google Slides output. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Tone\n\n- Type: `anyOf:schema / schema`\n- Required fields: none\n- Purpose: Inline tone metadata for the presentation. Use 'id' to reference a tones record (a bare id or 'name:id') and override selected fields, or use 'name' for a custom inline tone.\n- Conditional requirement: `id` or `name`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Optional tones reference (a bare id or 'name:id') resolved before applying the inline overrides. |\n| `name` | no | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: narrative ids this tone pairs well with, for picker UIs. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n\n\n### Organization\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: An organization associated with the presentation, typically the presenting company, but also hosts, partners, clients, or sponsors. The primary organization (role 'primary', else the first one) supplies the cover and section logo (unless design.logo overrides it) and the 'socials' header/footer field (its socials). Every field is also a built-in variable ('{{organization.name}}', 'organization.<id>.<field>'), and every logo a slide-scoped image ('var:organization.logo.icon', 'var:organization...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier for the organization, used to reference it from Speaker.organizationId and to address it in built-in variables as 'organization.<id>.<field>'. Must be unique among organizations (a duplicate is a val... |\n| `name` | yes | `string` | Display name. The built-in variable 'organization.name' (primary organization) or 'organization.<id>.name' carries it into any string, such as a header or footer text of '{{organization.name}}'. |\n| `legalName` | no | `string` | Optional legal entity name when it differs from the display name. Never drawn automatically; available as the built-in variable 'organization.legalName'. |\n| `logo` | no | `oneOf:ref:Asset / ref:OrganizationLogo` | The organization's logo. A path, URL or Asset is one logo for every shape; an object gives up to four shapes (full, stacked, icon, wordmark), each a path or Asset, or { onLight, onDark } for the two backgrounds. A pat... |\n| `domain` | no | `string` | Bare internet domain for the organization. Never drawn automatically and not used to look up assets; available as the built-in variable 'organization.domain'. |\n| `email` | no | `string` | General contact email for the organization. Never drawn automatically; available as the built-in variable 'organization.email'. |\n| `phone` | no | `string` | Main contact phone number for the organization. E.164 format is recommended. Never drawn automatically; available as the built-in variable 'organization.phone'. |\n| `tagline` | no | `string` | Short tagline. Never drawn automatically (cover slides do not show it); available as the built-in variable 'organization.tagline', for example in a footer 'text' of '{{organization.tagline}}'. |\n| `role` | no | `enum:primary \\| partner \\| client \\| sponsor \\| host` | Role of the organization relative to the presentation. Only 'primary' has behavior: it selects the primary organization (cover and section logo, the 'socials' header/footer field, and the 'organization.<field>' built-... |\n| `socials` | no | `ref:Socials` | Optional social media handles or URLs for the organization. The primary organization's socials render in header/footer zones that set socials: true; otherwise they are authoring metadata. |\n\n\n### Speaker\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A person presenting the deck. A speaker is drawn only through built-in variables ('{{speaker.name}}' inside any string, a header or footer text included, and 'var:speaker.photo' as a whole image field). No layout, cover or bio slide places a speaker on its own. See docs/templates-and-variables.md.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier for the speaker. Must be unique among speakers (a duplicate is a validation error). Addresses this speaker in built-in variables as 'speaker.<id>.<field>', such as '{{speaker.alice.title}}'. |\n| `name` | yes | `string` | Display name. Built-in variable 'speaker.name' (first speaker) or 'speaker.<id>.name'; every name is listed by 'speakers'. |\n| `title` | no | `string` | Role or title. Built-in variable 'speaker.title' (or 'speaker.<id>.title'), for example in a footer text of '{{speaker.name}}, {{speaker.title}}'. |\n| `photo` | no | `ref:Asset` | Source for the speaker's headshot image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are JPG or PNG; SVG is not appropr... |\n| `email` | no | `string` | Contact email. Never drawn automatically; available as the built-in variable 'speaker.email'. |\n| `phone` | no | `string` | Contact phone number for the speaker. E.164 format is recommended. Never drawn automatically; available as the built-in variable 'speaker.phone'. |\n| `bio` | no | `string` | Short biographical paragraph. Never drawn automatically; available as the built-in variable 'speaker.bio', for a bio or 'about the speaker' slide you write. |\n| `organizationId` | no | `string` | Reference to an Organization.id in organization; it must name an existing organization (a validation error otherwise). Attribution metadata for hosts: no engine draws it or changes which organization is primary becaus... |\n| `socials` | no | `ref:Socials` | Optional social media handles or URLs for the speaker. Authoring metadata only: nothing draws speaker socials and there is no built-in variable for them (the 'socials' header/footer field draws the primary organizatio... |\n\n\n### Socials\n\n- Type: `object`\n- Required fields: none\n- Purpose: Social media handles or URLs, keyed by platform. The keys are an engine vocabulary validated directly (no catalog lookup): engines link a handle with the platform's URL pattern and handle prefix (spec/reference/engine-vocabularies.json). Each value is a string, either a full URL or a platform handle (e.g., '@acme'). Brand colours and icons are catalog display metadata for authoring UIs; engines render the profile URL, not icons or brand colours. Used both for organization and speaker records.\n\n_No named properties._\n\n\n### Design\n\n- Type: `object`\n- Required fields: none\n- Purpose: Visual design system applied to the presentation; individual slides may override fields via Slide.design.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `theme` | no | `ref:CatalogReference` | Theme for the deck: a themes reference, a bare id ('minimal') or 'name:id' ('acme:brand'). The theme supplies the default colour scheme, font scheme, background and dimensions; design.colorScheme, design.fontScheme, d... |\n| `colorScheme` | no | `oneOf:string / ref:ColorScheme` | Color scheme for the presentation. Accepts two forms: - A colorSchemes reference: a bare id ('cool-horizon') or 'name:id' ('acme:ocean'). - Object form: a ColorScheme with an optional 'id' reference as the base plus s... |\n| `fontScheme` | no | `oneOf:string / ref:FontScheme` | Font scheme for heading, body, accent, and code text. Accepts two forms: - A fontSchemes reference: a bare id ('aptos') or 'name:id' ('acme:inter'). - Object form: a FontScheme with an optional 'id' reference as the b... |\n| `dimensions` | no | `oneOf:ref:DimensionPreset / ref:Dimensions` | Slide dimensions and aspect ratio. String shorthand such as 'widescreen' is equivalent to { preset: 'widescreen' }. |\n| `background` | no | `oneOf:ref:BackgroundShortcut / ref:Background` | Default slide background applied across the deck unless overridden on a slide (slides.N.design.background); the resolved theme's background is the fallback. A background is the only canvas fill and never moves content... |\n| `variant` | no | `oneOf:ref:ThemeBackgroundSlot / ref:ColorVariant` | Optional color variant, as a background slot ('light1', 'light2', 'dark1', 'dark2'; the same as { background: slot }) or the object form, which is needed only for an accent order: which slot of the color scheme is the... |\n| `logo` | no | `oneOf:string / const:false` | Which logo covers, section dividers and picture bullets draw. Logos live on the organization (Organization.logo); without this field covers and sections draw the primary organization's full logo and picture bullets it... |\n| `watermark` | no | `oneOf:const:false / ref:Asset / ref:Watermark` | Optional decorative watermark applied across slides: an image or a text stamp, in the fixed frame and at the opacity described on Watermark. Use false to suppress an inherited watermark in slide-level design; a string... |\n| `header` | no | `oneOf:const:false / ref:HeaderFooter` | Repeated header furniture rendered outside the main slide content. Use false to suppress an inherited header. |\n| `footer` | no | `oneOf:const:false / ref:HeaderFooter` | Repeated footer furniture rendered outside the main slide content. Use false to suppress an inherited footer. |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Default horizontal alignment for the title heading area (the title and the tag) in resolved layouts. Effective value: the slide's design, then the deck's design, then the theme's design.titleAlignment, then the layout... |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Default horizontal alignment for body/content regions in resolved layouts. Effective value: the slide's design, then the deck's design, then the theme's design.contentAlignment, then the layout record's design.content... |\n| `contentBox` | no | `boolean` | Whether body/content regions are rendered inside a visible card or surface. Effective value: the slide's design, then the deck's design, then the theme's design.contentBox, then the layout record's design.contentBox,... |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. Effective value: the slide's design, then the deck's design, then the theme's design.contentDirection, then the layout re... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content. Effective value: slide design, then deck design, then the layout record's design.chartPrimary. It applies to automatic slides only; a template layout says t... |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of image blocks (and Slide.image) that set no fit of their own: 'cover' fills the frame and crops what overflows around the block's focus, 'contain' shows the whole picture in a frame of its own proportion... |\n| `listBullet` | no | `enum:character \\| image \\| none` | Marker style for items and bullets lists. Effective value: the slide's design, then the deck's design, then the theme's design.listBullet, then the layout record's design.listBullet, then 'character'. 'character' (the... |\n| `mirror` | no | `boolean` | Draw the slide's template layout mirrored (OPF 0.19): each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Bin... |\n\n\n### ColorScheme\n\n- Type: `object`\n- Required fields: none\n- Purpose: Color palette used by the design system. Every slot and role is a hex color ('#RGB', '#RRGGBB' or '#RRGGBBAA'); a color-scheme name or a 'var:<id>' reference is not valid here, because those resolve through the scheme. The slot fields (accent1-accent6, dark1, dark2, light1, light2, hyperlink, followedHyperlink) mirror color-scheme.schema.json (https://openpresentation.org/schema/opf-color-scheme/v1) so library records and inline OPF overrides are interchangeable on those fields. Two parallel...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Colour-scheme reference (a bare id or 'name:id') resolved as the base; slot and role overrides on the surrounding ColorScheme object take precedence over the resolved scheme. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. Link runs with no color of their own are drawn underlined in it, in the preview and in PowerPoint, unless it has under 4.5:1 contrast against the slide background, then in the slide... |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `primary` | no | `ref:HexColor` | Abstract role: primary brand color (hex). The engine maps this onto an OOXML accent slot when serializing. |\n| `secondary` | no | `ref:HexColor` | Abstract role: secondary brand color (hex). |\n| `accent` | no | `ref:HexColor` | Abstract role: accent color used for highlights and emphasis (hex). |\n| `background` | no | `ref:HexColor` | Abstract role: default slide background color (hex), used when the design names no single-color background (and for gradient and picture backgrounds). Overrides light1 as that default. |\n| `surface` | no | `ref:HexColor` | Abstract role: color for elevated surfaces such as cards and panels (hex). Overrides the default of light2 (dark2 on a dark slide). |\n| `text` | no | `ref:HexColor` | Abstract role: primary body text color (hex). Overrides dark1 on a light slide only; a dark slide always uses light1. |\n| `textSecondary` | no | `ref:HexColor` | Abstract role: secondary or muted text color used for captions and supporting copy (hex). Overrides the default of dark2 (light2 on a dark slide). |\n\n\n### FontScheme\n\n- Type: `object`\n- Required fields: none\n- Purpose: Typography selections used by the design system. The pair fields (major, minor) and refinement fields (type, app, languageFamily) mirror font-scheme.schema.json (https://openpresentation.org/schema/opf-font-scheme/v1) so library records and inline OPF overrides are interchangeable on those fields. Two parallel models are supported and may be mixed: - OOXML pairs (major, minor) - heading and body family names that round-trip directly to PowerPoint majorFont/minorFont entries. - Abstract roles...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `ref:CatalogReference` | Font-scheme reference (a bare id or 'name:id') resolved as the base; field overrides on the surrounding FontScheme object take precedence over the resolved scheme. |\n| `major` | no | `string` | Heading (major) font family mirrors the OOXML majorFont entry. Pairs with 'minor'. |\n| `minor` | no | `string` | Body (minor) font family mirrors the OOXML minorFont entry. Pairs with 'major'. |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `heading` | no | `string` | Abstract role: font family name used for slide titles and headings. Maps onto the OOXML major slot when serializing. |\n| `body` | no | `string` | Abstract role: font family name used for body copy. Maps onto the OOXML minor slot when serializing. |\n| `accent` | no | `string` | Abstract role: font family name used for accent text. When set, the slide tag (eyebrow) and the quote body use this family instead of the body and heading families; nothing else changes. resolveFontFamilies() returns... |\n| `code` | no | `string` | Abstract role: monospaced font family name used for code blocks and for inline code runs (TextRun.code). No direct OOXML slot. Resolution: this override, then the resolved font-scheme record's 'code' (for example Cons... |\n\n\n### SlideDesign\n\n- Type: `allOf:ref:Design + schema`\n- Required fields: none\n- Purpose: A slide's design: every Design field except dimensions. A PPTX has one slide size, so the size is set once, on the deck's design.dimensions (or its theme), and a slide's design cannot set it. A slide-level theme whose resolved dimensions differ from the deck's is a validate warning (opf/slide-theme-dimensions); exporting such a deck to PPTX fails with mixed-slide-dimensions.\n\n_No named properties._\n\n\n### DimensionPreset\n\n- Type: `enum:16:9 | 4:3 | 16:10 | 1:1 | 4:5 | 9:16 | letter | a4 | widescreen | standard`\n- Required fields: none\n- Purpose: Named preset; sets aspect ratio and physical size. 16:9 ('widescreen'): 13.333 x 7.5 in, PowerPoint's Widescreen. 4:3 ('standard'): 10 x 7.5 in. 16:10: 10 x 6.25 in. letter (11 x 8.5 in) and a4 (11.69 x 8.27 in) are paper sizes, not PowerPoint's Letter Paper or A4 Paper. 1:1, 4:5 and 9:16 are 7.5 in on the short edge.\n\n_No named properties._\n\n\n### Dimensions\n\n- Type: `object`\n- Required fields: none\n- Purpose: Slide dimensions: a preset, or custom inches that override it. PowerPoint's Letter Paper (10 x 7.5 in), A4 Paper (10.8333 x 7.5 in) and On-screen Show (16:9) (10 x 5.625 in) are not OPF presets; use widthInches and heightInches. docs/slide-sizes.md compares every preset.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `preset` | no | `ref:DimensionPreset` | |\n| `widthInches` | no | `number` | Custom slide width in inches; overrides the preset width when provided. |\n| `heightInches` | no | `number` | Custom slide height in inches; overrides the preset height when provided. |\n\n\n### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n\n### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color shorthand accepted by selected string fields.\n\n_No named properties._\n\n\n### ColorRef\n\n- Type: `anyOf:ref:HexColor / enum:accent1 | accent2 | accent3 | accent4 | accent5 | accent6 | dark1 | dark2 | light1 | light2 | hyperlink | followedHyperlink | primary | secondary | accent | background | surface | surfaceAlt | text | textSecondary / string`\n- Required fields: none\n- Purpose: A color value or reference, enforced on styled table cell fill and text colors, cell border colors, solid background colors, gradient stop colors and pattern foreground and background colors. Three forms: - Literal hex: '#RGB', '#RRGGBB', or '#RRGGBBAA'. - Color-scheme name, resolved through the effective color scheme after design resolution: an OOXML slot ('accent1'-'accent6', 'dark1', 'dark2', 'light1', 'light2', 'hyperlink', 'followedHyperlink') or an abstract role ('primary', 'secondary',...\n\n_No named properties._\n\n\n### Variables\n\n- Type: `object`\n- Required fields: none\n- Purpose: Named variables, keyed by stable kebab-case id. A variable is a typed, named value the deck declares once and uses in many places: a color ('var:<id>' in color fields, the original use), or content that fills a template (text, number, date, image, url, list). Content is referenced inline as '{{<id>}}' inside any string, or whole as 'var:<id>' in a field of the matching type; '\\{{' writes a literal '{{'. A hex string is shorthand for a color variable. A variable with no 'value' is unfilled: ex...\n\n_No named properties._\n\n\n### Variable\n\n- Type: `oneOf:ref:HexColor / ref:ColorVariable / ref:TextVariable / ref:NumberVariable / ref:DateVariable / ref:ImageVariable / ref:UrlVariable / ref:ListVariable`\n- Required fields: none\n- Purpose: A single named variable: a hex string (shorthand for a color variable) or an object whose 'type' is color, text, number, date, image, url or list. 'value' is the current value and is optional: a variable with no value is unfilled, which a template allows and a normal deck does not. 'example' only illustrates the slot (fill forms, template previews) and never reaches output.\n\n_No named properties._\n\n\n### ColorVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A named color. Content color fields reference it as 'var:<id>'. Colors resolve at render time through the ordinary color-reference path, so a color variable keeps working as before.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"color\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `ref:HexColor` | Hex color this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `ref:HexColor` | Illustrative color shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### TextVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: Text content. Plain string or rich TextRun[]. Insert it inline as '{{<id>}}' inside any string (rich text is flattened to plain text there), or reference it whole as 'var:<id>' in a field that accepts string or TextRun[] (the rich runs are kept).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"text\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `oneOf:string / array<ref:TextRun>` | Text this variable resolves to: a plain string, or TextRun[] for rich text. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `oneOf:string / array<ref:TextRun>` | Illustrative text shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### NumberVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A number. '{{<id>}}' inserts it as text using 'format'; 'var:<id>' as a whole field supplies the number itself (chart values, font sizes).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"number\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `number` | Number this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `number` | Illustrative number shown in fill forms and used when a template is previewed with examples. Never written to output. |\n| `format` | no | `ref:NumberFormat` | Display pattern used by '{{<id>}}' (the shared NumberFormat syntax). A literal prefix, a numeric part of '#', '0', ',' and '.', and a literal suffix. '0' pads digits, '#' is optional, ',' groups thousands, digits afte... |\n\n\n### NumberFormat\n\n- Type: `string`\n- Required fields: none\n- Purpose: Number display pattern, shared by NumberVariable.format, data column formats (DataColumn.format) and table cell formats (StyledTableCell.format). An optional literal prefix, a numeric part of '#', '0', ',' and '.', and an optional literal suffix. '0' pads digits, '#' is optional, ',' groups thousands, digits after '.' fix the decimals ('0' required, '#' optional), and a '%' in the prefix or suffix multiplies the value by 100. English separators only. A format applies only to number values; st...\n\n_No named properties._\n\n\n### DateVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A calendar date as an ISO YYYY-MM-DD string. No time zone and no clock are involved. '{{<id>}}' inserts it formatted with 'format'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"date\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `string` | ISO calendar date (YYYY-MM-DD) this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `string` | Illustrative ISO date shown in fill forms and used when a template is previewed with examples. Never written to output. |\n| `format` | no | `string` | Date display pattern, the same LDML-style tokens as header/footer dateFormat: yyyy, yy, MMMM, MMM, MM, M, dd, d, EEEE, EEE and quoted literals. English names. Default 'MMMM d, yyyy'. |\n\n\n### ImageVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: An image source: any Asset (an 'asset:<id>' reference, HTTPS URL, data URI, relative or local path, or an object with src, alt and metadata). Reference it whole as 'var:<id>' in an image, asset or src field; '{{<id>}}' inserts the source string.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"image\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `ref:Asset` | Image source this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `ref:Asset` | Illustrative image source shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### UrlVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A link target (http, https, mailto or tel). Use it as '{{<id>}}' inside a link string or reference it whole as 'var:<id>' in a link field.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"url\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `string` | Link target this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `string` | Illustrative link shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### ListVariable\n\n- Type: `object`\n- Required fields: `type`\n- Purpose: A list of strings, for bullets and list items. A whole-string array element 'var:<id>' splices every entry into the array in place; a whole field 'var:<id>' becomes the array; '{{<id>}}' joins the entries with ', ' (or the separator after a pipe: '{{<id>|; }}').\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"list\"` | Variable kind. One of color, text, number, date, image, url or list. |\n| `value` | no | `array<string>` | Entries this variable resolves to. |\n| `required` | no | `boolean` | Whether the variable must be filled. Defaults to true: a variable with no 'value' is unfilled, and an unfilled required variable is an error in a normal deck and expected in a template. Set false for an optional slot:... |\n| `label` | no | `string` | Optional short human label for forms and fill panels. |\n| `description` | no | `string` | Optional prose describing what the variable is for, surfaced by pickers, fill forms and agents. |\n| `example` | no | `array<string>` | Illustrative entries shown in fill forms and used when a template is previewed with examples. Never written to output. |\n\n\n### BackgroundShortcut\n\n- Type: `oneOf:ref:ThemeBackgroundSlot / ref:HexColor / ref:ImageSource`\n- Required fields: none\n- Purpose: String shorthand for a background. Theme slots ('light1', 'light2', 'dark1', 'dark2') are equivalent to { type: 'theme', slot: value }; hex colors are equivalent to { type: 'solid', color: value }; an image source (starting with 'asset:', 'https://', 'data:', './' or '../') is equivalent to { type: 'image', src: value }, a cover image. Any other string is invalid.\n\n_No named properties._\n\n\n### ImageSource\n\n- Type: `string`\n- Required fields: none\n- Purpose: An image source in a string shorthand: an 'asset:<id>' reference, an HTTPS URL, a data URI, or a relative path starting with './' or '../' (resolved against the OPF file location).\n\n_No named properties._\n\n\n### ColorVariant\n\n- Type: `object`\n- Required fields: `background`\n- Purpose: A variant of the deck's single color scheme, as a mapping over its twelve slots (PowerPoint's clrMap): 'background' is the slot the slide background and the role bg1 take, and with it the other roles: light1 maps bg1 = light1, tx1 = dark1, bg2 = light2, tx2 = dark2; dark1 maps bg1 = dark1, tx1 = light1, bg2 = dark2, tx2 = light2; light2 and dark2 make that slot both the background and the alternate surface (light2: bg1 = bg2 = light2, tx1 = dark1, tx2 = dark2; dark2: bg1 = bg2 = dark2, tx1 =...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `background` | yes | `ref:ThemeBackgroundSlot` | The scheme slot that is the background (bg1) of the variant. |\n| `accents` | no | `array<enum:accent1 \\| accent2 \\| accent3 \\| accent4 \\| accent5 \\| accent6>` | Optional accent order: a permutation of accent1 to accent6. Entry i is the scheme slot drawn as accent i + 1, so primary, secondary and accent (the first three) and the chart series colors follow it. Default: the sche... |\n\n\n### Background\n\n- Type: `oneOf:ref:ThemeBackground / ref:SolidBackground / ref:GradientBackground / ref:ImageBackground / ref:PatternBackground`\n- Required fields: none\n- Purpose: Background fill applied to slides. Theme backgrounds preserve PowerPoint's color-scheme background choice; other variants represent fixed background fills.\n\n_No named properties._\n\n\n### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n\n### SolidBackground\n\n- Type: `object`\n- Required fields: `type`, `color`\n- Purpose: Fixed solid slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"solid\"` | Fixed solid background fill. |\n| `color` | yes | `ref:ColorRef` | Fixed solid fill color: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef, resolved against the effective color scheme and the deck variables). Use { type: 'theme', slot: ...... |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### GradientBackground\n\n- Type: `object`\n- Required fields: `type`, `gradient`\n- Purpose: Fixed gradient slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `tone` | no | `enum:dark \\| light` | Whether the background is dark or light, which picks the color map (dark1 or light1) so text, lines and neutrals read on it. Without a tone a picture or gradient takes the deck's variant, else counts as light. |\n| `type` | yes | `const:\"gradient\"` | Fixed gradient background fill. |\n| `gradient` | yes | `object` | Gradient fill definition. |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### ImageBackground\n\n- Type: `object`\n- Required fields: `type`, `src`\n- Purpose: Picture background: the image fills the whole canvas behind everything and never moves content (to push content aside, use an image block with placement). The string shorthand 'asset:x' (or any image source) is { type: 'image', src: 'asset:x' }, a cover image. Paint order: the colour scheme's default slide background, the picture, then the overlay, then furniture and content.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `tone` | no | `enum:dark \\| light` | Whether the background is dark or light, which picks the color map (dark1 or light1) so text, lines and neutrals read on it. Without a tone a picture or gradient takes the deck's variant, else counts as light. |\n| `type` | yes | `const:\"image\"` | Picture background. |\n| `src` | yes | `string` | Source for the background image: an 'asset:<id>' reference, HTTPS URL, data URI, or a relative path resolved against the OPF file location. |\n| `alt` | no | `string` | Alternative text. A background with alt is meaningful: PowerPoint export draws it as a full-slide picture at the back that carries the text, so screen readers see it. Without alt it is decorative and exports as the na... |\n| `fit` | no | `enum:cover \\| contain \\| stretch \\| tile` | How the picture fills the canvas: 'cover' fills it and crops what overflows around focus; 'contain' shows the whole picture centered over the colour scheme's default background; 'stretch' scales it to the canvas exact... |\n| `focus` | no | `ref:ImageFocus` | Point of the picture to keep in view when 'cover' crops it. Ignored by the other fits. |\n| `opacity` | no | `number` | Picture opacity from 0 (fully transparent) to 1 (fully opaque); the overlay keeps its own opacity. Default 1. |\n| `recolor` | no | `ref:ImageRecolor` | Grayscale or duotone treatment of the picture's pixels, as on an image block: recolor and opacity apply to the pixels, then the overlay is drawn above. |\n| `overlay` | no | `ref:Overlay` | Scrim over the whole picture or a band along one edge (edge, size), beneath furniture and content, for example to keep a title readable over a busy photo. |\n\n\n### ImageFocus\n\n- Type: `object`\n- Required fields: `x`, `y`\n- Purpose: A point of a picture as fractions of its width and height, from the top-left corner. A cover fit centers this point in the frame as far as the picture still covers the frame, so it always stays in view: { x: 0.5, y: 0.5 } (the default) is a center crop, { x: 0, y: 0 } keeps the top-left corner.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `x` | yes | `number` | Horizontal position, 0 (left edge) to 1 (right edge). |\n| `y` | yes | `number` | Vertical position, 0 (top edge) to 1 (bottom edge). |\n\n\n### Overlay\n\n- Type: `object`\n- Required fields: `color`, `opacity`\n- Purpose: Solid scrim over a picture: the whole frame in the frame's shape, or a band along one edge. Shared by image backgrounds and image blocks. Drawn directly above its picture and beneath headings and content; PowerPoint export writes one native shape above the picture.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Overlay fill color: a hex color (eight-digit hex keeps its alpha), a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `opacity` | yes | `number` | Overlay fill opacity. |\n| `edge` | no | `enum:top \\| bottom \\| left \\| right` | Cover only a band along this edge of the frame, for example a caption strip. Backgrounds and rectangle image frames only; on another shape the overlay is not drawn and composition reports unsupported-image-treatment.... |\n| `size` | no | `number` | Band share of the frame for an edge overlay. Default 0.3. Ignored without edge. |\n\n\n### ImagePlacement\n\n- Type: `object`\n- Required fields: `edge`\n- Purpose: Bleeds an image block to one edge of the slide: the image takes a band along that edge, edge to edge, and the headings and other content compose in the rest of the slide at the normal padding. Only top-level blocks (slides.N.blocks.I) can be placed, at most one per edge. Template layout regions do not supply image placement; set it on the image block itself. Bands are taken in block order, each spanning the free area left by earlier ones. Placement is the only mechanism that moves content asi...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `edge` | yes | `enum:left \\| right \\| top \\| bottom` | The slide edge the image bleeds to. |\n| `size` | no | `number` | Share of the slide width (left, right) or height (top, bottom) given to the band. Default 0.5. |\n| `inset` | no | `boolean` | Draw the frame inside the slide padding on every side of the band, like a card, instead of edge to edge. Default false. |\n\n\n### ImageBorder\n\n- Type: `object`\n- Required fields: `color`, `width`\n- Purpose: Solid line along the image frame's shape, centered on the outline with a miter join, like a native picture line. A thick dark border on a rounded portrait frame gives a device bezel.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Line color: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `width` | yes | `number` | Line width in reference pixels at a 720-pixel short edge. 0 removes the line. |\n\n\n### ImageRecolor\n\n- Type: `oneOf:const:\"grayscale\" / object`\n- Required fields: none\n- Purpose: Color treatment for the image pixels. Luminance uses Rec. 601 weights (0.299, 0.587, 0.114) on sRGB values.\n\n_No named properties._\n\n\n### PatternBackground\n\n- Type: `object`\n- Required fields: `type`, `pattern`\n- Purpose: Fixed pattern slide background fill.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"pattern\"` | Fixed pattern background fill. |\n| `pattern` | yes | `object` | Pattern fill definition. |\n| `opacity` | no | `number` | Background opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### OrganizationLogo\n\n- Type: `object`\n- Required fields: none\n- Purpose: An organization's logo in up to four shapes. Each shape is a path or Asset, or { onLight, onDark } for the two backgrounds. A missing shape falls back to full, and full to the first defined of wordmark, stacked and icon; within a shape a missing onLight or onDark uses the other. Shapes are placed with slide-scoped references such as 'var:organization.logo.icon'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `full` | no | `ref:LogoSource` | The full logo (lockup): mark and name side by side. Drawn on cover and section slides, and placed by 'var:organization.logo'. |\n| `stacked` | no | `ref:LogoSource` | The stacked lockup: mark above the name, for portrait or square slots. Placed by 'var:organization.logo.stacked'. |\n| `icon` | no | `ref:LogoSource` | The mark or symbol without the name, for tight spaces such as header and footer zones and picture bullets. Placed by 'var:organization.logo.icon'. |\n| `wordmark` | no | `ref:LogoSource` | The name set in the brand's lettering, without the mark. Placed by 'var:organization.logo.wordmark'. |\n\n\n### LogoSource\n\n- Type: `oneOf:ref:Asset / object`\n- Required fields: none\n- Purpose: One logo shape: a path or Asset for every background, or { onLight, onDark }. onLight is drawn on light backgrounds (usually dark artwork) and onDark on dark backgrounds (usually light artwork), by each slide's background; a missing one uses the other.\n\n_No named properties._\n\n\n### Watermark\n\n- Type: `oneOf:schema / schema`\n- Required fields: `opacity`\n- Purpose: Decorative watermark: an image ('src') or a text stamp ('text'), exactly one of the two, with its opacity. An image is drawn once per slide, contained and centered in a fixed frame (the middle 40% of the slide width and height, from 30% to 70% on each axis). A text watermark is one line of text in the heading font and the theme text color at the given opacity, centered on the slide and rotated 30 degrees counterclockwise (rising to the right), sized to span at most 70% of the slide width and...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `src` | no | `string` | Source for the watermark image: an 'asset:<id>' reference, HTTPS URL, data URI, relative path or local path, as for Asset.src. Exactly one of src and text is set. |\n| `text` | no | `string` | Text for a text watermark, such as DRAFT or CONFIDENTIAL; line breaks are drawn as spaces. Exactly one of src and text is set. |\n| `opacity` | yes | `number` | Watermark opacity from 0 (fully transparent) to 1 (fully opaque). |\n\n\n### HeaderFooter\n\n- Type: `object`\n- Required fields: none\n- Purpose: Repeated header or footer content split into left, center, and right zones. Header/footer content is slide furniture, separate from the main slide content payloads.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `left` | no | `ref:HeaderFooterItem` | Left-aligned header/footer content. |\n| `center` | no | `ref:HeaderFooterItem` | Centered header/footer content. |\n| `right` | no | `ref:HeaderFooterItem` | Right-aligned header/footer content. |\n\n\n### HeaderFooterItem\n\n- Type: `object`\n- Required fields: none\n- Purpose: One header/footer zone. Every configured field renders, side by side in one row in the order image, text, socials, date (mirrored in a right-to-left deck), aligned to the zone's edge (left, center or right) and vertically centered on each other; a line break inside text or socials ('\\n') stacks lines within that part. When the parts are wider than the zone their text wraps, and a row that cannot fit even so is reported as overflow. Generated values such as the slide number, the slide count, t...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `text` | no | `string` | Text rendered in this zone. Variables resolve inside it like any string: deck-wide ones ('{{organization.name}}', '{{speaker.name}}, {{speaker.title}}', '{{customer}}') before composition, and the slide-scoped built-i... |\n| `image` | no | `ref:Asset` | Image rendered first in this zone's row, such as a logo, partner mark, certification badge, or icon: a path, URL or Asset, or an organization logo reference ('var:organization.logo.icon', 'var:organization.<id>.logo.w... |\n| `date` | no | `oneOf:boolean / string` | true renders the current date: the renderer or exporter must be given an explicit ISO date by its host (core never reads a clock). PPTX export writes a native date field only for a supported dateFormat whose complete... |\n| `dateFormat` | no | `string` | Date pattern for date. Tokens: yyyy (2026), yy (26), MMMM (April), MMM (Apr), MM (04), M (4), dd (09), d (9), EEEE (Thursday), EEE (Thu). Text in single quotes and other non-letter characters are literal. Month and we... |\n| `socials` | no | `boolean` | Whether to render the primary organization's social profiles from organization.socials, one line per platform in key order. A handle is formatted through the platform's socialPlatforms record (companyUrlPattern, else... |\n\n\n### Slide\n\n- Type: `object`\n- Required fields: none\n- Purpose: A single slide. Content can be authored as a full-slide root payload, or inside promoted named region keys such as 'left', 'center+right', and 'top:left'.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `string` | Optional stable identifier for the slide within the document. Use when another system needs to reference a slide across edits, comments, generation state, exports, or narrative tooling. Slide order is defined by the s... |\n| `type` | no | `enum:text \\| list \\| image \\| chart \\| table \\| video \\| code \\| metric \\| quote \\| timeline` | Optional full-slide content kind. When omitted, engines infer the kind from root payload fields. |\n| `beat` | no | `oneOf:string / array<string>` | Optional reference to one or more beats of the deck's narrative (each value is the id of a beat in the narrative record that the root 'narrative' resolves to). A single string declares the slide's primary beat; an arr... |\n| `layout` | no | `ref:CatalogReference` | Optional layout: a layouts reference, a bare id ('pillars') or 'name:id' ('acme:hero'), or 'auto'. When omitted or 'auto', the slide is composed with no layout record: engines arrange title, subtitle, tag and content... |\n| `title` | no | `oneOf:string / array<ref:TextRun>` | Slide-level title content. A v2 layout renders this in its title heading area, or an implicit heading row when that area is absent. Use a string for plain text or TextRun[] for inline rich text (a colored word, bold,... |\n| `subtitle` | no | `oneOf:string / array<ref:TextRun>` | Slide-level subtitle or supporting line. A v2 layout renders this in its subtitle area when present, otherwise in the title heading group. Use a string for plain text or TextRun[] for inline rich text (a colored word,... |\n| `tag` | no | `oneOf:string / array<ref:TextRun>` | Small slide-level label or badge. A v2 layout renders this before the title in its title heading group; tag is not a separate layout area. Use a string for plain text or TextRun[] for inline rich text (a colored word,... |\n| `text` | no | `oneOf:string / array<ref:TextRun>` | Full-slide text payload. Use a string for plain text or TextRun[] for inline rich text. TextRun items may be plain strings or formatted run objects. |\n| `items` | no | `array<ref:ListItem>` | Full-slide structured list payload. Presence of this field infers type 'list'. Entries are strings, TextRun[] or objects with `text`, optional `description` (supporting detail under the entry), `level` and `start`. Us... |\n| `bullets` | no | `array<ref:BulletItem>` | Full-slide prose bullet payload: short talking points, each standing alone. Presence of this field infers type 'text', so it may sit beside `text` in one payload. Entries are strings, TextRun[] or objects with `text`,... |\n| `numbering` | no | `ref:NumberingSpec` | Number the full-slide `items` or `bullets` instead of bulleting them. A style name (arabic, roman-upper, roman-lower, alpha-upper, alpha-lower) or a Numbering object applies to every list level; an array gives one ent... |\n| `image` | no | `ref:Asset` | Shorthand for one image block: the picture composes as body content, like { type: 'image', image: value }, with the effective design.imageFit. A v2 layout binds it to a compatible body region; a region's bleed setting... |\n| `video` | no | `ref:Asset` | Full-slide video source. Presence of this field infers type 'video'. Engines do not play video: the preview draws a placeholder (a play badge with the asset's title or source as its caption), and PPTX export draws the... |\n| `chart` | no | `ref:Chart` | Full-slide chart payload. Presence of this field infers type 'chart'. |\n| `table` | no | `ref:Table` | Full-slide table payload. Presence of this field infers type 'table'. |\n| `code` | no | `oneOf:string / ref:Code` | Full-slide code payload. A string is shorthand for { \"source\": value }; object form carries optional syntax language and filename metadata. |\n| `metric` | no | `oneOf:string / number / ref:Metric` | Full-slide metric payload. A string or number is shorthand for { \"value\": value }; object form carries optional label, description, unit, delta, trend, and sentiment metadata. Numeric values remain numbers; renderers... |\n| `quote` | no | `oneOf:string / ref:Quote` | Full-slide quote payload. A string is shorthand for { \"text\": value }; object form carries optional attribution and source metadata. Presence of this field infers type 'quote'. |\n| `timeline` | no | `ref:Timeline` | Full-slide timeline payload. An array is shorthand for { \"events\": value }; object form carries optional name and description metadata. Presence of this field infers type 'timeline'. |\n| `caption` | no | `ref:Caption` | Caption for the slide's root image, chart, table or video payload. Valid only when the slide root holds exactly one of those payloads. |\n| `blocks` | no | `array<ref:ContentPayload>` | Layout-agnostic content blocks rendered together as a composed payload when exact placement is unspecified. At slide root, multiple content payload kinds with no explicit type, blocks, or regions are accepted as short... |\n| `design` | no | `ref:SlideDesign` | Slide-level design applied on top of the deck-wide design. |\n| `left` | no | `ref:ContentPayload` | |\n| `center` | no | `ref:ContentPayload` | |\n| `right` | no | `ref:ContentPayload` | |\n| `left+center` | no | `ref:ContentPayload` | |\n| `center+right` | no | `ref:ContentPayload` | |\n| `left+center+right` | no | `ref:ContentPayload` | |\n| `top` | no | `ref:ContentPayload` | |\n| `middle` | no | `ref:ContentPayload` | |\n| `bottom` | no | `ref:ContentPayload` | |\n| `top+middle` | no | `ref:ContentPayload` | |\n| `middle+bottom` | no | `ref:ContentPayload` | |\n| `top+middle+bottom` | no | `ref:ContentPayload` | |\n| `top:left` | no | `ref:ContentPayload` | |\n| `top:center` | no | `ref:ContentPayload` | |\n| `top:right` | no | `ref:ContentPayload` | |\n| `top:left+center` | no | `ref:ContentPayload` | |\n| `top:center+right` | no | `ref:ContentPayload` | |\n| `top:left+center+right` | no | `ref:ContentPayload` | |\n| `middle:left` | no | `ref:ContentPayload` | |\n| `middle:center` | no | `ref:ContentPayload` | |\n| `middle:right` | no | `ref:ContentPayload` | |\n| `middle:left+center` | no | `ref:ContentPayload` | |\n| `middle:center+right` | no | `ref:ContentPayload` | |\n| `middle:left+center+right` | no | `ref:ContentPayload` | |\n| `bottom:left` | no | `ref:ContentPayload` | |\n| `bottom:center` | no | `ref:ContentPayload` | |\n| `bottom:right` | no | `ref:ContentPayload` | |\n| `bottom:left+center` | no | `ref:ContentPayload` | |\n| `bottom:center+right` | no | `ref:ContentPayload` | |\n| `bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `top+middle:left` | no | `ref:ContentPayload` | |\n| `top+middle:center` | no | `ref:ContentPayload` | |\n| `top+middle:right` | no | `ref:ContentPayload` | |\n| `top+middle:left+center` | no | `ref:ContentPayload` | |\n| `top+middle:center+right` | no | `ref:ContentPayload` | |\n| `top+middle:left+center+right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left` | no | `ref:ContentPayload` | |\n| `middle+bottom:center` | no | `ref:ContentPayload` | |\n| `middle+bottom:right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left+center` | no | `ref:ContentPayload` | |\n| `middle+bottom:center+right` | no | `ref:ContentPayload` | |\n| `middle+bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:center` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left+center` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:center+right` | no | `ref:ContentPayload` | |\n| `top+middle+bottom:left+center+right` | no | `ref:ContentPayload` | |\n| `notes` | no | `string` | Speaker notes shown in presenter view. |\n| `section` | no | `string` | PowerPoint-style slide section label. Consecutive slides with the same value belong to the same section in presenter view, outlines, and PowerPoint section-aware exports. Also the slide-scoped built-in variable '{{sli... |\n| `hidden` | no | `boolean` | Whether the slide is hidden from the presented sequence. The player skips it, the PPTX export writes it as a hidden slide, and per-slide image and PDF output skips it unless the caller asks to include hidden slides (o... |\n| `composition` | no | `ref:Composition` | |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows at slide scope; ignored by the engine but preserved across read/write round-trips. Use for review state, generation provenance, or authoring conventions such as { \"authoring... |\n\n\n### ContentPayload\n\n- Type: `allOf:schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema + schema`\n- Required fields: none\n- Purpose: A content leaf or recursively composed group. A group contains blocks and optional composition; it cannot mix blocks with leaf payload fields. Groups may nest up to 32 levels.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | no | `string` | Optional stable identifier for this payload, unique among slide and payload ids in the document. Use when another system needs to address the payload across edits patch-style agent edits, comments, review state, or ge... |\n| `extensions` | no | `object` | Custom data passthrough for agent workflows at payload scope; ignored by the engine but preserved across read/write round-trips. |\n| `region` | no | `string` | Pin a root block (slides.N.blocks.I) to a region of the slide's template layout by name (OPF 0.19). The block goes to that region when the region exists, accepts the block's kind and has room; otherwise the pin is ign... |\n| `type` | no | `enum:text \\| list \\| image \\| chart \\| table \\| video \\| code \\| metric \\| quote \\| timeline \\| group` | Optional content kind. When omitted, engines infer the kind from the fields present. |\n| `text` | no | `oneOf:string / array<ref:TextRun>` | Text payload. Use a string for plain text or TextRun[] for inline rich text. TextRun items may be plain strings or formatted run objects. |\n| `items` | no | `array<ref:ListItem>` | Structured list payload (type 'list'). Each item is either a plain string, a TextRun[] rich text sequence, or a ListItem object with optional `description`. List nesting uses item.level rather than nested content payl... |\n| `bullets` | no | `array<ref:BulletItem>` | Prose bullet payload: short talking points, each standing alone. Presence of this field infers type 'text', so it may sit beside `text` in one payload. Entries have no `description`; use `items` for entries with suppo... |\n| `numbering` | no | `ref:NumberingSpec` | Number the payload's `items` or `bullets` instead of bulleting them. A style name (arabic, roman-upper, roman-lower, alpha-upper, alpha-lower) or a Numbering object applies to every list level; an array gives one entr... |\n| `columns` | no | `oneOf:const:\"auto\" / integer` | Lay the payload's `items` or `bullets` out in columns (OPF 0.19). 'auto' tries 1, 2 then 3 equal columns at the starting text size and uses the first count at which the list fits, as long as a column stays at least a... |\n| `image` | no | `ref:Asset` | Source for an image item. |\n| `fit` | no | `enum:cover \\| contain \\| stretch` | Image payloads only. How the picture fills its frame: 'cover' fills it and crops what overflows around focus; 'contain' shows the whole picture, in a frame of its own proportions placed at the region's anchor (top by... |\n| `focus` | no | `ref:ImageFocus` | Image payloads only. Point of the picture to keep in view when 'cover' crops it. Ignored by the other fits. |\n| `aspectRatio` | no | `number` | Image payloads only. Width-to-height ratio of the frame: the frame becomes the largest centered box with this ratio inside the block's region, for example 2.39 for a cinematic letterbox or 0.5 for a phone-shaped frame... |\n| `shape` | no | `enum:rectangle \\| rounded \\| circle \\| hexagon` | Image payloads only. Mask applied to the frame, exported as the picture's native preset geometry (rect, roundRect, ellipse, hexagon). 'circle' makes the frame square. Default 'rectangle'. |\n| `cornerRadius` | no | `number` | Image payloads only. Corner radius for shape 'rounded' as a fraction of the frame's shorter side. Default 0.16667, PowerPoint's roundRect default. |\n| `border` | no | `ref:ImageBorder` | Image payloads only. Line along the frame's shape. |\n| `opacity` | no | `number` | Image payloads only. Picture opacity from 0 to 1, exported as a native alphaModFix. The border and overlay keep their own opacity. Default 1. |\n| `recolor` | no | `ref:ImageRecolor` | Image payloads only. Grayscale or duotone treatment of the picture's pixels. |\n| `overlay` | no | `ref:Overlay` | Image payloads only. Scrim over the picture in the frame's shape, or a band along one edge of a rectangle frame. |\n| `placement` | no | `ref:ImagePlacement` | Image payloads only, and only on a top-level block (slides.N.blocks.I), at most one per edge. Bleeds the image to one slide edge; the headings and the other blocks compose in the rest of the slide. |\n| `video` | no | `ref:Asset` | Source for a video item. Engines do not play video: the preview draws a placeholder (a play badge with the asset's title or source as its caption), and PPTX export draws the same placeholder as native shapes linked to... |\n| `chart` | no | `ref:Chart` | Chart payload. Presence of this field infers type 'chart'. |\n| `table` | no | `ref:Table` | Table payload. Presence of this field infers type 'table'. |\n| `code` | no | `oneOf:string / ref:Code` | Code payload. A string is shorthand for { \"source\": value }; object form carries optional syntax language and filename metadata. |\n| `metric` | no | `oneOf:string / number / ref:Metric` | Metric payload. A string or number is shorthand for { \"value\": value }; object form carries optional label, description, unit, delta, trend, and sentiment metadata. Numeric values remain numbers; renderers format them... |\n| `quote` | no | `oneOf:string / ref:Quote` | Quote payload. A string is shorthand for { \"text\": value }; object form carries optional attribution, role, photo and source metadata. |\n| `timeline` | no | `ref:Timeline` | Timeline payload ordered by narrative or chronology. |\n| `caption` | no | `ref:Caption` | Caption for an image, chart, table or video payload, composed inside the block's region (below the media by default). Invalid on other payload kinds and on groups. |\n| `blocks` | no | `array<ref:ContentPayload>` | Ordered children of a group. Each child is a leaf or another group. |\n| `composition` | no | `ref:Composition` | Arrangement within this group. Only minFontSize and overflow inherit from the parent; strict overflow cannot be weakened. |\n\n\n### Quote\n\n- Type: `object`\n- Required fields: `text`\n- Purpose: Quote content with optional attribution metadata. Use 'text' for the quoted text, 'attribution' for the credited person or organization, 'role' for that person's title and organization, 'photo' for their headshot and 'source' for a citation or URL. A string value in a quote field is shorthand for { \"text\": value }.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `text` | yes | `oneOf:string / array<ref:TextRun>` | Quoted text: a string, or TextRun[] for inline rich text (a colored phrase, bold, a link, or a citation or footnote marker). The engine draws the surrounding quotation marks. |\n| `attribution` | no | `string` | Person or organization credited for the quote. Drawn after the quote text, in the muted text color. |\n| `role` | no | `string` | Title and organization of the attributed person. Drawn on its own line under the attribution; without an attribution it stands alone. It does not replace the attribution, so the person's name belongs in 'attribution'. |\n| `photo` | no | `ref:Asset` | Headshot of the attributed person, drawn as a circle beside the attribution and role lines (on the logical start side: left in a left-to-right deck, right in a right-to-left deck). Give it alt text (validate's opf/mis... |\n| `source` | no | `string` | Optional quote source, citation, or URL. Follows the attribution and role after ' - '. |\n\n\n### Code\n\n- Type: `object`\n- Required fields: `source`\n- Purpose: Code content with optional rendering metadata. Use 'source' for the code text, 'language' for syntax highlighting, 'filename' when the rendered block should show a file label, and 'highlight' to emphasize lines. A string value in a code field is shorthand for { \"source\": value }.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `source` | yes | `string` | Source code text to display. |\n| `language` | no | `string` | Language identifier used for syntax highlighting. |\n| `filename` | no | `string` | Optional file label shown with the code block. |\n| `highlight` | no | `array<oneOf:integer / array<integer>>` | Source lines to emphasize, 1-based: each entry is a line number or an inclusive [start, end] range, so [3, [5, 7]] marks lines 3 and 5 to 7. Lines are counted by line break in 'source', so a line that wraps stays one... |\n\n\n### Metric\n\n- Type: `object`\n- Required fields: `value`\n- Purpose: Metric content with optional display metadata. Use 'value' for the primary value, 'label' for the metric name, 'description' for supporting context, 'unit' for a suffix/currency marker, 'delta' for change, 'trend' for direction, and 'sentiment' for whether the change is good news. A string or number value in a metric field is shorthand for { \"value\": value }; numeric values remain numbers and are formatted by renderers.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `oneOf:string / number` | Primary metric value. |\n| `label` | no | `string` | Metric label. |\n| `description` | no | `string` | Optional supporting context for the metric. |\n| `unit` | no | `string` | Metric unit, suffix, or currency marker. |\n| `delta` | no | `oneOf:string / number` | Metric change value. |\n| `trend` | no | `enum:up \\| down \\| flat` | Metric trend direction. The preview and the PowerPoint export draw an arrow beside the trend word that always points the way the trend does (up, down or flat) and colour the arrow, the trend word and the delta text by... |\n| `sentiment` | no | `enum:positive \\| negative \\| neutral` | Whether the change is good news. Positive draws the trend arrow and the trend and delta text in green, negative in red and neutral in the neutral text colour, each kept at 4.5:1 contrast or more against the slide back... |\n\n\n### Timeline\n\n- Type: `oneOf:array<ref:TimelineEvent> / object`\n- Required fields: none\n- Purpose: Timeline content. An array is shorthand for { \"events\": value }; object form carries optional name and description metadata.\n\n_No named properties._\n\n\n### TimelineEvent\n\n- Type: `object`\n- Required fields: `what`\n- Purpose: A single event inside a timeline content payload.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `when` | no | `string` | Event time, date, or sequence label. Use ISO-like values when possible, but human labels are allowed for quarters, eras, and relative milestones. |\n| `what` | yes | `string` | Short event label. |\n| `description` | no | `string` | Optional event detail. |\n| `status` | no | `enum:done \\| current \\| planned` | Progress of the event, drawn from the deck's own colors by both the SVG renderer and the PPTX exporter. Absent means no status and the event is drawn as a plain filled marker with normal text, which is also how 'done'... |\n\n\n### ListItem\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A flat item inside a list. Strings cover the common case, TextRun[] supports inline rich text without an object wrapper, and object form adds description and nesting depth without creating nested slide content payloads.\n\n_No named properties._\n\n\n### BulletItem\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A flat bullet item. Strings cover the common case, TextRun[] supports inline rich text without an object wrapper, and object form adds nesting depth without list-item descriptions.\n\n_No named properties._\n\n\n### NumberingStyle\n\n- Type: `enum:arabic | roman-upper | roman-lower | alpha-upper | alpha-lower`\n- Required fields: none\n- Purpose: A list number style: 1, 2, 3; I, II, III; i, ii, iii; A, B, C; a, b, c. Alphabetic numbering past 26 repeats the letter as PowerPoint does (aa, bb, cc). Roman numerals stop at 3999; larger values are drawn in arabic with a numbering-adapted diagnostic.\n\n_No named properties._\n\n\n### Numbering\n\n- Type: `object`\n- Required fields: none\n- Purpose: Numbering of one list level.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `style` | no | `ref:NumberingStyle` | Number style. Default arabic. |\n| `start` | no | `integer` | First number counted at this level. Default 1. Native PowerPoint accepts 1 to 32767. |\n| `suffix` | no | `enum:period \\| paren \\| paren-both` | Text after the number: period (1.), paren (1)) or paren-both ((1)). Default period. |\n\n\n### NumberingSpec\n\n- Type: `oneOf:ref:NumberingStyle / ref:Numbering / array<oneOf:ref:NumberingStyle / ref:Numbering>`\n- Required fields: none\n- Purpose: The value of a numbering field: a style name or Numbering object for every level, or an array with one entry per level (at most 9, the native depth).\n\n_No named properties._\n\n\n### TextRun\n\n- Type: `oneOf:string / object`\n- Required fields: none\n- Purpose: A contiguous run of text. Strings cover unformatted spans; object form adds character formatting.\n\n_No named properties._\n\n\n### Caption\n\n- Type: `oneOf:string / array<ref:TextRun> / object`\n- Required fields: none\n- Purpose: A caption for an image, chart, table or video payload. A string or TextRun[] is the caption text placed below the media; object form adds the position and alignment.\n\n_No named properties._\n\n\n### Reference\n\n- Type: `object`\n- Required fields: `id`, `text`\n- Purpose: A source that runs cite with 'cite'. Cited references are listed in the footnote area of the slides that cite them, numbered per deck in order of first use; referencesSlide() builds an ordinary list slide of them.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Identifier runs cite. Unique within the references list. |\n| `text` | yes | `oneOf:string / array<ref:TextRun>` | The reference as it is listed: a string or TextRun[] for inline rich text. |\n| `url` | no | `string` | Optional link for the reference; a references slide links its entry to it. |\n\n\n### Chart\n\n- Type: `object`\n- Required fields: `type`, `data`\n- Purpose: Chart content. The chart object keeps chart-specific fields together so slides and regions do not expose loose chart fields. OPF charts are `{ type, data }` where `data` is `{ columns, rows }`: the first column holds the category labels and every further column is one series. There are no `categories`, `series`, `labels` or `datasets` keys (the Chart.js and ECharts shape); write them as columns and rows. Example: `{ \"type\": \"column\", \"data\": { \"columns\": [\"Quarter\", \"Decks\"], \"rows\": [[\"Q1\",...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `enum:100pct-stacked-area \\| 100pct-stacked-bar \\| 100pct-stacked-column \\| area \\| bar \\| box-and-whisker \\| column \\| combo \\| doughnut \\| filled-radar \\| funnel \\| histogram \\| line \\| line-with-markers \\| pareto \\| pie \\| radar \\| radar-with-markers \\| scatter \\| stacked-area \\| stacked-bar \\| stacked-column \\| stacked-line \\| stacked-line-with-markers \\| treemap \\| waterfall \\| world` | Chart type, an engine vocabulary validated directly (no catalog lookup): the chart types Aspose.Slides officially supports, plus 'combo' (clustered columns with line series, see 'line' and 'secondaryAxis'). Each engin... |\n| `data` | yes | `oneOf:ref:ChartData / ref:DatasetRef` | Chart data. Inline data uses a tabular columns/rows shape; renderers convert rows to chart series internally. A DatasetRef ({ \"dataset\": \"<id>\" }) plots a top-level dataset instead. Data from a spreadsheet or file is... |\n| `mapping` | no | `ref:ChartMapping` | Optional series mapping by column name: which column is the category, which is the X column of a scatter chart, and which columns are plotted, in order. Absent keeps the positional rule (first column the category, the... |\n| `line` | no | `array<string>` | Combo charts only (type 'combo'): the plotted series, by column name, that are drawn as lines with markers; every other plotted series is drawn as clustered columns. Absent: the last plotted series is the line. A comb... |\n| `secondaryAxis` | no | `array<string>` | Combo charts only: line series, by column name, plotted against a secondary value axis at the right of the plot. That axis has its own scale, and its tick labels use the number format of the first secondary series' co... |\n| `axisTitles` | no | `ref:ChartAxisTitles` | Optional axis titles. Absent keeps today's untitled axes. Supported on the chart types that have a category/value (or X/Y) axis pair (column, bar, line, area, scatter, and the histogram, pareto, waterfall and box-and-... |\n| `legend` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Optional legend position. 'none' hides the legend. Absent keeps today's behaviour exactly (a legend at the right of multi-series charts and of pie and doughnut charts, none for single-series charts). A named position... |\n| `dataLabels` | no | `oneOf:boolean / ref:ChartDataLabels` | Optional data labels. true shows value labels at each type's default position, false (or absent) shows none, which is today's behaviour. Use the object form for the label content, position and separator. |\n| `alt` | no | `string` | Text alternative for the chart: what the data shows (the point and the key numbers), not 'a chart' or 'bar chart'. The preview exposes it as the chart's accessible name (role img with aria-label) and the PowerPoint ex... |\n| `colors` | no | `array<ref:ColorRef>` | Optional series colors, by position: entry i replaces palette color i, the color of series i (the i-th plotted value column), of slice or category i on a pie, doughnut, treemap or funnel chart, and of the i-th color a... |\n| `highlight` | no | `ref:ChartHighlight` | Optional emphasis: the series and/or categories that carry the message. The engines draw the highlighted marks in the deck's primary (accent) color and every other mark in a muted neutral derived from the theme (a lig... |\n\n\n### ChartAxisTitles\n\n- Type: `object`\n- Required fields: none\n- Purpose: Titles for the axes of a chart. 'category' is the axis that carries the row labels (the horizontal axis of a column or line chart, the vertical axis of a bar chart, the X axis of a scatter chart); 'value' is the other axis; 'secondary' is the secondary value axis of a combo chart.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `category` | no | `string` | Title of the category (X) axis. |\n| `value` | no | `string` | Title of the value (Y) axis. On a combo chart, the primary (left) value axis. |\n| `secondary` | no | `string` | Combo charts only: title of the secondary value axis at the right of the plot, drawn rotated like the primary value axis title. Dropped with a 'chart-option-adapted' diagnostic on any other chart type and on a combo c... |\n\n\n### ChartHighlight\n\n- Type: `object`\n- Required fields: none\n- Purpose: Which series and categories a chart emphasizes. Names are matched exactly: series by data column name (after any 'fields' selection; a series is a plotted value column), categories by row label (the first column, or mapping.category), as text. A category label shared by several rows highlights each of them.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `series` | no | `array<string>` | Series (value columns) to highlight: all marks of each named series. A name that is not a column is a 'chart-highlight-unknown-name' error; a column that is not plotted as a series (the category or X column, or one ma... |\n| `categories` | no | `array<string>` | Categories (row labels) to highlight: the marks of each named category (a column or bar of every series, a line's points, a pie or doughnut slice). A label no row has is a 'chart-highlight-unknown-name' error. |\n\n\n### ChartDataLabels\n\n- Type: `object`\n- Required fields: none\n- Purpose: Data label settings. A label shows the selected content parts in the fixed order category, value, percent, joined by the separator.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `content` | no | `array<enum:value \\| percent \\| category>` | Which parts a label shows. Defaults to ['value']. 'percent' is the share of the total and exists only on pie and doughnut charts; 'category' shows the category name (the X value on a scatter chart). A part a chart typ... |\n| `position` | no | `enum:auto \\| center \\| inside-end \\| inside-base \\| outside-end \\| above \\| below \\| left \\| right` | Where a label sits relative to its mark. 'auto' (the default) is the type's default: outside-end for clustered columns and bars, pie slices and the histogram, pareto and waterfall constructs; center for stacked column... |\n| `separator` | no | `string` | Text between the parts of a label that shows more than one. Defaults to ', '. |\n\n\n### Table\n\n- Type: `object`\n- Required fields: none\n- Purpose: Table content, inline or dataset-backed. An inline table has 'rows' (required) and optional 'columns'. A dataset-backed table has 'dataset' (required) and optional 'fields', and no 'rows' or 'columns': it takes its headers, rows and column formats from the dataset (per-cell styles need an inline table).\n- Conditional requirement: `dataset`, or otherwise `rows`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `columns` | no | `array<oneOf:string / array<ref:TextRun> / ref:StyledTableCell / ref:DataColumn / null>` | Optional column labels. Labels may be strings, rich runs, styled cell objects or DataColumn objects ({ \"name\", \"format\" }). A header's 'format' (DataColumn or StyledTableCell) is the column's number format; a body cel... |\n| `rows` | no | `array<array<ref:TableCell>>` | Two-dimensional table row data; each row aligns by index with columns when columns are supplied. |\n| `dataset` | no | `ref:DatasetId` | Id of a top-level dataset that supplies this table's headers, rows and column formats. Excludes 'rows' and 'columns'. An unknown id is a 'dataset-unknown' error. |\n| `fields` | no | `ref:DatasetFields` | Dataset tables only: the dataset columns to show, by name and in order. Absent shows every column. An unknown name is a 'dataset-field-unknown' error. |\n| `alt` | no | `string` | Text alternative for the table: what it shows (the point and the key numbers), not 'a table'. The preview exposes it as the table's accessible name (role group with aria-label; the cells stay readable) and the PowerPo... |\n\n\n### ChartData\n\n- Type: `object`\n- Required fields: `columns`, `rows`\n- Purpose: Inline tabular data driving a chart, written as `{ \"columns\": [\"Quarter\", \"Decks\"], \"rows\": [[\"Q1\", 3], [\"Q2\", 8]] }`: one column of labels plus one column per series, not separate `categories` and `series` lists. The first column usually supplies category/x-axis labels; subsequent columns are plotted measures unless 'chart.mapping' or a chart type maps them differently. Value cells are numbers; a string is read only in strict decimal syntax ('12', '-3.5', '1e6'), and anything else ('12%', '$...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `columns` | yes | `array<oneOf:string / ref:DataColumn>` | Ordered column labels for the chart data table. A label is a string or a DataColumn ({ \"name\", \"format\" }) whose number format the data labels, value axis and exported workbook use. |\n| `rows` | yes | `array<array<ref:ChartDataCell>>` | Tabular chart rows. Each row aligns by index with columns. |\n| `source` | no | `ref:DataSourceRef` | Optional provenance: where this inline data came from. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. |\n\n\n### DataColumn\n\n- Type: `object`\n- Required fields: `name`\n- Purpose: A named data column with an optional number format. Anywhere a chart or dataset column is a string it may be a DataColumn; the string form is { \"name\": value } with no format. A table column header may also be a DataColumn.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `name` | yes | `string` | Column label, used as the series name, the table header and the name 'fields' and 'chart.mapping' address. |\n| `format` | no | `ref:NumberFormat` | Number format of the column's number values (data labels, value axis, table cells, exported workbook). Absent: the General form. |\n\n\n### DataSourceRef\n\n- Type: `object`\n- Required fields: `src`\n- Purpose: Provenance of inline data or a dataset: the file, sheet and range it came from and when. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. Re-import the data to update it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `src` | yes | `string` | Where the data came from: an 'asset:<id>' reference, a URL, or a relative or local path. |\n| `sheet` | no | `string` | Sheet or table name inside a spreadsheet source. |\n| `range` | no | `string` | A1-style range inside a spreadsheet source. |\n| `fields` | no | `array<string>` | The source fields the data was taken from, in order. |\n| `retrieved` | no | `anyOf:schema / schema` | When the data was taken from the source: an ISO 8601 date or date-time. |\n| `description` | no | `string` | Free-form note about the source. |\n\n\n### DatasetId\n\n- Type: `string`\n- Required fields: none\n- Purpose: Id of an entry in the top-level datasets map (the assets id pattern).\n\n_No named properties._\n\n\n### DatasetFields\n\n- Type: `array<string>`\n- Required fields: none\n- Purpose: Dataset column names to use, in order, each at most once. Selects and orders the dataset's columns; an unknown name is a 'dataset-field-unknown' error.\n\n_No named properties._\n\n\n### Datasets\n\n- Type: `object`\n- Required fields: none\n- Purpose: Shared data tables keyed by id (the assets id pattern). Charts reference one with 'chart.data': { \"dataset\": \"<id>\" } and tables with 'table': { \"dataset\": \"<id>\" }.\n\n_No named properties._\n\n\n### Dataset\n\n- Type: `object`\n- Required fields: `columns`, `rows`\n- Purpose: One shared data table: named columns (strings or DataColumn objects with a number format) and rows of scalar cells. Column names are unique ('data-column-duplicate' error). Rich text and styling belong in an inline table.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `title` | no | `string` | Human name of the dataset, shown by editors. |\n| `description` | no | `string` | What the dataset holds. |\n| `columns` | yes | `array<oneOf:string / ref:DataColumn>` | Ordered, uniquely named columns. |\n| `rows` | yes | `array<array<ref:ChartDataCell>>` | Rows of scalar cells (string, number, boolean, null); each row aligns by index with columns. |\n| `source` | no | `ref:DataSourceRef` | Optional provenance. Engines never read, fetch or refresh it. |\n\n\n### DatasetRef\n\n- Type: `object`\n- Required fields: `dataset`\n- Purpose: Chart data taken from a top-level dataset. Engines inline it before plotting; 'chart.mapping' names columns after the 'fields' selection.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `dataset` | yes | `ref:DatasetId` | Id of the dataset. An unknown id is a 'dataset-unknown' error. |\n| `fields` | no | `ref:DatasetFields` | The dataset columns to use, by name and in order. Absent uses every column. |\n\n\n### ChartMapping\n\n- Type: `object`\n- Required fields: none\n- Purpose: Series mapping by column name (after any 'fields' selection). Mapping only selects and orders columns; pie, doughnut and other single-series constructs still plot one series. An unknown name is a 'chart-mapping-unknown-column' error; a series that repeats the category or X column, and an X column on a chart type without an X axis, are dropped with a 'chart-mapping-adapted' warning.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `category` | no | `string` | The label (category) column. Default: the first column. |\n| `x` | no | `string` | The X column of an XY (scatter) chart. Default: the second column (the first, when the category is the second column), and only when there are three or more columns: with two, the second column is the one series, plot... |\n| `series` | no | `array<string>` | The plotted columns, in order. Default: every column that is not the category or the X column. |\n\n\n### ChartDataCell\n\n- Type: `oneOf:string / number / boolean / null`\n- Required fields: none\n- Purpose: A cell in inline chart data.\n\n_No named properties._\n\n\n### TableCell\n\n- Type: `oneOf:ref:TableCellValue / ref:StyledTableCell`\n- Required fields: none\n- Purpose: A scalar, rich-run array, or styled/spanning cell object. Existing scalar and rich forms remain valid.\n\n_No named properties._\n\n\n### TableCellValue\n\n- Type: `oneOf:string / number / boolean / null / array<ref:TextRun>`\n- Required fields: none\n- Purpose: A scalar table value or canonical rich text runs, without cell decoration or geometry.\n\n_No named properties._\n\n\n### StyledTableCell\n\n- Type: `object`\n- Required fields: `value`\n- Purpose: A cell with explicit visual style or merged geometry. Its position remains its array column index; use null placeholders for every covered grid position.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `ref:TableCellValue` | Editable cell content; styling and spans do not change its scalar type or rich runs. |\n| `style` | no | `ref:TableCellStyle` | |\n| `colSpan` | no | `integer` | Number of grid columns covered, starting at this cell. Covered positions must contain null. Default 1. |\n| `rowSpan` | no | `integer` | Number of grid rows covered, starting at this cell. Covered positions must contain null. Header cells cannot span into body rows. Default 1. |\n| `format` | no | `ref:NumberFormat` | Number format. On a body cell it formats that cell's number value (and wins over the column's format); on a header cell (in 'columns') it is the column's format, the same as a DataColumn header. Strings, booleans, nul... |\n\n\n### TableCellStyle\n\n- Type: `object`\n- Required fields: none\n- Purpose: Cell appearance. Sizes use reference pixels at a 720-pixel canvas short edge and scale with the slide.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `fill` | no | `ref:ColorRef` | Cell background: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. Eight-digit hex colors include alpha; #00000000 is transparent. |\n| `color` | no | `ref:ColorRef` | Default text color, overridden by individual rich run colors. Accepts a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. |\n| `bold` | no | `boolean` | Bold cell text. Runs that set their own bold keep it; a number cell stays bold and still uses the cell's or column's number format. |\n| `align` | no | `enum:left \\| center \\| right` | Horizontal text alignment inside the cell. |\n| `verticalAlign` | no | `enum:top \\| middle \\| bottom` | Vertical alignment inside the padded cell box. |\n| `padding` | no | `ref:TableCellPadding` | |\n| `borders` | no | `object` | Independent cell edges. An omitted edge keeps the table's default rule: a line under every row (and over the first row), none between columns. Width 0 removes an edge. |\n\n\n### TableCellPadding\n\n- Type: `object`\n- Required fields: none\n- Purpose: Text insets in reference pixels. Defaults: top 8, right 10, bottom 4, left 10.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `top` | no | `number` | |\n| `right` | no | `number` | |\n| `bottom` | no | `number` | |\n| `left` | no | `number` | |\n\n\n### TableCellBorder\n\n- Type: `object`\n- Required fields: `color`, `width`\n- Purpose: One explicit cell border.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `color` | yes | `ref:ColorRef` | Border color: a hex color, a color-scheme slot or role name, or a 'var:<id>' variable reference. Eight-digit hex colors include alpha; #00000000 is transparent. |\n| `width` | yes | `number` | Border width in reference pixels; 0 removes this edge. |\n| `dash` | no | `enum:solid \\| dash \\| dot` | Default solid. |\n\n\n### Catalogs\n\n- Type: `object`\n- Required fields: none\n- Purpose: The catalog records a document embeds, grouped by the catalog they came from. Each property is a group; inside a group, records are keyed by kind and then by id. - 'default': the catalog bare ids come from. It needs a 'source' when it holds records. Omitted: bare ids fall back to the host's default catalog. false: no catalog fallback, so every bare id must be embedded (under 'custom' or 'default'). - 'custom': the records the document defines itself. It has no source. - any other name ('acme'...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `default` | no | `oneOf:allOf:ref:CatalogGroup + schema / const:false` | The catalog bare ids come from: its source and the records of it this document embeds. false turns off the catalog fallback for bare ids. |\n| `custom` | no | `allOf:ref:CatalogGroup + schema` | Records the document defines itself, referenced by bare id. Checked first for every bare id. Forking a catalog record copies it here under a new id, so the change of ownership is visible. |\n\n\n### CatalogGroup\n\n- Type: `object`\n- Required fields: none\n- Purpose: One catalog group: the catalog's source and the records of it the document embeds, by kind and then by id.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `source` | no | `ref:CatalogSource` | |\n| `layouts` | no | `ref:CatalogRecords` | Layout records keyed by id, each with the fields of https://openpresentation.org/schema/opf-layout/v2. Referenced from Slide.layout. |\n| `themes` | no | `ref:CatalogRecords` | Theme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-theme/v1. Referenced from design.theme. |\n| `colorSchemes` | no | `ref:CatalogRecords` | Colour-scheme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-color-scheme/v1. Referenced from design.colorScheme and a theme's colorScheme. |\n| `fontSchemes` | no | `ref:CatalogRecords` | Font-scheme records keyed by id, each with the fields of https://openpresentation.org/schema/opf-font-scheme/v1. Referenced from design.fontScheme, a theme's fontScheme and a language's font schemes. |\n| `narratives` | no | `ref:CatalogRecords` | Narrative records keyed by id, each with the fields of https://openpresentation.org/schema/opf-narrative/v1. Referenced from narrative. |\n| `audiences` | no | `ref:CatalogRecords` | Audience records keyed by id, each with the fields of https://openpresentation.org/schema/opf-audience/v1. Referenced from audience. |\n| `purposes` | no | `ref:CatalogRecords` | Purpose records keyed by id, each with the fields of https://openpresentation.org/schema/opf-purpose/v1. Referenced from purpose. |\n| `tones` | no | `ref:CatalogRecords` | Tone records keyed by id, each with the fields of https://openpresentation.org/schema/opf-tone/v1. Referenced from tone. |\n\n\n### CatalogSource\n\n- Type: `string`\n- Required fields: none\n- Purpose: Identity of a catalog: an HTTPS URL ('https://www.pptx.gallery') or a package reference ('pkg:@acme/opf-catalog'). It names the catalog; engines never fetch or install it. A host registers the catalog's records under this exact string, and copying slides between documents matches groups by it.\n\n_No named properties._\n\n\n### CatalogRecords\n\n- Type: `object`\n- Required fields: none\n- Purpose: Embedded records of one kind, keyed by id. The key is the record's id; an embedded record carries no '$schema', 'id', origin or digest. Catalog display metadata ('x-*' members) is stripped when a record is embedded.\n\n_No named properties._\n\n\n### CatalogReference\n\n- Type: `string`\n- Required fields: none\n- Purpose: A content reference: a bare id ('two-column') or 'name:id' ('acme:hero'), where the prefix names a group of 'catalogs'. URLs and 'pkg:' strings are not references; a named catalog group replaces them. See Catalogs for the resolution order.\n\n_No named properties._\n\n\n### LanguageTag\n\n- Type: `string`\n- Required fields: none\n- Purpose: A BCP-47 language tag ('en-US', 'ja', 'zh-Hans', 'sr-Latn-ME'), an engine vocabulary. Use 'en-GB' for UK English; 'en-UK' is not a valid region.\n\n_No named properties._\n\n\n### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: Automatic slide and content-group arrangement. Nested content groups arrange their children independently, inheriting only minFontSize and overflow. Explicit promoted regions retain their positions. A layout defines its grid as one ordered list of regions; its composition supports gap, padding, minFontSize and overflow rather than mode, columns or weights.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | no | `enum:auto \\| grid \\| row \\| column` | auto chooses a grid from available space and content; grid uses columns; row and column use one horizontal or vertical track. |\n| `columns` | no | `integer` | Column count for grid. In auto mode this caps the number of columns. |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `weights` | no | `array<number>` | Relative track sizes: columns for row/grid/auto, rows for column. Omitted tracks have weight 1; extra weights are ignored. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"slug": "schema-versioning",
|
|
313
|
+
"file": "docs/schema-versioning.md",
|
|
314
|
+
"title": "Schema versioning",
|
|
315
|
+
"markdown": '# Schema versioning\n\nThe OPF schemas carry a `v1` (and the layout record schema a `v2`) in their `$id`, such as `https://openpresentation.org/schema/opf/v1`. Before OPF 1.0 that number names the **pre-1.0 line**, not a frozen contract.\n\n- **`v1` may change in any 0.x minor release.** A 0.x minor bump is a breaking-change bump (a patch release is never breaking). Fields are renamed, removed and tightened before 1.0 without deprecation aliases, so the same `$id` can describe different documents in 0.26 and 0.27.\n- **The release that produced a schema copy is the package version.** The `@openpresentation/opf` package ships `spec/schemas/` unchanged, so a schema copy belongs to the version of the package it came from. Each schema carries the same statement in its `$comment`.\n- **The `$id` is a name, not a version pin.** `$schema: "https://openpresentation.org/schema/opf/v1"` in a document says which line it is written for; it does not say which 0.x release.\n\n## Find the version\n\n```sh\nopf --version # { "cli": "0.27.0", "opf": "0.27.0", "schema": "https://openpresentation.org/schema/opf/v1" }\nopf schemas # [{ "name": "presentation", "file": "schemas/opf.schema.json", "id": ".../v1", "version": "0.27.0" }, ...]\nopf schema # the whole presentation schema; its $comment ends "This copy is from @openpresentation/opf 0.27.0."\n```\n\n`opf schemas` and `opf --version` read the version of the `@openpresentation/opf` package the CLI runs on, so the schema you read through the CLI is the one `opf validate` checks with. A part of a schema printed with a JSON Pointer (`opf schema presentation \'/$defs/Chart\'`) is printed exactly as it is, without the added sentence. In a source checkout, `spec/schemas/` is the schema of the checked-out commit and `packages/javascript/package.json` has its version.\n\n## Pin it\n\n- Read and validate with the schema of the package version you target: install `@openpresentation/opf@<version>` (and `@openpresentation/cli@<version>` with it) and use its `opf schema`, or `schemaEntries` from the package.\n- A schema fetched from the web (`https://openpresentation.org/schema/opf/v1`) is the latest release\'s copy. Use it for editor completion, not as a contract for an older document. Versioned URLs on the website are a separate piece of work.\n- Check a document against the version it was written for before you migrate it; OPF ships no migration tooling before 1.0, so a breaking change is listed in the [changelog](../CHANGELOG.md) and the examples and docs are converted with it.\n\nAt 1.0 the line becomes a contract: `v1` then changes only in ways that keep every valid `v1` document valid.\n'
|
|
302
316
|
},
|
|
303
317
|
{
|
|
304
318
|
"slug": "security-2026-09-09",
|
|
@@ -352,7 +366,7 @@ var docsData = Object.freeze([
|
|
|
352
366
|
"slug": "validate",
|
|
353
367
|
"file": "docs/validate.md",
|
|
354
368
|
"title": "OPF validate: one checker for format, references, accessibility, layout and content",
|
|
355
|
-
"markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run. A syntax finding names the root cause in plain words and not the token where a parser gave up: for `\"design\":` followed by the next key, one finding `` `design` needs a value. `` located at `\"design\":` (its `help` says what could come next), not four findings from the token after it. There is one finding per cause, in position order, at most five; the cascade of one mistake is not reported again. `analyzeJsonSyntax` (`@openpresentation/opf/source`, browser-safe) returns the same causes with the fixes that repair each one (`Insert comma`, `Add { }`, `Remove \"design\"`, ...) for editors; `locate` and `pathAt` in the same module turn a JSON Pointer into a range of the text and a caret into a path, in JSON, YAML and Markdown, and `scanMarkdown` lists the regions of a Markdown deck. A finding for an unknown property (`opf/schema`, keyword `additionalProperties`) carries up to three `suggestions` of the nearest property names of the same object (edit distance 2, or 1 for names of three characters or fewer; case-insensitive), so `slids` suggests `slides`.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\n[Validation results](finding-schema-reference.md) use the `FindingReport` contract (`spec/schemas/finding.schema.json`). Core's `ValidationReport` adds a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape and checks the record format only (`checks.references` is `not-run`): without an enclosing catalog it does not resolve linked ids. Validate a presentation with its registered catalogs to check those references.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs every category, including overflow findings; `valid` means no error findings, so an overflow warning alone does not make it false. Hot paths select what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; CLI document-writing commands check `format` and `references`, so a contrast warning does not block an export unless its severity or failure policy is explicitly raised.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"regions\":[{\"name\":\"title\",\"size\":1}]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--asset-dir <dir>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n**Local images (RR-93).** The CLI, not core `validate` (which reads no file), also checks that each relative image or logo path exists: a `cli/asset-missing` warning when `opf convert` could not find it, `cli/asset-blocked` when it is outside the folder or not an image file. It uses convert's resolver and rules: paths are relative to the deck's folder (the working directory for stdin), or to `--asset-dir`; URLs, `data:` URIs and `asset:` references to them are not read. `--only` skips the check and `--ignore asset-missing` (or `asset-blocked`) drops one kind.\n\n**Schema errors in a union.** Where a value can take several forms (a table cell is a scalar, runs or a styled object), a failing value reports the errors of the form it was written for, and each finding's `validation.alternatives` lists the other forms in one line each (`--format text` prints them as `other forms: ...`). When no form fits the value's type, one error names the types accepted.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `embed`, `ingest`, `convert`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Plain-words text and `explain`\n\nA finding is written for tools: `message` is the rule's own sentence or an Ajv phrase, `help` is advice for an agent. To show a finding to a person, `describeFinding(finding, { document })` (from `@openpresentation/opf/source`) returns `{ message, next }`: `message` says what is wrong, with fields in backticks and no JSON Pointers (\"`titleAlignment` must be left, center or right, but is `middle`.\"), and `next` is the continuation of \"Next:\" (\"did you mean `center`?\"). Every `opf/*` rule has its text, written from the structured data the finding carries (`path`, `validation.params`, `measured`, `suggestions`, `fixes`, the slide) and from the document, for example the datasets, assets or columns that do exist; a finding from another producer keeps its own sentence and the first sentence of its `help`. The call is pure, never throws and does not change the finding.\n\n`explain(ruleId)` (from the package root and `@openpresentation/opf/source`) returns what a rule checks and how to fix it: `{ id, name, category, severity, summary, rationale, fix, example, docs }`, where `example` is a small `{ problem, fixed, language }` pair of OPF fragments and `docs` is the rule's anchor in this guide. It accepts `opf/text-contrast` and `text-contrast`, and returns `undefined` for an id it does not know.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's region list is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions are not valid. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-colors-adjusted`](#opfchart-colors-adjusted) | Accessibility | warning | structure | A chart colour cannot be used as authored. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/furniture-wraps`](#opffurniture-wraps) | Layout | warning | composition | A header or footer zone wraps onto more than 2 lines. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON. One finding per root cause, in position order and at most five (a key with no value, a missing comma, a bracket never closed; not the damage each one causes further on): `message` says what is wrong, `help` what to do next and `location` is the range to underline. A text that looks like YAML or Markdown gets one finding with a hint to use fromYaml or fromMarkdown first. Source editors get the same causes with their quick fixes from analyzeJsonSyntax (@openpresentation/opf/source).\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's region list is malformed.\n\n**Why.** A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an \"auto\" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions are not valid.\n\n**Why.** Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference.\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-colors-adjusted`\n\nDefault severity: **warning**. Cost: structure. A chart colour cannot be used as authored.\n\n**Why.** `chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.\n\n**Basis.** WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA\n\n**Approximations.** Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/furniture-wraps`\n\nDefault severity: **warning**. Cost: composition. A header or footer zone wraps onto more than 2 lines.\n\n**Why.** Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.\n\n**Approximations.** Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
|
|
369
|
+
"markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run. A syntax finding names the root cause in plain words and not the token where a parser gave up: for `\"design\":` followed by the next key, one finding `` `design` needs a value. `` located at `\"design\":` (its `help` says what could come next), not four findings from the token after it. There is one finding per cause, in position order, at most five; the cascade of one mistake is not reported again. `analyzeJsonSyntax` (`@openpresentation/opf/source`, browser-safe) returns the same causes with the fixes that repair each one (`Insert comma`, `Add { }`, `Remove \"design\"`, ...) for editors; `locate` and `pathAt` in the same module turn a JSON Pointer into a range of the text and a caret into a path, in JSON, YAML and Markdown, and `scanMarkdown` lists the regions of a Markdown deck. A finding for an unknown property (`opf/schema`, keyword `additionalProperties`) carries up to three `suggestions` of the nearest property names of the same object (edit distance 2, or 1 for names of three characters or fewer; case-insensitive), so `slids` suggests `slides`.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\n[Validation results](finding-schema-reference.md) use the `FindingReport` contract (`spec/schemas/finding.schema.json`). Core's `ValidationReport` adds a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape and checks the record format only (`checks.references` is `not-run`): without an enclosing catalog it does not resolve linked ids. Validate a presentation with its registered catalogs to check those references.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs every category, including overflow findings; `valid` means no error findings, so an overflow warning alone does not make it false. Hot paths select what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; CLI document-writing commands check `format` and `references`, so a contrast warning does not block an export unless its severity or failure policy is explicitly raised.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"regions\":[{\"name\":\"title\",\"size\":1}]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--asset-dir <dir>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n**Local images (RR-93).** The CLI, not core `validate` (which reads no file), also checks that each relative image or logo path exists: a `cli/asset-missing` warning when `opf convert` could not find it, `cli/asset-blocked` when it is outside the folder or not an image file. It uses convert's resolver and rules: paths are relative to the deck's folder (the working directory for stdin), or to `--asset-dir`; URLs, `data:` URIs and `asset:` references to them are not read. `--only` skips the check and `--ignore asset-missing` (or `asset-blocked`) drops one kind.\n\n**Schema errors in a union.** Where a value can take several forms (a table cell is a scalar, runs or a styled object), a failing value reports the errors of the form it was written for, and each finding's `validation.alternatives` lists the other forms in one line each (`--format text` prints them as `other forms: ...`). When no form fits the value's type, one error names the types accepted.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `embed`, `ingest`, `convert`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n**Done means `--fail-on warning` passes.** A layout, theme or other catalog id that resolves nowhere is the warning `opf/unresolved-reference`, not an error, because catalogs are open and a host may register the id; so a deck with a made-up `layout` still reports `ok: true` and exits 0 by default. Agents and CI should treat a deck as finished only when `opf validate deck.opf.json --fail-on warning` exits 0. The finding's hint names the nearest records that exist (\"Did you mean 'two-column'?\").\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Plain-words text and `explain`\n\nA finding is written for tools: `message` is the rule's own sentence or an Ajv phrase, `help` is advice for an agent. To show a finding to a person, `describeFinding(finding, { document })` (from `@openpresentation/opf/source`) returns `{ message, next }`: `message` says what is wrong, with fields in backticks and no JSON Pointers (\"`titleAlignment` must be left, center or right, but is `middle`.\"), and `next` is the continuation of \"Next:\" (\"did you mean `center`?\"). Every `opf/*` rule has its text, written from the structured data the finding carries (`path`, `validation.params`, `measured`, `suggestions`, `fixes`, the slide) and from the document, for example the datasets, assets or columns that do exist; a finding from another producer keeps its own sentence and the first sentence of its `help`. The call is pure, never throws and does not change the finding.\n\n`explain(ruleId)` (from the package root and `@openpresentation/opf/source`) returns what a rule checks and how to fix it: `{ id, name, category, severity, summary, rationale, fix, example, docs }`, where `example` is a small `{ problem, fixed, language }` pair of OPF fragments and `docs` is the rule's anchor in this guide. It accepts `opf/text-contrast` and `text-contrast`, and returns `undefined` for an id it does not know.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's region list is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions are not valid. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-colors-adjusted`](#opfchart-colors-adjusted) | Accessibility | warning | structure | A chart colour cannot be used as authored. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/furniture-wraps`](#opffurniture-wraps) | Layout | warning | composition | A header or footer zone wraps onto more than 2 lines. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON. One finding per root cause, in position order and at most five (a key with no value, a missing comma, a bracket never closed; not the damage each one causes further on): `message` says what is wrong, `help` what to do next and `location` is the range to underline. A text that looks like YAML or Markdown gets one finding with a hint to use fromYaml or fromMarkdown first. Source editors get the same causes with their quick fixes from analyzeJsonSyntax (@openpresentation/opf/source).\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category. A chart written in the Chart.js or ECharts shape (`categories` and `series`, or `labels` and `datasets`, and no `data`) is one finding on the chart whose hint shows its own values as `data: { columns, rows }`, with a patch fix; the additional-property findings that repeat it are dropped.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's region list is malformed.\n\n**Why.** A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an \"auto\" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions are not valid.\n\n**Why.** Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference. It is a warning, not an error, because catalogs are open and a host may register the id; `--fail-on warning` is the check that a deck is done. With a catalog registered, the hint names the nearest records that exist (\"Did you mean `two-column`?\").\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Content drawn entirely above the title, over the columns of the title, is reported; content in a column beside the title is read after it. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-colors-adjusted`\n\nDefault severity: **warning**. Cost: structure. A chart colour cannot be used as authored.\n\n**Why.** `chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.\n\n**Basis.** WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA\n\n**Approximations.** Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/furniture-wraps`\n\nDefault severity: **warning**. Cost: composition. A header or footer zone wraps onto more than 2 lines.\n\n**Why.** Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.\n\n**Approximations.** Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
|
|
356
370
|
},
|
|
357
371
|
{
|
|
358
372
|
"slug": "yaml",
|