@openpresentation/opf 0.29.1 → 0.31.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.
@@ -4,8 +4,8 @@ import { SchemaName } from './schemas.js';
4
4
  /**
5
5
  * RR-78: the version of the catalog record schemas (spec/schemas/<kind>.schema.json) this core reads. A catalog package
6
6
  * declares the version its records target (`opf.catalogSchema` in its package.json, `CATALOG_SCHEMA` in its exports, as
7
- * @openpresentation/gallery does); the Node defaults refuse a gallery that targets another one. A record schema change that
8
- * old records no longer validate against raises it.
7
+ * @openpresentation/gallery does). Core and the gallery release in lockstep, and `pnpm check:catalog-schema` holds the two
8
+ * constants equal. A record schema change that old records no longer validate against raises it.
9
9
  */
10
10
  declare const CATALOG_SCHEMA = 4;
11
11
  /** Kinds of catalog display metadata: published by catalogs for pickers, never referenced by documents. */
@@ -0,0 +1,9 @@
1
+ import { gallery } from '@openpresentation/gallery';
2
+
3
+ // src/node/catalogs.ts
4
+ var DEFAULTS = Object.freeze([gallery]);
5
+ function defaultCatalogs() {
6
+ return DEFAULTS;
7
+ }
8
+
9
+ export { defaultCatalogs };
@@ -1,4 +1,4 @@
1
- import { defaultCatalogs } from './chunk-Z7XNHIW7.js';
1
+ import { defaultCatalogs } from './chunk-AQEAZDL2.js';
2
2
  import { localAssetSources, OPFApiError, parseSlideSelection, OPFExportError, asApiError, OPFImportError } from './chunk-I6G32TB5.js';
3
3
  import { paginate } from './chunk-7UO6H32I.js';
4
4
  import { deckFormatOf, readDeckReport, stringify } from './chunk-SYCHWS7C.js';
@@ -117,7 +117,7 @@ function localAssetProblems(deck, root) {
117
117
  }
118
118
  var RENDER_PACKAGE = "@openpresentation/opf-render";
119
119
  var PPTX_PACKAGE = "@openpresentation/opf-pptx";
120
- var PEER_RANGES = { [RENDER_PACKAGE]: "^0.29.1", [PPTX_PACKAGE]: "^0.29.1" };
120
+ var PEER_RANGES = { [RENDER_PACKAGE]: "^0.31.0", [PPTX_PACKAGE]: "^0.31.0" };
121
121
  var PPTX_TEXT_OPTIONS_FROM = "0.22.0";
122
122
  var numbers = (version) => version.replace(/[-+].*$/, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
123
123
  function atLeast(version, minimum) {
@@ -1,5 +1,5 @@
1
- export { convertFiles, deckFileFormat, exportFormatOf, openDeck, planConversion, readInput, saveDeck, writePlanned } from './chunk-TML2473C.js';
2
- import './chunk-Z7XNHIW7.js';
1
+ export { convertFiles, deckFileFormat, exportFormatOf, openDeck, planConversion, readInput, saveDeck, writePlanned } from './chunk-XSCZZWYI.js';
2
+ import './chunk-AQEAZDL2.js';
3
3
  import './chunk-I6G32TB5.js';
4
4
  import './chunk-UJSAR6GF.js';
5
5
  import './chunk-PJSPHHRK.js';
@@ -43,8 +43,8 @@ declare const RENDER_PACKAGE = "@openpresentation/opf-render";
43
43
  declare const PPTX_PACKAGE = "@openpresentation/opf-pptx";
44
44
  /** Peer ranges. The CLI checks the features it calls rather than the version, so a newer release in range keeps working. */
45
45
  declare const PEER_RANGES: {
46
- readonly "@openpresentation/opf-render": "^0.29.1";
47
- readonly "@openpresentation/opf-pptx": "^0.29.1";
46
+ readonly "@openpresentation/opf-render": "^0.31.0";
47
+ readonly "@openpresentation/opf-pptx": "^0.31.0";
48
48
  };
49
49
  interface Diagnostic {
50
50
  code: string;
package/dist/docs.js CHANGED
@@ -66,7 +66,7 @@ var docsData = Object.freeze([
66
66
  "slug": "default-catalog",
67
67
  "file": "docs/default-catalog.md",
68
68
  "title": "Catalogs and the gallery package",
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'
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) and the gallery declares the one its records target\n(`CATALOG_SCHEMA`, `opf.catalogSchema`). The seven packages release in lockstep at one version, so there is no runtime\ncheck (the former `gallery-schema-mismatch` refusal of the Node defaults and the CLI was removed in MR-62, with no alias):\n`pnpm check:catalog-schema` fails the build when the two constants differ. A third-party catalog still declares its own\ngeneration the same way. 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'
70
70
  },
71
71
  {
72
72
  "slug": "design-resolution",
@@ -216,7 +216,7 @@ var docsData = Object.freeze([
216
216
  "slug": "live-editor",
217
217
  "file": "docs/live-editor.md",
218
218
  "title": "Browser preview and live editing",
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"
219
+ "markdown": "# Browser preview and live editing\n\nThe editor (`@openpresentation/opf-editor`) 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## Toolbar, presets and the web component\n\nThe editor's title bar is a toolbar of named items. `layout` picks the preset: `\"embed\"` is the slide alone under a compact bar (the deck's name, Undo and Redo, the slide counter, Design, Expand and Present) for a page that shows a deck, `\"viewer\"` is the same read-only, `\"canvas\"` adds the slide row, and `\"full\"` is the whole editor with the source pane. `toolbar: [\"title\", \"history\", \"spacer\", \"expand\"]` replaces a preset's items and order, `actions` adds the host's own buttons (Save, Share, Download), and `guardUnload`, `editor.dirty`, the `save` event (Ctrl/Cmd+S) and `editor.markSaved()` connect the editor to the host's storage. Each item can instead sit in the page's own header (`toolbar: false`, `slots: { title, history, counter, expand }`), or the host builds its own controls from the handle (`getDeckName`, `setDeckName`, `undo`, `setSlide`, `expand`) and its events (`deckName`, `slide`, `dirty`, `expand`). The bar and its parts are plain DOM with stable classes, state attributes and `--opf-ui-*` tokens for plain CSS, and an `icons` option.\n\nThe same editor is one HTML tag, `<opf-editor layout=\"embed\" toolbar=\"title,history,counter,expand\">` with an inline or fetched deck and `opf-deck-name`, `opf-dirty` and `opf-save` events, and a React component with `useOpfDeckName` and `useOpfDirty`. The [chrome guide](../packages/editor/docs/chrome-guide.md) walks through each task with code from runnable pages (embed, viewer, full, your own header, headless, React, the web component and styling), [Embedding the editor](../packages/editor/docs/embedding.md#toolbar-and-presets) lists every item and priority, [Styling the chrome](../packages/editor/docs/styling.md) is the contract of its classes and icons, and [The web component](../packages/editor/docs/web-component.md) covers the tag.\n\n## Install the published packages\n\nUse Node 24. Core, editor, PPTX, renderer and Gallery are released together at one shared version, so install them in one command and keep them on the same version (pin it with `--save-exact` once you have one). The toolbar, the presets, the save contract and the web component above are in the releases after 0.29.1. The [quickstart](quickstart.md) is the shared command-line first run:\n\n```sh\nnpm install @openpresentation/opf @openpresentation/opf-render @openpresentation/opf-editor @openpresentation/opf-pptx @openpresentation/gallery\n```\n\nNo paid service or provider account is required. The agent skills install with `npx @openpresentation/cli 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"
220
220
  },
221
221
  {
222
222
  "slug": "llm-authoring",
@@ -258,7 +258,7 @@ var docsData = Object.freeze([
258
258
  "slug": "node",
259
259
  "file": "docs/node.md",
260
260
  "title": "OPF files in Node: `@openpresentation/opf`",
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'
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. Core and the gallery release together and\n`pnpm check:catalog-schema` holds their `CATALOG_SCHEMA` constants equal, so Node does not check it at runtime.\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'
262
262
  },
263
263
  {
264
264
  "slug": "numbered-lists",
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { SchemaEntry, SchemaName, audience, chartType, colorScheme, fontScheme, language, layout, narrative, presentation, purpose, schemaEntries, schemaNames, schemas, socialPlatform, theme, tone } from './schemas.js';
2
- import { a3 as ConvertInput, a4 as ConvertOptions, a5 as ConvertResult, a6 as ConvertFormat, a7 as OpenOptions, a8 as SaveOptions, a9 as SaveResult } from './conversion-ywYaW4Au.js';
3
- export { aa as ConvertedFile, h as ExportFormat, F as FontsHandle, O as OPFApiError, n as OPFExportError, o as OPFImportError, S as SlideSelection, U as parseSlideSelection } from './conversion-ywYaW4Au.js';
2
+ import { a3 as ConvertInput, a4 as ConvertOptions, a5 as ConvertResult, a6 as ConvertFormat, a7 as OpenOptions, a8 as SaveOptions, a9 as SaveResult } from './conversion-Bh-skyMc.js';
3
+ export { aa as ConvertedFile, h as ExportFormat, F as FontsHandle, O as OPFApiError, n as OPFExportError, o as OPFImportError, S as SlideSelection, U as parseSlideSelection } from './conversion-Bh-skyMc.js';
4
4
  export { R as RuleExplanation, e as explain } from './explain-iZeNOUc6.js';
5
5
  import { validate as validate$1 } from './validator.js';
6
6
  export { DEFAULT_VALIDATION_THRESHOLDS, OPFValidationError, assertValid, assertValidCatalogRecord, findValidationRule, validateCatalogRecord, validationRules } from './validator.js';
@@ -15,7 +15,7 @@ import { a as CatalogKind, U as UnresolvedReferenceDiagnostic, b as CatalogOptio
15
15
  export { c as CATALOG_GROUP_PATTERN, d as CATALOG_REFERENCE_PATTERN, e as CatalogRecordMap, f as CatalogRecords, g as CatalogReferenceSite, O as OPFCatalogsOptionError, h as OPFUnresolvedReferenceError, i as ResolvedReference, j as catalogGroupSource, k as catalogKinds, l as catalogRecords, m as catalogReferenceSites, p as parseReference, r as resolveReference, u as unresolvedReference } from './catalog-refs-kAefsZWT.js';
16
16
  import { a as Finding, J as JsonPatchOperation } from './finding-l9dlQ2NF.js';
17
17
  export { d as FindingCategory, e as FindingFix, f as FindingFocus, b as FindingLocation, F as FindingReport, c as FindingSeverity, g as FindingSuggestion, V as ValidationIssue } from './finding-l9dlQ2NF.js';
18
- export { a as CATALOG_SCHEMA, b as CatalogDisplayKind, C as CatalogRecordKind, c as catalogDisplayKinds, d as catalogSchemaNames } from './catalog-schemas-CC1d_h8s.js';
18
+ export { a as CATALOG_SCHEMA, b as CatalogDisplayKind, C as CatalogRecordKind, c as catalogDisplayKinds, d as catalogSchemaNames } from './catalog-schemas-LdqOPebq.js';
19
19
  import { paginate as paginate$1 } from './pagination.js';
20
20
  export { O as OPFPaginationError, P as PaginatedPage, c as PaginationMapping, d as PaginationOptions, e as PaginationResult, a as PresentationPaginationOptions, b as PresentationPaginationResult, S as SlideContext, f as SlideContextDiagnostic, g as SlideContextOptions, h as paginateSlide, r as resolveSlideContext } from './pagination-DyAKYpXu.js';
21
21
  export { ApplyPatchOptions, OPFPatchError, OPFPatchHashError, OPFPatchValidationError, PatchResult, applyPatch, documentHash, idToken, isIdToken, resolveIdPatch, resolveIdPointer, toIdPatch, toIdPointer } from './patch.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defaultCatalogs } from './chunk-Z7XNHIW7.js';
1
+ import { defaultCatalogs } from './chunk-AQEAZDL2.js';
2
2
  import { stats, embed, edit, OPFApiError, asApiError } from './chunk-I6G32TB5.js';
3
3
  export { OPFApiError, OPFExportError, OPFImportError, OPFMoveToCustomError, copySlides, fill, fillRecords, localAssetSources, moveToCustom, parseSlideSelection, sameRecord, updateFromCatalog } from './chunk-I6G32TB5.js';
4
4
  export { specFileEntries, specFileKinds, specFilePaths } from './chunk-UJSAR6GF.js';
@@ -36,7 +36,7 @@ var stats2 = (presentation2, options) => stats(presentation2, withDefaultCatalog
36
36
  var paginate2 = (input, options) => paginate(input, withDefaultCatalog(options));
37
37
  var embed2 = (document, options) => embed(document, withDefaultCatalog(options));
38
38
  var edit2 = (deck, patch, options) => edit(deck, patch, withDefaultCatalog(options));
39
- var engine = () => import('./conversion-URZHNQIU.js');
39
+ var engine = () => import('./conversion-7OPM5MMJ.js');
40
40
  var rethrow = (error, fallback) => {
41
41
  if (error instanceof OPFValidationError) throw error;
42
42
  throw asApiError(OPFApiError, error, fallback);
@@ -1,6 +1,6 @@
1
1
  export { b as DeckReport, c as DeckReportOptions, r as readDeckReport } from './deck-report-DQMsvyBI.js';
2
- import { F as FontsHandle, E as EmbeddedFace, R as Renderer, a as Reporter, P as Peer, b as PptxModule, O as OPFApiError } from './conversion-ywYaW4Au.js';
3
- export { C as ConversionPlan, D as Diagnostic, c as EXPORT_FORMATS, d as EXPORT_TEXT, e as ExportContext, f as ExportFile, g as ExportFontSummary, h as ExportFormat, i as ExportOptions, j as ExportResult, k as ExportRun, l as ExportText, I as ImportOptions, m as ImportResult, n as OPFExportError, o as OPFImportError, p as PEER_RANGES, q as PPTX_PACKAGE, r as PreparedExport, s as RENDER_EXTRAS, t as RENDER_PACKAGE, u as ReportFinding, v as ResolvedExportOptions, S as SlideSelection, w as Source, V as VERSIONS, x as asApiError, y as assetRoot, z as checkDate, A as checkOptions, B as checkRenamedOptions, G as checkScale, H as deckFileFormat, J as exportFormatOf, K as finishReport, L as importPresentation, M as loadPptx, N as loadRenderer, Q as locatePackage, T as missingPeerFrom, U as parseSlideSelection, W as planConversion, X as prepareExport, Y as reaches, Z as readInput, _ as reportThrown, $ as resolveExportOptions, a0 as runExport, a1 as runPreparedExport, a2 as writePlanned } from './conversion-ywYaW4Au.js';
2
+ import { F as FontsHandle, E as EmbeddedFace, R as Renderer, a as Reporter, P as Peer, b as PptxModule, O as OPFApiError } from './conversion-Bh-skyMc.js';
3
+ export { C as ConversionPlan, D as Diagnostic, c as EXPORT_FORMATS, d as EXPORT_TEXT, e as ExportContext, f as ExportFile, g as ExportFontSummary, h as ExportFormat, i as ExportOptions, j as ExportResult, k as ExportRun, l as ExportText, I as ImportOptions, m as ImportResult, n as OPFExportError, o as OPFImportError, p as PEER_RANGES, q as PPTX_PACKAGE, r as PreparedExport, s as RENDER_EXTRAS, t as RENDER_PACKAGE, u as ReportFinding, v as ResolvedExportOptions, S as SlideSelection, w as Source, V as VERSIONS, x as asApiError, y as assetRoot, z as checkDate, A as checkOptions, B as checkRenamedOptions, G as checkScale, H as deckFileFormat, J as exportFormatOf, K as finishReport, L as importPresentation, M as loadPptx, N as loadRenderer, Q as locatePackage, T as missingPeerFrom, U as parseSlideSelection, W as planConversion, X as prepareExport, Y as reaches, Z as readInput, _ as reportThrown, $ as resolveExportOptions, a0 as runExport, a1 as runPreparedExport, a2 as writePlanned } from './conversion-Bh-skyMc.js';
4
4
  import { Catalog } from './composition.js';
5
5
  import { Fonts } from './composition.js';
6
6
  import { PresentationPaginationResult } from './index.js';
@@ -167,10 +167,8 @@ interface MeasuringFonts {
167
167
  */
168
168
  declare function measuringFonts(deck: unknown, options: MeasuringFontsOptions): Promise<MeasuringFonts>;
169
169
 
170
- /** Why a gallery that declares `declared` cannot be the default catalog of a core that reads `reads`, or undefined when it can. */
171
- declare function galleryMismatch(declared: unknown, reads?: number): string | undefined;
172
- /** The catalogs a call registers when it names none: `[gallery]`. Throws `gallery-schema-mismatch` for a gallery of another schema. */
170
+ /** The catalogs a call registers when it names none: `[gallery]`. */
173
171
  declare function defaultCatalogs(): readonly Catalog[];
174
172
 
175
- export { FontsHandle, OPFApiError, Peer, PptxModule, Renderer, Reporter, checkDestinations, checkOutputPattern, createZip, deckStem, defaultCatalogs, embeddedFor, galleryMismatch, leaseSharedFonts, listFontDirectories, localAssetProblems, measuringFonts, padNumber, paginateDeck, pointerOf, prepareFonts, publishNew, runImport, stemOf, writeFiles };
173
+ export { FontsHandle, OPFApiError, Peer, PptxModule, Renderer, Reporter, checkDestinations, checkOutputPattern, createZip, deckStem, defaultCatalogs, embeddedFor, leaseSharedFonts, listFontDirectories, localAssetProblems, measuringFonts, padNumber, paginateDeck, pointerOf, prepareFonts, publishNew, runImport, stemOf, writeFiles };
176
174
  export type { AssetProblem, ImportRun, MeasuringFonts, MeasuringFontsOptions, PaginateDeckOptions, PaginatedDeck, PlannedFile };
@@ -1,7 +1,7 @@
1
- import { loadRenderer, listFontDirectories, leaseSharedFonts, prepareFonts } from './chunk-TML2473C.js';
2
- export { EXPORT_FORMATS, EXPORT_TEXT, PEER_RANGES, PPTX_PACKAGE, RENDER_EXTRAS, RENDER_PACKAGE, Reporter, VERSIONS, assetRoot, checkDate, checkDestinations, checkOptions, checkOutputPattern, checkRenamedOptions, checkScale, createZip, deckFileFormat, deckStem, embeddedFor, exportFormatOf, finishReport, importPresentation, leaseSharedFonts, listFontDirectories, loadPptx, loadRenderer, localAssetProblems, locatePackage, missingPeerFrom, padNumber, planConversion, pointerOf, prepareExport, prepareFonts, publishNew, reaches, readInput, reportThrown, resolveExportOptions, runExport, runImport, runPreparedExport, stemOf, writeFiles, writePlanned } from './chunk-TML2473C.js';
3
- import { defaultCatalogs } from './chunk-Z7XNHIW7.js';
4
- export { defaultCatalogs, galleryMismatch } from './chunk-Z7XNHIW7.js';
1
+ import { loadRenderer, listFontDirectories, leaseSharedFonts, prepareFonts } from './chunk-XSCZZWYI.js';
2
+ export { EXPORT_FORMATS, EXPORT_TEXT, PEER_RANGES, PPTX_PACKAGE, RENDER_EXTRAS, RENDER_PACKAGE, Reporter, VERSIONS, assetRoot, checkDate, checkDestinations, checkOptions, checkOutputPattern, checkRenamedOptions, checkScale, createZip, deckFileFormat, deckStem, embeddedFor, exportFormatOf, finishReport, importPresentation, leaseSharedFonts, listFontDirectories, loadPptx, loadRenderer, localAssetProblems, locatePackage, missingPeerFrom, padNumber, planConversion, pointerOf, prepareExport, prepareFonts, publishNew, reaches, readInput, reportThrown, resolveExportOptions, runExport, runImport, runPreparedExport, stemOf, writeFiles, writePlanned } from './chunk-XSCZZWYI.js';
3
+ import { defaultCatalogs } from './chunk-AQEAZDL2.js';
4
+ export { defaultCatalogs } from './chunk-AQEAZDL2.js';
5
5
  import { OPFApiError } from './chunk-I6G32TB5.js';
6
6
  export { OPFApiError, OPFExportError, OPFImportError, asApiError, parseSlideSelection } from './chunk-I6G32TB5.js';
7
7
  import './chunk-UJSAR6GF.js';
package/dist/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CatalogRecordKind } from './catalog-schemas-CC1d_h8s.js';
1
+ import { C as CatalogRecordKind } from './catalog-schemas-LdqOPebq.js';
2
2
  import { SchemaName } from './schemas.js';
3
3
  import './catalog-refs-kAefsZWT.js';
4
4
  import './json-BFSdESOE.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf",
3
- "version": "0.29.1",
3
+ "version": "0.31.0",
4
4
  "description": "Canonical Open Presentation Format schemas, TypeScript types, local validation and composition; in Node, the file API and the CLI defaults register the pptx.gallery catalog of @openpresentation/gallery.",
5
5
  "license": "MIT",
6
6
  "author": "Open Presentation",
@@ -164,7 +164,7 @@
164
164
  "ajv-formats": "^3.0.1",
165
165
  "jsonc-parser": "3.3.1",
166
166
  "yaml": "^2.9.1",
167
- "@openpresentation/gallery": "^0.29.1"
167
+ "@openpresentation/gallery": "^0.31.0"
168
168
  },
169
169
  "devDependencies": {
170
170
  "@expo-google-fonts/arimo": "0.4.3",
@@ -185,8 +185,8 @@
185
185
  "workerd": "1.20261023.1"
186
186
  },
187
187
  "peerDependencies": {
188
- "@openpresentation/opf-pptx": "^0.29.1",
189
- "@openpresentation/opf-render": "^0.29.1"
188
+ "@openpresentation/opf-pptx": "^0.31.0",
189
+ "@openpresentation/opf-render": "^0.31.0"
190
190
  },
191
191
  "peerDependenciesMeta": {
192
192
  "@openpresentation/opf-pptx": {
@@ -207,5 +207,5 @@
207
207
  "test:packed": "node test/packed-install-smoke.mjs",
208
208
  "pack:dry-run": "pnpm run build:deps && pnpm run build && npm pack --dry-run --cache /tmp/opf-npm-cache"
209
209
  },
210
- "gitHead": "e9f46c24606ce0f474347b984eacea5dce7fd839"
210
+ "gitHead": "d68561fad7b2b319cd7cdb8d7e75fefdd1dffa4c"
211
211
  }
@@ -1,16 +0,0 @@
1
- import { OPFApiError } from './chunk-I6G32TB5.js';
2
- import { CATALOG_SCHEMA } from './chunk-M7YZU3DX.js';
3
- import { CATALOG_SCHEMA as CATALOG_SCHEMA$1, gallery } from '@openpresentation/gallery';
4
-
5
- function galleryMismatch(declared, reads = CATALOG_SCHEMA) {
6
- if (declared === reads) return void 0;
7
- return `@openpresentation/gallery targets catalog schema ${JSON.stringify(declared)}, and this @openpresentation/opf reads catalog schema ${reads}. Install the gallery release for schema ${reads} (the range in @openpresentation/opf's dependencies), or pass catalogs explicitly.`;
8
- }
9
- var mismatch = galleryMismatch(CATALOG_SCHEMA$1);
10
- var DEFAULTS = Object.freeze([gallery]);
11
- function defaultCatalogs() {
12
- if (mismatch) throw new OPFApiError(mismatch, "gallery-schema-mismatch", { details: { gallery: CATALOG_SCHEMA$1, core: CATALOG_SCHEMA } });
13
- return DEFAULTS;
14
- }
15
-
16
- export { defaultCatalogs, galleryMismatch };