@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.
- package/LICENSE +21 -0
- package/README.md +182 -2
- package/dist/authoring/builder.d.ts +351 -0
- package/dist/authoring/builder.js +64 -0
- package/dist/authoring/index.d.ts +6 -0
- package/dist/authoring/index.js +6 -0
- package/dist/authoring/lint.d.ts +36 -0
- package/dist/authoring/lint.js +325 -0
- package/dist/authoring/outlines.d.ts +34 -0
- package/dist/authoring/outlines.js +255 -0
- package/dist/authoring/patch.d.ts +23 -0
- package/dist/authoring/patch.js +166 -0
- package/dist/authoring/select-layouts.d.ts +19 -0
- package/dist/authoring/select-layouts.js +90 -0
- package/dist/engine/auto-slide.d.ts +32 -0
- package/dist/engine/auto-slide.js +67 -0
- package/dist/engine/canvas.d.ts +56 -0
- package/dist/engine/canvas.js +48 -0
- package/dist/engine/controller.d.ts +126 -0
- package/dist/engine/controller.js +248 -0
- package/dist/engine/controls.d.ts +128 -0
- package/dist/engine/controls.js +460 -0
- package/dist/engine/hash.d.ts +42 -0
- package/dist/engine/hash.js +86 -0
- package/dist/engine/index.d.ts +88 -0
- package/dist/engine/index.js +88 -0
- package/dist/engine/messages.d.ts +138 -0
- package/dist/engine/messages.js +140 -0
- package/dist/engine/motion.d.ts +64 -0
- package/dist/engine/motion.js +112 -0
- package/dist/engine/navigation.d.ts +87 -0
- package/dist/engine/navigation.js +156 -0
- package/dist/engine/presenter.d.ts +58 -0
- package/dist/engine/presenter.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/markdown/attributes.d.ts +24 -0
- package/dist/markdown/attributes.js +94 -0
- package/dist/markdown/attrs-closed.d.ts +52 -0
- package/dist/markdown/attrs-closed.js +520 -0
- package/dist/markdown/blocks.d.ts +112 -0
- package/dist/markdown/blocks.js +766 -0
- package/dist/markdown/frontmatter.d.ts +30 -0
- package/dist/markdown/frontmatter.js +165 -0
- package/dist/markdown/index.d.ts +29 -0
- package/dist/markdown/index.js +29 -0
- package/dist/markdown/issues.d.ts +68 -0
- package/dist/markdown/issues.js +83 -0
- package/dist/markdown/json.d.ts +21 -0
- package/dist/markdown/json.js +236 -0
- package/dist/markdown/limits.d.ts +32 -0
- package/dist/markdown/limits.js +32 -0
- package/dist/markdown/parse.d.ts +31 -0
- package/dist/markdown/parse.js +283 -0
- package/dist/markdown/scan.d.ts +26 -0
- package/dist/markdown/scan.js +138 -0
- package/dist/markdown/slides.d.ts +32 -0
- package/dist/markdown/slides.js +854 -0
- package/dist/markdown/text.d.ts +76 -0
- package/dist/markdown/text.js +447 -0
- package/dist/markdown/twin.d.ts +41 -0
- package/dist/markdown/twin.js +374 -0
- package/dist/markdown/write.d.ts +17 -0
- package/dist/markdown/write.js +688 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +14 -0
- package/dist/math/parse.d.ts +9 -0
- package/dist/math/parse.js +648 -0
- package/dist/math/symbols.d.ts +44 -0
- package/dist/math/symbols.js +333 -0
- package/dist/math/types.d.ts +59 -0
- package/dist/math/types.js +69 -0
- package/dist/model/backgrounds/contrast.d.ts +77 -0
- package/dist/model/backgrounds/contrast.js +186 -0
- package/dist/model/backgrounds/index.d.ts +7 -0
- package/dist/model/backgrounds/index.js +7 -0
- package/dist/model/backgrounds/resolve.d.ts +86 -0
- package/dist/model/backgrounds/resolve.js +215 -0
- package/dist/model/backgrounds/schema.d.ts +178 -0
- package/dist/model/backgrounds/schema.js +203 -0
- package/dist/model/codes.d.ts +73 -0
- package/dist/model/codes.js +67 -0
- package/dist/model/deck.d.ts +392 -0
- package/dist/model/deck.js +338 -0
- package/dist/model/index.d.ts +19 -0
- package/dist/model/index.js +19 -0
- package/dist/model/layouts/catalog.d.ts +433 -0
- package/dist/model/layouts/catalog.js +495 -0
- package/dist/model/layouts/index.d.ts +5 -0
- package/dist/model/layouts/index.js +5 -0
- package/dist/model/layouts/props.d.ts +302 -0
- package/dist/model/layouts/props.js +272 -0
- package/dist/model/loader.d.ts +37 -0
- package/dist/model/loader.js +317 -0
- package/dist/model/migrate.d.ts +34 -0
- package/dist/model/migrate.js +453 -0
- package/dist/model/motion/index.d.ts +6 -0
- package/dist/model/motion/index.js +6 -0
- package/dist/model/motion/transition.d.ts +96 -0
- package/dist/model/motion/transition.js +127 -0
- package/dist/model/parse.d.ts +57 -0
- package/dist/model/parse.js +307 -0
- package/dist/model/plain.d.ts +40 -0
- package/dist/model/plain.js +275 -0
- package/dist/model/shared.d.ts +63 -0
- package/dist/model/shared.js +86 -0
- package/dist/model/steps/index.d.ts +7 -0
- package/dist/model/steps/index.js +7 -0
- package/dist/model/steps/phase.d.ts +34 -0
- package/dist/model/steps/phase.js +50 -0
- package/dist/model/steps/slide-steps.d.ts +129 -0
- package/dist/model/steps/slide-steps.js +256 -0
- package/dist/model/style/index.d.ts +8 -0
- package/dist/model/style/index.js +8 -0
- package/dist/model/style/resolve.d.ts +87 -0
- package/dist/model/style/resolve.js +174 -0
- package/dist/model/style/schema.d.ts +85 -0
- package/dist/model/style/schema.js +53 -0
- package/dist/model/style/theme.d.ts +69 -0
- package/dist/model/style/theme.js +84 -0
- package/dist/schema/builders.d.ts +146 -0
- package/dist/schema/builders.js +285 -0
- package/dist/schema/diagnostics.d.ts +78 -0
- package/dist/schema/diagnostics.js +81 -0
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.js +14 -0
- package/dist/schema/internal.d.ts +40 -0
- package/dist/schema/internal.js +209 -0
- package/dist/schema/json-schema.d.ts +12 -0
- package/dist/schema/json-schema.js +213 -0
- package/dist/schema/types.d.ts +166 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validate.d.ts +51 -0
- package/dist/schema/validate.js +765 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/templates.d.ts +11 -0
- package/dist/templates/templates.js +47 -0
- package/package.json +60 -4
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deckToMarkdown`: a validated v2 deck → canonical deck Markdown, so
|
|
3
|
+
* JSON → Markdown → JSON gives the same deck. The writer prefers readable
|
|
4
|
+
* Markdown and proves it: every free block and every slide is read back
|
|
5
|
+
* and compared, and whatever does not survive falls back to an exact JSON
|
|
6
|
+
* fence (`::: block`, `::: props`, `::: slide`). Deck-level fields the
|
|
7
|
+
* dialect cannot express are reported, never silently dropped.
|
|
8
|
+
*/
|
|
9
|
+
import { DECK_SOLID_TOKENS, isDeckBackgroundPreset } from "../model/backgrounds/schema.js";
|
|
10
|
+
import { createDiagnostic } from "../schema/index.js";
|
|
11
|
+
import { attributeWritable, writeAttribute } from "./attributes.js";
|
|
12
|
+
import { FRONTMATTER_KEYS, writeScalar } from "./frontmatter.js";
|
|
13
|
+
import { deriveSlideId, readDeckDraft, stackSectionId } from "./parse.js";
|
|
14
|
+
import { escapeInline, fenceFor, hasUnsafeLink, inertJson, jsonEqual } from "./text.js";
|
|
15
|
+
const e = escapeInline;
|
|
16
|
+
function isBag(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function attrText(list) {
|
|
20
|
+
return list.map(([name, value]) => writeAttribute(name, value)).join(' ');
|
|
21
|
+
}
|
|
22
|
+
function comment(kind, list) {
|
|
23
|
+
return `<!-- .${kind}: ${attrText(list)} -->`;
|
|
24
|
+
}
|
|
25
|
+
function brace(list) {
|
|
26
|
+
if (list.length === 0)
|
|
27
|
+
return '';
|
|
28
|
+
const parts = list.map(([name, value]) => name === 'id' && typeof value === 'string' ? `#${value}` : writeAttribute(name, value));
|
|
29
|
+
return ` {${parts.join(' ')}}`;
|
|
30
|
+
}
|
|
31
|
+
/** Fragment attributes, auto-numbered (free slides) or explicit (layouts). */
|
|
32
|
+
class Steps {
|
|
33
|
+
counter = 0;
|
|
34
|
+
explicit;
|
|
35
|
+
constructor(explicit) {
|
|
36
|
+
this.explicit = explicit;
|
|
37
|
+
}
|
|
38
|
+
attrs(target, span = 1) {
|
|
39
|
+
const out = [];
|
|
40
|
+
if (typeof target.id === 'string')
|
|
41
|
+
out.push(['id', target.id]);
|
|
42
|
+
if (typeof target.step === 'number') {
|
|
43
|
+
if (typeof target.fragment === 'string')
|
|
44
|
+
out.push(['fragment', target.fragment]);
|
|
45
|
+
else if (typeof target.reveal === 'string')
|
|
46
|
+
out.push(['reveal', target.reveal]);
|
|
47
|
+
else
|
|
48
|
+
out.push(['fragment', true]);
|
|
49
|
+
if (this.explicit || target.step !== this.counter + 1)
|
|
50
|
+
out.push(['step', target.step]);
|
|
51
|
+
this.counter = Math.max(this.counter, target.step + span - 1);
|
|
52
|
+
}
|
|
53
|
+
if (typeof target.tone === 'string')
|
|
54
|
+
out.push(['tone', target.tone]);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function withoutSteps(block) {
|
|
59
|
+
const out = { ...block };
|
|
60
|
+
delete out.step;
|
|
61
|
+
delete out.fragment;
|
|
62
|
+
delete out.reveal;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function json(name, value, head = '') {
|
|
66
|
+
return `::: ${name}${head}\n${inertJson(value)}\n:::`;
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Free blocks
|
|
70
|
+
function blockMarkdown(block, steps) {
|
|
71
|
+
const own = (list, text) => list.length > 0 ? `${text}\n${comment('element', list)}` : text;
|
|
72
|
+
switch (block.type) {
|
|
73
|
+
case 'heading':
|
|
74
|
+
return own(steps.attrs(block), `${'#'.repeat(Number(block.level))} ${e(String(block.text))}`);
|
|
75
|
+
case 'text':
|
|
76
|
+
return own(steps.attrs(block), String(block.markdown));
|
|
77
|
+
case 'list': {
|
|
78
|
+
const items = block.items.map((item, i) => {
|
|
79
|
+
const marker = block.ordered === true ? `${i + 1}. ` : '- ';
|
|
80
|
+
const lines = String(item.text).split('\n');
|
|
81
|
+
const body = [
|
|
82
|
+
lines[0] ?? '',
|
|
83
|
+
...lines
|
|
84
|
+
.slice(1)
|
|
85
|
+
.map((line) => (line === '' ? '' : `${' '.repeat(marker.length)}${line}`)),
|
|
86
|
+
].join('\n');
|
|
87
|
+
return `${marker}${body}`;
|
|
88
|
+
});
|
|
89
|
+
const head = [];
|
|
90
|
+
if (typeof block.id === 'string')
|
|
91
|
+
head.push(['id', block.id]);
|
|
92
|
+
if (typeof block.tone === 'string' && typeof block.step !== 'number')
|
|
93
|
+
head.push(['tone', block.tone]);
|
|
94
|
+
if (typeof block.step === 'number') {
|
|
95
|
+
const n = items.length;
|
|
96
|
+
const first = {
|
|
97
|
+
step: block.step,
|
|
98
|
+
fragment: block.fragment,
|
|
99
|
+
reveal: block.reveal,
|
|
100
|
+
tone: block.tone,
|
|
101
|
+
};
|
|
102
|
+
const firstAttrs = steps.attrs(first, n).filter(([name]) => name !== 'id');
|
|
103
|
+
const rest = firstAttrs.filter(([name]) => name !== 'step');
|
|
104
|
+
items.forEach((item, i) => {
|
|
105
|
+
const list = i === 0 ? firstAttrs : rest;
|
|
106
|
+
items[i] = appendTrailing(item, list);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return own(head, items.join('\n'));
|
|
110
|
+
}
|
|
111
|
+
case 'quote': {
|
|
112
|
+
const lines = String(block.quote)
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map((line) => (line ? `> ${line}` : '>'));
|
|
115
|
+
if (typeof block.cite === 'string')
|
|
116
|
+
lines.push('>', `> — ${e(block.cite)}`);
|
|
117
|
+
return own(steps.attrs(block), lines.join('\n'));
|
|
118
|
+
}
|
|
119
|
+
case 'table': {
|
|
120
|
+
const columns = block.columns;
|
|
121
|
+
const header = `| ${columns.map((column) => e(String(column.label))).join(' | ')} |`;
|
|
122
|
+
const align = `| ${columns.map(() => '---').join(' | ')} |`;
|
|
123
|
+
const rows = block.rows.map((row) => `| ${columns.map((column) => (row[String(column.id)] === null ? '' : e(String(row[String(column.id)])))).join(' | ')} |`);
|
|
124
|
+
return own(steps.attrs(block), [header, align, ...rows].join('\n'));
|
|
125
|
+
}
|
|
126
|
+
case 'image': {
|
|
127
|
+
const title = typeof block.caption === 'string' ? ` "${block.caption}"` : '';
|
|
128
|
+
const alt = typeof block.alt === 'string' ? e(block.alt) : '';
|
|
129
|
+
const list = steps.attrs(block);
|
|
130
|
+
if (typeof block.width === 'number')
|
|
131
|
+
list.push(['width', block.width]);
|
|
132
|
+
if (typeof block.height === 'number')
|
|
133
|
+
list.push(['height', block.height]);
|
|
134
|
+
if (block.decorative === true)
|
|
135
|
+
list.push(['decorative', true]);
|
|
136
|
+
return own(list, `}${title})`);
|
|
137
|
+
}
|
|
138
|
+
case 'divider':
|
|
139
|
+
return own(steps.attrs(block), '***');
|
|
140
|
+
case 'code': {
|
|
141
|
+
const code = String(block.code);
|
|
142
|
+
const fence = fenceFor(code);
|
|
143
|
+
const info = [];
|
|
144
|
+
if (typeof block.lang === 'string')
|
|
145
|
+
info.push(block.lang);
|
|
146
|
+
if (Array.isArray(block.highlights))
|
|
147
|
+
info.push(`[${block.highlights.join('|')}]`);
|
|
148
|
+
const list = steps.attrs(block);
|
|
149
|
+
if (typeof block.filename === 'string')
|
|
150
|
+
list.push(['filename', block.filename]);
|
|
151
|
+
if (typeof block.caption === 'string')
|
|
152
|
+
list.push(['caption', block.caption]);
|
|
153
|
+
if (block.lineNumbers === true)
|
|
154
|
+
list.push(['numbers', true]);
|
|
155
|
+
return `${fence}${info.join(' ')}${brace(list)}\n${code}\n${fence}`;
|
|
156
|
+
}
|
|
157
|
+
case 'math':
|
|
158
|
+
return own(steps.attrs(block), `$$\n${String(block.tex)}\n$$`);
|
|
159
|
+
case 'component': {
|
|
160
|
+
const list = steps.attrs(block);
|
|
161
|
+
if (typeof block.height === 'string')
|
|
162
|
+
list.push(['height', block.height]);
|
|
163
|
+
return json(`component ${String(block.component)}`, block.props ?? {}, brace(list));
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
return json('block', block);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function appendTrailing(line, list) {
|
|
170
|
+
if (list.length === 0)
|
|
171
|
+
return line;
|
|
172
|
+
const lines = line.split('\n');
|
|
173
|
+
lines[0] = `${lines[0]} ${comment('element', list)}`;
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
/** A block that reads back unchanged on its own (fragment fields aside). */
|
|
177
|
+
function blockSurvives(block) {
|
|
178
|
+
if (block.type === 'component' ||
|
|
179
|
+
!['heading', 'text', 'list', 'quote', 'table', 'image', 'divider', 'code', 'math'].includes(String(block.type))) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
const bare = withoutSteps(block);
|
|
183
|
+
const text = blockMarkdown(bare, new Steps(false));
|
|
184
|
+
const read = readDeckDraft(`${text}\n`);
|
|
185
|
+
if (read.sink.errors > 0)
|
|
186
|
+
return false;
|
|
187
|
+
const slide = read.draft?.slides?.[0];
|
|
188
|
+
const blocks = slide?.blocks;
|
|
189
|
+
return blocks?.length === 1 && jsonEqual(blocks[0], bare);
|
|
190
|
+
}
|
|
191
|
+
const italic = (text, prefix = '') => `*${prefix}${e(String(text))}*`;
|
|
192
|
+
const opt = (value, write) => value === undefined ? [] : [write(value)];
|
|
193
|
+
function region(name, target, steps, extra, body) {
|
|
194
|
+
return [`::: ${name}${brace([...steps.attrs(target), ...extra])}`, ...body, ':::'];
|
|
195
|
+
}
|
|
196
|
+
function imageLines(image, steps, extra = []) {
|
|
197
|
+
const list = steps.attrs(image);
|
|
198
|
+
if (typeof image.width === 'number')
|
|
199
|
+
list.push(['width', image.width]);
|
|
200
|
+
if (typeof image.height === 'number')
|
|
201
|
+
list.push(['height', image.height]);
|
|
202
|
+
list.push(...extra);
|
|
203
|
+
const line = `})`;
|
|
204
|
+
return list.length > 0 ? [`${line}\n${comment('element', list)}`] : [line];
|
|
205
|
+
}
|
|
206
|
+
function videoLines(video, steps) {
|
|
207
|
+
const list = [['src', String(video.src)]];
|
|
208
|
+
if (typeof video.label === 'string')
|
|
209
|
+
list.push(['label', video.label]);
|
|
210
|
+
if (typeof video.width === 'number')
|
|
211
|
+
list.push(['width', video.width]);
|
|
212
|
+
if (typeof video.height === 'number')
|
|
213
|
+
list.push(['height', video.height]);
|
|
214
|
+
for (const flag of ['autoplay', 'loop', 'muted']) {
|
|
215
|
+
if (video[flag] === true)
|
|
216
|
+
list.push([flag, true]);
|
|
217
|
+
else if (video[flag] === false)
|
|
218
|
+
list.push([flag, 'false']);
|
|
219
|
+
}
|
|
220
|
+
return region('video', video, steps, list, []);
|
|
221
|
+
}
|
|
222
|
+
const title2 = (props) => opt(props.title, (v) => `## ${e(String(v))}`);
|
|
223
|
+
const sub = (target) => opt(target.heading, (v) => `### ${e(String(v))}`);
|
|
224
|
+
const LAYOUT_WRITERS = {
|
|
225
|
+
'title-hero': (p) => [
|
|
226
|
+
`# ${e(String(p.title))}`,
|
|
227
|
+
...opt(p.subtitle, (v) => e(String(v))),
|
|
228
|
+
...opt(p.meta, (v) => italic(v)),
|
|
229
|
+
],
|
|
230
|
+
'section-divider': (p) => [...opt(p.eyebrow, (v) => italic(v)), `# ${e(String(p.title))}`],
|
|
231
|
+
'two-column-split': (p, s) => [
|
|
232
|
+
...title2(p),
|
|
233
|
+
...['left', 'right'].flatMap((side) => {
|
|
234
|
+
const column = p[side];
|
|
235
|
+
return region(side, column, s, [], [...sub(column), e(String(column.body))]);
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
'three-column-cards': (p, s) => [
|
|
239
|
+
...title2(p),
|
|
240
|
+
...p.cards.flatMap((card) => region('card', card, s, typeof card.icon === 'string' ? [['icon', card.icon]] : [], [
|
|
241
|
+
...sub(card),
|
|
242
|
+
e(String(card.body)),
|
|
243
|
+
])),
|
|
244
|
+
],
|
|
245
|
+
'image-left': imageSide,
|
|
246
|
+
'image-right': imageSide,
|
|
247
|
+
'image-centered': (p, s) => [
|
|
248
|
+
...title2(p),
|
|
249
|
+
...imageLines(p.image, s),
|
|
250
|
+
...opt(p.caption, (v) => italic(v)),
|
|
251
|
+
],
|
|
252
|
+
'image-full-bleed': (p, s) => [
|
|
253
|
+
...imageLines(p.image, s, typeof p.overlay === 'string' ? [['overlay', p.overlay]] : []),
|
|
254
|
+
...opt(p.caption, (v) => italic(v)),
|
|
255
|
+
...opt(p.credit, (v) => italic(v, 'Credit: ')),
|
|
256
|
+
],
|
|
257
|
+
quote: (p) => [
|
|
258
|
+
[
|
|
259
|
+
`> ${e(String(p.quote))}`,
|
|
260
|
+
...(p.cite === undefined ? [] : ['>', `> — ${e(String(p.cite))}`]),
|
|
261
|
+
].join('\n'),
|
|
262
|
+
],
|
|
263
|
+
'code-with-callout': (p, s) => {
|
|
264
|
+
const code = p.code;
|
|
265
|
+
const source = String(code.source);
|
|
266
|
+
const fence = fenceFor(source);
|
|
267
|
+
const steps = Array.isArray(code.highlights) ? ` [${code.highlights.join('|')}]` : '';
|
|
268
|
+
return [
|
|
269
|
+
...title2(p),
|
|
270
|
+
`${fence}${String(code.lang)}${steps}${brace(s.attrs(code))}\n${source}\n${fence}`,
|
|
271
|
+
...sub(p),
|
|
272
|
+
p.bullets.map((bullet) => `- ${e(String(bullet))}`).join('\n'),
|
|
273
|
+
];
|
|
274
|
+
},
|
|
275
|
+
'big-stat': (p) => [
|
|
276
|
+
`**${e(String(p.number))}** ${e(String(p.label))}`,
|
|
277
|
+
...opt(p.context, (v) => e(String(v))),
|
|
278
|
+
...opt(p.source, (v) => italic(v, 'Source: ')),
|
|
279
|
+
],
|
|
280
|
+
'comparison-table': (p, s) => {
|
|
281
|
+
const cell = (value) => value === true ? 'Yes' : value === false ? 'No' : value === null ? '' : e(String(value));
|
|
282
|
+
const columns = p.columns;
|
|
283
|
+
const rows = p.rows.map((row) => appendTrailing(`| ${[e(String(row.label)), ...row.values.map(cell)].join(' | ')} |`, s.attrs(row)));
|
|
284
|
+
return [
|
|
285
|
+
...title2(p),
|
|
286
|
+
[
|
|
287
|
+
`| | ${columns.map((c) => e(String(c))).join(' | ')} |`,
|
|
288
|
+
`| ${['', ...columns].map(() => '---').join(' | ')} |`,
|
|
289
|
+
...rows,
|
|
290
|
+
].join('\n'),
|
|
291
|
+
];
|
|
292
|
+
},
|
|
293
|
+
'process-steps': (p, s) => [
|
|
294
|
+
...title2(p),
|
|
295
|
+
p.steps
|
|
296
|
+
.map((step, i) => appendTrailing(`${i + 1}. **${e(String(step.title))}**${step.body === undefined ? '' : ` ${e(String(step.body))}`}`, s.attrs(step)))
|
|
297
|
+
.join('\n'),
|
|
298
|
+
],
|
|
299
|
+
timeline: (p, s) => [
|
|
300
|
+
...title2(p),
|
|
301
|
+
p.events
|
|
302
|
+
.map((event) => appendTrailing(`- **${e(String(event.time))}** ${e(String(event.text))}`, s.attrs(event)))
|
|
303
|
+
.join('\n'),
|
|
304
|
+
],
|
|
305
|
+
'team-avatars': (p, s) => [
|
|
306
|
+
...title2(p),
|
|
307
|
+
p.people
|
|
308
|
+
.map((person) => {
|
|
309
|
+
const list = s.attrs(person);
|
|
310
|
+
if (typeof person.avatar === 'string')
|
|
311
|
+
list.push(['avatar', person.avatar]);
|
|
312
|
+
return appendTrailing(`- **${e(String(person.name))}**${person.role === undefined ? '' : ` — ${e(String(person.role))}`}`, list);
|
|
313
|
+
})
|
|
314
|
+
.join('\n'),
|
|
315
|
+
],
|
|
316
|
+
'closing-cta': (p, s) => {
|
|
317
|
+
const cta = p.cta;
|
|
318
|
+
const list = s.attrs(cta);
|
|
319
|
+
const link = `[${e(String(cta.label))}](${String(cta.href)})`;
|
|
320
|
+
return [
|
|
321
|
+
`# ${e(String(p.title))}`,
|
|
322
|
+
...opt(p.text, (v) => e(String(v))),
|
|
323
|
+
list.length > 0 ? `${link}\n${comment('element', list)}` : link,
|
|
324
|
+
...opt(p.footnote, (v) => italic(v)),
|
|
325
|
+
];
|
|
326
|
+
},
|
|
327
|
+
'video-centered': (p, s) => [
|
|
328
|
+
...title2(p),
|
|
329
|
+
...videoLines(p.video, s),
|
|
330
|
+
...opt(p.caption, (v) => italic(v)),
|
|
331
|
+
],
|
|
332
|
+
'video-left': videoSide,
|
|
333
|
+
'video-right': videoSide,
|
|
334
|
+
chart: (p) => {
|
|
335
|
+
const list = [];
|
|
336
|
+
if (typeof p.height === 'string')
|
|
337
|
+
list.push(['height', p.height]);
|
|
338
|
+
if (p.showTable === true)
|
|
339
|
+
list.push(['table', true]);
|
|
340
|
+
else if (p.showTable === false)
|
|
341
|
+
list.push(['table', 'false']);
|
|
342
|
+
return [...title2(p), json('chart', p.spec, brace(list))];
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
function imageSide(p, s) {
|
|
346
|
+
return [...title2(p), ...imageLines(p.image, s), ...sub(p), e(String(p.body))];
|
|
347
|
+
}
|
|
348
|
+
function videoSide(p, s) {
|
|
349
|
+
return [...title2(p), ...videoLines(p.video, s), ...sub(p), e(String(p.body))];
|
|
350
|
+
}
|
|
351
|
+
function backgroundAttrs(background) {
|
|
352
|
+
if (background === undefined)
|
|
353
|
+
return [];
|
|
354
|
+
if (typeof background === 'string')
|
|
355
|
+
return [['background', background]];
|
|
356
|
+
if (!isBag(background))
|
|
357
|
+
return null;
|
|
358
|
+
switch (background.kind) {
|
|
359
|
+
case 'solid':
|
|
360
|
+
if (typeof background.color === 'string')
|
|
361
|
+
return [['background-color', background.color]];
|
|
362
|
+
return [
|
|
363
|
+
[
|
|
364
|
+
'background-color',
|
|
365
|
+
typeof background.token === 'string' ? background.token : 'background',
|
|
366
|
+
],
|
|
367
|
+
];
|
|
368
|
+
case 'gradient':
|
|
369
|
+
return [
|
|
370
|
+
[
|
|
371
|
+
'background-gradient',
|
|
372
|
+
background.angle === undefined
|
|
373
|
+
? String(background.gradient)
|
|
374
|
+
: `${String(background.gradient)}, ${String(background.angle)}deg`,
|
|
375
|
+
],
|
|
376
|
+
];
|
|
377
|
+
case 'pattern':
|
|
378
|
+
return [['background-pattern', String(background.pattern)]];
|
|
379
|
+
case 'image':
|
|
380
|
+
case 'image-full':
|
|
381
|
+
case 'video':
|
|
382
|
+
case 'video-full': {
|
|
383
|
+
const image = String(background.kind).startsWith('image');
|
|
384
|
+
const out = [
|
|
385
|
+
[image ? 'background-image' : 'background-video', String(background.src)],
|
|
386
|
+
];
|
|
387
|
+
if (String(background.kind).endsWith('-full'))
|
|
388
|
+
out.push(['background-full', true]);
|
|
389
|
+
if (typeof background.alt === 'string')
|
|
390
|
+
out.push(['background-alt', background.alt]);
|
|
391
|
+
if (typeof background.poster === 'string')
|
|
392
|
+
out.push(['background-poster', background.poster]);
|
|
393
|
+
if (typeof background.fit === 'string')
|
|
394
|
+
out.push(['background-fit', background.fit]);
|
|
395
|
+
if (typeof background.position === 'string')
|
|
396
|
+
out.push(['background-position', background.position]);
|
|
397
|
+
if (typeof background.tone === 'string')
|
|
398
|
+
out.push(['tone', background.tone]);
|
|
399
|
+
if (typeof background.overlay === 'number')
|
|
400
|
+
out.push(['background-overlay', background.overlay]);
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** The `.slide` attributes of a slide, or null when one cannot be written. */
|
|
408
|
+
function slideAttrs(slide, plan) {
|
|
409
|
+
const out = [];
|
|
410
|
+
if (plan.idAttr)
|
|
411
|
+
out.push(['id', String(slide.id)]);
|
|
412
|
+
if (slide.layout !== 'free')
|
|
413
|
+
out.push(['layout', String(slide.layout)]);
|
|
414
|
+
if (typeof slide.master === 'string')
|
|
415
|
+
out.push(['master', slide.master]);
|
|
416
|
+
if (typeof slide.title === 'string')
|
|
417
|
+
out.push(['title', slide.title]);
|
|
418
|
+
if (slide.theme !== undefined) {
|
|
419
|
+
if (typeof slide.theme !== 'string')
|
|
420
|
+
return null;
|
|
421
|
+
out.push(['theme', slide.theme]);
|
|
422
|
+
}
|
|
423
|
+
if (typeof slide.transition === 'string')
|
|
424
|
+
out.push(['transition', slide.transition]);
|
|
425
|
+
if (slide.hidden === true)
|
|
426
|
+
out.push(['hidden', true]);
|
|
427
|
+
else if (slide.hidden === false)
|
|
428
|
+
out.push(['hidden', 'false']);
|
|
429
|
+
if (Array.isArray(slide.steps)) {
|
|
430
|
+
if (slide.steps.some((label) => String(label).includes('|') || String(label) !== String(label).trim()))
|
|
431
|
+
return null;
|
|
432
|
+
out.push(['steps', slide.steps.join('|')]);
|
|
433
|
+
}
|
|
434
|
+
const background = backgroundAttrs(slide.background);
|
|
435
|
+
if (background === null)
|
|
436
|
+
return null;
|
|
437
|
+
out.push(...background, ...plan.section);
|
|
438
|
+
for (const [, value] of out)
|
|
439
|
+
if (typeof value === 'string' && !attributeWritable(value))
|
|
440
|
+
return null;
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
function notesText(notes) {
|
|
444
|
+
if (typeof notes !== 'string' || notes === '')
|
|
445
|
+
return [];
|
|
446
|
+
const lines = notes
|
|
447
|
+
.split('\n')
|
|
448
|
+
.map((line) => /^(?:-{2,}[ \t]*$| {0,3}(?::::|`{3}|~{3}|\$\$))/.test(line) ? `\\${line}` : line);
|
|
449
|
+
return [`Note: ${lines.join('\n')}`];
|
|
450
|
+
}
|
|
451
|
+
/** Slide content in Markdown; `jsonBlocks` names free blocks written as JSON fences. */
|
|
452
|
+
function slideBody(slide, jsonBlocks, propsJson) {
|
|
453
|
+
if (slide.layout === 'free') {
|
|
454
|
+
const steps = new Steps(false);
|
|
455
|
+
return slide.blocks.map((block, i) => jsonBlocks.has(i) ? json('block', block) : blockMarkdown(block, steps));
|
|
456
|
+
}
|
|
457
|
+
if (slide.layout === 'component') {
|
|
458
|
+
const component = slide.component;
|
|
459
|
+
return [json(`component ${String(component.component)}`, component.props ?? {})];
|
|
460
|
+
}
|
|
461
|
+
if (slide.layout === 'custom') {
|
|
462
|
+
const props = slide.props;
|
|
463
|
+
return [
|
|
464
|
+
props.data === undefined
|
|
465
|
+
? `::: custom ${String(props.component)}\n:::`
|
|
466
|
+
: json(`custom ${String(props.component)}`, props.data),
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
if (propsJson)
|
|
470
|
+
return [json('props', slide.props)];
|
|
471
|
+
const writer = LAYOUT_WRITERS[String(slide.layout)];
|
|
472
|
+
if (!writer)
|
|
473
|
+
return null;
|
|
474
|
+
return writer(slide.props, new Steps(true));
|
|
475
|
+
}
|
|
476
|
+
function slideMarkdown(slide, plan, jsonBlocks, propsJson) {
|
|
477
|
+
const attrs = slideAttrs(slide, plan);
|
|
478
|
+
const body = slideBody(slide, jsonBlocks, propsJson);
|
|
479
|
+
if (attrs === null || body === null)
|
|
480
|
+
return null;
|
|
481
|
+
const parts = [
|
|
482
|
+
...(attrs.length > 0 ? [comment('slide', attrs)] : []),
|
|
483
|
+
...body,
|
|
484
|
+
...notesText(slide.notes),
|
|
485
|
+
];
|
|
486
|
+
return parts.join('\n\n');
|
|
487
|
+
}
|
|
488
|
+
function withoutId(slide) {
|
|
489
|
+
if (!slide)
|
|
490
|
+
return undefined;
|
|
491
|
+
const out = { ...slide };
|
|
492
|
+
delete out.id;
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
/** True when `text` reads back as `slide` (ids aside). */
|
|
496
|
+
function slideSurvives(text, slide) {
|
|
497
|
+
const read = readDeckDraft(`${text}\n`);
|
|
498
|
+
if (read.sink.errors > 0)
|
|
499
|
+
return false;
|
|
500
|
+
const slides = read.draft?.slides;
|
|
501
|
+
return slides?.length === 1 && jsonEqual(withoutId(slides[0]), withoutId(slide));
|
|
502
|
+
}
|
|
503
|
+
function writeSlide(slide, plan) {
|
|
504
|
+
const head = plan.section.length > 0 ? `${comment('slide', plan.section)}\n\n` : '';
|
|
505
|
+
// Notes are written verbatim after `Note:`: a link whose target the text
|
|
506
|
+
// twins would drop (`[x](javascript:…)`) keeps the slide in an exact,
|
|
507
|
+
// inert JSON fence instead of becoming live Markdown.
|
|
508
|
+
if (typeof slide.notes === 'string' && hasUnsafeLink(slide.notes)) {
|
|
509
|
+
return `${head}${json('slide', slide)}`;
|
|
510
|
+
}
|
|
511
|
+
const jsonBlocks = new Set();
|
|
512
|
+
if (slide.layout === 'free') {
|
|
513
|
+
slide.blocks.forEach((block, i) => {
|
|
514
|
+
if (!blockSurvives(block))
|
|
515
|
+
jsonBlocks.add(i);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const first = slideMarkdown(slide, plan, jsonBlocks, false);
|
|
519
|
+
if (first !== null && slideSurvives(first, slide))
|
|
520
|
+
return first;
|
|
521
|
+
if (slide.layout !== 'free' && slide.layout !== 'component' && slide.layout !== 'custom') {
|
|
522
|
+
const props = slideMarkdown(slide, plan, jsonBlocks, true);
|
|
523
|
+
if (props !== null && slideSurvives(props, slide))
|
|
524
|
+
return props;
|
|
525
|
+
}
|
|
526
|
+
if (slide.layout === 'free') {
|
|
527
|
+
const all = new Set(slide.blocks.map((_, i) => i));
|
|
528
|
+
const blocks = slideMarkdown(slide, plan, all, false);
|
|
529
|
+
if (blocks !== null && slideSurvives(blocks, slide))
|
|
530
|
+
return blocks;
|
|
531
|
+
}
|
|
532
|
+
return `${head}${json('slide', slide)}`;
|
|
533
|
+
}
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// The deck
|
|
536
|
+
function frontmatter(deck, issues) {
|
|
537
|
+
const values = new Map();
|
|
538
|
+
const meta = deck.meta;
|
|
539
|
+
for (const key of ['title', 'description', 'author', 'date', 'locale', 'dir'])
|
|
540
|
+
values.set(key, meta[key]);
|
|
541
|
+
const style = (deck.style ?? {});
|
|
542
|
+
const theme = style.theme;
|
|
543
|
+
if (typeof theme === 'string')
|
|
544
|
+
values.set('theme', theme);
|
|
545
|
+
else if (isBag(theme)) {
|
|
546
|
+
if (theme.kind === 'catalog')
|
|
547
|
+
values.set('theme', theme.id);
|
|
548
|
+
else if (theme.kind === 'seed')
|
|
549
|
+
values.set('themeSeed', theme.seed);
|
|
550
|
+
else
|
|
551
|
+
unrepresentable(issues, ['style', 'theme'], 'An embedded theme object cannot be written as frontmatter.');
|
|
552
|
+
if (theme.kind !== 'embedded' && theme.mode !== undefined)
|
|
553
|
+
values.set('themeMode', theme.mode);
|
|
554
|
+
}
|
|
555
|
+
for (const key of [
|
|
556
|
+
'fontPair',
|
|
557
|
+
'aspect',
|
|
558
|
+
'density',
|
|
559
|
+
'radius',
|
|
560
|
+
'motion',
|
|
561
|
+
'transition',
|
|
562
|
+
'logo',
|
|
563
|
+
'footer',
|
|
564
|
+
'slideNumbers',
|
|
565
|
+
'progress',
|
|
566
|
+
'codeTheme',
|
|
567
|
+
'math',
|
|
568
|
+
]) {
|
|
569
|
+
values.set(key, style[key]);
|
|
570
|
+
}
|
|
571
|
+
const background = style.background;
|
|
572
|
+
if (typeof background === 'string' && isDeckBackgroundPreset(background))
|
|
573
|
+
values.set('background', background);
|
|
574
|
+
else if (isBag(background) &&
|
|
575
|
+
background.kind === 'solid' &&
|
|
576
|
+
typeof background.color === 'string') {
|
|
577
|
+
values.set('background', background.color);
|
|
578
|
+
}
|
|
579
|
+
else if (isBag(background) &&
|
|
580
|
+
background.kind === 'solid' &&
|
|
581
|
+
(background.token === undefined ||
|
|
582
|
+
DECK_SOLID_TOKENS.includes(String(background.token)))) {
|
|
583
|
+
values.set('background', background.token ?? 'background');
|
|
584
|
+
}
|
|
585
|
+
else if (background !== undefined) {
|
|
586
|
+
unrepresentable(issues, ['style', 'background'], 'Deck-wide gradient, pattern and media backgrounds cannot be written as frontmatter; set them per slide or use a preset.');
|
|
587
|
+
}
|
|
588
|
+
const navigation = (deck.navigation ?? {});
|
|
589
|
+
values.set('start', navigation.start);
|
|
590
|
+
values.set('loop', navigation.loop);
|
|
591
|
+
values.set('autoSlide', navigation.autoSlideMs);
|
|
592
|
+
values.set('outlineTitle', deck.outline?.title);
|
|
593
|
+
const lines = ['---'];
|
|
594
|
+
for (const key of Object.keys(FRONTMATTER_KEYS)) {
|
|
595
|
+
const value = values.get(key);
|
|
596
|
+
if (value === undefined)
|
|
597
|
+
continue;
|
|
598
|
+
if (typeof value === 'string' && value.includes('\n')) {
|
|
599
|
+
unrepresentable(issues, FRONTMATTER_KEYS[key]?.path ?? [], `"${key}" holds a line break.`);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
lines.push(`${key}: ${writeScalar(value)}`);
|
|
603
|
+
}
|
|
604
|
+
lines.push('---');
|
|
605
|
+
return lines;
|
|
606
|
+
}
|
|
607
|
+
function unrepresentable(issues, path, message) {
|
|
608
|
+
issues.push(createDiagnostic('md-unrepresentable', message, path, {
|
|
609
|
+
severity: 'warning',
|
|
610
|
+
fix: 'Keep the JSON deck as the source of truth for this field.',
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Write a validated deck as canonical deck Markdown, with `md-unrepresentable`
|
|
615
|
+
* warnings for deck-level fields the dialect cannot hold. Slides and blocks
|
|
616
|
+
* always round-trip (as Markdown when it reads back exactly, otherwise as JSON
|
|
617
|
+
* fences).
|
|
618
|
+
*/
|
|
619
|
+
export function writeDeckMarkdown(deck) {
|
|
620
|
+
const issues = [];
|
|
621
|
+
const slides = deck.slides;
|
|
622
|
+
const lines = frontmatter(deck, issues);
|
|
623
|
+
// Sections: which slide opens one, which are stacked, where a named one ends.
|
|
624
|
+
const sectionAttrs = new Map();
|
|
625
|
+
const vertical = new Set();
|
|
626
|
+
const index = new Map();
|
|
627
|
+
slides.forEach((slide, i) => {
|
|
628
|
+
index.set(String(slide.id), i);
|
|
629
|
+
});
|
|
630
|
+
const inNamed = new Set();
|
|
631
|
+
for (const section of deck.outline?.sections ?? []) {
|
|
632
|
+
const positions = section.slideIds.map((id) => index.get(id) ?? -1);
|
|
633
|
+
const first = positions[0] ?? -1;
|
|
634
|
+
if (first < 0)
|
|
635
|
+
continue;
|
|
636
|
+
const list = [];
|
|
637
|
+
if (section.stack === true) {
|
|
638
|
+
if (section.id !== stackSectionId(section.slideIds[0] ?? ''))
|
|
639
|
+
list.push(['section', section.id]);
|
|
640
|
+
for (const position of positions.slice(1))
|
|
641
|
+
vertical.add(position);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
list.push(['section', section.id]);
|
|
645
|
+
for (const position of positions)
|
|
646
|
+
inNamed.add(position);
|
|
647
|
+
}
|
|
648
|
+
if (section.title !== undefined)
|
|
649
|
+
list.push(['section-title', section.title]);
|
|
650
|
+
sectionAttrs.set(first, list);
|
|
651
|
+
}
|
|
652
|
+
const ends = new Set();
|
|
653
|
+
slides.forEach((_, i) => {
|
|
654
|
+
if (i > 0 &&
|
|
655
|
+
inNamed.has(i - 1) &&
|
|
656
|
+
!inNamed.has(i) &&
|
|
657
|
+
!vertical.has(i) &&
|
|
658
|
+
!sectionAttrs.has(i)) {
|
|
659
|
+
ends.add(i);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
const used = new Set();
|
|
663
|
+
const parts = [];
|
|
664
|
+
slides.forEach((slide, i) => {
|
|
665
|
+
const derived = deriveSlideId(slide, i, used);
|
|
666
|
+
const id = String(slide.id);
|
|
667
|
+
used.add(id);
|
|
668
|
+
const section = [...(sectionAttrs.get(i) ?? [])];
|
|
669
|
+
if (ends.has(i))
|
|
670
|
+
section.push(['section-end', true]);
|
|
671
|
+
const text = writeSlide(slide, { idAttr: derived !== id, section });
|
|
672
|
+
parts.push(i === 0 ? text : `${vertical.has(i) ? '--' : '---'}\n\n${text}`);
|
|
673
|
+
});
|
|
674
|
+
const markdown = `${lines.join('\n')}\n\n${parts.join('\n\n')}\n`;
|
|
675
|
+
// Prove the deck-level fields; report what did not survive.
|
|
676
|
+
const read = readDeckDraft(markdown).draft ?? {};
|
|
677
|
+
for (const key of ['meta', 'style', 'navigation', 'outline']) {
|
|
678
|
+
const before = deck[key];
|
|
679
|
+
if (!jsonEqual(before, read[key]) && !issues.some((issue) => issue.path[0] === key)) {
|
|
680
|
+
unrepresentable(issues, [key], `Part of \`${key}\` does not survive the Markdown form.`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { markdown, issues };
|
|
684
|
+
}
|
|
685
|
+
/** A validated deck as canonical deck Markdown (see `writeDeckMarkdown` for the warnings). */
|
|
686
|
+
export function deckToMarkdown(deck) {
|
|
687
|
+
return writeDeckMarkdown(deck).markdown;
|
|
688
|
+
}
|