@openpresentation/opf 0.3.0 → 0.4.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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/dist/catalogs-x1oFFQnd.d.ts +1198 -0
  4. package/dist/catalogs.d.ts +2 -32413
  5. package/dist/catalogs.js +1 -18505
  6. package/dist/chunk-75N5I6M4.js +229 -0
  7. package/dist/chunk-EMTMQRX4.js +1007 -0
  8. package/dist/chunk-GCXOU5MZ.js +6515 -0
  9. package/dist/chunk-HJNJHB5H.js +454 -0
  10. package/dist/chunk-OZULJBSG.js +3368 -0
  11. package/dist/chunk-PIQXLBBJ.js +445 -0
  12. package/dist/chunk-SOSOBKQ2.js +145 -0
  13. package/dist/chunk-TWMRZ43O.js +18576 -0
  14. package/dist/composition.d.ts +190 -0
  15. package/dist/composition.js +1 -0
  16. package/dist/data.d.ts +32 -0
  17. package/dist/data.js +1 -0
  18. package/dist/docs.js +69 -5
  19. package/dist/examples.d.ts +1 -1
  20. package/dist/examples.js +112 -2
  21. package/dist/index.d.ts +6 -4
  22. package/dist/index.js +8 -29555
  23. package/dist/pagination.d.ts +52 -0
  24. package/dist/pagination.js +5 -0
  25. package/dist/{presentation-C8BOK0-5.d.ts → presentation-BjP4yTQ1.d.ts} +86 -3
  26. package/dist/previews.js +1 -447
  27. package/dist/repo-readme.js +1 -3
  28. package/dist/schemas-C1DxapX2.d.ts +206 -0
  29. package/dist/schemas.d.ts +1 -12373
  30. package/dist/schemas.js +1 -6275
  31. package/dist/spec/README.md +18 -3
  32. package/dist/spec/catalogs/audiences/index.json +1 -1
  33. package/dist/spec/catalogs/chart-types/index.json +1 -1
  34. package/dist/spec/catalogs/color-schemes/index.json +1 -1
  35. package/dist/spec/catalogs/font-schemes/index.json +1 -1
  36. package/dist/spec/catalogs/languages/index.json +1 -1
  37. package/dist/spec/catalogs/layouts/chart-1x.json +4 -1
  38. package/dist/spec/catalogs/layouts/chart-2x.json +5 -1
  39. package/dist/spec/catalogs/layouts/chart-3x.json +5 -1
  40. package/dist/spec/catalogs/layouts/code-1x.json +4 -1
  41. package/dist/spec/catalogs/layouts/extract/canonical.json +94 -21
  42. package/dist/spec/catalogs/layouts/image-1x.json +4 -1
  43. package/dist/spec/catalogs/layouts/image-2x.json +5 -1
  44. package/dist/spec/catalogs/layouts/image-3x.json +5 -1
  45. package/dist/spec/catalogs/layouts/index.json +1 -1
  46. package/dist/spec/catalogs/layouts/list-2x.json +4 -1
  47. package/dist/spec/catalogs/layouts/list-3x.json +4 -1
  48. package/dist/spec/catalogs/layouts/list-4x.json +5 -1
  49. package/dist/spec/catalogs/layouts/list-5x.json +5 -1
  50. package/dist/spec/catalogs/layouts/list-6x.json +5 -1
  51. package/dist/spec/catalogs/layouts/media-1x.json +4 -1
  52. package/dist/spec/catalogs/layouts/number-2x.json +4 -1
  53. package/dist/spec/catalogs/layouts/number-3x.json +4 -1
  54. package/dist/spec/catalogs/layouts/number-4x.json +5 -1
  55. package/dist/spec/catalogs/layouts/number-5x.json +5 -1
  56. package/dist/spec/catalogs/layouts/number-6x.json +5 -1
  57. package/dist/spec/catalogs/layouts/table-1x.json +4 -1
  58. package/dist/spec/catalogs/layouts/text-2x.json +4 -1
  59. package/dist/spec/catalogs/layouts/text-3x.json +4 -1
  60. package/dist/spec/catalogs/narratives/index.json +2 -2
  61. package/dist/spec/catalogs/purposes/index.json +1 -1
  62. package/dist/spec/catalogs/social-platforms/index.json +1 -1
  63. package/dist/spec/catalogs/themes/index.json +1 -1
  64. package/dist/spec/catalogs/tones/index.json +1 -1
  65. package/dist/spec/openapi.yaml +54 -14
  66. package/dist/spec/previews/layouts/index.json +71 -71
  67. package/dist/spec/schemas/catalog-index.schema.json +53 -0
  68. package/dist/spec/schemas/layout-preview-index.schema.json +54 -0
  69. package/dist/spec/schemas/layout.schema.json +63 -0
  70. package/dist/spec/schemas/opf.schema.json +182 -3
  71. package/dist/spec-files.d.ts +14 -2391
  72. package/dist/spec-files.js +1 -3356
  73. package/dist/types.d.ts +3 -1102
  74. package/dist/types.js +0 -2
  75. package/dist/validator.d.ts +3 -10
  76. package/dist/validator.js +4 -7243
  77. package/package.json +16 -3
  78. package/dist/catalogs.js.map +0 -1
  79. package/dist/docs.js.map +0 -1
  80. package/dist/examples.js.map +0 -1
  81. package/dist/index.js.map +0 -1
  82. package/dist/previews.js.map +0 -1
  83. package/dist/repo-readme.js.map +0 -1
  84. package/dist/schemas.js.map +0 -1
  85. package/dist/spec-files.js.map +0 -1
  86. package/dist/types.js.map +0 -1
  87. package/dist/validator.js.map +0 -1
@@ -0,0 +1,229 @@
1
+ import { assertValidPresentation } from './chunk-EMTMQRX4.js';
2
+ import { resolveFontFamilies, resolveCanvasDimensions, composeSlide } from './chunk-HJNJHB5H.js';
3
+ import { catalogs } from './chunk-TWMRZ43O.js';
4
+
5
+ // src/pagination.ts
6
+ var OPFPaginationError = class extends Error {
7
+ constructor(message, diagnostics = []) {
8
+ super(message);
9
+ this.diagnostics = diagnostics;
10
+ this.name = "OPFPaginationError";
11
+ }
12
+ diagnostics;
13
+ code = "pagination-unresolved";
14
+ };
15
+ var contentFields = /* @__PURE__ */ new Set(["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"]);
16
+ var headingFields = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
17
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
18
+ var clone = (value) => structuredClone(value);
19
+ var textOf = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.map(textOf).join("") : value?.text ?? "";
20
+ var graphemes = new Intl.Segmenter("und", { granularity: "grapheme" });
21
+ function sliceRichText(value, start, end) {
22
+ if (typeof value === "string") return value.slice(start, end);
23
+ let offset = 0;
24
+ return value.flatMap((run) => {
25
+ const text = textOf(run), left = Math.max(0, start - offset), right = Math.min(text.length, end - offset);
26
+ offset += text.length;
27
+ if (right <= left) return [];
28
+ return [typeof run === "string" ? text.slice(left, right) : { ...run, text: text.slice(left, right) }];
29
+ });
30
+ }
31
+ function leafFor(path, field, value) {
32
+ const leaf = { path, field, value };
33
+ let text;
34
+ if (field === "text") {
35
+ text = textOf(value);
36
+ leaf.slice = (a, b) => sliceRichText(value, a, b);
37
+ }
38
+ if (field === "code" || field === "quote") {
39
+ const key = field === "code" ? "source" : "text";
40
+ text = typeof value === "string" ? value : value[key];
41
+ leaf.slice = (a, b) => typeof value === "string" ? value.slice(a, b) : { ...value, [key]: text.slice(a, b) };
42
+ }
43
+ if (text !== void 0) {
44
+ leaf.unit = "utf16";
45
+ leaf.boundaries = [0, ...Array.from(graphemes.segment(text), (segment) => segment.index + segment.segment.length)];
46
+ } else if (field === "items" || field === "bullets") {
47
+ leaf.unit = "items";
48
+ leaf.boundaries = Array.from({ length: value.length + 1 }, (_, i) => i);
49
+ leaf.slice = (a, b) => value.slice(a, b);
50
+ } else if (field === "table" || field === "timeline") {
51
+ const key = field === "table" ? "rows" : "events";
52
+ const values = Array.isArray(value) ? value : value[key];
53
+ if (Array.isArray(values)) {
54
+ leaf.unit = "items";
55
+ leaf.boundaries = Array.from({ length: values.length + 1 }, (_, i) => i);
56
+ leaf.slice = (a, b) => Array.isArray(value) ? value.slice(a, b) : { ...value, [key]: values.slice(a, b) };
57
+ }
58
+ }
59
+ return leaf;
60
+ }
61
+ function paginateSlide(input, options = {}) {
62
+ assertValidPresentation({ slides: [input] });
63
+ const source = clone(input);
64
+ const maxSlides = options.maxSlides ?? 100;
65
+ if (!Number.isInteger(maxSlides) || maxSlides < 1 || maxSlides > 1e4) throw new RangeError("maxSlides must be an integer between 1 and 10000.");
66
+ const minFontSize = options.minFontSize ?? 24;
67
+ if (!Number.isFinite(minFontSize) || minFontSize < 8 || minFontSize > 32) throw new RangeError("Pagination minFontSize must be between 8 and 32.");
68
+ const sourceIndex = options.slideIndex ?? 0, sourceBase = `slides.${sourceIndex}`;
69
+ const warnOnly = (slide) => {
70
+ const result = clone(slide);
71
+ const visit = (node) => {
72
+ if (node.composition) node.composition = { ...node.composition, overflow: "warn", minFontSize: Math.max(minFontSize, node.composition.minFontSize ?? 16) };
73
+ if (Array.isArray(node.blocks)) node.blocks.forEach(visit);
74
+ };
75
+ visit(result);
76
+ for (const [key, value] of Object.entries(result)) if (isRecord(value) && /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key)) visit(value);
77
+ result.composition = { ...result.composition, overflow: "warn", minFontSize: Math.max(minFontSize, result.composition?.minFontSize ?? options.layout?.composition?.minFontSize ?? 16) };
78
+ return result;
79
+ };
80
+ let evaluations = 0;
81
+ const geometry = (slide) => {
82
+ if (++evaluations > 2e4) throw new OPFPaginationError("Pagination exceeded its layout evaluation limit. Split the input into smaller sections.");
83
+ return composeSlide(warnOnly(slide), options);
84
+ };
85
+ const initial = geometry(source);
86
+ if (!initial.diagnostics.length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })) }] };
87
+ const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)));
88
+ if (headerIssues.length) throw new OPFPaginationError("The repeated heading does not fit. Shorten it or change the slide design before pagination.", headerIssues);
89
+ const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, item.value));
90
+ const leafPaths = new Set(leaves.map((leaf) => leaf.path));
91
+ const slides = [], pages = [];
92
+ let selected = /* @__PURE__ */ new Map();
93
+ const reservedIds = new Set(options.reservedIds ?? []);
94
+ if (typeof source.id === "string") reservedIds.add(source.id);
95
+ const project = (portions, pageIndex) => {
96
+ const mappings = [];
97
+ const visit = (node, path, out, root = false) => {
98
+ const result = {};
99
+ let hasContent = false;
100
+ for (const [key, value] of Object.entries(node)) {
101
+ const childPath = `${path}.${key}`, outputPath = `${out}.${key}`;
102
+ if (leafPaths.has(childPath)) {
103
+ const portion = portions.get(childPath);
104
+ if (portion) {
105
+ result[key] = clone(portion.value);
106
+ hasContent = true;
107
+ mappings.push({ sourcePath: childPath, outputPath, ...portion.leaf.unit ? { range: { unit: portion.leaf.unit, start: portion.start, end: portion.end } } : {} });
108
+ }
109
+ } else if (key === "blocks" && Array.isArray(value)) {
110
+ const children = [];
111
+ value.forEach((child, index) => {
112
+ const projected = visit(child, `${childPath}.${index}`, `${outputPath}.${children.length}`);
113
+ if (projected) children.push(projected);
114
+ });
115
+ if (children.length) {
116
+ result.blocks = children;
117
+ hasContent = true;
118
+ }
119
+ } else if (root && isRecord(value) && /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key)) {
120
+ const child = visit(value, childPath, outputPath);
121
+ if (child) {
122
+ result[key] = child;
123
+ hasContent = true;
124
+ }
125
+ } else if (!contentFields.has(key)) result[key] = clone(value);
126
+ }
127
+ return root || hasContent ? result : void 0;
128
+ };
129
+ const slide = visit(source, sourceBase, `slides.${sourceIndex + pageIndex}`, true);
130
+ if (pageIndex > 0) delete slide.notes;
131
+ return { slide, mappings };
132
+ };
133
+ const diagnosticsFor = (portions) => geometry(project(portions, slides.length).slide).diagnostics;
134
+ const finish = () => {
135
+ if (!selected.size) return;
136
+ if (slides.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
137
+ const { slide, mappings } = project(selected, slides.length);
138
+ if (slides.length && typeof source.id === "string") {
139
+ let suffix = slides.length + 1, id = `${source.id}--${suffix}`;
140
+ while (reservedIds.has(id)) id = `${source.id}--${++suffix}`;
141
+ slide.id = id;
142
+ reservedIds.add(id);
143
+ }
144
+ assertValidPresentation({ slides: [slide] });
145
+ slides.push(slide);
146
+ pages.push({ slideIndex: sourceIndex + slides.length - 1, mappings });
147
+ selected = /* @__PURE__ */ new Map();
148
+ };
149
+ for (const leaf of leaves) {
150
+ const boundaries = leaf.boundaries ?? [0, 1];
151
+ const end = boundaries.at(-1);
152
+ let start = 0;
153
+ do {
154
+ const make = (limit) => ({ leaf, start, end: limit, value: leaf.slice ? leaf.slice(start, limit) : leaf.value });
155
+ let candidate = new Map(selected).set(leaf.path, make(end));
156
+ let issues = diagnosticsFor(candidate);
157
+ if (!issues.length) {
158
+ selected = candidate;
159
+ break;
160
+ }
161
+ let best = start;
162
+ if (leaf.slice && end > start) {
163
+ let low = boundaries.findIndex((value) => value > start), high = boundaries.length - 2;
164
+ while (low <= high) {
165
+ const mid = Math.floor((low + high) / 2), limit = boundaries[mid];
166
+ candidate = new Map(selected).set(leaf.path, make(limit));
167
+ if (!diagnosticsFor(candidate).length) {
168
+ best = limit;
169
+ low = mid + 1;
170
+ } else high = mid - 1;
171
+ }
172
+ if (best > start && leaf.unit === "utf16" && leaf.field !== "code") {
173
+ const text = textOf(leaf.field === "quote" ? typeof leaf.value === "string" ? leaf.value : leaf.value.text : leaf.value);
174
+ const prefix = text.slice(start, best);
175
+ const sentences = [...prefix.matchAll(/[.!?][”"')]*\s+|\n\s*\n/gu)];
176
+ const lastSentence = sentences.at(-1);
177
+ const sentenceEnd = lastSentence ? start + lastSentence.index + lastSentence[0].length : start;
178
+ const match = /\s+\S*$/u.exec(prefix);
179
+ const wordEnd = match ? start + match.index + match[0].length - match[0].trimStart().length : best;
180
+ const preferred = sentenceEnd > start + (best - start) * 0.6 ? sentenceEnd : wordEnd;
181
+ if (preferred > start && boundaries.includes(preferred) && !diagnosticsFor(new Map(selected).set(leaf.path, make(preferred))).length) best = preferred;
182
+ }
183
+ }
184
+ if (best > start) {
185
+ selected.set(leaf.path, make(best));
186
+ finish();
187
+ start = best;
188
+ } else if (selected.size) finish();
189
+ else throw new OPFPaginationError(`Content at ${leaf.path} cannot fit on an otherwise empty slide. Change its layout or split an atomic item.`, issues);
190
+ } while (start < end);
191
+ }
192
+ finish();
193
+ if (!slides.length) throw new OPFPaginationError("No body content can be paginated.", initial.diagnostics);
194
+ return { slides, pages };
195
+ }
196
+ function paginatePresentation(input, options = {}) {
197
+ assertValidPresentation(input);
198
+ const presentation = clone(input);
199
+ const maxSlides = options.maxSlides ?? 100;
200
+ if (!Number.isInteger(maxSlides) || maxSlides < 1 || maxSlides > 1e4) throw new RangeError("maxSlides must be an integer between 1 and 10000.");
201
+ const output = [], pages = [];
202
+ const reservedIds = presentation.slides.map((slide) => slide.id).filter(Boolean);
203
+ const resolve = (kind, id) => presentation.catalogs?.[kind]?.records?.find((record) => record.id === id) ?? catalogs[kind].find((record) => record.id === id);
204
+ presentation.slides.forEach((slide, index) => {
205
+ const overrides = options.slideOptions?.(slide, index) ?? {};
206
+ const layout = overrides.layout ?? (slide.layout ? resolve("layouts", slide.layout) : void 0);
207
+ if (slide.layout && !layout) throw new OPFPaginationError(`Layout '${slide.layout}' must be supplied inline or resolved by the host before pagination.`);
208
+ const design = { ...presentation.design, ...slide.design };
209
+ const reference = design.theme ?? "minimal";
210
+ const theme = typeof reference === "string" ? resolve("themes", reference) : { ...resolve("themes", reference.id), ...reference };
211
+ if (!theme) throw new OPFPaginationError(`Theme '${reference}' must be supplied inline before pagination.`);
212
+ if (output.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
213
+ const fontReference = design.fontScheme ?? theme.fontScheme ?? "roboto";
214
+ const fontScheme = typeof fontReference === "string" ? resolve("fontSchemes", fontReference) : { ...resolve("fontSchemes", fontReference.id), ...fontReference };
215
+ const fonts = resolveFontFamilies(fontScheme);
216
+ const result = paginateSlide(slide, { ...resolveCanvasDimensions(design.dimensions ?? theme.dimensions), layout, fonts, textMeasurement: options.textMeasurement, ...overrides, slideIndex: index, maxSlides: maxSlides - output.length, minFontSize: options.minFontSize, reservedIds });
217
+ const outputStart = output.length;
218
+ result.pages.forEach((page, pageIndex) => {
219
+ pages.push({ sourceSlideIndex: index, slideIndex: outputStart + pageIndex, mappings: page.mappings.map((mapping) => ({ ...mapping, outputPath: mapping.outputPath.replace(/^slides\.\d+/, `slides.${outputStart + pageIndex}`) })) });
220
+ });
221
+ output.push(...result.slides);
222
+ reservedIds.push(...result.slides.map((slide2) => slide2.id).filter(Boolean));
223
+ });
224
+ presentation.slides = output;
225
+ assertValidPresentation(presentation);
226
+ return { presentation, pages };
227
+ }
228
+
229
+ export { OPFPaginationError, paginatePresentation, paginateSlide };