YAML authoring
@openpresentation/opf/yaml (RR-56) reads and writes an OPF deck as YAML, in both directions, with no renderer, fonts, network or model: the same input always gives the same output. The CLI exposes it through opf convert (deck.opf.yaml to deck.opf.json and back; opf from-yaml and opf to-yaml before 0.18), and every command that reads or writes a deck understands .opf.yaml files (and, the same way, .opf.md Markdown decks); parse and stringify read and write a deck in JSON, YAML or Markdown with one call, and open and save of @openpresentation/opf read and write the files in Node.
- JSON stays canonical. The schema, the interchange form and every package describe the JSON. A
.opf.yamlfile is an authoring serialization of the same data: the parsed YAML is exactly the JSON document, and the schema validates that parsed data. - Write a deck by hand. YAML has no braces, no quotes around keys and no trailing-comma errors, and a block scalar (
text: |) keeps a paragraph readable. Comments are allowed in the file (they are not data). - One strict dialect. JSON-compatible YAML 1.2, core schema. Anything that could mean something else in another reader, or cannot exist in JSON, is an error with a line and a column.
- Round trip.
toYamlwrites canonical text thatfromYamlreads back to the same deck, and that converts back to itself byte for byte. Every one of the 127 example decks round-trips, JSON to YAML to JSON and YAML to JSON to YAML. - Errors have places. Findings use the shared Finding format of validate and carry the source offset, length, line and column of the YAML that caused them, including the OPF findings mapped back to the YAML of the field they name.
import { fromYaml, toYaml } from "@openpresentation/opf/yaml";
const { presentation, valid, findings } = fromYaml(text);
const { yaml } = toYaml(presentation, { schemaComment: true });opf convert deck.opf.yaml deck.opf.json
opf convert deck.opf.json deck.opf.yaml --schema-comment
opf validate deck.opf.yaml
opf convert deck.opf.yaml slides/deck.svgAn example
# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1
$schema: https://openpresentation.org/schema/opf/v1
name: Q3 Business Review
language: en-US
slides:
- id: cover
layout: cover
title: Q3 Business Review
subtitle: Operations and growth
notes: Two minutes on the agenda, then straight into the numbers.
- title: Revenue grew every quarter
blocks:
- chart:
type: column
data:
columns:
- Quarter
- Revenue
rows:
- - Q1
- 12
- - Q2
- 18
- quote:
text: We would rather spend a week on capacity than a month on an outage.
attribution: Priya Raman, Head of Platformis the same deck as the JSON it converts to: name, language and slides with the values above. cover is a 0.19 intent layout. To pin a block to a named area, write region: <name> on that block after reading the selected layout's regions; layout: auto explicitly asks for automatic composition. A comment and the first line are not data.
The dialect
fromYaml accepts exactly this, and nothing else:
| Rule | What happens otherwise |
|---|---|
| One document, with a mapping at the root | yaml/empty (nothing, or only comments), yaml/multiple-documents (a --- separates two), yaml/not-mapping (a list or a bare value) |
| YAML 1.2 core schema | 2026-10-01, yes, no, on, off and 1_000 stay strings: only true/false, null/~ and numbers (decimal, 0x1F, 0o17, exponents) are typed. Quote a value to force text |
| No duplicate keys | yaml/duplicate-key, at the repeated key |
| Keys are strings | yaml/key for 1:, true:, null: or a collection used as a key. Quote it: "1": |
| No custom tags | yaml/tag for !custom, !!binary, !!set and the like. The core tags !!str, !!int, !!float, !!bool, !!null, !!map and !!seq are allowed |
| Finite numbers only | yaml/number for .inf, -.inf and .nan, with the hint to quote them |
| No anchors, aliases or merge keys | yaml/alias, with help that points at aliases: true |
| Syntax | yaml/syntax for anything the parser rejects, at the position it reports |
A leading byte order mark and CRLF line endings are accepted. Offsets are UTF-16 units into the text you passed (a BOM counts as one); lines and columns are one-based and ignore the BOM. After the YAML parses, the deck goes through validate and each finding (opf/... rule ids) is located at the YAML node of the field it names: the value for a scalar, the key through the value for a mapping or a list, and the nearest existing ancestor for something that is missing.
aliases: true
Anchors (&name), aliases (*name) and the << merge key are valid YAML and handy for repeated values, but they are not JSON, many tools handle them differently, and an alias hides the size of what it expands to. They are refused by default. fromYaml(text, { aliases: true }) (CLI: opf convert deck.opf.yaml deck.opf.json --aliases) expands them to plain data:
slides:
- title: One
extensions:
owner: &owner { team: ops, oncall: true }
- title: Two
extensions:
owner: *owner
other:
<<: *owner
extra: 1The expansion is capped by the yaml package's maxAliasCount (100; each alias counts the nodes it expands to, so nested aliases that multiply exponentially are refused with yaml/alias-limit), and an alias that sits inside its own anchor is yaml/alias-cycle. What comes out is always finite, tree-shaped JSON data, and keys, tags and numbers are checked exactly as without the option. Writing never produces anchors or aliases.
Writing OPF as YAML
toYaml(presentation, { schemaComment }) first validates the presentation (OPFYamlError, code: "invalid-document", when it is not valid OPF), then writes the canonical form:
- keys in the order of the schema (the same order as
opf format):$schema,name, ...,slides,assets, ..., with keys the schema does not declare after them in their own order; - two-space indentation, block style throughout, no line folding (
lineWidth: 0), a block scalar (|) for text with line breaks, one trailing newline, LF line endings; - strings that would read as another type, or as another type in a YAML 1.1 reader, are quoted:
"yes","No","on","2026-10-01","123","null","0x1F","12:30", and any string with leading or trailing spaces, a#or:. Mapping keys such as"yes":follow the same rule; - the text is read back before it is returned: it is parsed again in the strict dialect and compared with the input, key order included. If it does not read back to the same data,
toYamlthrowsOPFYamlErrorwithcode: "not-representable"instead of writing something wrong (for example a lone surrogate in a string).
schemaComment: true starts the file with # yaml-language-server: $schema=<url>, the deck's own $schema or else the canonical https://openpresentation.org/schema/opf/v1.
Editor autocomplete
Editors that use the YAML language server (VS Code with the Red Hat YAML extension, Neovim, Zed, JetBrains) read the modeline on the first line and validate and complete the file against the schema:
# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1opf convert deck.opf.json deck.opf.yaml --schema-comment (and opf create --schema-comment) writes it. A command that rewrites a YAML file keeps the file's own first-line modeline verbatim (whatever URL it names, even a local schema path) and generates one only for --schema-comment. It is a comment: fromYaml ignores it.
Other exports
These three exports are not part of the fromYaml/toYaml pair. They exist so the CLI and other tools can reuse the reader.
| Export | Purpose |
|---|---|
parseYamlData(yaml, options) | The same strict dialect for any JSON-compatible root, for files that are not decks (a JSON Patch, a settings file). { value, valid, findings } |
scanYamlComments(yaml) | { count, modeline? }: the comments a rewrite would lose, and the editor modeline (the first-line # yaml-language-server: comment, verbatim) |
YAML_SCHEMA_URL | The canonical schema URL |
Findings
fromYaml(text, options) never throws for malformed content. It returns the deck and the validate report of it, { presentation, valid, findings, counts, schemaValid, checks } (plus template and unfilledVariables for a deck with content variables), so a YAML file reports exactly what the same deck as JSON text does. presentation is a best effort when valid is false (an empty object after a YAML error). Each finding is a Finding (ruleId, severity, category, path, scope, message, help) plus location (offset, length, line, column). Options: validate (true, the default, checks format and references; validate options pick other rules or categories, and {} runs every rule; false skips the check, so schemaValid is null) and aliases (default false).
YAML errors are format findings: yaml/syntax, yaml/empty, yaml/not-mapping, yaml/multiple-documents, yaml/duplicate-key, yaml/key, yaml/tag, yaml/number, yaml/alias, yaml/alias-limit, yaml/alias-cycle. After one, no OPF check runs (schemaValid: null). Ids starting opf/ are the validate findings of the deck, located in the YAML.
Command line
opf convert <deck.opf.yaml|-> <output.opf.json|-> [--from yaml] [--to json] [--aliases] [--force] [--fail-on <level>]
opf convert <deck.opf.json|-> <output.opf.yaml|-> [--to yaml] [--schema-comment] [--force] [--fail-on <level>]Output - writes stdout (with --to) and the JSON report goes to stderr. Exit codes: 0 success, 1 a deck that is not valid OPF, an output conflict or a finding at or above --fail-on, 2 usage, a YAML syntax error (with the located findings), a read error or I/O. Nothing is written on a failure. --aliases applies to a YAML input and --schema-comment to a YAML output; anywhere else they are usage errors. opf from-yaml and opf to-yaml were removed in 0.18.
YAML everywhere
Every command that reads a deck or a JSON Patch reads YAML too, through one reader:
- A file whose name ends
.yamlor.ymlis YAML, parsed in the strict dialect (no aliases). A name ending.opf.mdis a Markdown deck, read the same way. - Anything else (stdin and every other name) is JSON when the text starts with
{or[; otherwise--from yaml|md(--from json) says what it is, and without it the command exits 2 and asks for it.--fromapplies to every deck input of the command and wins over a name. - A YAML syntax or dialect error exits 2, like invalid JSON, and the message carries the position:
Invalid YAML in deck.opf.yaml at line 5, column 5: Map keys must be unique [yaml/duplicate-key], with the locatedfindingsandcode: "invalid-yaml"in the JSON error report.opf validateinstead reports a YAML error as ayaml/<rule>finding and exits 1, as it does for JSON that does not parse. A deck with a syntax-valid but invalid OPF structure is reported by the command as it is for JSON (validateandconvertlocate their findings in the YAML).
A deck is written as YAML when the output name ends .yaml/.yml or --to yaml is given (--to json forces JSON, --to md writes a Markdown deck). When the output is stdout or has another name, the format of the deck that was read is used (a YAML deck is edited, merged, filled, paginated and embedded back to YAML); create has no deck input and defaults to JSON. opf format canonicalizes a YAML file in place of the form it has (-i, --check); opf convert changes forms.
Left JSON-only on purpose: validate --config (a settings file), the data of fill (CSV, TSV or JSON, not a deck), the ingest source file, and --signals output. In fill and ingest, --data-format csv|tsv|json names the data format and --to yaml selects the output.
Comments are not preserved. A command that rewrites a YAML file (edit, format, merge, paginate, bundle, fill, ingest --into, to-yaml) writes the canonical text of the data, so comments and the original quoting and layout are lost. The CLI prints warning: <file> has N comments; comments are not preserved when OPF rewrites a YAML file. on stderr, and does not fail. The # yaml-language-server: line is kept.
Decisions and limits
These choices are the defaults of the module and can be changed.
- JSON stays canonical and the interchange form. The schemas, the catalogs, the reports, the renderer, the editor and the PPTX converter all take the JSON document; YAML is converted at the edge, by
fromYamlandtoYaml, andopf convert deck.opf.yaml deck.opf.jsongives you the JSON. Nothing in OPF depends on a YAML library at run time except this subpath and the Markdown front matter. - Comments are not preserved by commands that rewrite. The YAML is parsed to plain data and written again from the data, which is what makes the output canonical and the same deck always the same bytes. Preserving comments would need a lossless syntax tree through every edit and would make output depend on the history of the file. Use an editor to change a file that you annotate heavily, and keep the notes in
description,notesorextensionswhen they should survive. - Strict by default. Anchors, aliases, merge keys, custom tags, duplicate keys, non-string keys and non-finite numbers are where YAML readers disagree (YAML 1.1 and 1.2,
<<,on/yes, sexagesimal numbers) or where a document stops being JSON. A deck should mean the same thing in every tool, so the reader accepts the intersection and says where the text leaves it.aliases: trueopts into expansion for files that already use it. - pptx.dev's playground uses
aliases: trueso that YAML pasted into it from other sources keeps working; the default of the module and of the CLI stays strict. - One YAML reader. The
yamlpackage (ISC, no dependencies) reads and writes, with the core schema. The Markdown front matter and theopf-slideandopf-blockfences follow the same rules (JSON-compatible YAML 1.2 core schema: no anchors, tags or duplicate keys). - Block style only. Tables and chart data are lists of lists, which block style spreads over many lines. The text is longer than the JSON but diffs line by line, and one style means one canonical form.
- Quoting is conservative. The core schema reads
yesand2026-10-01as text, but a YAML 1.1 reader does not, so the writer quotes them anyway. - No new fields. YAML adds nothing to the schema: there is no YAML-only key, and a file that does not validate as JSON does not validate as YAML.