@openpresentation/opf 0.11.3 → 0.12.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.
Files changed (64) hide show
  1. package/README.md +16 -0
  2. package/dist/audit.d.ts +156 -0
  3. package/dist/audit.js +7 -0
  4. package/dist/{catalogs-CUClB3nl.d.ts → catalogs-u6TEeMcz.d.ts} +1 -1
  5. package/dist/catalogs.d.ts +2 -2
  6. package/dist/catalogs.js +2 -1
  7. package/dist/{chunk-UDPSGTZI.js → chunk-32DIB5BN.js} +12 -9
  8. package/dist/chunk-4FWCY4ZV.js +198 -0
  9. package/dist/chunk-6TSDIPSH.js +3624 -0
  10. package/dist/chunk-FYLJVF7X.js +18448 -0
  11. package/dist/{chunk-HJL64ETN.js → chunk-GJKJL4YY.js} +14 -0
  12. package/dist/chunk-JENJZR3F.js +1690 -0
  13. package/dist/{chunk-57NWIYX3.js → chunk-JVTN3HPP.js} +18 -4
  14. package/dist/{chunk-JTXCMVVP.js → chunk-RVN5C2FX.js} +754 -39
  15. package/dist/chunk-S5W34SIJ.js +1 -0
  16. package/dist/chunk-S667QI3M.js +261 -0
  17. package/dist/chunk-UQKJWHSS.js +1332 -0
  18. package/dist/{chunk-RXNFDGPC.js → chunk-WXSPETQ6.js} +311 -40
  19. package/dist/composition-DWbCiMwF.d.ts +1523 -0
  20. package/dist/composition.d.ts +1 -1
  21. package/dist/composition.js +2 -1
  22. package/dist/convert.d.ts +261 -0
  23. package/dist/convert.js +1107 -0
  24. package/dist/diff.d.ts +145 -0
  25. package/dist/diff.js +604 -0
  26. package/dist/docs.js +69 -15
  27. package/dist/examples.d.ts +1 -1
  28. package/dist/font-policy.d.ts +15 -0
  29. package/dist/font-policy.js +1 -1
  30. package/dist/format.d.ts +20 -0
  31. package/dist/format.js +83 -0
  32. package/dist/index.d.ts +173 -24
  33. package/dist/index.js +926 -262
  34. package/dist/lint.d.ts +4 -4
  35. package/dist/lint.js +6 -5
  36. package/dist/markdown.d.ts +74 -0
  37. package/dist/markdown.js +1941 -0
  38. package/dist/pagination.d.ts +1 -1
  39. package/dist/pagination.js +6 -5
  40. package/dist/patch.d.ts +99 -0
  41. package/dist/patch.js +6 -0
  42. package/dist/{presentation-Cn6vTOCP.d.ts → presentation-DRx4hpNZ.d.ts} +479 -32
  43. package/dist/repo-readme.js +1 -1
  44. package/dist/{schemas-X_NniU4A.d.ts → schemas-BSjlG8-R.d.ts} +28 -0
  45. package/dist/schemas.d.ts +1 -1
  46. package/dist/schemas.js +1 -1
  47. package/dist/spec/reference/font-policy.json +311 -40
  48. package/dist/spec/reference/font-policy.schema.json +40 -0
  49. package/dist/spec/reference/symbol-font-encodings.json +18354 -0
  50. package/dist/spec/reference/symbol-font-encodings.schema.json +156 -0
  51. package/dist/spec/schemas/opf.schema.json +608 -35
  52. package/dist/spec-files.d.ts +1 -1
  53. package/dist/spec-files.js +1 -1
  54. package/dist/symbol-font-encodings.d.ts +128 -0
  55. package/dist/symbol-font-encodings.js +1 -0
  56. package/dist/types.d.ts +4 -4
  57. package/dist/validator-bu396ffT.d.ts +172 -0
  58. package/dist/validator.d.ts +4 -36
  59. package/dist/validator.js +5 -4
  60. package/package.json +31 -2
  61. package/dist/chunk-BVLWFMDX.js +0 -1812
  62. package/dist/chunk-JCXHSTSM.js +0 -1366
  63. package/dist/composition-DzLwY-Mi.d.ts +0 -887
  64. /package/dist/{chunk-AEUSVEUY.js → chunk-3ZGSWEHK.js} +0 -0
package/README.md CHANGED
@@ -69,12 +69,28 @@ import type { Presentation, Audience, Tone } from "@openpresentation/opf/types";
69
69
 
70
70
  The root entry exports every schema, catalog, and validation helper for convenience. Prefer the focused subpaths above when a package consumer only needs one surface, so the root bundle's full catalog/schema payload is not loaded unnecessarily.
71
71
 
72
+ ### Content conversions
73
+
74
+ `@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](../../docs/conversions.md). Not in releases before the one that lists it in the changelog.
75
+
76
+ ### Markdown and outlines
77
+
78
+ `@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). `markdownToOpf(markdown, { split, defaults, validate })` returns `{ document, valid, diagnostics, counts }` with lint-shaped diagnostics that carry the line and column of the Markdown; `opfToMarkdown(document, { 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](../../docs/markdown.md). Not in releases before the one that lists it in the changelog.
79
+
80
+ ### Design and accessibility audit
81
+
82
+ `@openpresentation/opf/audit` exports `auditPresentation(document, options)`, `auditSource(source, options)` and `auditRules`: contrast, overflow, type size, alt text, reading order, fonts, links, charts and more, with stable `audit/<rule>` ids in lint's report shape. Read-only and deterministic; see [the audit guide](../../docs/audit.md).
83
+
72
84
  ### Contextual lint (0.10.0)
73
85
 
74
86
  Version 0.10.0 adds `lintSource(source, options)` and `lintPresentation(document, options)` from `@openpresentation/opf/lint` and the root API. They report strict JSON syntax, duplicate keys, schema constraints, local catalog alternatives, asset registry errors, and explicit host contracts. Source diagnostics retain original UTF-16 ranges without rewriting the document. Options accept already loaded `catalogs` and `contracts`; no remote resources are fetched.
75
87
 
76
88
  Earlier versions do not include these APIs. See the [lint guide](../../docs/lint.md) for configuration and the source CLI. Passing lint does not certify layout, fonts, or native export fidelity.
77
89
 
90
+ ### Patch, diff, merge and format (0.12.0)
91
+
92
+ 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 `diffPresentations` (slide matching by id, content and similarity, moves, a readable report and a JSON Patch) and `mergePresentations` (three-way merge with conflict objects that never drop a side); `@openpresentation/opf/format` has `formatPresentation` (canonical key order and layout, idempotent). See [the guide](../../docs/patch-diff-merge-format.md).
93
+
78
94
  ### Layout previews
79
95
 
80
96
  `@openpresentation/opf/previews` ships pre-rendered HTML thumbnails for the
@@ -0,0 +1,156 @@
1
+ import { C as CatalogKind } from './catalogs-u6TEeMcz.js';
2
+ import { T as TextMeasurement } from './composition-DWbCiMwF.js';
3
+ import { LintDiagnostic, LintSeverity } from './lint.js';
4
+ import './schemas-BSjlG8-R.js';
5
+ import './validator-bu396ffT.js';
6
+ import './presentation-DRx4hpNZ.js';
7
+
8
+ /** Same three levels as `lintSource`. */
9
+ type AuditSeverity = LintSeverity;
10
+ type AuditCategory = 'accessibility' | 'design' | 'content';
11
+ /** One JSON Patch operation (RFC 6902 subset) a quick fix applies. Paths are JSON Pointers into the audited document. */
12
+ interface AuditPatchOperation {
13
+ op: 'add' | 'replace' | 'remove';
14
+ path: string;
15
+ value?: unknown;
16
+ }
17
+ /**
18
+ * A suggested repair. Core never applies it: a host shows it and applies it through its own undoable edit path.
19
+ * `focus` fixes only name the field the author must fill in; `patch` fixes are small, deterministic and leave
20
+ * every unrelated field untouched.
21
+ */
22
+ interface AuditFix {
23
+ /** Stable id within one diagnostic, such as `focus-alt` or `inherit-text-color`. */
24
+ id: string;
25
+ label: string;
26
+ kind: 'patch' | 'focus';
27
+ /** True when applying the fix cannot change the meaning of the content (a colour switched to a readable one, a field focused). */
28
+ safe: boolean;
29
+ patch?: AuditPatchOperation[];
30
+ /** For `focus`: the JSON Pointer of the field to edit and a hint of what it is. */
31
+ focus?: {
32
+ path: string;
33
+ field: 'alt' | 'title' | 'text' | 'link' | 'language' | 'fontSize';
34
+ value?: unknown;
35
+ };
36
+ }
37
+ interface AuditDiagnostic extends LintDiagnostic {
38
+ /** `audit/<rule>`; ids are stable across releases. */
39
+ ruleId: string;
40
+ /** Zero-based slide index when the finding belongs to one slide. */
41
+ slide?: number;
42
+ slideId?: string;
43
+ category: AuditCategory;
44
+ /** Measured values behind the finding (ratios, sizes, counts), for reporters and tests. */
45
+ measured?: Record<string, number | string | boolean | null>;
46
+ fixes?: AuditFix[];
47
+ }
48
+ interface AuditThresholds {
49
+ /** Minimum WCAG contrast ratio for normal text. WCAG 2.x AA: 4.5. */
50
+ contrastNormal: number;
51
+ /** Minimum ratio for large text (at least 18 pt, or 14 pt bold). WCAG 2.x AA: 3. */
52
+ contrastLarge: number;
53
+ /** Smallest rendered text size, in points, before `audit/min-font-size` reports. */
54
+ minFontSizePt: number;
55
+ /** Words on one slide above which `audit/slide-word-count` reports. */
56
+ maxWordsPerSlide: number;
57
+ /** Distinct font families across the deck above which `audit/font-family-count` reports. */
58
+ maxFontFamilies: number;
59
+ /** Minimum effective pixels per inch of an embedded raster image at its displayed size. */
60
+ minImagePpi: number;
61
+ /** Largest title offset between slides of the same layout, as a fraction of the slide width/height. */
62
+ titlePositionTolerance: number;
63
+ /** Smallest CIE76 colour difference between two chart series (after colour-vision simulation) before they count as confusable. */
64
+ minSeriesColorDifference: number;
65
+ }
66
+ interface AuditOptions {
67
+ /** Per-rule severity, or `off`. Keys are full ids (`audit/text-contrast`) or the bare name (`text-contrast`). */
68
+ rules?: Record<string, AuditSeverity | 'off'>;
69
+ /** Rules not to run (full ids or bare names). */
70
+ ignore?: readonly string[];
71
+ /** When set, run only these rules (full ids or bare names). */
72
+ only?: readonly string[];
73
+ /**
74
+ * Suppress a rule below a JSON Pointer prefix, for one accepted exception, for example
75
+ * `{rule: 'audit/text-contrast', path: '/slides/3'}`. A prefix matches whole path segments.
76
+ */
77
+ ignorePaths?: readonly {
78
+ rule: string;
79
+ path: string;
80
+ }[];
81
+ thresholds?: Partial<AuditThresholds>;
82
+ /** Loaded catalog records, consulted after the document's own `catalogs` and before the bundled ones. */
83
+ catalogs?: Partial<Record<CatalogKind, readonly unknown[]>>;
84
+ /**
85
+ * The host's measured fonts. Without it composition uses core's portable estimate; see docs/audit.md.
86
+ * A function receives the zero-based slide index, so a host can supply a script-aware measurement per slide.
87
+ */
88
+ textMeasurement?: TextMeasurement | ((slideIndex: number) => TextMeasurement | undefined);
89
+ /** Chart series palette (opaque `#RRGGBB`), in the engine's order. Default: the palette opf-render and opf-pptx use. */
90
+ chartPalette?: readonly string[];
91
+ }
92
+ interface AuditRuleInfo {
93
+ /** Stable `audit/<name>` id. */
94
+ id: string;
95
+ name: string;
96
+ category: AuditCategory;
97
+ /** Default severity. */
98
+ severity: AuditSeverity;
99
+ summary: string;
100
+ /** Why the rule exists. */
101
+ rationale: string;
102
+ /** The accessibility or design standard it follows, when there is one. */
103
+ standard?: string;
104
+ /** What the check cannot see or approximates. */
105
+ approximations?: string;
106
+ /** Threshold names (`AuditThresholds`) the rule reads. */
107
+ thresholds?: (keyof AuditThresholds)[];
108
+ }
109
+ interface AuditReport {
110
+ /** True when no finding has severity `error` (the same meaning as lint's `valid`). */
111
+ valid: boolean;
112
+ /** False when the document failed schema validation, so no design rule was run. */
113
+ documentValid: boolean;
114
+ diagnostics: AuditDiagnostic[];
115
+ counts: Record<AuditSeverity, number>;
116
+ /** Rule ids that ran, in registry order. */
117
+ rulesRun: string[];
118
+ slideCount: number;
119
+ thresholds: AuditThresholds;
120
+ checks: {
121
+ /** `estimated` unless the host supplied `textMeasurement`. */
122
+ textMeasurement: 'estimated' | 'provided';
123
+ /** Text contrast is computed against solid, gradient and pattern backgrounds; picture pixels are never read. */
124
+ backgroundPixels: 'not-read';
125
+ /** Image resolution reads only embedded `data:` images; URLs are never fetched. */
126
+ imageBytes: 'embedded-only';
127
+ nativeExport: 'not-checked';
128
+ };
129
+ }
130
+
131
+ /**
132
+ * The series palette opf-render and opf-pptx draw charts with (`CHART_COLORS`), in series order. A host with
133
+ * its own palette passes `AuditOptions.chartPalette`.
134
+ */
135
+ declare const DEFAULT_CHART_PALETTE: readonly string[];
136
+
137
+ /**
138
+ * Design and accessibility audit of a presentation: contrast, overflow, minimum type size, alt text,
139
+ * reading order, titles, fonts, links, charts, placeholders and more. Read-only and deterministic; no
140
+ * network, clock or model. Each rule has a stable `audit/<name>` id (see docs/audit.md).
141
+ */
142
+ declare const DEFAULT_AUDIT_THRESHOLDS: Readonly<AuditThresholds>;
143
+ /** Every rule the audit can report, with its default severity, rationale and approximations, in report order. */
144
+ declare const auditRules: readonly AuditRuleInfo[];
145
+ /** The rule a full id (`audit/text-contrast`) or bare name (`text-contrast`) names, or undefined. */
146
+ declare function findAuditRule(name: string): AuditRuleInfo | undefined;
147
+ /**
148
+ * Audit a parsed presentation. A document that fails schema validation is not audited; its validation
149
+ * errors come back as `audit/invalid-document` findings. Findings have lint's report shape (rule id,
150
+ * severity, JSON Pointer path, message, help) plus `slide`, `measured` and suggested `fixes`.
151
+ */
152
+ declare function auditPresentation(document: unknown, options?: AuditOptions): AuditReport;
153
+ /** Audit JSON source text. Findings carry original-source UTF-16 ranges and one-based line/column, like `lintSource`. */
154
+ declare function auditSource(source: string, options?: AuditOptions): AuditReport;
155
+
156
+ export { type AuditCategory, type AuditDiagnostic, type AuditFix, type AuditOptions, type AuditPatchOperation, type AuditReport, type AuditRuleInfo, type AuditSeverity, type AuditThresholds, DEFAULT_AUDIT_THRESHOLDS, DEFAULT_CHART_PALETTE, auditPresentation, auditRules, auditSource, findAuditRule };
package/dist/audit.js ADDED
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_AUDIT_THRESHOLDS, DEFAULT_CHART_PALETTE, auditPresentation, auditRules, auditSource, findAuditRule } from './chunk-JENJZR3F.js';
2
+ import './chunk-JVTN3HPP.js';
3
+ import './chunk-UQKJWHSS.js';
4
+ import './chunk-6TSDIPSH.js';
5
+ import './chunk-S5W34SIJ.js';
6
+ import './chunk-3ZGSWEHK.js';
7
+ import './chunk-RVN5C2FX.js';
@@ -1,4 +1,4 @@
1
- import { a as SchemaName } from './schemas-X_NniU4A.js';
1
+ import { a as SchemaName } from './schemas-BSjlG8-R.js';
2
2
 
3
3
  /**
4
4
  * Schema for audience records in the pptx.gallery library. Each record names an audience archetype (e.g. 'executives', 'engineering-team', 'investors') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Audiences are referenced from OPF documents via audience; the engine resolves the reference against catalogs.audiences (inline) → catalogs.audiences.source → the default catalog at https://www.pptx.gallery/audiences. The audience field also accepts free-form strings; this catalog is for engine-aware audience archetypes that carry generation hints.
@@ -1,2 +1,2 @@
1
- export { a as CatalogEntry, b as CatalogIndex, c as CatalogIndexRecord, C as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-CUClB3nl.js';
2
- import './schemas-X_NniU4A.js';
1
+ export { a as CatalogEntry, b as CatalogIndex, c as CatalogIndexRecord, C as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-u6TEeMcz.js';
2
+ import './schemas-BSjlG8-R.js';
package/dist/catalogs.js CHANGED
@@ -1 +1,2 @@
1
- export { audiences, catalogEntries, catalogIndexes, catalogKinds, catalogSchemaNames, catalogs, chartTypes, colorSchemes, fontSchemes, languages, layouts, narratives, purposes, socialPlatforms, themes, tones } from './chunk-AEUSVEUY.js';
1
+ import './chunk-S5W34SIJ.js';
2
+ export { audiences, catalogEntries, catalogIndexes, catalogKinds, catalogSchemaNames, catalogs, chartTypes, colorSchemes, fontSchemes, languages, layouts, narratives, purposes, socialPlatforms, themes, tones } from './chunk-3ZGSWEHK.js';
@@ -1,6 +1,6 @@
1
- import { assertValidPresentation, visitContentPayloads } from './chunk-JCXHSTSM.js';
2
- import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference, resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-BVLWFMDX.js';
3
- import { catalogs } from './chunk-AEUSVEUY.js';
1
+ import { assertValidPresentation } from './chunk-UQKJWHSS.js';
2
+ import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference, resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries, visitContentPayloads, sliceNumberedItems } from './chunk-6TSDIPSH.js';
3
+ import { catalogs } from './chunk-3ZGSWEHK.js';
4
4
 
5
5
  // src/pagination.ts
6
6
  var OPFPaginationError = class extends Error {
@@ -66,7 +66,8 @@ function leafFor(path, field, value) {
66
66
  return leaf;
67
67
  }
68
68
  function paginateSlide(input, options = {}) {
69
- assertValidPresentation({ slides: [input] });
69
+ const deckReferences = options.presentation?.references;
70
+ assertValidPresentation({ ...Array.isArray(deckReferences) ? { references: deckReferences } : {}, slides: [input] });
70
71
  let source = clone(input);
71
72
  const maxSlides = options.maxSlides ?? 100;
72
73
  if (!Number.isInteger(maxSlides) || maxSlides < 1 || maxSlides > 1e4) throw new RangeError("maxSlides must be an integer between 1 and 10000.");
@@ -95,6 +96,7 @@ function paginateSlide(input, options = {}) {
95
96
  return composeSlide(withReadability(slide, true), { ...options, slideNumber: (options.slideNumber ?? sourceIndex + 1) + pageIndex });
96
97
  };
97
98
  const initial = geometry(source);
99
+ const fit = (diagnostics) => diagnostics.filter((issue) => issue.code === "text-overflow" || issue.code === "small-cell" || !!initial.furniture?.diagnostics.includes(issue));
98
100
  const repeatedMappings = (pageIndex) => {
99
101
  if (!initial.furniture) return [];
100
102
  const paths = new Set(initial.items.filter((item) => headingFields.has(item.field)).map((item) => item.path));
@@ -104,7 +106,7 @@ function paginateSlide(input, options = {}) {
104
106
  }
105
107
  return [...paths].map((sourcePath) => ({ sourcePath, outputPath: sourcePath.startsWith(`${sourceBase}.`) ? `slides.${sourceIndex + pageIndex}${sourcePath.slice(sourceBase.length)}` : sourcePath }));
106
108
  };
107
- if (!initial.diagnostics.length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })), ...initial.furniture ? { repeatedMappings: repeatedMappings(0) } : {} }] };
109
+ if (!fit(initial.diagnostics).length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })), ...initial.furniture ? { repeatedMappings: repeatedMappings(0) } : {} }] };
108
110
  const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)) || initial.furniture?.diagnostics.includes(issue));
109
111
  if (headerIssues.length) throw new OPFPaginationError("Repeated headings or header/footer content cannot fit or resolve. Change the repeated content or slide design before pagination.", headerIssues);
110
112
  const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, item.value));
@@ -142,7 +144,7 @@ function paginateSlide(input, options = {}) {
142
144
  if (leafPaths.has(childPath)) {
143
145
  const portion = portions.get(childPath);
144
146
  if (portion) {
145
- result[key] = clone(portion.value);
147
+ result[key] = clone(portion.leaf.unit === "items" && portion.start > 0 && (key === "items" || key === "bullets") && node.numbering !== void 0 ? sliceNumberedItems(portion.leaf.value, node.numbering, portion.start, portion.end) : portion.value);
146
148
  hasContent = true;
147
149
  mappings.push({ sourcePath: childPath, outputPath, ...portion.leaf.unit ? { range: { unit: portion.leaf.unit, start: portion.start, end: portion.end } } : {} });
148
150
  }
@@ -170,15 +172,16 @@ function paginateSlide(input, options = {}) {
170
172
  if (pageIndex > 0) delete slide.notes;
171
173
  return { slide, mappings };
172
174
  };
173
- const diagnosticsFor = (portions) => geometry(project(portions, slides.length).slide, slides.length).diagnostics;
175
+ const diagnosticsFor = (portions) => fit(geometry(project(portions, slides.length).slide, slides.length).diagnostics);
174
176
  const finish = () => {
175
177
  if (!selected.size) return;
176
178
  if (slides.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
177
179
  const { slide, mappings } = project(selected, slides.length);
178
- const issues = geometry(slide, slides.length).diagnostics;
180
+ const issues = fit(geometry(slide, slides.length).diagnostics);
179
181
  if (issues.length) throw new OPFPaginationError("A continuation page does not fit at its final page number. No partial result was returned.", issues);
180
182
  assignPageIds(slide, slides.length);
181
- assertValidPresentation({ slides: [slide] });
183
+ const references = options.presentation?.references;
184
+ assertValidPresentation({ ...Array.isArray(references) ? { references } : {}, slides: [slide] });
182
185
  slides.push(slide);
183
186
  pages.push({ slideIndex: sourceIndex + slides.length - 1, mappings, ...initial.furniture ? { repeatedMappings: repeatedMappings(slides.length - 1) } : {} });
184
187
  selected = /* @__PURE__ */ new Map();
@@ -0,0 +1,198 @@
1
+ import { validatePresentation } from './chunk-UQKJWHSS.js';
2
+
3
+ // src/convert/shared.ts
4
+ var CONTENT_KEYS = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
5
+ var SLIDE_ONLY_KEYS = ["title", "subtitle", "tag", "layout", "design", "notes", "section", "hidden", "beat"];
6
+ var OPFConversionError = class extends Error {
7
+ code;
8
+ details;
9
+ constructor(code, message, details = {}) {
10
+ super(message);
11
+ this.name = "OPFConversionError";
12
+ this.code = code;
13
+ this.details = details;
14
+ }
15
+ };
16
+ var refuse = (message, details = {}) => new OPFConversionError("not-convertible", message, details);
17
+ var Loss = class {
18
+ list = [];
19
+ note(message) {
20
+ if (!this.list.includes(message)) this.list.push(message);
21
+ }
22
+ merge(other) {
23
+ for (const message of other) this.note(message);
24
+ }
25
+ };
26
+ var report = (loss) => ({ lossless: loss.length === 0, loss: [...loss] });
27
+ var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
28
+ var clone = (value) => structuredClone(value);
29
+ var own = (value, key) => Object.hasOwn(value, key);
30
+ var same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
31
+ function canonical(value) {
32
+ if (Array.isArray(value)) return value.map(canonical);
33
+ if (isRecord(value)) return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
34
+ return value;
35
+ }
36
+ var isSlideLike = (payload) => SLIDE_ONLY_KEYS.some((key) => own(payload, key));
37
+ var contentKeysOf = (payload) => CONTENT_KEYS.filter((key) => payload[key] !== void 0);
38
+ function assertValidOutput(value, as = "block") {
39
+ const slides = as === "slides" ? value : as === "slide" ? [value] : [{ blocks: [value] }];
40
+ const result = validatePresentation({ slides });
41
+ if (!result.valid) {
42
+ const first = result.errors[0];
43
+ throw new OPFConversionError("invalid-output", `The converted content is not valid OPF: ${first?.message ?? "unknown error"}${first?.path ? ` (${first.path})` : ""}.`, { issues: result.errors });
44
+ }
45
+ }
46
+ var assertValidOwner = (owner) => assertValidOutput(owner, isSlideLike(owner) ? "slide" : "block");
47
+ var FORMATTING = ["bold", "italic", "underline", "strikethrough", "color", "fontSize", "fontFamily", "link", "superscript", "subscript"];
48
+ var isPlainRun = (run) => typeof run === "string" || isRecord(run) && !FORMATTING.some((key) => run[key] !== void 0);
49
+ var runText = (run) => typeof run === "string" ? run : run.text ?? "";
50
+ var runsOf = (value) => typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
51
+ var plainOf = (value) => runsOf(value).map(runText).join("");
52
+ var hasFormatting = (value) => runsOf(value).some((run) => !isPlainRun(run));
53
+ var NEWLINE = /\r\n|\r|\n/;
54
+ function splitRuns(runs) {
55
+ const lines = [[]];
56
+ for (const run of runs) {
57
+ const parts = runText(run).split(NEWLINE);
58
+ parts.forEach((part, index) => {
59
+ if (index > 0) lines.push([]);
60
+ if (part === "") return;
61
+ lines[lines.length - 1].push(typeof run === "string" ? part : { ...run, text: part });
62
+ });
63
+ }
64
+ return lines;
65
+ }
66
+ function mergeRuns(runs) {
67
+ const merged = [];
68
+ for (const run of runs) {
69
+ if (typeof run === "string" && typeof merged.at(-1) === "string") merged[merged.length - 1] += run;
70
+ else merged.push(run);
71
+ }
72
+ return merged;
73
+ }
74
+ var linePlain = (line) => line.map(runText).join("");
75
+ var lineIsPlain = (line) => line.every(isPlainRun);
76
+ var lineValue = (line) => lineIsPlain(line) ? linePlain(line) : mergeRuns(line);
77
+ function joinLines(lines) {
78
+ if (lines.every(lineIsPlain)) return lines.map(linePlain).join("\n");
79
+ const runs = [];
80
+ lines.forEach((line, index) => {
81
+ if (index > 0) runs.push("\n");
82
+ runs.push(...line);
83
+ });
84
+ return mergeRuns(runs);
85
+ }
86
+ function concatText(a, b) {
87
+ if (typeof a === "string" && typeof b === "string") return a + b;
88
+ return mergeRuns([...runsOf(a), ...runsOf(b)]);
89
+ }
90
+
91
+ // src/convert/text-lines.ts
92
+ var LIST_PREFIX = /^([ \t]*)(?:([-*•+]|\d{1,3}[.)])[ \t]+)?/;
93
+ var indentWidth = (space) => [...space].reduce((width, char) => width + (char === " " ? 4 : 1), 0);
94
+ function parseListLine(line) {
95
+ const first = line[0];
96
+ if (first === void 0) return { line, indent: 0, numbered: false, marked: false };
97
+ const text = runText(first);
98
+ const match = LIST_PREFIX.exec(text);
99
+ const rest = text.slice(match[0].length);
100
+ const next = [...line];
101
+ if (rest === "") next.shift();
102
+ else next[0] = typeof first === "string" ? rest : { ...first, text: rest };
103
+ return { line: next, indent: indentWidth(match[1] ?? ""), numbered: /^\d/.test(match[2] ?? ""), marked: match[2] !== void 0 };
104
+ }
105
+ function levelsFromIndents(indents) {
106
+ const stack = [];
107
+ return indents.map((width) => {
108
+ while (stack.length && stack.at(-1) > width) stack.pop();
109
+ if (!stack.length || stack.at(-1) < width) stack.push(width);
110
+ return stack.length - 1;
111
+ });
112
+ }
113
+ var STRONG_DASH = /^\s*(?:—|–|―|--|~)\s*(\S.*?)\s*$/;
114
+ var WEAK_DASH = /^\s*-\s+(\S.*?)\s*$/;
115
+ var MAX_ATTRIBUTION = 120;
116
+ var OPEN_QUOTE = /^\s*[“"‘«„]/;
117
+ var INLINE_ATTRIBUTION = /^(.*[”"’»“])\s+[—–]\s+(\S.*?)\s*$/;
118
+ function parseQuoteLines(lines) {
119
+ const attributionOf = (line, body) => {
120
+ const strong = STRONG_DASH.exec(line);
121
+ const weak = strong ? void 0 : WEAK_DASH.exec(line);
122
+ const text = (strong ?? weak)?.[1];
123
+ if (!text || text.length > MAX_ATTRIBUTION) return void 0;
124
+ if (weak && body.length && body.every((entry) => /^\s*-\s+/.test(entry))) return void 0;
125
+ return text;
126
+ };
127
+ if (lines.length >= 2) {
128
+ const last = attributionOf(lines.at(-1), lines.slice(0, -1));
129
+ if (last !== void 0) {
130
+ if (lines.length >= 3) {
131
+ const middle = attributionOf(lines.at(-2), lines.slice(0, -2));
132
+ if (middle !== void 0) return { text: lines.slice(0, -2), attribution: middle, source: last };
133
+ }
134
+ return { text: lines.slice(0, -1), attribution: last };
135
+ }
136
+ }
137
+ const final = lines.at(-1);
138
+ if (final !== void 0) {
139
+ const inline = INLINE_ATTRIBUTION.exec(final);
140
+ if (inline && OPEN_QUOTE.test(lines[0]) && inline[2].length <= MAX_ATTRIBUTION && !/[—–]/.test(inline[2])) return { text: [...lines.slice(0, -1), inline[1]], attribution: inline[2] };
141
+ }
142
+ return { text: lines };
143
+ }
144
+ var MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\.?";
145
+ var YEAR = "(?:1[5-9]\\d\\d|2[01]\\d\\d)";
146
+ var WHEN = new RegExp(
147
+ `^(?:${[
148
+ `${YEAR}(?:-\\d{2}(?:-\\d{2})?)?`,
149
+ `${YEAR}[\u2013-]${YEAR}`,
150
+ `Q[1-4](?:\\s*(?:'\\d{2}|${YEAR}))?`,
151
+ `${YEAR}\\s*Q[1-4]`,
152
+ `H[12](?:\\s*${YEAR})?`,
153
+ `${YEAR}\\s*H[12]`,
154
+ `${MONTH}(?:\\s+\\d{1,2}(?:st|nd|rd|th)?)?(?:,?\\s+${YEAR})?`,
155
+ `\\d{1,2}\\s+${MONTH}(?:\\s+${YEAR})?`,
156
+ "(?:Week|Wk|Day|Month|Sprint|Phase|Step|Stage)\\s+\\d{1,3}"
157
+ ].join("|")})$`,
158
+ "i"
159
+ );
160
+ var WHEN_SEPARATOR = /\s*:\s+|\s+[–—-]\s+|\s*[—–]\s*/g;
161
+ var looksLikeWhen = (text) => text.length <= 24 && WHEN.test(text.trim());
162
+ function splitWhen(text, loose = false) {
163
+ for (const match of text.matchAll(WHEN_SEPARATOR)) {
164
+ const when = text.slice(0, match.index).trim();
165
+ const what = text.slice(match.index + match[0].length).trim();
166
+ if (!when || !what) continue;
167
+ if (looksLikeWhen(when) || loose && match[0].trim() === ":" && when.length <= 24) return { when, what };
168
+ }
169
+ return void 0;
170
+ }
171
+ var FENCE_OPEN = /^\s*(`{3,}|~{3,})[ \t]*([^`\r\n]*?)[ \t]*$/;
172
+ var fenceLine = (char, length) => new RegExp(`^\\s*${char === "`" ? "`" : "~"}{${length},}\\s*$`);
173
+ function parseCodeFence(lines) {
174
+ if (lines.length < 2) return void 0;
175
+ const open = FENCE_OPEN.exec(lines[0]);
176
+ if (!open) return void 0;
177
+ const fence = open[1];
178
+ const closing = fenceLine(fence[0], fence.length);
179
+ if (!closing.test(lines.at(-1))) return void 0;
180
+ const body = lines.slice(1, -1);
181
+ if (body.some((line) => closing.test(line))) return void 0;
182
+ const info = open[2] ?? "";
183
+ const result = { source: body.join("\n") };
184
+ const attribute = /(?:^|\s)(?:title|filename|file)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(info);
185
+ const filename = attribute ? attribute[1] ?? attribute[2] ?? attribute[3] : void 0;
186
+ const language = /^(?:title|filename|file)\s*=/.test(info) ? void 0 : /^\{?\.?([A-Za-z0-9_+#.-]+)/.exec(info)?.[1];
187
+ if (language) result.language = language;
188
+ if (filename) result.filename = filename;
189
+ return result;
190
+ }
191
+ function fenceCode(source, language, filename) {
192
+ const longest = Math.max(2, ...[...source.matchAll(/`+/g)].map((match) => match[0].length));
193
+ const fence = "`".repeat(longest + 1);
194
+ const info = [language, filename ? `title="${filename.replaceAll('"', "'")}"` : void 0].filter(Boolean).join(" ");
195
+ return [`${fence}${info}`, ...source.split(/\r\n|\r|\n/), fence];
196
+ }
197
+
198
+ export { CONTENT_KEYS, FORMATTING, Loss, OPFConversionError, assertValidOutput, assertValidOwner, clone, concatText, contentKeysOf, fenceCode, hasFormatting, isPlainRun, isRecord, isSlideLike, joinLines, levelsFromIndents, lineIsPlain, linePlain, lineValue, parseCodeFence, parseListLine, parseQuoteLines, plainOf, refuse, report, runText, runsOf, same, splitRuns, splitWhen };