@openpresentation/opf 0.26.3 → 0.26.5

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.
Files changed (39) hide show
  1. package/README.md +3 -1
  2. package/dist/browser.js +14 -11
  3. package/dist/{chunk-NJWI6MPT.js → chunk-CIWC7DRZ.js} +1985 -366
  4. package/dist/{chunk-7RY7QWWJ.js → chunk-DXTPLVAK.js} +1 -1
  5. package/dist/{chunk-54OV2WQC.js → chunk-EFMR32A5.js} +1 -1
  6. package/dist/{chunk-PXOXTJTN.js → chunk-HPEZCAYC.js} +1 -1
  7. package/dist/{chunk-2ZQBMZTZ.js → chunk-K5Q3LLYK.js} +2 -2
  8. package/dist/chunk-KPALXWX3.js +712 -0
  9. package/dist/{chunk-34LIUOT7.js → chunk-L7YEVQHP.js} +6 -6
  10. package/dist/chunk-MD3CJ5YJ.js +452 -0
  11. package/dist/{chunk-WASENX2R.js → chunk-MLH5LIGF.js} +29 -647
  12. package/dist/chunk-RYZPBJLB.js +4 -0
  13. package/dist/{chunk-FXNUM2PN.js → chunk-STIYINEL.js} +4 -4
  14. package/dist/{chunk-YVNFACTC.js → chunk-TAV5S2JG.js} +1 -1
  15. package/dist/{chunk-GLLR3AQ5.js → chunk-UFEWQDQI.js} +1 -1
  16. package/dist/chunk-UY6SQVR2.js +639 -0
  17. package/dist/{chunk-PB5X2VSV.js → chunk-WAZDP2WS.js} +12 -20
  18. package/dist/{conversion-BjsKgxBZ.d.ts → conversion-C7o2EpDb.d.ts} +2 -2
  19. package/dist/conversion-PMJWLHDV.js +21 -0
  20. package/dist/convert.js +5 -5
  21. package/dist/deck.js +9 -7
  22. package/dist/diff.js +2 -2
  23. package/dist/docs.js +3 -3
  24. package/dist/explain-JSuESV-Y.d.ts +71 -0
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +18 -15
  27. package/dist/markdown.js +7 -5
  28. package/dist/node-engine.d.ts +2 -2
  29. package/dist/node-engine.js +18 -15
  30. package/dist/pagination.js +2 -2
  31. package/dist/patch.js +1 -1
  32. package/dist/schema.js +1 -1
  33. package/dist/source.d.ts +184 -5
  34. package/dist/source.js +391 -11
  35. package/dist/validator.js +1 -1
  36. package/dist/yaml.js +5 -4
  37. package/package.json +5 -5
  38. package/dist/chunk-DEI2M2H6.js +0 -194
  39. package/dist/conversion-F4UCOGR7.js +0 -18
@@ -1,8 +1,8 @@
1
- import { defaultCatalogs } from './chunk-2ZQBMZTZ.js';
2
- import { localAssetSources, OPFApiError, parseSlideSelection, OPFExportError, asApiError, OPFImportError } from './chunk-GLLR3AQ5.js';
3
- import { paginate } from './chunk-54OV2WQC.js';
4
- import { deckFormatOf, readDeckReport, stringify } from './chunk-FXNUM2PN.js';
5
- import { validate, OPFValidationError } from './chunk-NJWI6MPT.js';
1
+ import { defaultCatalogs } from './chunk-K5Q3LLYK.js';
2
+ import { localAssetSources, OPFApiError, parseSlideSelection, OPFExportError, asApiError, OPFImportError } from './chunk-UFEWQDQI.js';
3
+ import { paginate } from './chunk-EFMR32A5.js';
4
+ import { deckFormatOf, readDeckReport, stringify } from './chunk-STIYINEL.js';
5
+ import { validate, OPFValidationError } from './chunk-CIWC7DRZ.js';
6
6
  import { link, stat, readdir, lstat, readFile, open, unlink, mkdir, rename } from 'fs/promises';
7
7
  import path6 from 'path';
8
8
  import { readFileSync, statSync, realpathSync } from 'fs';
@@ -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.26.3", [PPTX_PACKAGE]: "^0.26.3" };
120
+ var PEER_RANGES = { [RENDER_PACKAGE]: "^0.26.5", [PPTX_PACKAGE]: "^0.26.5" };
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) {
@@ -0,0 +1,452 @@
1
+ import { SOURCE_EXPLAIN, ruleDocumentation, findValidationRule, us } from './chunk-CIWC7DRZ.js';
2
+
3
+ // src/finding-text-explain-util.ts
4
+ var ex = (fix, problem, fixed) => ({ fix, problem, fixed });
5
+ var exampleText = (side) => typeof side === "string" ? side : JSON.stringify(side, null, 2);
6
+ var CHART = { type: "column", alt: "Revenue by quarter", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12], ["Q2", 15]] } };
7
+
8
+ // src/finding-text-explain-audit.ts
9
+ var WHITE = { background: { type: "solid", color: "#FFFFFF" } };
10
+ var ROADMAP = { name: "Roadmap", regions: [{ name: "title" }, { direction: "row", regions: [{ name: "timeline", accepts: ["timeline"], max: 1 }, { name: "notes", accepts: ["text", "list"], flow: "column" }] }] };
11
+ var TIMELINE = { timeline: { events: [{ when: "Q1", what: "Pilot" }, { when: "Q2", what: "Launch" }] } };
12
+ var PERSON = (n) => ({ blocks: [{ image: { src: `https://example.com/${n}.png`, alt: `Person ${n}` } }, { text: `Person ${n}` }], composition: { mode: "column" } });
13
+ var SERIES = (count) => ({ columns: ["Quarter", ...Array.from({ length: count }, (_, i) => `S${i + 1}`)], rows: [["Q1", ...Array.from({ length: count }, (_, i) => i + 1)]] });
14
+ var AUDIT_EXPLAIN = {
15
+ "opf/text-contrast": ex(
16
+ "Pick a text color with enough contrast against the background (3:1 for large text, 4.5:1 for normal text), or remove the color override so the theme chooses a readable one.",
17
+ { design: WHITE, slides: [{ title: "Results", text: [{ text: "Faint note", color: "#CCCCCC" }] }] },
18
+ { design: WHITE, slides: [{ title: "Results", text: [{ text: "Readable note", color: "text" }] }] }
19
+ ),
20
+ "opf/text-on-image": ex(
21
+ "Add a full-frame overlay to the background image, or put the text on a solid or gradient background or a card, so contrast does not depend on the picture.",
22
+ { design: WHITE, slides: [{ title: "Welcome", design: { background: { type: "image", src: "https://example.com/hero.jpg" } } }] },
23
+ { design: WHITE, slides: [{ title: "Welcome", design: { background: { type: "image", src: "https://example.com/hero.jpg", overlay: { color: "#FFFFFF", opacity: 1 } } } }] }
24
+ ),
25
+ "opf/missing-alt-text": ex(
26
+ 'Describe the picture in `alt`, or write `alt: ""` when it is only decoration.',
27
+ { title: "Team", image: "https://example.com/team.png" },
28
+ { title: "Team", image: { src: "https://example.com/team.png", alt: "The team at the 2026 offsite" } }
29
+ ),
30
+ "opf/poor-alt-text": ex(
31
+ 'Write what a person who cannot see the picture needs to know, in plain words and without a leading "image of".',
32
+ { title: "Team", image: { src: "https://example.com/team.png", alt: "IMG_2041.png" } },
33
+ { title: "Team", image: { src: "https://example.com/team.png", alt: "Five people around a whiteboard" } }
34
+ ),
35
+ "opf/missing-slide-title": ex(
36
+ "Give the slide a `title`. Screen reader users find slides by title, and PowerPoint's outline uses it.",
37
+ { text: "Revenue grew in every quarter." },
38
+ { title: "Revenue grew", text: "Revenue grew in every quarter." }
39
+ ),
40
+ "opf/duplicate-slide-title": ex(
41
+ "Give each slide a title that tells it apart, for example by adding the part of the story it covers.",
42
+ { slides: [{ title: "Results", text: "Q1" }, { title: "Results", text: "Q2" }] },
43
+ { slides: [{ title: "Results: Q1", text: "Q1" }, { title: "Results: Q2", text: "Q2" }] }
44
+ ),
45
+ "opf/reading-order": ex(
46
+ "Reorder or regroup the blocks so the order they are read in follows the order they appear in, or choose a layout that puts the title first.",
47
+ { title: "Team", composition: { mode: "row" }, blocks: [PERSON(1), PERSON(2)] },
48
+ { title: "Team", composition: { mode: "column" }, blocks: [PERSON(1), PERSON(2)] }
49
+ ),
50
+ "opf/link-text": ex(
51
+ "Make the link text say where the link goes, for example the name of the page or file.",
52
+ { title: "Report", text: ["Read the report: ", { text: "click here", link: "https://example.com/report" }] },
53
+ { title: "Report", text: ["Read ", { text: "the 2026 annual report (PDF)", link: "https://example.com/report" }] }
54
+ ),
55
+ "opf/chart-color-only": ex(
56
+ "Show fewer series, choose more distinct colors, or add data labels or a direct legend so the chart does not depend on telling colors apart.",
57
+ { title: "Trend", chart: { type: "line", alt: "Fourteen series", data: SERIES(14) } },
58
+ { title: "Trend", chart: { type: "line", alt: "Two series", data: SERIES(2) } }
59
+ ),
60
+ "opf/chart-colors-adjusted": ex(
61
+ "Use a color that resolves (a hex color, a scheme color or a declared `var:`) and has at least 3:1 contrast against the chart panel, or remove `colors` to use the palette.",
62
+ { title: "Revenue", chart: { ...CHART, colors: ["var:missing"] } },
63
+ { title: "Revenue", chart: { ...CHART, colors: ["accent1"] } }
64
+ ),
65
+ "opf/chart-text-alternative": ex(
66
+ "Write the message the chart carries in `alt`, or add a subtitle, text or table beside it that states what it shows.",
67
+ { title: "Revenue", chart: { type: "column", data: CHART.data } },
68
+ { title: "Revenue", chart: { type: "column", alt: "Revenue rose from 12 to 15 million between Q1 and Q2.", data: CHART.data } }
69
+ ),
70
+ "opf/missing-language": ex(
71
+ "Set the deck's `language` to a BCP 47 tag such as `en-US`. It chooses fonts, text direction and the proofing language, and screen readers pronounce the text with it.",
72
+ { slides: [{ title: "Welcome" }] },
73
+ { language: "en-US", slides: [{ title: "Welcome" }] }
74
+ ),
75
+ "opf/chart-option-adapted": ex(
76
+ "Remove the option, or choose a chart type that can show it. Every engine draws the adapted result.",
77
+ { title: "Share", chart: { type: "pie", alt: "Share by region", data: { columns: ["Region", "Share"], rows: [["EMEA", 60], ["APAC", 40]] }, axisTitles: { value: "Share" } } },
78
+ { title: "Share", chart: { type: "pie", alt: "Share by region", data: { columns: ["Region", "Share"], rows: [["EMEA", 60], ["APAC", 40]] } } }
79
+ ),
80
+ "opf/text-overflow": ex(
81
+ "Shorten the text, give it more space (fewer blocks, a different composition) or split the slide. Raising the readable minimum makes it worse.",
82
+ { title: "Notes", composition: { minFontSize: 16 }, blocks: [{ text: "word ".repeat(600).trim() }, { text: "More" }] },
83
+ { title: "Notes", blocks: [{ text: "The three points that matter." }, { text: "More" }] }
84
+ ),
85
+ "opf/unresolved-content": ex(
86
+ "Supply what the message asks for (for example the organization logo a picture bullet needs), or remove the setting that cannot be honored.",
87
+ { design: { listBullet: "image" }, slides: [{ title: "Steps", items: ["One", "Two"] }] },
88
+ { slides: [{ title: "Steps", items: ["One", "Two"] }] }
89
+ ),
90
+ "opf/layout-failed": ex(
91
+ "Fix the composition setting or the content structure the message names. The checks that need the layout are skipped for that slide until it can be laid out.",
92
+ { catalogs: { custom: { layouts: { broken: { name: "Broken", regions: [{ name: "title" }], composition: { padding: 5 } } } } }, slides: [{ title: "Welcome", layout: "broken" }] },
93
+ { catalogs: { custom: { layouts: { broken: { name: "Broken", regions: [{ name: "title" }] } } } }, slides: [{ title: "Welcome", layout: "broken" }] }
94
+ ),
95
+ "opf/min-font-size": ex(
96
+ "Raise the font size to the readable minimum or more, or shorten the text so it can be drawn larger.",
97
+ { title: "Notes", text: [{ text: "Tiny print", fontSize: 8 }] },
98
+ { title: "Notes", text: [{ text: "Readable print", fontSize: 14 }] }
99
+ ),
100
+ "opf/region-unknown": ex(
101
+ "Pin the block to a region the layout has, or remove `region` so the block is placed by its kind.",
102
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "timelin", ...TIMELINE }] }] },
103
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "timeline", ...TIMELINE }] }] }
104
+ ),
105
+ "opf/region-kind": ex(
106
+ "Pin the block to a region that accepts its kind, or remove `region`.",
107
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "timeline", text: "Note" }] }] },
108
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "notes", text: "Note" }] }] }
109
+ ),
110
+ "opf/region-full": ex(
111
+ "Pin the block to another region with room, or remove `region` so it is placed by its kind.",
112
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "timeline", ...TIMELINE }, { region: "timeline", ...TIMELINE }] }] },
113
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ region: "timeline", ...TIMELINE }] }] }
114
+ ),
115
+ "opf/layout-unplaced": ex(
116
+ "Paginate the slide, choose a layout with more room, pin the block to a region with room, or remove a block.",
117
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ ...TIMELINE }, { ...TIMELINE }] }] },
118
+ { catalogs: { custom: { layouts: { roadmap: ROADMAP } } }, slides: [{ layout: "roadmap", blocks: [{ ...TIMELINE }] }] }
119
+ ),
120
+ "opf/furniture-wraps": ex(
121
+ "Shorten the header or footer text, or move part of it to another zone. Headers and footers should fit on two lines.",
122
+ { design: { footer: { left: { text: "A footer that keeps going and going and going and going and going and going and going and going and going and going and going and going" }, center: { text: "Confidential" }, right: { text: "{{slide.number}}" } } }, slides: [{ title: "Welcome" }] },
123
+ { design: { footer: { left: { text: "Acme, confidential" }, right: { text: "{{slide.number}}" } } }, slides: [{ title: "Welcome" }] }
124
+ ),
125
+ "opf/font-outside-scheme": ex(
126
+ "Remove `fontFamily` so the text uses the scheme font, or add the family to the deck's font scheme so it follows scheme changes.",
127
+ { design: { fontScheme: "aptos" }, slides: [{ title: "Notes", text: [{ text: "Hand-set", fontFamily: "Comic Sans MS" }] }] },
128
+ { design: { fontScheme: "aptos" }, slides: [{ title: "Notes", text: [{ text: "Hand-set" }] }] }
129
+ ),
130
+ "opf/image-resolution": ex(
131
+ "Use an image with more pixels for the size it is shown at (about 96 per inch), or show it smaller.",
132
+ { title: "Team", image: { src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAAAAAAAAAAAA", alt: "Team" } },
133
+ { title: "Team", image: { src: "https://example.com/team-2400x1350.png", alt: "Team" } }
134
+ )
135
+ };
136
+
137
+ // src/finding-text-explain-content.ts
138
+ var COLUMNS = ["Quarter", "Revenue", "Costs"];
139
+ var ROWS = [["Q1", 12, 8], ["Q2", 15, 9]];
140
+ var ARC = (duration) => ({ name: "Arc", duration, beats: [{ id: "hook", name: "Hook" }, { id: "proof", name: "Proof" }, { id: "ask", name: "Ask" }] });
141
+ var CONTENT_EXPLAIN = {
142
+ "opf/chart-value-not-numeric": ex(
143
+ "Store the value as a number and put units, currency or percent in the column `format`. A text value is plotted as a gap.",
144
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", "about 12"]] } } },
145
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: ["Quarter", { name: "Revenue", format: "$#,##0" }], rows: [["Q1", 12]] } } }
146
+ ),
147
+ "opf/chart-mapping-adapted": ex(
148
+ "Remove the mapping entry, or name a column the chart type can use. The entry is ignored.",
149
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: COLUMNS, rows: ROWS }, mapping: { x: "Costs" } } },
150
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: COLUMNS, rows: ROWS } } }
151
+ ),
152
+ "opf/chart-highlight-adapted": ex(
153
+ "Name a series the chart plots in `highlight`, or remove the entry. A column that is the category or X column, or that the mapping leaves out, highlights nothing.",
154
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: COLUMNS, rows: ROWS }, highlight: { series: ["Quarter"] } } },
155
+ { title: "Revenue", chart: { type: "column", alt: "Revenue", data: { columns: COLUMNS, rows: ROWS }, highlight: { series: ["Revenue"] } } }
156
+ ),
157
+ "opf/code-highlight-out-of-range": ex(
158
+ "Change the entry to a line the code has (counted from 1), or remove it.",
159
+ { title: "Code", code: { source: "a\nb\nc", highlight: [7] } },
160
+ { title: "Code", code: { source: "a\nb\nc", highlight: [3] } }
161
+ ),
162
+ "opf/code-highlight-range-reversed": ex(
163
+ "Write the range with the smaller line number first, or remove it.",
164
+ { title: "Code", code: { source: "a\nb\nc", highlight: [[3, 1]] } },
165
+ { title: "Code", code: { source: "a\nb\nc", highlight: [[1, 3]] } }
166
+ ),
167
+ "opf/variable-builtin-missing": ex(
168
+ "Add the field the built-in reads (for example `speaker.name`, `organization.tagline` or the deck's `name`), or remove the token.",
169
+ { slides: [{ title: "{{speaker.name}}" }] },
170
+ { speaker: { name: "Ada Lovelace" }, slides: [{ title: "{{speaker.name}}" }] }
171
+ ),
172
+ "opf/duration-outside-narrative": ex(
173
+ "Change `duration`, or choose a narrative that suits it. The range describes the narrative, so nothing is drawn differently.",
174
+ { duration: 5, narrative: "arc", catalogs: { custom: { narratives: { arc: ARC({ min: 10, max: 20 }) } } }, slides: [{ title: "Intro", beat: "hook" }] },
175
+ { duration: 15, narrative: "arc", catalogs: { custom: { narratives: { arc: ARC({ min: 10, max: 20 }) } } }, slides: [{ title: "Intro", beat: "hook" }] }
176
+ ),
177
+ "opf/narrative-duration-range": ex(
178
+ "Swap the bounds so `min` is the shortest and `max` the longest talk length, in minutes.",
179
+ { catalogs: { custom: { narratives: { arc: ARC({ min: 30, max: 10 }) } } }, slides: [{ title: "Intro" }] },
180
+ { catalogs: { custom: { narratives: { arc: ARC({ min: 10, max: 30 }) } } }, slides: [{ title: "Intro" }] }
181
+ ),
182
+ "opf/placeholder-text": ex(
183
+ 'Replace the placeholder (lorem ipsum, "Click to add title", TBD, [Insert ...]) with the real text.',
184
+ { title: "Overview", text: "Lorem ipsum dolor sit amet" },
185
+ { title: "Overview", text: "Revenue grew in every quarter." }
186
+ ),
187
+ "opf/empty-text": ex(
188
+ "Write the text, or remove the empty field or item.",
189
+ { title: "Next steps", items: ["Hire two engineers", ""] },
190
+ { title: "Next steps", items: ["Hire two engineers", "Ship the beta"] }
191
+ ),
192
+ "opf/unused-beat": ex(
193
+ "Link a slide to the beat with `beat`, or ignore this when the beat is optional for your talk.",
194
+ { duration: 15, narrative: "arc", catalogs: { custom: { narratives: { arc: ARC({ min: 10, max: 20 }) } } }, slides: [{ title: "One", beat: "hook" }, { title: "Two", beat: "proof" }] },
195
+ { duration: 15, narrative: "arc", catalogs: { custom: { narratives: { arc: ARC({ min: 10, max: 20 }) } } }, slides: [{ title: "One", beat: "hook" }, { title: "Two", beat: "proof" }, { title: "Three", beat: "ask" }] }
196
+ ),
197
+ "opf/empty-slide": ex(
198
+ "Add content or a title, remove the slide, or give it the `blank` layout if it is meant to be empty.",
199
+ { slides: [{ title: "Intro", text: "Welcome" }, { id: "spare" }] },
200
+ { slides: [{ title: "Intro", text: "Welcome" }, { id: "spare", title: "Questions", text: "Ask away" }] }
201
+ ),
202
+ "opf/language-tag": ex(
203
+ "Write a well-formed BCP 47 tag with an assigned region, such as `en-GB` (the United Kingdom is `GB`, not `UK`), `pt-BR` or `es-419`.",
204
+ { language: { bcp47: "fr-UK" }, slides: [{ title: "Bienvenue" }] },
205
+ { language: { bcp47: "fr-GB" }, slides: [{ title: "Bienvenue" }] }
206
+ )
207
+ };
208
+
209
+ // src/finding-text-explain-format.ts
210
+ var IMG = (name) => ({ src: `https://example.com/${name}.png`, alt: "Team at the offsite" });
211
+ var FORMAT_EXPLAIN = {
212
+ "opf/json-syntax": ex(
213
+ "Fix the error at the marked position; the message names the root cause (a missing comma, a quote, a bracket that is never closed), and later errors usually disappear with it.",
214
+ '{\n "name": "Q3 review"\n "slides": [{ "title": "Revenue" }]\n}',
215
+ '{\n "name": "Q3 review",\n "slides": [{ "title": "Revenue" }]\n}'
216
+ ),
217
+ "opf/duplicate-key": ex(
218
+ "Keep one of the two keys, or merge their values into one. JSON keeps only the last value, so the earlier one is ignored.",
219
+ '{\n "name": "Q3 review",\n "slides": [{ "title": "Revenue" }],\n "name": "Q4 review"\n}',
220
+ '{\n "name": "Q4 review",\n "slides": [{ "title": "Revenue" }]\n}'
221
+ ),
222
+ "opf/schema": ex(
223
+ "Change the value to the form the message names (its type, one of the allowed values, or the missing field). The message says which field and what it should be.",
224
+ { title: "Revenue", items: "Grew 12%" },
225
+ { title: "Revenue", items: ["Grew 12%"] }
226
+ ),
227
+ "opf/reference-id-duplicate": ex(
228
+ "Give each reference its own id. Runs cite a reference by its id, so two with the same id cannot be told apart.",
229
+ { references: [{ id: "gartner", text: "Gartner, 2026" }, { id: "gartner", text: "IDC, 2026" }], slides: [{ title: "Market", text: [{ text: "Up 12%", cite: "gartner" }] }] },
230
+ { references: [{ id: "gartner", text: "Gartner, 2026" }, { id: "idc", text: "IDC, 2026" }], slides: [{ title: "Market", text: [{ text: "Up 12%", cite: "gartner" }] }] }
231
+ ),
232
+ "opf/cite-unknown-reference": ex(
233
+ "Change `cite` to the id of a reference under `references`, or add the reference it names.",
234
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", text: [{ text: "Up 12%", cite: "gartnr" }] }] },
235
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", text: [{ text: "Up 12%", cite: "gartner" }] }] }
236
+ ),
237
+ "opf/cite-unsupported-location": ex(
238
+ "Move the cited run into a text block, bullets or a list item (titles and quotes work too). Table cells, captions and reference text do not draw citation markers.",
239
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", table: { columns: ["Region", "Growth"], rows: [["EMEA", [{ text: "12%", cite: "gartner" }]]] } }] },
240
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", text: [{ text: "EMEA grew 12%", cite: "gartner" }], table: { columns: ["Region", "Growth"], rows: [["EMEA", "12%"]] } }] }
241
+ ),
242
+ "opf/caption-unsupported-payload": ex(
243
+ "Put the `caption` on the image, chart, table or video it describes, or remove it.",
244
+ { title: "Revenue", blocks: [{ text: "Revenue grew", caption: "Source: finance" }] },
245
+ { title: "Revenue", blocks: [{ chart: CHART, caption: "Source: finance" }] }
246
+ ),
247
+ "opf/image-option-unsupported-payload": ex(
248
+ "Remove the image option, or move it onto an image block. Options such as `fit`, `shape` and `overlay` describe how a picture is drawn.",
249
+ { title: "Revenue", chart: CHART, fit: "cover" },
250
+ { title: "Revenue", image: IMG("team"), fit: "cover" }
251
+ ),
252
+ "opf/image-placement-invalid": ex(
253
+ "Use `placement` on a top-level block of the slide, with one placed image per slide edge.",
254
+ { title: "Team", blocks: [{ image: IMG("a"), placement: { edge: "left" } }, { image: IMG("b"), placement: { edge: "left" } }] },
255
+ { title: "Team", blocks: [{ image: IMG("a"), placement: { edge: "left" } }, { image: IMG("b"), placement: { edge: "right" } }] }
256
+ ),
257
+ "opf/layout-placeholder-group": ex(
258
+ "Flatten the group so it nests at most three levels, and keep the headings (title, subtitle, tag) at the record's top level. Layout records written for current versions use `regions`, where the same limits apply to splits.",
259
+ { placeholders: [{ type: "group", placeholders: [{ type: "title" }, { type: "text" }] }] },
260
+ { placeholders: [{ type: "title" }, { type: "group", placeholders: [{ type: "text" }, { type: "chart" }] }] }
261
+ ),
262
+ "opf/dataset-unknown": ex(
263
+ "Name a dataset that exists under `datasets`, or add the one the chart or table asks for.",
264
+ { datasets: { sales: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12]] } }, slides: [{ title: "Revenue", chart: { type: "column", data: { dataset: "sale" } } }] },
265
+ { datasets: { sales: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12]] } }, slides: [{ title: "Revenue", chart: { type: "column", data: { dataset: "sales" } } }] }
266
+ ),
267
+ "opf/dataset-field-unknown": ex(
268
+ "Name only columns the dataset has. The message lists them.",
269
+ { datasets: { sales: { columns: ["Region", "Revenue"], rows: [["EMEA", 12]] } }, slides: [{ title: "Revenue", table: { dataset: "sales", fields: ["Regon"] } }] },
270
+ { datasets: { sales: { columns: ["Region", "Revenue"], rows: [["EMEA", 12]] } }, slides: [{ title: "Revenue", table: { dataset: "sales", fields: ["Region"] } }] }
271
+ ),
272
+ "opf/data-column-duplicate": ex(
273
+ "Rename one of the columns so every name is unique. Series, mappings and fields address columns by name.",
274
+ { datasets: { sales: { columns: ["Region", "Region"], rows: [["EMEA", 12]] } }, slides: [{ title: "Revenue", table: { dataset: "sales" } }] },
275
+ { datasets: { sales: { columns: ["Region", "Region-2"], rows: [["EMEA", 12]] } }, slides: [{ title: "Revenue", table: { dataset: "sales" } }] }
276
+ ),
277
+ "opf/chart-mapping-unknown-column": ex(
278
+ "Name a column the chart data has in `mapping`. The message lists them.",
279
+ { title: "Revenue", chart: { type: "column", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12]] }, mapping: { series: ["Profit"] } } },
280
+ { title: "Revenue", chart: { type: "column", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12]] }, mapping: { series: ["Revenue"] } } }
281
+ ),
282
+ "opf/number-format-invalid": ex(
283
+ "Write the format as an Excel-style pattern with a numeric part, such as `0.0`, `#,##0`, `0%` or `$#,##0.00`.",
284
+ { title: "Revenue", table: { columns: [{ name: "Revenue", format: "units" }], rows: [[12]] } },
285
+ { title: "Revenue", table: { columns: [{ name: "Revenue", format: "$#,##0" }], rows: [[12]] } }
286
+ ),
287
+ "opf/variable-invalid-value": ex(
288
+ "Give the variable a value of its declared type: a plain number for `number`, a date for `date`, a hex color for `color`. The values are what a host passes when it fills the deck (`validate` option `values`, `opf fill`); a declared `value` of the wrong type is reported by the schema.",
289
+ { values: { seats: "many" } },
290
+ { values: { seats: 40 } }
291
+ ),
292
+ "opf/variable-unknown-builtin": ex(
293
+ "Use a built-in that exists (`deck.name`, `deck.description`, `deck.author`, `speaker.name`, `organization.name`, `slide.number`, ...), or declare your own variable under `variables`.",
294
+ { slides: [{ title: "{{deck.nmae}}" }] },
295
+ { name: "Q3 review", slides: [{ title: "{{deck.name}}" }] }
296
+ ),
297
+ "opf/chart-highlight-unknown-name": ex(
298
+ "Name a series or category the chart data has in `highlight`; a name that matches nothing highlights nothing.",
299
+ { title: "Revenue", chart: { type: "column", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12], ["Q2", 15]] }, highlight: { categories: ["Q9"] } } },
300
+ { title: "Revenue", chart: { type: "column", data: { columns: ["Quarter", "Revenue"], rows: [["Q1", 12], ["Q2", 15]] }, highlight: { categories: ["Q2"] } } }
301
+ ),
302
+ "opf/variable-format": ex(
303
+ "Give the variable a `format` that fits its type and its values: `#,##0.0` for a number, a date pattern for a date.",
304
+ { variables: { amount: { type: "number", value: 1e308, format: "0%" } }, slides: [{ title: "Value {{amount}}" }] },
305
+ { variables: { amount: { type: "number", value: 12.5, format: "#,##0.0" } }, slides: [{ title: "Value {{amount}}" }] }
306
+ ),
307
+ "opf/variable-unfilled": ex(
308
+ "Give the variable a `value`, fill the deck before use, or mark the deck as a template with `template: true`. Otherwise the `{{token}}` would show as typed.",
309
+ { variables: { client: { type: "text", label: "Client" } }, slides: [{ title: "Proposal for {{client}}" }] },
310
+ { variables: { client: { type: "text", label: "Client", value: "Acme" } }, slides: [{ title: "Proposal for {{client}}" }] }
311
+ )
312
+ };
313
+
314
+ // src/finding-text-explain-references.ts
315
+ var NARRATIVE = { name: "Arc", beats: [{ id: "hook", name: "Hook" }, { id: "proof", name: "Proof" }] };
316
+ var GALLERY_LAYOUT = { name: "Mine", regions: [{ name: "title" }] };
317
+ var DATA = { columns: ["Quarter", "Revenue"], rows: [["Q1", 12]] };
318
+ var REFERENCES_EXPLAIN = {
319
+ "opf/run-color-unrecognized": ex(
320
+ "Write a hex color (`#1F4E79`), a color scheme name (`accent1`, `text`) or a `var:` reference to a declared color variable.",
321
+ { title: "Results", text: [{ text: "Up", color: "accnt1" }] },
322
+ { title: "Results", text: [{ text: "Up", color: "accent1" }] }
323
+ ),
324
+ "opf/numbering-start-ignored": ex(
325
+ "Add `numbering` to the list, or remove `start`. A start number only matters on a numbered list.",
326
+ { title: "Steps", items: [{ text: "Third step", start: 3 }] },
327
+ { title: "Steps", numbering: "decimal", items: [{ text: "Third step", start: 3 }] }
328
+ ),
329
+ "opf/slide-size-out-of-range": ex(
330
+ "Use a custom `widthInches` and `heightInches` between 1 and 56, or a preset size.",
331
+ { design: { dimensions: { widthInches: 0.5, heightInches: 60 } }, slides: [{ title: "Poster" }] },
332
+ { design: { dimensions: { widthInches: 13.333, heightInches: 7.5 } }, slides: [{ title: "Poster" }] }
333
+ ),
334
+ "opf/undeclared-catalog": ex(
335
+ "Declare the catalog group under `catalogs` (with its source), or write the reference without the group prefix.",
336
+ { slides: [{ title: "Welcome", layout: "acme:hero" }] },
337
+ { slides: [{ title: "Welcome", layout: "hero" }] }
338
+ ),
339
+ "opf/asset-reference": ex(
340
+ "Add the asset to `assets` under that id, or name an asset that exists. External files and URLs are never fetched.",
341
+ { assets: { logo: { src: "https://example.com/logo.png", alt: "Acme logo" } }, slides: [{ title: "Welcome", image: "asset:lgog" }] },
342
+ { assets: { logo: { src: "https://example.com/logo.png", alt: "Acme logo" } }, slides: [{ title: "Welcome", image: "asset:logo" }] }
343
+ ),
344
+ "opf/asset-cycle": ex(
345
+ "Give one asset in the loop a real source (an https:// address or embedded data) instead of an `asset:` reference to the next one.",
346
+ { assets: { a: "asset:b", b: "asset:a" }, slides: [{ title: "Welcome", image: "asset:a" }] },
347
+ { assets: { a: "asset:b", b: { src: "https://example.com/b.png", alt: "Team" } }, slides: [{ title: "Welcome", image: "asset:a" }] }
348
+ ),
349
+ "opf/catalog-record": ex(
350
+ "Fix the record the way the message says (it names the field and what it should be). References to an invalid record are reported too until it is valid.",
351
+ { catalogs: { custom: { themes: { mine: { name: "Mine", background: "light1" } } } }, slides: [{ title: "Welcome" }] },
352
+ { catalogs: { custom: { themes: { mine: { name: "Mine", background: { type: "theme", slot: "light1" } } } } }, slides: [{ title: "Welcome" }] }
353
+ ),
354
+ "opf/layout-template": ex(
355
+ "Correct the region list: 1 to 12 entries, a `direction` of `row` or `column`, numeric sizes above 0 (or `auto` only in a column), and at most three levels of splits.",
356
+ { catalogs: { custom: { layouts: { mine: { name: "Mine", direction: "row", regions: [{ name: "title" }, { name: "a", accepts: ["text"], size: "auto" }, { name: "b", accepts: ["text"] }] } } } }, slides: [{ title: "Welcome", layout: "mine" }] },
357
+ { catalogs: { custom: { layouts: { mine: { name: "Mine", regions: [{ name: "title" }, { name: "a", accepts: ["text"], size: "auto" }, { name: "b", accepts: ["text"] }] } } } }, slides: [{ title: "Welcome", layout: "mine" }] }
358
+ ),
359
+ "opf/layout-region": ex(
360
+ "Name each body region with a unique lowercase id that is not reserved (`title`, `subtitle`, `tag`, `auto`, `left`, `center`, `right`, `top`, `middle`, `bottom`), and give it something to accept.",
361
+ { catalogs: { custom: { layouts: { mine: { name: "Mine", regions: [{ name: "title" }, { name: "left", accepts: ["text"] }] } } } }, slides: [{ title: "Welcome", layout: "mine" }] },
362
+ { catalogs: { custom: { layouts: { mine: { name: "Mine", regions: [{ name: "title" }, { name: "body", accepts: ["text"] }] } } } }, slides: [{ title: "Welcome", layout: "mine" }] }
363
+ ),
364
+ "opf/unresolved-reference": ex(
365
+ "Choose an id the catalog has (the finding lists the nearest), embed the record under `catalogs.custom`, register the catalog that defines it, or remove the reference.",
366
+ { slides: [{ title: "Pillars", layout: "pilars" }] },
367
+ { slides: [{ title: "Pillars", layout: "pillars" }] }
368
+ ),
369
+ "opf/catalog-record-not-in-source": ex(
370
+ "Move the record to `catalogs.custom` (and drop the group prefix from its references), or use an id the catalog publishes.",
371
+ { catalogs: { default: { source: "https://www.pptx.gallery", layouts: { "my-special": GALLERY_LAYOUT } } }, slides: [{ title: "Welcome", layout: "my-special" }] },
372
+ { catalogs: { custom: { layouts: { "my-special": GALLERY_LAYOUT } } }, slides: [{ title: "Welcome", layout: "my-special" }] }
373
+ ),
374
+ "opf/unused-reference": ex(
375
+ "Cite the reference from a text, bullet or list item, or remove it. Nothing is drawn for a reference nobody cites.",
376
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", text: "Up 12%" }] },
377
+ { references: [{ id: "gartner", text: "Gartner, 2026" }], slides: [{ title: "Market", text: [{ text: "Up 12%", cite: "gartner" }] }] }
378
+ ),
379
+ "opf/unused-dataset": ex(
380
+ "Point a chart or table at the dataset with `dataset`, or remove it.",
381
+ { datasets: { sales: DATA }, slides: [{ title: "Revenue", text: "Up 12%" }] },
382
+ { datasets: { sales: DATA }, slides: [{ title: "Revenue", chart: { type: "column", alt: "Revenue", data: { dataset: "sales" } } }] }
383
+ ),
384
+ "opf/unknown-beat": ex(
385
+ "Use one of the narrative's beat ids, add the beat to the narrative record, or remove the `beat` link.",
386
+ { narrative: "arc", catalogs: { custom: { narratives: { arc: NARRATIVE } } }, slides: [{ title: "Intro", beat: "intro" }] },
387
+ { narrative: "arc", catalogs: { custom: { narratives: { arc: NARRATIVE } } }, slides: [{ title: "Intro", beat: "hook" }] }
388
+ ),
389
+ "opf/slide-theme-dimensions": ex(
390
+ "Set `design.dimensions` on the deck, or give every slide's theme the same dimensions. A PPTX has one slide size.",
391
+ { design: { theme: "minimal" }, catalogs: { custom: { themes: { "wide-a4": { name: "A4", dimensions: "a4" } } } }, slides: [{ title: "Letter", design: { theme: "wide-a4" } }] },
392
+ { design: { theme: "minimal", dimensions: "a4" }, catalogs: { custom: { themes: { "wide-a4": { name: "A4", dimensions: "a4" } } } }, slides: [{ title: "Letter", design: { theme: "wide-a4" } }] }
393
+ ),
394
+ "opf/variable-unknown": ex(
395
+ "Declare the variable under `variables`, correct the name, or write `\\{{` for literal braces.",
396
+ { variables: { name: { type: "text", value: "Acme" } }, slides: [{ title: "Hello {{nmae}}" }] },
397
+ { variables: { name: { type: "text", value: "Acme" } }, slides: [{ title: "Hello {{name}}" }] }
398
+ ),
399
+ "opf/variable-unknown-value": ex(
400
+ "Declare the variable the value belongs to under `variables`, or drop the value. The values are what a host passes when it fills the deck (`validate` option `values`, `opf fill`); one for an undeclared variable is ignored.",
401
+ { values: { name: "Acme", nope: 1 } },
402
+ { values: { name: "Acme" } }
403
+ ),
404
+ "opf/variable-unused": ex(
405
+ "Use the variable as `{{id}}` or `var:id` where it belongs, or remove its declaration.",
406
+ { variables: { client: { type: "text", value: "Acme" } }, slides: [{ title: "Proposal" }] },
407
+ { variables: { client: { type: "text", value: "Acme" } }, slides: [{ title: "Proposal for {{client}}" }] }
408
+ ),
409
+ "opf/variable-reference-unknown": ex(
410
+ "Declare the color variable under `variables`, or use another color. Until then a default color is drawn.",
411
+ { slides: [{ title: "Results", text: [{ text: "Up", color: "var:brand" }] }] },
412
+ { variables: { brand: { type: "color", value: "#1F4E79" } }, slides: [{ title: "Results", text: [{ text: "Up", color: "var:brand" }] }] }
413
+ ),
414
+ "opf/contract": ex(
415
+ "Change the value to one the host's policy allows (the finding lists them), or ask the host to widen the policy.",
416
+ { slides: [{ title: "Welcome", layout: "cover" }] },
417
+ { slides: [{ title: "Welcome", layout: "text" }] }
418
+ )
419
+ };
420
+
421
+ // src/finding-text-explain.ts
422
+ var RULE_EXPLAIN = Object.freeze({
423
+ ...FORMAT_EXPLAIN,
424
+ ...REFERENCES_EXPLAIN,
425
+ ...AUDIT_EXPLAIN,
426
+ ...CONTENT_EXPLAIN
427
+ });
428
+
429
+ // src/explain.ts
430
+ function explain(ruleId) {
431
+ if (typeof ruleId !== "string") return void 0;
432
+ if (/^(yaml|markdown)\//.test(ruleId)) {
433
+ const entry = SOURCE_EXPLAIN[ruleId];
434
+ return entry ? { id: ruleId, name: ruleId.slice(ruleId.indexOf("/") + 1), docs: ruleDocumentation(ruleId), ...entry } : void 0;
435
+ }
436
+ const info = findValidationRule(ruleId);
437
+ if (!info) return void 0;
438
+ const detail = RULE_EXPLAIN[info.id];
439
+ return {
440
+ id: info.id,
441
+ name: info.name,
442
+ category: info.category,
443
+ severity: info.severity,
444
+ summary: us(info.summary),
445
+ rationale: us(info.standard && !info.standard.startsWith("spec/") ? `${info.rationale} Basis: ${info.standard}.` : info.rationale),
446
+ fix: detail?.fix ?? us(info.summary),
447
+ ...detail ? { example: { problem: exampleText(detail.problem), fixed: exampleText(detail.fixed), language: "json" } } : {},
448
+ docs: ruleDocumentation(info.id)
449
+ };
450
+ }
451
+
452
+ export { explain };