@texuli/deck 0.0.0-stage → 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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -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,520 @@
1
+ /**
2
+ * The closed attribute sets: `.slide` attributes (with their reveal.js
3
+ * aliases), `.element` and brace attributes, fragment effect mapping, and the
4
+ * background they assemble. Anything outside the sets is a diagnostic:
5
+ * `class`, `style`, `on*` and iframes are refused by name.
6
+ */
7
+ import { DECK_BACKGROUND_PRESET_IDS, DECK_GRADIENT_IDS, DECK_MEDIA_FITS, DECK_MEDIA_POSITIONS, DECK_PATTERNS, DECK_SOLID_TOKENS, } from "../model/backgrounds/schema.js";
8
+ import { BLOCK_TONES } from "../model/deck.js";
9
+ import { BLOCK_REVEALS, DECK_TRANSITIONS, FRAGMENT_EFFECTS, MAX_AUTHORED_STEP, SLIDE_ID_PATTERN, } from "../model/shared.js";
10
+ const SLUG = new RegExp(SLIDE_ID_PATTERN, 'u');
11
+ function report(sink, code, message, attr, fix) {
12
+ sink.report(code, message, { line: attr.line, column: attr.column }, [], fix);
13
+ }
14
+ /** reveal.js and deck effect names → a deck fragment effect (and tone). */
15
+ export function mapEffect(name) {
16
+ if (FRAGMENT_EFFECTS.includes(name)) {
17
+ return { effect: name };
18
+ }
19
+ switch (name) {
20
+ case 'fade-in':
21
+ case 'fade':
22
+ return { effect: 'in' };
23
+ case 'fade-out':
24
+ return { effect: 'out' };
25
+ case 'fade-in-then-out':
26
+ return { effect: 'in-then-out' };
27
+ case 'highlight-red':
28
+ return { effect: 'highlight', tone: 'danger' };
29
+ case 'highlight-blue':
30
+ return { effect: 'highlight', tone: 'info' };
31
+ case 'highlight-green':
32
+ return { effect: 'highlight', tone: 'success' };
33
+ case 'highlight-current-red':
34
+ return { effect: 'highlight', tone: 'danger', approximate: true };
35
+ case 'highlight-current-blue':
36
+ return { effect: 'highlight', tone: 'info', approximate: true };
37
+ case 'highlight-current-green':
38
+ return { effect: 'highlight', tone: 'success', approximate: true };
39
+ case 'fade-down':
40
+ case 'fade-left':
41
+ case 'fade-right':
42
+ case 'fade-in-then-semi-out':
43
+ return { effect: 'fade-up', approximate: true };
44
+ case 'semi-fade-out':
45
+ return { effect: 'out', approximate: true };
46
+ case 'zoom-in':
47
+ return { effect: 'grow', approximate: true };
48
+ case 'current-visible':
49
+ return { effect: 'in-then-out', approximate: true };
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+ function applyEffect(sink, mark, name, attr) {
55
+ const mapped = mapEffect(name);
56
+ if (!mapped)
57
+ return false;
58
+ if (mapped.approximate) {
59
+ report(sink, 'md-effect-approximated', `"${name}" has no exact deck equivalent; it plays as "${mapped.effect}".`, attr);
60
+ }
61
+ mark.effect = mapped.effect;
62
+ if (mapped.tone !== undefined && mark.tone === undefined)
63
+ mark.tone = mapped.tone;
64
+ return true;
65
+ }
66
+ /** Refusals shared by every attribute set. True when the attribute was handled. */
67
+ function refuse(sink, attr) {
68
+ const name = attr.name.toLowerCase();
69
+ if (name === 'style') {
70
+ report(sink, 'md-style-refused', 'A `style` attribute is refused.', attr, 'Use a layout, a background or a theme.');
71
+ return true;
72
+ }
73
+ if (/^on/.test(name)) {
74
+ report(sink, 'md-handler-refused', `The event handler "${attr.name.slice(0, 40)}" is refused.`, attr);
75
+ return true;
76
+ }
77
+ if (name.includes('iframe')) {
78
+ report(sink, 'md-iframe-refused', 'iframes are refused.', attr, 'Use `::: component <id>` instead.');
79
+ return true;
80
+ }
81
+ if (name === 'tier') {
82
+ report(sink, 'md-tier-ignored', 'A document cannot choose its trust tier; the host does.', attr, 'Remove `tier`.');
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ const ELEMENT_DATA_UNSUPPORTED = /^data-/;
88
+ /**
89
+ * Read `.element` or brace attributes. `allowed` names the extra attributes
90
+ * this target accepts (width, height, avatar…); the fragment attributes and
91
+ * `id` are always read.
92
+ */
93
+ export function readElement(attrs, allowed, sink) {
94
+ const out = { other: new Map() };
95
+ let mark;
96
+ const ensure = (attr) => {
97
+ mark ??= { fragment: false, line: attr.line, column: attr.column };
98
+ return mark;
99
+ };
100
+ const seen = new Set();
101
+ for (const attr of attrs) {
102
+ if (refuse(sink, attr))
103
+ continue;
104
+ const name = attr.name === 'data-fragment-index' ? 'step' : attr.name;
105
+ if (seen.has(name) && name !== 'class') {
106
+ report(sink, 'md-attr-duplicate', `"${name}" is set twice.`, attr);
107
+ continue;
108
+ }
109
+ seen.add(name);
110
+ const value = attr.value;
111
+ switch (name) {
112
+ case 'fragment': {
113
+ const m = ensure(attr);
114
+ m.fragment = true;
115
+ if (value !== true && !applyEffect(sink, m, value, attr)) {
116
+ report(sink, 'md-attr-value', `Unknown fragment effect "${value.slice(0, 40)}".`, attr, `Use one of: ${FRAGMENT_EFFECTS.join(', ')}.`);
117
+ }
118
+ continue;
119
+ }
120
+ case 'class': {
121
+ const tokens = value === true ? [] : value.split(/\s+/).filter(Boolean);
122
+ if (!tokens.includes('fragment')) {
123
+ report(sink, 'md-class-refused', 'Classes are refused; only `class="fragment <effect>"` is read.', attr, 'Use `fragment="<effect>"`.');
124
+ continue;
125
+ }
126
+ const m = ensure(attr);
127
+ m.fragment = true;
128
+ for (const token of tokens) {
129
+ if (token === 'fragment')
130
+ continue;
131
+ if (!applyEffect(sink, m, token, attr)) {
132
+ report(sink, 'md-class-refused', `The class "${token.slice(0, 40)}" maps to nothing.`, attr);
133
+ }
134
+ }
135
+ continue;
136
+ }
137
+ case 'step': {
138
+ const step = value === true ? Number.NaN : Number(value);
139
+ if (!Number.isInteger(step) ||
140
+ step < 0 ||
141
+ step > MAX_AUTHORED_STEP ||
142
+ !/^\d+$/.test(String(value))) {
143
+ report(sink, 'md-attr-value', `"step" is a whole number from 0 to ${MAX_AUTHORED_STEP}.`, attr);
144
+ continue;
145
+ }
146
+ const m = ensure(attr);
147
+ m.fragment = true;
148
+ m.step = step;
149
+ continue;
150
+ }
151
+ case 'reveal': {
152
+ if (value === true || !BLOCK_REVEALS.includes(value)) {
153
+ report(sink, 'md-attr-value', `"reveal" is one of: ${BLOCK_REVEALS.join(', ')}.`, attr);
154
+ continue;
155
+ }
156
+ const m = ensure(attr);
157
+ m.fragment = true;
158
+ m.reveal = value;
159
+ continue;
160
+ }
161
+ case 'tone': {
162
+ if (value === true || !BLOCK_TONES.includes(value)) {
163
+ report(sink, 'md-attr-value', `"tone" is one of: ${BLOCK_TONES.join(', ')}.`, attr);
164
+ continue;
165
+ }
166
+ ensure(attr).tone = value;
167
+ continue;
168
+ }
169
+ case 'id': {
170
+ if (value === true || !SLUG.test(value)) {
171
+ report(sink, 'md-attr-value', '"id" is a lowercase slug (a-z, 0-9, -).', attr);
172
+ continue;
173
+ }
174
+ out.id = value;
175
+ continue;
176
+ }
177
+ default:
178
+ break;
179
+ }
180
+ if (allowed.has(name)) {
181
+ out.other.set(name, attr);
182
+ continue;
183
+ }
184
+ if (ELEMENT_DATA_UNSUPPORTED.test(name)) {
185
+ report(sink, 'md-attr-unsupported', `"${name.slice(0, 40)}" has no deck equivalent; dropped.`, attr);
186
+ continue;
187
+ }
188
+ report(sink, 'md-attr-unknown', `Unknown attribute "${name.slice(0, 40)}" here.`, attr);
189
+ }
190
+ if (mark)
191
+ out.mark = mark;
192
+ return out;
193
+ }
194
+ /** reveal.js names → canonical names (values are mapped where they differ). */
195
+ const SLIDE_ALIASES = Object.freeze({
196
+ 'data-background-color': 'background-color',
197
+ 'data-background-image': 'background-image',
198
+ 'data-background-video': 'background-video',
199
+ 'data-background-size': 'background-fit',
200
+ 'data-background-position': 'background-position',
201
+ 'data-background-gradient': 'background-gradient',
202
+ 'data-transition': 'transition',
203
+ 'data-auto-animate': 'auto-animate',
204
+ });
205
+ const SLIDE_UNSUPPORTED = new Set([
206
+ 'data-transition-speed',
207
+ 'data-background-transition',
208
+ 'data-autoslide',
209
+ 'data-state',
210
+ 'data-background-repeat',
211
+ 'data-background-interactive',
212
+ 'data-auto-animate-id',
213
+ 'data-auto-animate-easing',
214
+ 'data-auto-animate-duration',
215
+ 'data-auto-animate-restart',
216
+ 'data-timing',
217
+ ]);
218
+ /** Accepted and implied: background video always loops, muted. */
219
+ const SLIDE_IMPLIED = new Set([
220
+ 'data-background-video-loop',
221
+ 'data-background-video-muted',
222
+ 'data-background-video-mute',
223
+ ]);
224
+ const SLIDE_NAMES = new Set([
225
+ 'id',
226
+ 'layout',
227
+ 'master',
228
+ 'title',
229
+ 'theme',
230
+ 'transition',
231
+ 'auto-animate',
232
+ 'hidden',
233
+ 'steps',
234
+ 'background',
235
+ 'background-color',
236
+ 'background-gradient',
237
+ 'background-pattern',
238
+ 'background-image',
239
+ 'background-video',
240
+ 'background-poster',
241
+ 'background-alt',
242
+ 'background-full',
243
+ 'background-fit',
244
+ 'background-position',
245
+ 'background-overlay',
246
+ 'tone',
247
+ 'section',
248
+ 'section-title',
249
+ 'section-end',
250
+ ]);
251
+ const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
252
+ function expandHex(value) {
253
+ if (value.length === 7)
254
+ return value;
255
+ return `#${[...value.slice(1)].map((c) => c + c).join('')}`;
256
+ }
257
+ /** Read `.slide` attributes: aliases mapped, refusals reported, the background assembled. */
258
+ export function readSlideAttrs(attrs, sink) {
259
+ const out = { at: new Map() };
260
+ const values = new Map();
261
+ let overlayClass;
262
+ let classTone;
263
+ for (const attr of attrs) {
264
+ if (refuse(sink, attr))
265
+ continue;
266
+ const lower = attr.name.toLowerCase();
267
+ if (SLIDE_IMPLIED.has(lower))
268
+ continue;
269
+ if (lower === 'data-background-iframe')
270
+ continue; // refused above (name contains iframe)
271
+ if (SLIDE_UNSUPPORTED.has(lower)) {
272
+ report(sink, 'md-attr-unsupported', `"${attr.name}" has no deck equivalent; dropped.`, attr);
273
+ continue;
274
+ }
275
+ if (lower === 'data-visibility') {
276
+ if (attr.value === 'hidden')
277
+ values.set('hidden', { ...attr, value: true });
278
+ else
279
+ report(sink, 'md-attr-value', '`data-visibility` is read only as "hidden".', attr);
280
+ continue;
281
+ }
282
+ if (lower === 'data-background-opacity') {
283
+ const opacity = attr.value === true ? Number.NaN : Number(attr.value);
284
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
285
+ report(sink, 'md-attr-value', '`data-background-opacity` is a number from 0 to 1.', attr);
286
+ continue;
287
+ }
288
+ const overlay = Math.round(Math.min(0.9, Math.max(0, 1 - opacity)) * 100) / 100;
289
+ values.set('background-overlay', { ...attr, value: String(overlay) });
290
+ continue;
291
+ }
292
+ if (lower === 'class') {
293
+ const tokens = attr.value === true ? [] : attr.value.split(/\s+/).filter(Boolean);
294
+ for (const token of tokens) {
295
+ if (token === 'has-dark-background')
296
+ classTone = 'dark';
297
+ else if (token === 'has-light-background')
298
+ classTone = 'light';
299
+ else if (token === 'slide-bg-overlay--dark')
300
+ overlayClass = { tone: 'dark', overlay: 0.6 };
301
+ else if (token === 'slide-bg-overlay--light')
302
+ overlayClass = { tone: 'light', overlay: 0.65 };
303
+ else {
304
+ report(sink, 'md-class-refused', `The class "${token.slice(0, 40)}" maps to nothing; classes are refused.`, attr, 'Use a `layout`, `background-*` or `tone` attribute.');
305
+ }
306
+ }
307
+ continue;
308
+ }
309
+ const name = Object.hasOwn(SLIDE_ALIASES, lower) ? (SLIDE_ALIASES[lower] ?? lower) : lower;
310
+ if (!SLIDE_NAMES.has(name)) {
311
+ if (lower.startsWith('data-')) {
312
+ report(sink, 'md-attr-unsupported', `"${attr.name.slice(0, 40)}" has no deck equivalent; dropped.`, attr);
313
+ }
314
+ else
315
+ report(sink, 'md-attr-unknown', `Unknown slide attribute "${attr.name.slice(0, 40)}".`, attr);
316
+ continue;
317
+ }
318
+ if (values.has(name)) {
319
+ report(sink, 'md-attr-duplicate', `"${name}" is set twice for this slide.`, attr);
320
+ continue;
321
+ }
322
+ values.set(name, { ...attr, name });
323
+ }
324
+ const text = (name) => {
325
+ const attr = values.get(name);
326
+ if (!attr)
327
+ return undefined;
328
+ if (attr.value === true) {
329
+ report(sink, 'md-attr-value', `"${name}" needs a value.`, attr);
330
+ return undefined;
331
+ }
332
+ out.at.set(name, attr);
333
+ return attr.value;
334
+ };
335
+ const flag = (name) => {
336
+ const attr = values.get(name);
337
+ if (!attr)
338
+ return undefined;
339
+ out.at.set(name, attr);
340
+ if (attr.value === true || attr.value === 'true')
341
+ return true;
342
+ if (attr.value === 'false')
343
+ return false;
344
+ report(sink, 'md-attr-value', `"${name}" is a flag (or true / false).`, attr);
345
+ return undefined;
346
+ };
347
+ const oneOf = (name, allowed) => {
348
+ const value = text(name);
349
+ if (value === undefined)
350
+ return undefined;
351
+ if (allowed.includes(value))
352
+ return value;
353
+ report(sink, 'md-attr-value', `"${name}" is one of: ${allowed.join(', ')}.`, values.get(name));
354
+ return undefined;
355
+ };
356
+ const id = text('id');
357
+ if (id !== undefined)
358
+ out.id = id;
359
+ const layout = text('layout');
360
+ if (layout !== undefined)
361
+ out.layout = layout;
362
+ const master = text('master');
363
+ if (master !== undefined)
364
+ out.master = master;
365
+ const title = text('title');
366
+ if (title !== undefined)
367
+ out.title = title;
368
+ const theme = text('theme');
369
+ if (theme !== undefined)
370
+ out.theme = theme;
371
+ const hidden = flag('hidden');
372
+ if (hidden !== undefined)
373
+ out.hidden = hidden;
374
+ const steps = text('steps');
375
+ if (steps !== undefined)
376
+ out.steps = steps.split('|').map((label) => label.trim());
377
+ const section = text('section');
378
+ if (section !== undefined)
379
+ out.section = section;
380
+ const sectionTitle = text('section-title');
381
+ if (sectionTitle !== undefined)
382
+ out.sectionTitle = sectionTitle;
383
+ const sectionEnd = flag('section-end');
384
+ if (sectionEnd === true)
385
+ out.sectionEnd = true;
386
+ // Transition (auto-animate is reveal's morph)
387
+ const transitionAttr = values.get('transition');
388
+ const autoAnimate = flag('auto-animate');
389
+ if (transitionAttr) {
390
+ const raw = text('transition');
391
+ if (raw !== undefined) {
392
+ const first = raw.split(/\s+/)[0] ?? '';
393
+ const base = first.replace(/-(?:in|out)$/, '');
394
+ if (DECK_TRANSITIONS.includes(raw))
395
+ out.transition = raw;
396
+ else if (raw === 'convex' || raw === 'concave' || base === 'convex' || base === 'concave') {
397
+ out.transition = 'slide';
398
+ report(sink, 'md-attr-unsupported', `The transition "${raw}" plays as "slide".`, transitionAttr);
399
+ }
400
+ else if (DECK_TRANSITIONS.includes(base)) {
401
+ out.transition = base;
402
+ report(sink, 'md-attr-unsupported', `The transition "${raw}" plays as "${base}".`, transitionAttr);
403
+ }
404
+ else {
405
+ report(sink, 'md-attr-value', `"transition" is one of: ${DECK_TRANSITIONS.join(', ')}.`, transitionAttr);
406
+ }
407
+ }
408
+ }
409
+ if (autoAnimate === true) {
410
+ if (out.transition !== undefined && out.transition !== 'morph') {
411
+ report(sink, 'md-attr-conflict', '`auto-animate` is the morph transition; it conflicts with `transition`.', values.get('auto-animate'));
412
+ }
413
+ else
414
+ out.transition = 'morph';
415
+ }
416
+ // Background
417
+ const kinds = [];
418
+ const preset = oneOf('background', DECK_BACKGROUND_PRESET_IDS);
419
+ const colorRaw = text('background-color');
420
+ const gradientRaw = text('background-gradient');
421
+ const pattern = oneOf('background-pattern', DECK_PATTERNS);
422
+ const image = text('background-image');
423
+ const video = text('background-video');
424
+ let background;
425
+ if (preset !== undefined) {
426
+ kinds.push('background');
427
+ background = preset;
428
+ }
429
+ if (colorRaw !== undefined) {
430
+ kinds.push('background-color');
431
+ if (DECK_SOLID_TOKENS.includes(colorRaw)) {
432
+ background = { kind: 'solid', token: colorRaw };
433
+ }
434
+ else if (HEX.test(colorRaw)) {
435
+ background = { kind: 'solid', color: expandHex(colorRaw) };
436
+ }
437
+ else {
438
+ report(sink, /[(;]/.test(colorRaw) ? 'md-css-refused' : 'md-attr-value', `"${colorRaw.slice(0, 40)}" is not a palette token or #rrggbb.`, values.get('background-color'), `Use ${DECK_SOLID_TOKENS.join(', ')} or #rrggbb.`);
439
+ }
440
+ }
441
+ if (gradientRaw !== undefined) {
442
+ kinds.push('background-gradient');
443
+ const match = /^(?:gradient-)?([a-z]+)(?:[ \t]*(?:,[ \t]*)?(\d{1,3})(?:deg)?)?$/.exec(gradientRaw.trim());
444
+ const name = match?.[1];
445
+ if (match && name !== undefined && DECK_GRADIENT_IDS.includes(name)) {
446
+ const spec = { kind: 'gradient', gradient: name };
447
+ if (match[2] !== undefined)
448
+ spec.angle = Number(match[2]);
449
+ background = spec;
450
+ }
451
+ else {
452
+ report(sink, /[(;:]/.test(gradientRaw) ? 'md-css-refused' : 'md-attr-value', 'CSS gradients are refused; name a gradient preset.', values.get('background-gradient'), `Use one of: ${DECK_GRADIENT_IDS.join(', ')} (optionally with an angle: "dusk, 135deg").`);
453
+ }
454
+ }
455
+ if (pattern !== undefined) {
456
+ kinds.push('background-pattern');
457
+ background = { kind: 'pattern', pattern };
458
+ }
459
+ if (image !== undefined || video !== undefined) {
460
+ if (image !== undefined)
461
+ kinds.push('background-image');
462
+ if (video !== undefined)
463
+ kinds.push('background-video');
464
+ const full = flag('background-full') === true;
465
+ const spec = image !== undefined
466
+ ? { kind: full ? 'image-full' : 'image', src: image }
467
+ : { kind: full ? 'video-full' : 'video', src: video };
468
+ const alt = text('background-alt');
469
+ if (alt !== undefined)
470
+ spec.alt = alt;
471
+ const poster = text('background-poster');
472
+ if (poster !== undefined)
473
+ spec.poster = poster;
474
+ const fit = oneOf('background-fit', DECK_MEDIA_FITS);
475
+ if (fit !== undefined)
476
+ spec.fit = fit;
477
+ const position = oneOf('background-position', DECK_MEDIA_POSITIONS);
478
+ if (position !== undefined)
479
+ spec.position = position;
480
+ const tone = oneOf('tone', ['dark', 'light']) ?? classTone ?? overlayClass?.tone;
481
+ if (tone !== undefined)
482
+ spec.tone = tone;
483
+ const overlayRaw = text('background-overlay');
484
+ if (overlayRaw !== undefined) {
485
+ const overlay = Number(overlayRaw);
486
+ if (!Number.isFinite(overlay)) {
487
+ report(sink, 'md-attr-value', '"background-overlay" is a number from 0 to 0.9.', values.get('background-overlay'));
488
+ }
489
+ else
490
+ spec.overlay = overlay;
491
+ }
492
+ else if (overlayClass)
493
+ spec.overlay = overlayClass.overlay;
494
+ background = spec;
495
+ }
496
+ else {
497
+ for (const name of [
498
+ 'background-full',
499
+ 'background-alt',
500
+ 'background-poster',
501
+ 'background-fit',
502
+ 'background-position',
503
+ 'background-overlay',
504
+ 'tone',
505
+ ]) {
506
+ const attr = values.get(name);
507
+ if (attr) {
508
+ report(sink, 'md-attr-conflict', `"${name}" applies only to an image or video background.`, attr);
509
+ }
510
+ }
511
+ }
512
+ if (kinds.length > 1) {
513
+ report(sink, 'md-attr-conflict', `A slide has one background; it sets ${kinds.join(' and ')}.`, values.get(kinds[1] ?? ''));
514
+ }
515
+ if (background !== undefined) {
516
+ out.background = background;
517
+ out.at.set('background', values.get(kinds[0] ?? ''));
518
+ }
519
+ return out;
520
+ }
@@ -0,0 +1,112 @@
1
+ import { type Attr } from './attributes.js';
2
+ import type { IssueSink } from './issues.js';
3
+ import type { Line } from './scan.js';
4
+ export interface Src {
5
+ line: number;
6
+ column: number;
7
+ }
8
+ /** Attributes from an `.element` comment or a brace list, with where they were written. */
9
+ export interface Attrs {
10
+ list: Attr[];
11
+ src: Src;
12
+ }
13
+ interface Base {
14
+ src: Src;
15
+ /** `.element` attributes (trailing or on the next line). */
16
+ attrs?: Attrs;
17
+ }
18
+ export interface ListItemNode {
19
+ text: string;
20
+ src: Src;
21
+ attrs?: Attrs;
22
+ }
23
+ export interface TableRowNode {
24
+ cells: string[];
25
+ src: Src;
26
+ attrs?: Attrs;
27
+ }
28
+ export type MdNode = (Base & {
29
+ kind: 'heading';
30
+ level: number;
31
+ text: string;
32
+ }) | (Base & {
33
+ kind: 'paragraph';
34
+ text: string;
35
+ }) | (Base & {
36
+ kind: 'list';
37
+ ordered: boolean;
38
+ items: ListItemNode[];
39
+ }) | (Base & {
40
+ kind: 'quote';
41
+ text: string;
42
+ cite?: string;
43
+ }) | (Base & {
44
+ kind: 'table';
45
+ header: string[];
46
+ rows: TableRowNode[];
47
+ }) | (Base & {
48
+ kind: 'image';
49
+ alt: string;
50
+ url: string;
51
+ title?: string;
52
+ }) | (Base & {
53
+ kind: 'rule';
54
+ }) | (Base & {
55
+ kind: 'code';
56
+ lang?: string;
57
+ highlights?: string[];
58
+ brace?: Attrs;
59
+ code: string;
60
+ }) | (Base & {
61
+ kind: 'math';
62
+ tex: string;
63
+ }) | (Base & {
64
+ kind: 'directive';
65
+ name: string;
66
+ arg?: string;
67
+ brace?: Attrs;
68
+ /** JSON directives: the raw body and the line it starts on. */
69
+ body?: string;
70
+ bodyLine?: number;
71
+ /** Region directives: the nodes inside. */
72
+ children?: MdNode[];
73
+ });
74
+ export interface SlideSource {
75
+ nodes: MdNode[];
76
+ /** `<!-- .slide: … -->` attributes, in order. */
77
+ slideAttrs: Attr[];
78
+ notes?: {
79
+ text: string;
80
+ src: Src;
81
+ };
82
+ }
83
+ /** Directives whose body is one JSON value. */
84
+ export declare const JSON_DIRECTIVES: Set<string>;
85
+ /** Directives whose body is Markdown (layout regions). */
86
+ export declare const REGION_DIRECTIVES: Set<string>;
87
+ /**
88
+ * Notes lines the writer escapes with a leading `\`: separators and fence
89
+ * openers, which the slide splitter would otherwise read.
90
+ */
91
+ export declare const NOTE_ESCAPED: RegExp;
92
+ /**
93
+ * An image alone on its line: `![alt](url "title")`, with `<url>` allowed.
94
+ * Scanned by index: the regex it replaces (`\(\s*<?([^\s<>]*)>?(?:\s+"…")?\s*\)`)
95
+ * backtracked over a long run of spaces between the parentheses.
96
+ */
97
+ export declare function matchImageLine(line: string): {
98
+ alt: string;
99
+ url: string;
100
+ title?: string;
101
+ } | null;
102
+ /**
103
+ * A table alignment row (`| :--- | ---: |`): split on `|`, every cell
104
+ * `:?-+:?` between spaces or tabs; outer pipes optional. Scanned by
105
+ * splitting: the single regex it replaces backtracked across cells.
106
+ */
107
+ export declare function isAlignRow(line: string): boolean;
108
+ /** Split a pipe-table row into trimmed cells (`\|` stays inside a cell). */
109
+ export declare function splitCells(row: string): string[];
110
+ /** Read one slide's lines into nodes, slide attributes and notes. */
111
+ export declare function readSlide(lines: Line[], sink: IssueSink): SlideSource;
112
+ export {};