@openpresentation/opf

中文文档
0.30.0发布于 2026/11/02MITTypeScriptES Module构建来源已验证

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.

原始使用文档
文档目录17 节
  1. File naming
  2. Install
  3. Usage
  4. Facts, slide context and the fonts handle
  5. Content conversions
  6. Markdown and outlines
  7. Reading and writing a deck in any form
  8. OPF as YAML
  9. Source editing: schema field queries, text addressing and s…
  10. Validate (0.14.0)
  11. Patch, diff, merge and format (0.12.0)
  12. Ids, document hash and patch scope (CE-04, CE-05)
  13. Layout previews
  14. Example decks
  15. Documentation pages
  16. Upstream README
  17. Development

@openpresentation/opf

Canonical Open Presentation Format package for JavaScript and TypeScript.

Publishes the OPF schemas, catalog presets, raw spec files, generated TypeScript types, examples, docs, and local validation helpers. The schema is pre-stable (0.x — expect breaking changes between minor versions until 1.0). This package does not render PowerPoint files, parse .pptx, fetch remote catalogs, call hosted APIs, provide telemetry, or use AI.

The canonical npm package remains @openpresentation/opf; a separate @openpresentation/opf-spec package is not used for v0.2.0 so existing downstream imports stay stable. The packed npm artifact includes package-addressable OPF schemas, catalogs, reference files, and an optional downstream-service reference openapi.yaml under spec/.

Repository: https://github.com/OpenPresentation/opf

File naming

Use *.opf.json for complete Open Presentation Format documents, such as deck.opf.json.

OPF documents are plain JSON, and the .opf.json suffix keeps that visible to editors, validators, agents, and repository tooling. Avoid bare *.opf for OPF JSON because that extension is already used by other document and project formats.

Install

pnpm add @openpresentation/opf
# or: npm install @openpresentation/opf
# or: yarn add @openpresentation/opf

Requires Node 22 or later (engines.node >=22) from the release after 0.12.1. Versions 0.10.0 to 0.12.1 declared 24.x, so npm on Node 22 or 26 silently installed an older release that matched (RR-20). Earlier published versions retain their recorded runtime requirements.

Usage

Use the common root API for most application code:

import { presentation, validate, embed } from "@openpresentation/opf";
import { gallery } from "@openpresentation/gallery";

import type { Presentation } from "@openpresentation/opf";

const deck: Presentation = {
  name: "Quarterly Review",
  slides: [{ title: "Quarterly Review", items: ["Revenue", "Product", "Hiring"] }],
};

console.log(presentation.$id);
// Register the catalogs your host trusts; core ships no records. In Node, validate, stats, paginate, embed and edit
// register the gallery (@openpresentation/gallery, a dependency of core) when a call names none.
const catalogs = [gallery];
console.log(validate({ ...deck, design: { theme: "classic" } }, { catalogs }).valid);
// Save with every record the deck uses embedded, so it renders the same with no catalog registered.
const saved = embed({ ...deck, design: { theme: "classic" } }, { catalogs }).document;
console.log(Object.keys(saved.catalogs.default.themes)); // ["classic"]
console.log(Object.keys(gallery.tones));

In Node (and Bun and Deno), the root adds files: convert turns one file into another (a deck to PDF, PNG, SVG or PPTX, a PowerPoint file to a deck, one deck form to another, or bytes with no output path), open reads a deck file or imports a .pptx, and save writes one. The same import works in every runtime, so one namespace import covers an application:

import * as opf from "@openpresentation/opf";

await opf.convert("deck.opf.md", "deck.pdf");
const deck = await opf.open("deck.opf.md");
deck.slides.push({ title: "Q4" });
await opf.save(deck, "deck.opf.md");
const { files } = await opf.convert(deck, { format: "pptx" }); // bytes, nothing written

Drawing and PowerPoint go through the optional peers @openpresentation/opf-render and @openpresentation/opf-pptx, which core loads the first time a call needs them; npm does not install them for you. The root has conditional exports (OPF 0.18 removed /node): node, bun and deno get this full build, and browser, worker, workerd and default get a browser-safe build with the same names and the same types, where open, save and convert reject with OPFApiError code node-only. Every other subpath runs in a browser. parseSlideSelection(3 | "1-3" | [1, 3], total) reads slide selections in both. See OPF files in Node.

On Cloudflare Workers, Vercel Edge and other hosts that refuse code generation from strings, validate runs validators compiled at build time instead of compiling the schemas with Ajv. The workerd, worker and edge-light conditions choose them, so a Worker needs no alias and no option; code bundled for Node that runs on a worker (and a page under a strict Content-Security-Policy) imports @openpresentation/opf/precompiled once. The verdicts and findings are the same, and a CI lane runs the package in the real workerd runtime. See Edge runtimes.

Use focused imports when you only need one surface:

import { presentation, audience } from "@openpresentation/opf/schemas";
import { specFileEntries } from "@openpresentation/opf/spec-files";
import { validate, assertValid, validationRules } from "@openpresentation/opf/validator";
import type { Presentation, Audience, Tone } from "@openpresentation/opf/types";

The root entry holds the application-level API: schemas, catalog references and their resolution (resolveReference, embed, copySlides, updateFromCatalog), validation, variables, pagination, data helpers, font policy, stats, resolveSlideContext and resolveSlideVariables. Engines compose and draw context.slide from resolveSlideContext(deck, index, { slideNumber, slideCount }): the slide with the slide-scoped built-ins {{slide.number}}, {{slide.section}} and {{deck.slideCount}} substituted for the same numbers its options carry (resolveSlideVariables(slide, { slideNumber, slideCount }) does that substitution alone). Header and footer text is substituted by layoutFurniture, which records each slide number in FurnitureTextPart.fields. See templates and variables. It carries no catalog records: the pptx.gallery catalog is the package @openpresentation/gallery (gallery, catalogDisplay, and the layout previews of @openpresentation/gallery/previews), a dependency of core released on its own version line, which the Node build registers when a call names no catalogs (OPF 0.19 removed @openpresentation/opf/catalog and defaultCatalog, with no alias). Every entry point that resolves references takes { catalogs }, the catalogs the host registered; the first one is the default for documents that omit catalogs.default. See catalogs and the default catalog.

The layout engine's names are not on the root. composeSlide, fitText, layoutQuote and the other layoutX/fitX functions, numbering, footnotes and citations, colour contrast, code syntax, pattern fills, metric trends, chart options, script fonts and text direction come from @openpresentation/opf/composition; the symbol-font tables come from @openpresentation/opf/symbol-font-encodings.

Facts, slide context and the fonts handle

stats(presentation, options?) reports neutral facts about a deck (structure, words, notes, images, charts, tables, datasets, citations, variables, assets, fonts) without composing, measuring or validating; see the stats guide. resolveSlideContext(presentation, index, { fonts, catalogs }) resolves one slide's canvas, layout, theme and font families (slide design, then deck design, then theme, then the engine defaults) into the ComposeSlideOptions that composeSlide takes, with one unresolved-reference diagnostic per reference that resolves nowhere (strictReferences: true throws OPFUnresolvedReferenceError instead). Deck-level verbs take { fonts }, a handle whose textMeasurement is how wide the host's real fonts draw text (paginate(deck, { fonts })); the renderer's font loader returns a richer handle that extends it. composeSlide itself is engine-level and takes textMeasurement and fontFamilies ({ heading, body, code, accent? }) directly.

Content conversions

@openpresentation/opf/convert converts one payload between text, list, quote, metric, code, timeline, chart, table and a group of metric blocks (convertContent, contentConversionTargets), nests list items, groups blocks, moves an image between the content and the slide design, and splits or merges slides (splitSlide, splitSlideOnOverflow, mergeSlides, unpaginate). Every function is pure, never invents content, reports lossless and loss, refuses with a reason and validates its output. See the conversions guide. Not in releases before the one that lists it in the changelog.

Markdown and outlines

@openpresentation/opf/markdown reads and writes a deck as Markdown in a small documented dialect (YAML front matter, --- between slides, # title, ## subtitle, lists, quotes, tables, images, chart, metric and timeline fences, Note: speaker notes). fromMarkdown(markdown, { split, defaults, validate }) returns { presentation, valid, findings, counts } with findings (the shared Finding format) that carry the line and column of the Markdown; toMarkdown(presentation, { unsupported }) writes any valid deck, embedding what the dialect has no syntax for as YAML (or dropping it into report.loss), and the Markdown it writes converts back to the same deck. Deterministic and offline. Reads front matter with the yaml package. See the Markdown guide. Not in releases before the one that lists it in the changelog.

Reading and writing a deck in any form

parse(text, { format, filename, catalogs }) and stringify(presentation, { format | filename }) (the root and @openpresentation/opf/deck) read and write a deck as JSON (canonical), YAML or Markdown. The format is the format option, else the file name (.opf.md is Markdown, .yaml and .yml are YAML, anything else JSON; a plain .md is not a deck), and never the content. parse returns the presentation and throws OPFValidationError for a syntax, schema or reference error; the error's report.findings are located by line and column in the text (opf/json-syntax, yaml/<rule> or markdown/<rule> for syntax errors). Warnings do not throw: validate(text) returns the full report. Both run in a browser; files are the Node build's job (above).

OPF as YAML

@openpresentation/opf/yaml reads and writes a deck as YAML, the authoring form of the same data (JSON stays canonical). fromYaml(text, { aliases, validate }) reads strict JSON-compatible YAML 1.2 (one document, a mapping at the root, no custom tags, duplicate keys or non-finite numbers; anchors, aliases and merge keys only with aliases: true) and returns { presentation, valid, findings, counts, schemaValid, checks }: the validate report of the deck, with every finding (the shared Finding format, YAML syntax errors as yaml/<rule> findings) located at the line and column of the YAML. toYaml(presentation, { schemaComment }) writes canonical YAML (schema key order, block style, ambiguous strings quoted) that reads back to the same deck, and throws OPFYamlError for an invalid one. parseYamlData and scanYamlComments are the helpers. See the YAML guide.

Source editing: schema field queries, text addressing and syntax recovery

Two browser-safe subpaths for source editors, the CLI and agents. @openpresentation/opf/schema reads the field model of a document from the JSON Schemas: describeField(path, document?) returns the types, description, enum values, default, examples, whether the parent requires it, the oneOf variants (and primary, the form a template should use), the ranked children of an object (required first, then the curated COMMON_KEYS order, then alphabetical; promoted regions, extensions and $schema are advanced), the item schema of an array (itemInfo) or map (entryInfo), requiredChildren and freeText, and { known: false } for a path the schema does not describe; catalogKindAt(path, document?) names the catalog a reference field points into (layouts, themes, colorSchemes, fontSchemes, narratives, audiences, purposes, tones) from schemaCatalogKinds (also exported), core-side metadata keyed by schema name and JSON Pointer; the published schemas carry no vendor keyword and compile under strict Ajv, so a new reference field is one entry there and needs no editor change. The navigation helpers it is built on are exported too (resolveSchema, schemaVariants, activeSchema, schemaAtPath, createSchemaValue, listSchemaFields, splitOpfPath). @openpresentation/opf/source turns text into positions and back: locate(text, format, pointer, { part }) is the range of a JSON Pointer in JSON, YAML or Markdown (value, key, entry, open or colon; the nearest ancestor with exact: false when the node is missing; a Markdown slide field is located by the same ranges its findings use: the layout= token of an options comment, the text after # or - ), pathAt(text, format, offset) the path under a caret, scanMarkdown(text) the regions of a Markdown deck (front matter, slides, option comments, fences, notes) with the dialect's vocabulary (SLIDE_OPTION_KEYS, BLOCK_OPTION_KEYS, FENCE_KINDS, METRIC_KEYS), and analyzeJsonSyntax(text, { limit }) the root cause of each JSON syntax error, once, in position order (at most five), with a range, a message, a hint and the fixes that repair it (Insert comma, Add { }, Remove "design", Insert }, Use double quotes, ...); validate(text) reports the same causes as opf/json-syntax findings. See validate.

Validate (0.14.0)

validate(input, options?) is the one checker, from the root and from @openpresentation/opf/validator. input is a parsed presentation or strict JSON text; the result is a ValidationReport whose findings (the shared Finding format, spec/schemas/finding.schema.json) each have a stable opf/<rule> id, a severity and one of six categories: format (JSON syntax, duplicate keys, schema), references (catalog references, assets, citations, datasets), policy (host contracts), accessibility (contrast, alt text, reading order, links), layout (overflow, type size, image resolution, fonts) and content (placeholders, empty slides, non-numeric chart cells). valid means no finding has severity error, which by default only format, references and policy findings can have. Text input adds line and column; only, ignore and severity pick and promote rules or categories; composition is lazy, so validate(deck, { only: ["format"] }) costs what a schema check costs. Read-only and deterministic: no remote resources are fetched. See the validate guide. A clean report does not certify layout, fonts, or native export fidelity.

describeFinding(finding, { document }) and explain(ruleId) put findings in plain words. describeFinding (from @openpresentation/opf/source) returns { message, next } for any finding: what is wrong with the field in backticks, and what could come next ("did you mean center?"), written from the finding's own data and the document; every opf/* rule has its text. explain (also on the package root) returns a rule's summary, rationale, fix and a small before/after example. See Plain-words text and explain.

Patch, diff, merge and format (0.12.0)

Added in 0.12.0 (not in 0.11.4 or earlier): @openpresentation/opf/patch is the one RFC 6902 implementation the CLI and the editor share (applyPatch, applyPatchWithInverse, invertPatch, strict pointer helpers, optional schema validation of the result); @openpresentation/opf/diff has diff (slide matching by id, content and similarity, moves, a readable report and a JSON Patch) and merge (three-way merge with conflict objects that never drop a side); @openpresentation/opf/format has format (canonical key order and layout, idempotent). See the guide.

Ids, document hash and patch scope (CE-04, CE-05)

@openpresentation/opf/ids (all of it also on the package root, the pointer and hash parts also on /patch) makes edits addressable and checkable. assignIds(document, { generate? }) fills the missing id of slides and content payloads deterministically ({ document, patch, assigned }; existing ids are never changed). In an array, the pointer token @intro names the element whose id is intro, so /slides/@intro/blocks/@kpi/text survives reordering: applyPatch and readPointer resolve it, resolveIdPointer and toIdPointer convert a pointer, resolveIdPatch and toIdPatch a whole patch. documentHash(document) is sha256: and a SHA-256 of the key-sorted JSON (pure JavaScript, the same in Node, browsers and edge runtimes); applyPatch(document, patch, { expectHash }) and edit(deck, patch, { expectHash }) apply only to the document that hash was taken from, else throw OPFPatchHashError and change nothing. touches(patch, document?) returns the pointers (index and id form), ids, slide ids and kinds a patch reads and writes, for conflict detection and permission checks. See the guide.

Layout previews

@openpresentation/gallery/previews ships pre-rendered SVG thumbnails for the slide layouts catalogued at pptx.gallery. Each preview is a self-contained SVG sized to fill a 16:9 container, with its colors and layout already rendered.

import { getLayoutPreview } from "@openpresentation/gallery/previews";

export function LayoutThumbnail({ slug }: { slug: string }) {
  const html = getLayoutPreview(slug);
  if (!html) return null;
  return (
    <div
      className="aspect-[16/9] overflow-hidden rounded-lg border border-border bg-card"
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}

The preview source lives in packages/gallery of this repository, which the checks read.

Example decks

@openpresentation/opf/examples ships every .opf.json file from examples/ in the upstream repo, already parsed and validated against the presentation schema at build time.

import {
  examples,
  galleries,
  getExample,
  getExamplesByGallery,
} from "@openpresentation/opf/examples";

const compliance = getExample("compliance-readiness-review");
const businessDecks = getExamplesByGallery("business-functions");

console.log(`${examples.length} example decks across ${galleries.length} galleries`);

Each ExampleRecord includes the parsed Presentation object plus its repo-relative path, top-level category (gallery, technical, …), and gallery slug when applicable.

Documentation pages

@openpresentation/opf/docs ships the top-level docs/*.md reference pages (schema-reference, catalog-schema-reference, content-payloads, content-item-design-overrides, examples). Subdirectories like docs/migrations and docs/plans are intentionally excluded — those move too quickly to ship inside a pinned npm release.

import { docs, getDoc } from "@openpresentation/opf/docs";

for (const doc of docs) {
  console.log(`${doc.title} — ${doc.file}`);
}

const schemaRef = getDoc("schema-reference");
console.log(schemaRef?.markdown.slice(0, 200));

Upstream README

@openpresentation/opf/repo-readme exposes the raw markdown of the upstream OpenPresentation/opf README.md at the version pinned by this release. Use it when you want to mirror the canonical README inside another site or app without doing a network fetch.

import { repoReadme } from "@openpresentation/opf/repo-readme";

console.log(repoReadme.split("\n").slice(0, 3).join("\n"));

Validation reports carry findings. A finding with severity error is a structural problem that makes valid false; warning and info findings are advisory, such as a narrative, design or layout reference that resolves nowhere (opf/unresolved-reference) or a missing alt text. A reference resolves in the records the document embeds (catalogs.custom, catalogs.default, named groups), then in the catalogs the host registered with { catalogs }.

const report = validate(deck, { catalogs: [gallery] });
if (!report.valid) console.error(report.findings.filter((finding) => finding.severity === "error"));
for (const finding of report.findings) console.warn(finding.ruleId, finding.path, finding.message);

Validate catalog records locally:

import { validateCatalogRecord } from "@openpresentation/opf";
import { gallery } from "@openpresentation/gallery";

for (const [id, record] of Object.entries(gallery.audiences)) {
  // A published record file carries its $schema and id; the gallery keys records by id without them.
  const result = validateCatalogRecord("audiences", { $schema: "https://openpresentation.org/schema/opf-audience/v1", id, ...record });
  if (!result.valid) {
    console.error(result.findings);
  }
}

Raw canonical JSON is published under spec/:

import presentationSchema from "@openpresentation/opf/spec/schemas/opf.schema.json" with {
  type: "json",
};

The raw spec manifest exposes typed package paths for files that should be resolved from npm instead of GitHub. openapi.yaml is a reference contract for downstream services that choose to expose OPF over HTTP; OpenPresentation does not host that API.

import { specFileEntries } from "@openpresentation/opf/spec-files";

const openApi = specFileEntries.find((entry) => entry.path === "openapi.yaml");
console.log(openApi?.packagePath);

Core ships no catalog record files (RR-78): the records are @openpresentation/gallery's, and pptx.gallery serves each published record file at https://www.pptx.gallery/<kind>/<id>.json. Documents never point at them. A host registers the catalog instead:

import { gallery } from "@openpresentation/gallery";
validate(deck, { catalogs: [gallery] });

Development

pnpm --filter @openpresentation/opf typecheck
pnpm --filter @openpresentation/opf test
pnpm --filter @openpresentation/opf pack:dry-run

src/generated/ and dist/ are generated from the root spec/ directory and are intentionally ignored by git.

版本与发布记录

@openpresentation/opf 共发布 60 个版本,首次发布于 2026/05/06;过去 12 个月发布了 60 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.30.0latest2026/11/021 天前
0.29.12026/10/276 天前
0.29.02026/10/277 天前
0.28.02026/10/277 天前
0.27.02026/10/268 天前
0.26.102026/10/259 天前

其他发布通道:next → 0.26.0-rc.0。

关于 @openpresentation/opf 的常见问题

@openpresentation/opf 是什么?

规范开放演示格式模式、TypeScript类型、本地验证和组合;在Node中,文件API和CLI默认注册pptx。@openpresentation/ Gallery的画廊目录。

如何安装 @openpresentation/opf?

在项目目录运行 npm install @openpresentation/opf,或使用 pnpm add / yarn add / bun add。

@openpresentation/opf 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@openpresentation/opf 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@openpresentation/opf 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@openpresentation/opf 可以商用吗?

@openpresentation/opf 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@openpresentation/opf 还在维护吗?

最新版本 0.30.0 发布于 2026/11/02(1 天前);至今共发布 60 个版本,其中过去 12 个月发布了 60 个;首次发布于 2026/05/06。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。