@texuli/deck 0.0.0-stage → 0.5.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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/dist/authoring/builder.d.ts +351 -0
  4. package/dist/authoring/builder.js +64 -0
  5. package/dist/authoring/index.d.ts +6 -0
  6. package/dist/authoring/index.js +6 -0
  7. package/dist/authoring/lint.d.ts +36 -0
  8. package/dist/authoring/lint.js +325 -0
  9. package/dist/authoring/outlines.d.ts +34 -0
  10. package/dist/authoring/outlines.js +255 -0
  11. package/dist/authoring/patch.d.ts +23 -0
  12. package/dist/authoring/patch.js +166 -0
  13. package/dist/authoring/select-layouts.d.ts +19 -0
  14. package/dist/authoring/select-layouts.js +90 -0
  15. package/dist/engine/auto-slide.d.ts +32 -0
  16. package/dist/engine/auto-slide.js +67 -0
  17. package/dist/engine/canvas.d.ts +56 -0
  18. package/dist/engine/canvas.js +48 -0
  19. package/dist/engine/controller.d.ts +126 -0
  20. package/dist/engine/controller.js +248 -0
  21. package/dist/engine/controls.d.ts +128 -0
  22. package/dist/engine/controls.js +460 -0
  23. package/dist/engine/hash.d.ts +42 -0
  24. package/dist/engine/hash.js +86 -0
  25. package/dist/engine/index.d.ts +88 -0
  26. package/dist/engine/index.js +88 -0
  27. package/dist/engine/messages.d.ts +138 -0
  28. package/dist/engine/messages.js +140 -0
  29. package/dist/engine/motion.d.ts +64 -0
  30. package/dist/engine/motion.js +112 -0
  31. package/dist/engine/navigation.d.ts +87 -0
  32. package/dist/engine/navigation.js +156 -0
  33. package/dist/engine/presenter.d.ts +58 -0
  34. package/dist/engine/presenter.js +157 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +9 -0
  37. package/dist/markdown/attributes.d.ts +24 -0
  38. package/dist/markdown/attributes.js +94 -0
  39. package/dist/markdown/attrs-closed.d.ts +52 -0
  40. package/dist/markdown/attrs-closed.js +520 -0
  41. package/dist/markdown/blocks.d.ts +112 -0
  42. package/dist/markdown/blocks.js +766 -0
  43. package/dist/markdown/frontmatter.d.ts +30 -0
  44. package/dist/markdown/frontmatter.js +165 -0
  45. package/dist/markdown/index.d.ts +29 -0
  46. package/dist/markdown/index.js +29 -0
  47. package/dist/markdown/issues.d.ts +68 -0
  48. package/dist/markdown/issues.js +83 -0
  49. package/dist/markdown/json.d.ts +21 -0
  50. package/dist/markdown/json.js +236 -0
  51. package/dist/markdown/limits.d.ts +32 -0
  52. package/dist/markdown/limits.js +32 -0
  53. package/dist/markdown/parse.d.ts +31 -0
  54. package/dist/markdown/parse.js +283 -0
  55. package/dist/markdown/scan.d.ts +26 -0
  56. package/dist/markdown/scan.js +138 -0
  57. package/dist/markdown/slides.d.ts +32 -0
  58. package/dist/markdown/slides.js +854 -0
  59. package/dist/markdown/text.d.ts +76 -0
  60. package/dist/markdown/text.js +447 -0
  61. package/dist/markdown/twin.d.ts +41 -0
  62. package/dist/markdown/twin.js +374 -0
  63. package/dist/markdown/write.d.ts +17 -0
  64. package/dist/markdown/write.js +688 -0
  65. package/dist/math/index.d.ts +14 -0
  66. package/dist/math/index.js +14 -0
  67. package/dist/math/parse.d.ts +9 -0
  68. package/dist/math/parse.js +648 -0
  69. package/dist/math/symbols.d.ts +44 -0
  70. package/dist/math/symbols.js +333 -0
  71. package/dist/math/types.d.ts +59 -0
  72. package/dist/math/types.js +69 -0
  73. package/dist/model/backgrounds/contrast.d.ts +77 -0
  74. package/dist/model/backgrounds/contrast.js +186 -0
  75. package/dist/model/backgrounds/index.d.ts +7 -0
  76. package/dist/model/backgrounds/index.js +7 -0
  77. package/dist/model/backgrounds/resolve.d.ts +86 -0
  78. package/dist/model/backgrounds/resolve.js +215 -0
  79. package/dist/model/backgrounds/schema.d.ts +178 -0
  80. package/dist/model/backgrounds/schema.js +203 -0
  81. package/dist/model/codes.d.ts +73 -0
  82. package/dist/model/codes.js +67 -0
  83. package/dist/model/deck.d.ts +392 -0
  84. package/dist/model/deck.js +338 -0
  85. package/dist/model/index.d.ts +19 -0
  86. package/dist/model/index.js +19 -0
  87. package/dist/model/layouts/catalog.d.ts +433 -0
  88. package/dist/model/layouts/catalog.js +495 -0
  89. package/dist/model/layouts/index.d.ts +5 -0
  90. package/dist/model/layouts/index.js +5 -0
  91. package/dist/model/layouts/props.d.ts +302 -0
  92. package/dist/model/layouts/props.js +272 -0
  93. package/dist/model/loader.d.ts +37 -0
  94. package/dist/model/loader.js +317 -0
  95. package/dist/model/migrate.d.ts +34 -0
  96. package/dist/model/migrate.js +453 -0
  97. package/dist/model/motion/index.d.ts +6 -0
  98. package/dist/model/motion/index.js +6 -0
  99. package/dist/model/motion/transition.d.ts +96 -0
  100. package/dist/model/motion/transition.js +127 -0
  101. package/dist/model/parse.d.ts +57 -0
  102. package/dist/model/parse.js +307 -0
  103. package/dist/model/plain.d.ts +40 -0
  104. package/dist/model/plain.js +275 -0
  105. package/dist/model/shared.d.ts +63 -0
  106. package/dist/model/shared.js +86 -0
  107. package/dist/model/steps/index.d.ts +7 -0
  108. package/dist/model/steps/index.js +7 -0
  109. package/dist/model/steps/phase.d.ts +34 -0
  110. package/dist/model/steps/phase.js +50 -0
  111. package/dist/model/steps/slide-steps.d.ts +129 -0
  112. package/dist/model/steps/slide-steps.js +256 -0
  113. package/dist/model/style/index.d.ts +8 -0
  114. package/dist/model/style/index.js +8 -0
  115. package/dist/model/style/resolve.d.ts +87 -0
  116. package/dist/model/style/resolve.js +174 -0
  117. package/dist/model/style/schema.d.ts +85 -0
  118. package/dist/model/style/schema.js +53 -0
  119. package/dist/model/style/theme.d.ts +69 -0
  120. package/dist/model/style/theme.js +84 -0
  121. package/dist/schema/builders.d.ts +146 -0
  122. package/dist/schema/builders.js +285 -0
  123. package/dist/schema/diagnostics.d.ts +78 -0
  124. package/dist/schema/diagnostics.js +81 -0
  125. package/dist/schema/index.d.ts +15 -0
  126. package/dist/schema/index.js +14 -0
  127. package/dist/schema/internal.d.ts +40 -0
  128. package/dist/schema/internal.js +209 -0
  129. package/dist/schema/json-schema.d.ts +12 -0
  130. package/dist/schema/json-schema.js +213 -0
  131. package/dist/schema/types.d.ts +166 -0
  132. package/dist/schema/types.js +1 -0
  133. package/dist/schema/validate.d.ts +51 -0
  134. package/dist/schema/validate.js +765 -0
  135. package/dist/templates/index.d.ts +2 -0
  136. package/dist/templates/index.js +2 -0
  137. package/dist/templates/templates.d.ts +11 -0
  138. package/dist/templates/templates.js +47 -0
  139. package/package.json +60 -4
@@ -0,0 +1,453 @@
1
+ /**
2
+ * `migrateDeck`: convert an earlier deck to model v2 (model-v2 §3.10). The
3
+ * input shape is detected, not read from a version number:
4
+ *
5
+ * - **v2** (`formatVersion` present): returned unchanged.
6
+ * - **Texuli v1** (`meta` + `slides[].blocks`, optional `masters` and
7
+ * `settings`): every slide becomes a lossless free slide with its master;
8
+ * `settings` moves to `style` and `navigation`, `startIndex` to a start id.
9
+ * - **Texuli grammar tree** (`layout-deck` with `deck-slide` or legacy
10
+ * `layout-card` children, or a `slides[]` grammar): block nodes become
11
+ * blocks, `charts-*` nodes chart blocks, other catalog nodes component items.
12
+ * - **Protoyard** (`style` + `slides` with `layout`, no `meta`): a title is
13
+ * derived, `paletteSeed` becomes a seed theme, ChartSpec becomes a chart
14
+ * block body, unknown background slugs become `solid-color`.
15
+ *
16
+ * The result is not validated (run `parseDeck`); every conversion that is a
17
+ * judgement call is reported as a `migrated` warning. Idempotent, pure, and
18
+ * it never throws. Lifting a master to a catalog layout is a lint
19
+ * suggestion, never done here.
20
+ */
21
+ import { createDiagnostic } from "../schema/index.js";
22
+ import { isDeckBackgroundPreset } from "./backgrounds/schema.js";
23
+ import { DECK_MODEL_LIMITS } from "./deck.js";
24
+ import { MEDIA_SRC_PATTERN } from "./shared.js";
25
+ import { slideSteps } from "./steps/slide-steps.js";
26
+ export const DECK_SOURCES = ['v2', 'texuli-v1', 'texuli-grammar', 'protoyard', 'unknown'];
27
+ function isBag(value) {
28
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+ const MEDIA_SRC = new RegExp(MEDIA_SRC_PATTERN, 'u');
31
+ /** The chart kinds with inline `series`/`cells`/`points` in the Texuli chart block. */
32
+ const INLINE_CHART_KINDS = [
33
+ 'line',
34
+ 'area',
35
+ 'bar',
36
+ 'stacked-bar',
37
+ 'combo',
38
+ 'heatmap',
39
+ 'scatter',
40
+ 'bubble',
41
+ ];
42
+ const GRAMMAR_ROOT = 'layout-deck';
43
+ const GRAMMAR_SLIDE = 'deck-slide';
44
+ const GRAMMAR_CARD = 'layout-card';
45
+ const DEFAULT_TITLE = 'Untitled deck';
46
+ /** Which earlier shape `input` has (see the module comment). */
47
+ export function detectDeckShape(input) {
48
+ if (!isBag(input))
49
+ return 'unknown';
50
+ if (Object.hasOwn(input, 'formatVersion'))
51
+ return 'v2';
52
+ if (input.id === GRAMMAR_ROOT)
53
+ return 'texuli-grammar';
54
+ if (!Array.isArray(input.slides))
55
+ return 'unknown';
56
+ if (isBag(input.meta))
57
+ return 'texuli-v1';
58
+ const slides = input.slides;
59
+ if (isBag(input.style) ||
60
+ slides.some((slide) => isBag(slide) && typeof slide.layout === 'string')) {
61
+ return 'protoyard';
62
+ }
63
+ return 'unknown';
64
+ }
65
+ class Notes {
66
+ issues = [];
67
+ warn(message, path = [], fix) {
68
+ if (this.issues.length >= 200)
69
+ return;
70
+ this.issues.push(createDiagnostic('migrated', message, path, {
71
+ severity: 'warning',
72
+ ...(fix === undefined ? {} : { fix }),
73
+ }));
74
+ }
75
+ }
76
+ function clip(value, max) {
77
+ if (typeof value !== 'string')
78
+ return undefined;
79
+ const trimmed = value.trim();
80
+ if (trimmed.length === 0)
81
+ return undefined;
82
+ const points = Array.from(trimmed);
83
+ return points.length > max ? points.slice(0, max).join('') : trimmed;
84
+ }
85
+ function assign(target, key, value) {
86
+ if (value !== undefined)
87
+ target[key] = value;
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Texuli v1
91
+ /**
92
+ * A catalog theme without a mode becomes the string shorthand, so every
93
+ * authoring form (JSON, Markdown `theme:`) round-trips it unchanged.
94
+ */
95
+ function shorthandTheme(theme) {
96
+ if (theme === null || typeof theme !== 'object' || Array.isArray(theme))
97
+ return theme;
98
+ const ref = theme;
99
+ const keys = Object.keys(ref);
100
+ if (ref.kind === 'catalog' &&
101
+ typeof ref.id === 'string' &&
102
+ keys.every((key) => key === 'kind' || key === 'id')) {
103
+ return ref.id;
104
+ }
105
+ return theme;
106
+ }
107
+ const V1_SLIDE_KEYS = ['id', 'title', 'notes', 'master', 'themeRef', 'transition', 'blocks'];
108
+ function fromTexuliV1(input, notes) {
109
+ const meta = isBag(input.meta) ? input.meta : {};
110
+ const settings = isBag(input.settings) ? input.settings : {};
111
+ const slidesIn = Array.isArray(input.slides) ? input.slides : [];
112
+ const outMeta = {};
113
+ assign(outMeta, 'title', meta.title);
114
+ assign(outMeta, 'description', meta.description);
115
+ assign(outMeta, 'locale', meta.locale);
116
+ const style = {};
117
+ assign(style, 'aspect', meta.aspect);
118
+ assign(style, 'theme', shorthandTheme(meta.themeRef));
119
+ if (settings.showProgress === false)
120
+ style.progress = 'none';
121
+ if (settings.defaultTransition !== undefined && settings.defaultTransition !== 'fade') {
122
+ style.transition = settings.defaultTransition;
123
+ }
124
+ const navigation = {};
125
+ const startIndex = settings.startIndex;
126
+ if (typeof startIndex === 'number' && Number.isInteger(startIndex) && startIndex > 0) {
127
+ const target = slidesIn[startIndex];
128
+ if (isBag(target) && typeof target.id === 'string')
129
+ navigation.start = target.id;
130
+ else
131
+ notes.warn(`settings.startIndex ${startIndex} names no slide; the deck starts at the first.`, ['navigation']);
132
+ }
133
+ if (settings.loop === true)
134
+ navigation.loop = true;
135
+ const slides = slidesIn.map((slide, i) => {
136
+ if (!isBag(slide))
137
+ return slide;
138
+ const path = ['slides', i];
139
+ const out = {};
140
+ assign(out, 'id', slide.id);
141
+ assign(out, 'title', slide.title);
142
+ assign(out, 'notes', slide.notes);
143
+ assign(out, 'theme', shorthandTheme(slide.themeRef));
144
+ assign(out, 'transition', slide.transition);
145
+ out.layout = 'free';
146
+ assign(out, 'master', slide.master);
147
+ // An empty v1 slide stays empty: v2 free slides may have no blocks, and
148
+ // the stage already shows the slide title.
149
+ const blocks = Array.isArray(slide.blocks) ? slide.blocks : [];
150
+ out.blocks = blocks;
151
+ const dropped = Object.keys(slide).filter((key) => !V1_SLIDE_KEYS.includes(key));
152
+ if (dropped.length > 0) {
153
+ notes.warn(`Dropped keys v1 ignored: ${dropped.slice(0, 8).join(', ')}.`, path);
154
+ }
155
+ const steps = slideSteps(out);
156
+ const renumbered = steps.steps.some((step) => step.fragments.some((fragment) => fragment.authored > 0 && fragment.authored !== step.index));
157
+ if (renumbered) {
158
+ notes.warn('Steps are renumbered densely in v2: gaps between authored steps no longer cost an empty click.', path);
159
+ }
160
+ return out;
161
+ });
162
+ const out = { formatVersion: 2, meta: outMeta };
163
+ if (Object.keys(style).length > 0)
164
+ out.style = style;
165
+ if (Object.keys(navigation).length > 0)
166
+ out.navigation = navigation;
167
+ out.slides = slides;
168
+ if (Array.isArray(input.masters) && input.masters.length > 0) {
169
+ notes.warn('The `masters` declaration is dropped; each free slide keeps its own `master`.', [
170
+ 'slides',
171
+ ]);
172
+ }
173
+ return out;
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Texuli grammar trees
177
+ function chartFromNode(id, props) {
178
+ const kind = id === 'charts-cartesian' ? props.kind : id.slice('charts-'.length);
179
+ const titleKey = props.title === undefined ? 'label' : 'title';
180
+ const { [titleKey]: title, description, kind: _kind, ...rest } = props;
181
+ const block = { type: 'chart', kind, title, description };
182
+ if (INLINE_CHART_KINDS.includes(kind)) {
183
+ for (const key of ['series', 'cells', 'points'])
184
+ assign(block, key, props[key]);
185
+ }
186
+ else
187
+ block.props = rest;
188
+ return block;
189
+ }
190
+ function itemFromNode(node, path, notes) {
191
+ if (!isBag(node))
192
+ return node;
193
+ const id = typeof node.id === 'string' ? node.id : '';
194
+ const props = isBag(node.props) ? node.props : {};
195
+ if (Object.hasOwn(props, 'block'))
196
+ return props.block;
197
+ if (id.startsWith('charts-'))
198
+ return chartFromNode(id, props);
199
+ notes.warn(`The catalog node "${id.slice(0, 64)}" became a component item.`, path);
200
+ return { type: 'component', component: id, props };
201
+ }
202
+ function slideFromGrammar(props, children, id, path, notes) {
203
+ const out = {};
204
+ assign(out, 'id', id);
205
+ assign(out, 'title', props.title);
206
+ assign(out, 'notes', props.notes);
207
+ assign(out, 'master', props.master);
208
+ assign(out, 'themeRef', props.themeRef);
209
+ assign(out, 'transition', props.transition);
210
+ const nodes = Array.isArray(children) ? children : [];
211
+ out.blocks = nodes.map((node, b) => itemFromNode(node, [...path, 'blocks', b], notes));
212
+ return out;
213
+ }
214
+ function fromGrammar(input, notes) {
215
+ const props = isBag(input.props) ? input.props : {};
216
+ const slides = [];
217
+ if (Array.isArray(props.slides)) {
218
+ props.slides.forEach((slide, i) => {
219
+ const value = isBag(slide) ? slide : {};
220
+ slides.push(slideFromGrammar(value, value.children, value.id, ['slides', i], notes));
221
+ });
222
+ }
223
+ else if (Array.isArray(input.children)) {
224
+ input.children.forEach((node, i) => {
225
+ const value = isBag(node) ? node : {};
226
+ const nodeProps = isBag(value.props) ? value.props : {};
227
+ const path = ['slides', i];
228
+ if (value.id === GRAMMAR_CARD) {
229
+ const card = slideFromGrammar({ title: nodeProps.title }, value.children, `slide-${i + 1}`, path, notes);
230
+ slides.push(card);
231
+ }
232
+ else {
233
+ if (value.id !== GRAMMAR_SLIDE) {
234
+ notes.warn(`Expected a ${GRAMMAR_SLIDE} node; read it as one.`, path);
235
+ }
236
+ slides.push(slideFromGrammar(nodeProps, value.children, nodeProps.slideId, path, notes));
237
+ }
238
+ });
239
+ }
240
+ const v1 = {
241
+ meta: {
242
+ title: props.title,
243
+ ...(props.description === undefined ? {} : { description: props.description }),
244
+ ...(props.locale === undefined ? {} : { locale: props.locale }),
245
+ ...(props.aspect === undefined ? {} : { aspect: props.aspect }),
246
+ ...(props.themeRef === undefined ? {} : { themeRef: props.themeRef }),
247
+ },
248
+ slides,
249
+ };
250
+ assign(v1, 'masters', props.masters);
251
+ assign(v1, 'settings', props.settings);
252
+ return fromTexuliV1(v1, notes);
253
+ }
254
+ // ---------------------------------------------------------------------------
255
+ // Protoyard
256
+ const PART_WHOLE = ['pie', 'donut'];
257
+ const STEP_KEYS = ['step', 'fragment', 'reveal', 'tone'];
258
+ /** Protoyard's ChartSpec (`kind`, `labels`, `series[{label, values}]`) as a chart block body. */
259
+ function chartBody(spec, path, notes) {
260
+ const labels = Array.isArray(spec.labels) ? spec.labels : [];
261
+ const series = Array.isArray(spec.series) ? spec.series : [];
262
+ const out = { kind: spec.kind, title: spec.title };
263
+ if (typeof spec.description === 'string' && spec.description.trim() !== '') {
264
+ out.description = spec.description;
265
+ }
266
+ else {
267
+ out.description = spec.title;
268
+ notes.warn('The chart had no description; its title is used (write a real one).', [
269
+ ...path,
270
+ 'description',
271
+ ]);
272
+ }
273
+ const values = (entry) => isBag(entry) && Array.isArray(entry.values) ? entry.values : [];
274
+ /** A series entry's step marker (`step`, `fragment`, `reveal`, `tone`), kept as authored. */
275
+ const marker = (entry) => {
276
+ const out = {};
277
+ if (isBag(entry))
278
+ for (const key of STEP_KEYS)
279
+ assign(out, key, entry[key]);
280
+ return out;
281
+ };
282
+ if (PART_WHOLE.includes(spec.kind) || spec.kind === 'funnel') {
283
+ const first = values(series[0]);
284
+ const data = labels.map((label, j) => ({
285
+ id: `item-${j + 1}`,
286
+ label,
287
+ value: typeof first[j] === 'number' ? first[j] : 0,
288
+ }));
289
+ out.props = spec.kind === 'funnel' ? { steps: data } : { data };
290
+ // One data list: a marker on the first series moves to the chart itself.
291
+ const firstMarker = marker(series[0]);
292
+ if (Object.keys(firstMarker).length > 0) {
293
+ if (spec.step === undefined)
294
+ Object.assign(out, firstMarker);
295
+ else
296
+ notes.warn('The first series step marker was dropped; the chart has its own.', [
297
+ ...path,
298
+ 'series',
299
+ 0,
300
+ ]);
301
+ }
302
+ }
303
+ else {
304
+ out.series = series.map((entry, s) => ({
305
+ id: `series-${s + 1}`,
306
+ label: isBag(entry) ? entry.label : undefined,
307
+ points: labels.map((x, j) => {
308
+ const y = values(entry)[j];
309
+ return { x, y: typeof y === 'number' ? y : null };
310
+ }),
311
+ ...marker(entry),
312
+ }));
313
+ }
314
+ if ((spec.unit !== undefined && spec.unit !== 'number') || spec.currency !== undefined) {
315
+ notes.warn('The chart `unit`/`currency` was dropped; set a chart format in props.', path);
316
+ }
317
+ for (const key of STEP_KEYS)
318
+ assign(out, key, spec[key]);
319
+ notes.warn('The Protoyard ChartSpec became a Texuli chart block body.', path);
320
+ return out;
321
+ }
322
+ function isChartSpec(value) {
323
+ return (isBag(value) &&
324
+ Array.isArray(value.labels) &&
325
+ Array.isArray(value.series) &&
326
+ value.series.every((entry) => isBag(entry) && Array.isArray(entry.values)));
327
+ }
328
+ function background(value, path, notes) {
329
+ if (typeof value !== 'string' || isDeckBackgroundPreset(value))
330
+ return value;
331
+ notes.warn(`Unknown background "${value.slice(0, 64)}" became "solid-color".`, path, 'Pick one of the 13 background presets or a spec.');
332
+ return 'solid-color';
333
+ }
334
+ function deriveTitle(input, slides) {
335
+ const outline = isBag(input.outline) ? input.outline : {};
336
+ const fromOutline = clip(outline.title, DECK_MODEL_LIMITS.titleLength);
337
+ if (fromOutline)
338
+ return { title: fromOutline, derived: 'outline.title' };
339
+ for (const slide of slides) {
340
+ if (isBag(slide) && slide.layout === 'title-hero' && isBag(slide.props)) {
341
+ const title = clip(slide.props.title, DECK_MODEL_LIMITS.titleLength);
342
+ if (title)
343
+ return { title, derived: 'the title-hero slide' };
344
+ }
345
+ }
346
+ for (const slide of slides) {
347
+ const title = isBag(slide) ? clip(slide.title, DECK_MODEL_LIMITS.titleLength) : undefined;
348
+ if (title)
349
+ return { title, derived: 'the first slide title' };
350
+ }
351
+ return { title: DEFAULT_TITLE, derived: 'a placeholder' };
352
+ }
353
+ function fromProtoyard(input, notes) {
354
+ const slidesIn = Array.isArray(input.slides) ? input.slides : [];
355
+ const { title, derived } = deriveTitle(input, slidesIn);
356
+ notes.warn(`meta.title was derived from ${derived}.`, ['meta', 'title']);
357
+ const styleIn = isBag(input.style) ? input.style : {};
358
+ const style = {};
359
+ for (const [key, value] of Object.entries(styleIn)) {
360
+ if (key === 'paletteSeed')
361
+ continue;
362
+ if (key === 'background')
363
+ style.background = background(value, ['style', 'background'], notes);
364
+ else if (key === 'logo') {
365
+ if (typeof value === 'string' && value.length <= 512 && MEDIA_SRC.test(value))
366
+ style.logo = value;
367
+ else
368
+ notes.warn('The logo is not a supported media source and was dropped.', ['style', 'logo']);
369
+ }
370
+ else
371
+ style[key] = value;
372
+ }
373
+ if (styleIn.paletteSeed !== undefined) {
374
+ if (style.theme === undefined)
375
+ style.theme = { kind: 'seed', seed: styleIn.paletteSeed };
376
+ else
377
+ notes.warn('`paletteSeed` was dropped: the named theme wins.', ['style', 'theme']);
378
+ }
379
+ const slides = slidesIn.map((slide, i) => {
380
+ if (!isBag(slide))
381
+ return slide;
382
+ const path = ['slides', i];
383
+ const out = { ...slide };
384
+ if (slide.background !== undefined) {
385
+ out.background = background(slide.background, [...path, 'background'], notes);
386
+ }
387
+ if (slide.layout === 'chart' && isBag(slide.props) && isChartSpec(slide.props.spec)) {
388
+ out.props = {
389
+ ...slide.props,
390
+ spec: chartBody(slide.props.spec, [...path, 'props', 'spec'], notes),
391
+ };
392
+ }
393
+ return out;
394
+ });
395
+ const out = { formatVersion: 2, meta: { title } };
396
+ if (Object.keys(style).length > 0)
397
+ out.style = style;
398
+ for (const [key, value] of Object.entries(input)) {
399
+ if (key !== 'style' && key !== 'slides')
400
+ out[key] = value;
401
+ }
402
+ out.slides = slides;
403
+ return out;
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ const LABELS = Object.freeze({
407
+ v2: 'a v2 deck',
408
+ 'texuli-v1': 'a Texuli v1 deck',
409
+ 'texuli-grammar': 'a Texuli grammar tree',
410
+ protoyard: 'a Protoyard deck',
411
+ unknown: 'an unknown shape',
412
+ });
413
+ /** Convert `input` to model v2; see the module comment. Never throws. */
414
+ export function migrateDeck(input) {
415
+ const notes = new Notes();
416
+ let from = 'unknown';
417
+ try {
418
+ from = detectDeckShape(input);
419
+ if (from === 'v2')
420
+ return { deck: input, from, issues: [] };
421
+ if (from === 'unknown') {
422
+ return {
423
+ deck: input,
424
+ from,
425
+ issues: [
426
+ createDiagnostic('invalid-deck', 'Not a recognized deck: expected a v2 deck, a Texuli v1 deck, a layout-deck tree or a Protoyard deck.', [], { fix: 'Start from {"formatVersion": 2, "meta": {"title": "…"}, "slides": […]}.' }),
427
+ ],
428
+ };
429
+ }
430
+ const source = input;
431
+ const deck = from === 'texuli-v1'
432
+ ? fromTexuliV1(source, notes)
433
+ : from === 'texuli-grammar'
434
+ ? fromGrammar(source, notes)
435
+ : fromProtoyard(source, notes);
436
+ notes.issues.unshift(createDiagnostic('migrated', `Converted from ${LABELS[from]} to model v2.`, [], {
437
+ severity: 'warning',
438
+ fix: 'Save the converted deck so it loads without migration.',
439
+ }));
440
+ return { deck, from, issues: notes.issues };
441
+ }
442
+ catch {
443
+ return {
444
+ deck: input,
445
+ from,
446
+ issues: [
447
+ createDiagnostic('invalid-input', 'The deck could not be read as plain data.', [], {
448
+ fix: 'Pass JSON data (loadUntrustedDeck makes a safe copy).',
449
+ }),
450
+ ],
451
+ };
452
+ }
453
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Slide transitions and fragment motion of deck model v2 (model-v2 §3.1,
3
+ * §3.2): precedence slide → deck → `fade`, the motion ceiling and the
4
+ * reduced-motion floor, capped at `MAX_MOTION_MS`.
5
+ */
6
+ export { DEFAULT_MOTION, DEFAULT_TRANSITION, type DeckSlideTransition, effectiveMotion, FRAGMENT_MS, type FragmentMotionOptions, type FragmentMotionReason, isDeckTransition, isMotionLevel, MAX_MOTION_MS, MOTION_LEVELS, type MotionLevel, type MotionOptions, REDUCED_FADE_MS, type ResolvedFragmentMotion, type ResolvedTransition, resolveDeckTransitions, resolveFragmentMotion, resolveSlideTransition, type SlideTransitionInput, SUBTLE_SCALE, TRANSITION_MS, type TransitionDeck, type TransitionReason, type TransitionSlide, } from './transition.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Slide transitions and fragment motion of deck model v2 (model-v2 §3.1,
3
+ * §3.2): precedence slide → deck → `fade`, the motion ceiling and the
4
+ * reduced-motion floor, capped at `MAX_MOTION_MS`.
5
+ */
6
+ export { DEFAULT_MOTION, DEFAULT_TRANSITION, effectiveMotion, FRAGMENT_MS, isDeckTransition, isMotionLevel, MAX_MOTION_MS, MOTION_LEVELS, REDUCED_FADE_MS, resolveDeckTransitions, resolveFragmentMotion, resolveSlideTransition, SUBTLE_SCALE, TRANSITION_MS, } from "./transition.js";
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Slide transitions and fragment motion under a motion level (ported from
3
+ * Protoyard's `motion/transition.ts`, plus v1's `fade-up` transition).
4
+ *
5
+ * - The deck's `style.motion` (`full` | `subtle` | `none`) is the ceiling;
6
+ * an explicit `motion` option replaces it.
7
+ * - The viewer's reduced-motion preference only lowers it, never raises it:
8
+ * with it, every transition is `none` or a short fade, and moving
9
+ * fragments become plain fades.
10
+ * - Durations are opacity and transform only and never exceed
11
+ * `MAX_MOTION_MS`.
12
+ *
13
+ * Inputs are structural plain objects; unknown values fall through to the
14
+ * next source (slide → deck → `fade`) instead of throwing.
15
+ */
16
+ import { type DeckTransition, type FragmentEffect } from '../shared.js';
17
+ export declare const MOTION_LEVELS: readonly ["full", "subtle", "none"];
18
+ export type MotionLevel = (typeof MOTION_LEVELS)[number];
19
+ export declare const MAX_MOTION_MS = 400;
20
+ /** A reduced-motion fade: short enough to read as a cut, long enough not to flash. */
21
+ export declare const REDUCED_FADE_MS = 150;
22
+ /** Subtle motion runs at this share of the full duration. */
23
+ export declare const SUBTLE_SCALE = 0.6;
24
+ /** A fragment's full-motion duration. */
25
+ export declare const FRAGMENT_MS = 300;
26
+ export declare const DEFAULT_TRANSITION: DeckTransition;
27
+ export declare const DEFAULT_MOTION: MotionLevel;
28
+ /** Full-motion transition durations, in milliseconds. */
29
+ export declare const TRANSITION_MS: Readonly<Record<DeckTransition, number>>;
30
+ /** The slide fields the resolver reads. */
31
+ export interface TransitionSlide {
32
+ id?: unknown;
33
+ transition?: unknown;
34
+ }
35
+ /** The deck fields the resolver reads. */
36
+ export interface TransitionDeck {
37
+ style?: {
38
+ transition?: unknown;
39
+ motion?: unknown;
40
+ } | null;
41
+ slides?: readonly TransitionSlide[];
42
+ }
43
+ export interface MotionOptions {
44
+ /** The motion ceiling; defaults to `deck.style.motion`, then `full`. */
45
+ motion?: MotionLevel;
46
+ /** The viewer's preference (`prefers-reduced-motion` or a host toggle). */
47
+ reducedMotion?: boolean;
48
+ }
49
+ export interface SlideTransitionInput extends MotionOptions {
50
+ slide?: TransitionSlide | null;
51
+ deck?: TransitionDeck | null;
52
+ }
53
+ export type TransitionReason = 'motion-none' | 'reduced-motion' | 'subtle' | 'slide-override' | 'deck-default' | 'engine-default';
54
+ export interface ResolvedTransition {
55
+ /** The transition to play. */
56
+ name: DeckTransition;
57
+ durationMs: number;
58
+ /** What the author asked for before the motion level applied. */
59
+ requested: DeckTransition;
60
+ /** The level after the viewer's preference was applied. */
61
+ motion: MotionLevel;
62
+ /** Why the result is what it is, for DevTools and tests. */
63
+ reason: TransitionReason;
64
+ }
65
+ export declare function isDeckTransition(value: unknown): value is DeckTransition;
66
+ export declare function isMotionLevel(value: unknown): value is MotionLevel;
67
+ /** The requested level (option, then deck style, then `full`), lowered by reduced motion. */
68
+ export declare function effectiveMotion(options?: MotionOptions & {
69
+ deck?: TransitionDeck | null;
70
+ }): MotionLevel;
71
+ /** The transition into `slide`: slide override → deck default → `fade`, under the motion level. */
72
+ export declare function resolveSlideTransition(input?: SlideTransitionInput): ResolvedTransition;
73
+ export interface DeckSlideTransition {
74
+ index: number;
75
+ slideId?: string;
76
+ transition: ResolvedTransition;
77
+ }
78
+ /** The transition into every slide of a deck, in order. */
79
+ export declare function resolveDeckTransitions(deck: TransitionDeck, options?: MotionOptions): DeckSlideTransition[];
80
+ export type FragmentMotionReason = 'motion-none' | 'reduced-motion' | 'subtle' | 'full';
81
+ export interface ResolvedFragmentMotion {
82
+ /**
83
+ * The effect to animate. Moving effects (`fade-up`, `grow`, `shrink`)
84
+ * become `in` under `none` and reduced motion; the fragment's phase still
85
+ * comes from its own effect (`fragmentPhase`).
86
+ */
87
+ effect: FragmentEffect;
88
+ durationMs: number;
89
+ motion: MotionLevel;
90
+ reason: FragmentMotionReason;
91
+ }
92
+ export interface FragmentMotionOptions extends MotionOptions {
93
+ deck?: TransitionDeck | null;
94
+ }
95
+ /** How a fragment animates under the motion level. */
96
+ export declare function resolveFragmentMotion(effect: FragmentEffect, options?: FragmentMotionOptions): ResolvedFragmentMotion;