@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.
- package/LICENSE +21 -0
- package/README.md +183 -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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text twins of a deck, ported from Protoyard's `text/deck.ts` onto model
|
|
3
|
+
* v2: `deckToText` (a readable Markdown document for accessibility, artifact
|
|
4
|
+
* descriptions and LLM context; one-way, unlike `deckToMarkdown`) and
|
|
5
|
+
* `deckOutline` (one compact row per slide). Both are pure and
|
|
6
|
+
* deterministic, and nothing an author typed can become HTML or a link
|
|
7
|
+
* target in them: plain fields are escaped and Markdown fields made inert
|
|
8
|
+
* (no raw HTML, and a link whose target is not `https:`, `http:`,
|
|
9
|
+
* `mailto:`, a fragment or a relative path keeps its label and loses its
|
|
10
|
+
* URL; see `neutralizeLinks`).
|
|
11
|
+
*/
|
|
12
|
+
import { mathText, texToMath } from "../math/index.js";
|
|
13
|
+
import { escapeInline, escapeText, fenceFor, inertMarkdown, inlineCode, linkTarget, neutralizeLinks, stripHidden, trimCharEnd, } from "./text.js";
|
|
14
|
+
const join = (parts) => parts
|
|
15
|
+
.filter((part) => typeof part === 'string' && part.trim().length > 0)
|
|
16
|
+
.join('\n\n');
|
|
17
|
+
const h = (level, text) => `${'#'.repeat(Math.min(6, Math.max(1, level)))} ${escapeInline(text)}`;
|
|
18
|
+
const str = (value) => typeof value === 'string' ? value : value === undefined || value === null ? '' : String(value);
|
|
19
|
+
const italic = (text) => (text.trim() ? `*${escapeInline(text)}*` : '');
|
|
20
|
+
function image(alt, src) {
|
|
21
|
+
const source = str(src);
|
|
22
|
+
return source && !source.startsWith('data:') && !source.startsWith('avatar:')
|
|
23
|
+
? `})`
|
|
24
|
+
: `*\\[Image: ${escapeInline(str(alt))}\\]*`;
|
|
25
|
+
}
|
|
26
|
+
const video = (label) => `*\\[Video${str(label) ? `: ${escapeInline(str(label))}` : ''}\\]*`;
|
|
27
|
+
function table(header, rows) {
|
|
28
|
+
if (header.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const cell = (value) => escapeInline(value ?? '') || ' ';
|
|
31
|
+
const line = (cells) => `| ${header.map((_, i) => cell(cells[i])).join(' | ')} |`;
|
|
32
|
+
return [line(header), `| ${header.map(() => '---').join(' | ')} |`, ...rows.map(line)].join('\n');
|
|
33
|
+
}
|
|
34
|
+
function codeFence(code, lang) {
|
|
35
|
+
const body = trimCharEnd(stripHidden(code), '\n');
|
|
36
|
+
const fence = fenceFor(body);
|
|
37
|
+
return `${fence}${lang.replace(/[^\w+-]/g, '').slice(0, 24)}\n${body}\n${fence}`;
|
|
38
|
+
}
|
|
39
|
+
const markdown = (value) => inertMarkdown(neutralizeLinks(str(value))).trim();
|
|
40
|
+
/** Every string of a value, labelled by key path (unknown shapes, component props). */
|
|
41
|
+
function strings(value, path = [], out = [], depth = 0) {
|
|
42
|
+
if (depth > 6 || out.length >= 100)
|
|
43
|
+
return out;
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
out.push(`- ${path.length ? `${escapeInline(path.join('.'))}: ` : ''}${escapeInline(value)}`);
|
|
46
|
+
}
|
|
47
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
48
|
+
out.push(`- ${escapeInline(path.join('.'))}: ${String(value)}`);
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(value)) {
|
|
51
|
+
value.forEach((item, i) => {
|
|
52
|
+
strings(item, [...path, String(i + 1)], out, depth + 1);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (value && typeof value === 'object') {
|
|
56
|
+
for (const [key, item] of Object.entries(value))
|
|
57
|
+
strings(item, [...path, key], out, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function chartText(spec) {
|
|
62
|
+
const series = Array.isArray(spec.series) ? spec.series : [];
|
|
63
|
+
const labels = Array.isArray(spec.labels) ? spec.labels.map(str) : [];
|
|
64
|
+
const head = `*Chart (${escapeInline(str(spec.kind))}): ${escapeInline(str(spec.title))}*`;
|
|
65
|
+
if (series.length === 0 || labels.length === 0)
|
|
66
|
+
return join([head, spec.description ? escapeInline(str(spec.description)) : '']);
|
|
67
|
+
const rows = labels.map((label, i) => [
|
|
68
|
+
label,
|
|
69
|
+
...series.map((s) => {
|
|
70
|
+
const values = Array.isArray(s.values) ? s.values : [];
|
|
71
|
+
const v = values[i];
|
|
72
|
+
return typeof v === 'number' ? String(Math.round(v * 1000) / 1000) : '';
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
return join([
|
|
76
|
+
head,
|
|
77
|
+
spec.description ? escapeInline(str(spec.description)) : '',
|
|
78
|
+
table(['', ...series.map((s) => str(s.label))], rows),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
function blockText(block, level) {
|
|
82
|
+
switch (block.type) {
|
|
83
|
+
case 'heading':
|
|
84
|
+
return join([
|
|
85
|
+
block.eyebrow ? italic(str(block.eyebrow)) : '',
|
|
86
|
+
h(level + Math.max(0, Number(block.level ?? 2) - 1), str(block.text)),
|
|
87
|
+
block.subtitle ? escapeText(str(block.subtitle)) : '',
|
|
88
|
+
]);
|
|
89
|
+
case 'text':
|
|
90
|
+
return markdown(block.markdown);
|
|
91
|
+
case 'list':
|
|
92
|
+
return (Array.isArray(block.items) ? block.items : [])
|
|
93
|
+
.map((item, i) => `${block.ordered === true ? `${i + 1}.` : '-'} ${markdown(item.text).split('\n').join('\n ')}`)
|
|
94
|
+
.join('\n');
|
|
95
|
+
case 'quote':
|
|
96
|
+
return [
|
|
97
|
+
...markdown(block.quote).split('\n'),
|
|
98
|
+
...(block.cite ? ['', `— ${escapeInline(str(block.cite))}`] : []),
|
|
99
|
+
]
|
|
100
|
+
.map((line) => (line ? `> ${line}` : '>'))
|
|
101
|
+
.join('\n');
|
|
102
|
+
case 'callout':
|
|
103
|
+
return join([
|
|
104
|
+
block.title ? `**${escapeInline(str(block.title))}**` : '',
|
|
105
|
+
markdown(block.body),
|
|
106
|
+
])
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => (line ? `> ${line}` : '>'))
|
|
109
|
+
.join('\n');
|
|
110
|
+
case 'divider':
|
|
111
|
+
return '***';
|
|
112
|
+
case 'image':
|
|
113
|
+
return join([image(block.alt, block.src), block.caption ? italic(str(block.caption)) : '']);
|
|
114
|
+
case 'video':
|
|
115
|
+
return video(block.label);
|
|
116
|
+
case 'table': {
|
|
117
|
+
const columns = Array.isArray(block.columns) ? block.columns : [];
|
|
118
|
+
const rows = Array.isArray(block.rows) ? block.rows : [];
|
|
119
|
+
return join([
|
|
120
|
+
block.caption ? italic(str(block.caption)) : '',
|
|
121
|
+
table(columns.map((c) => str(c.label)), rows.map((row) => columns.map((c) => str(row[str(c.id)])))),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
case 'code':
|
|
125
|
+
return join([
|
|
126
|
+
block.filename ? italic(str(block.filename)) : '',
|
|
127
|
+
codeFence(str(block.code), str(block.lang)),
|
|
128
|
+
block.caption ? italic(str(block.caption)) : '',
|
|
129
|
+
]);
|
|
130
|
+
case 'math': {
|
|
131
|
+
const result = texToMath(str(block.tex), { display: true });
|
|
132
|
+
return `*Formula: ${escapeInline(result.ok ? mathText(result.node) : str(block.tex))}* ${inlineCode(str(block.tex))}`;
|
|
133
|
+
}
|
|
134
|
+
case 'chart':
|
|
135
|
+
return chartText(block);
|
|
136
|
+
case 'component': {
|
|
137
|
+
const lines = strings(block.props);
|
|
138
|
+
return join([`*Component ${inlineCode(str(block.component))}*`, lines.join('\n')]);
|
|
139
|
+
}
|
|
140
|
+
default: {
|
|
141
|
+
const nested = [];
|
|
142
|
+
for (const key of ['blocks', 'children']) {
|
|
143
|
+
if (Array.isArray(block[key]))
|
|
144
|
+
nested.push(blocksText(block[key], level));
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(block.columns) && block.type === 'columns') {
|
|
147
|
+
for (const column of block.columns)
|
|
148
|
+
if (Array.isArray(column))
|
|
149
|
+
nested.push(blocksText(column, level));
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(block.panels)) {
|
|
152
|
+
for (const panel of block.panels) {
|
|
153
|
+
nested.push(join([
|
|
154
|
+
h(level, str(panel.label)),
|
|
155
|
+
Array.isArray(panel.blocks) ? blocksText(panel.blocks, level + 1) : '',
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (nested.length > 0)
|
|
160
|
+
return join(nested);
|
|
161
|
+
const copy = { ...block };
|
|
162
|
+
delete copy.type;
|
|
163
|
+
delete copy.id;
|
|
164
|
+
const lines = strings(copy);
|
|
165
|
+
return join([`*\\[${escapeInline(str(block.type))}\\]*`, lines.join('\n')]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function blocksText(blocks, level) {
|
|
170
|
+
return join(blocks.map((block) => blockText(block, level)));
|
|
171
|
+
}
|
|
172
|
+
/** Body Markdown per catalog layout, from its validated props. */
|
|
173
|
+
const LAYOUT_TEXT = {
|
|
174
|
+
'title-hero': (p) => join([p.subtitle ? escapeText(str(p.subtitle)) : '', p.meta ? italic(str(p.meta)) : '']),
|
|
175
|
+
'section-divider': (p) => (p.eyebrow ? italic(str(p.eyebrow)) : ''),
|
|
176
|
+
'two-column-split': (p, level) => join([p.left, p.right].flatMap((side) => [
|
|
177
|
+
side?.heading ? h(level, str(side.heading)) : '',
|
|
178
|
+
escapeText(str(side?.body)),
|
|
179
|
+
])),
|
|
180
|
+
'three-column-cards': (p, level) => join(p.cards.flatMap((card) => [
|
|
181
|
+
h(level, str(card.heading)),
|
|
182
|
+
escapeText(str(card.body)),
|
|
183
|
+
])),
|
|
184
|
+
'image-left': imageSide,
|
|
185
|
+
'image-right': imageSide,
|
|
186
|
+
'image-centered': (p) => join([
|
|
187
|
+
image(p.image.alt, p.image.src),
|
|
188
|
+
p.caption ? italic(str(p.caption)) : '',
|
|
189
|
+
]),
|
|
190
|
+
'image-full-bleed': (p) => join([
|
|
191
|
+
image(p.image.alt, p.image.src),
|
|
192
|
+
p.caption ? italic(str(p.caption)) : '',
|
|
193
|
+
p.credit ? italic(`Credit: ${str(p.credit)}`) : '',
|
|
194
|
+
]),
|
|
195
|
+
quote: (p) => [
|
|
196
|
+
...escapeText(str(p.quote)).split('\n'),
|
|
197
|
+
...(p.cite ? ['', `— ${escapeInline(str(p.cite))}`] : []),
|
|
198
|
+
]
|
|
199
|
+
.map((line) => (line ? `> ${line}` : '>'))
|
|
200
|
+
.join('\n'),
|
|
201
|
+
'code-with-callout': (p, level) => {
|
|
202
|
+
const code = p.code;
|
|
203
|
+
const highlights = Array.isArray(code.highlights)
|
|
204
|
+
? code.highlights.map(str)
|
|
205
|
+
: [];
|
|
206
|
+
return join([
|
|
207
|
+
codeFence(str(code.source), str(code.lang)),
|
|
208
|
+
highlights.length
|
|
209
|
+
? `*Highlighted lines, step by step: ${highlights.map(escapeInline).join(' → ')}*`
|
|
210
|
+
: '',
|
|
211
|
+
p.heading ? h(level, str(p.heading)) : '',
|
|
212
|
+
p.bullets.map((bullet) => `- ${escapeInline(str(bullet))}`).join('\n'),
|
|
213
|
+
]);
|
|
214
|
+
},
|
|
215
|
+
'big-stat': (p) => join([
|
|
216
|
+
`**${escapeInline(str(p.number))}** ${escapeInline(str(p.label))}`,
|
|
217
|
+
p.context ? escapeText(str(p.context)) : '',
|
|
218
|
+
p.source ? italic(`Source: ${str(p.source)}`) : '',
|
|
219
|
+
]),
|
|
220
|
+
'comparison-table': (p) => table(['', ...p.columns.map(str)], p.rows.map((row) => [
|
|
221
|
+
str(row.label),
|
|
222
|
+
...row.values.map((v) => (v === true ? 'Yes' : v === false ? 'No' : str(v))),
|
|
223
|
+
])),
|
|
224
|
+
'process-steps': (p) => p.steps
|
|
225
|
+
.map((step, i) => `${i + 1}. **${escapeInline(str(step.title))}**${step.body ? ` ${escapeInline(str(step.body))}` : ''}`)
|
|
226
|
+
.join('\n'),
|
|
227
|
+
timeline: (p) => p.events
|
|
228
|
+
.map((event) => `- **${escapeInline(str(event.time))}** ${escapeInline(str(event.text))}`)
|
|
229
|
+
.join('\n'),
|
|
230
|
+
'team-avatars': (p) => p.people
|
|
231
|
+
.map((person) => `- **${escapeInline(str(person.name))}**${person.role ? ` — ${escapeInline(str(person.role))}` : ''}`)
|
|
232
|
+
.join('\n'),
|
|
233
|
+
'closing-cta': (p) => {
|
|
234
|
+
const cta = p.cta;
|
|
235
|
+
const href = str(cta.href);
|
|
236
|
+
const safe = /^(?:https:|mailto:|#)/.test(href) || (/^[A-Za-z0-9._~/-]/.test(href) && !href.includes(':'));
|
|
237
|
+
return join([
|
|
238
|
+
p.text ? escapeText(str(p.text)) : '',
|
|
239
|
+
safe
|
|
240
|
+
? `[${escapeInline(str(cta.label))}](${linkTarget(href)})`
|
|
241
|
+
: escapeInline(str(cta.label)),
|
|
242
|
+
p.footnote ? italic(str(p.footnote)) : '',
|
|
243
|
+
]);
|
|
244
|
+
},
|
|
245
|
+
'video-centered': (p) => join([video(p.video.label), p.caption ? italic(str(p.caption)) : '']),
|
|
246
|
+
'video-left': videoSide,
|
|
247
|
+
'video-right': videoSide,
|
|
248
|
+
chart: (p) => chartText(p.spec),
|
|
249
|
+
};
|
|
250
|
+
function imageSide(p, level) {
|
|
251
|
+
return join([
|
|
252
|
+
image(p.image.alt, p.image.src),
|
|
253
|
+
p.heading ? h(level, str(p.heading)) : '',
|
|
254
|
+
escapeText(str(p.body)),
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
function videoSide(p, level) {
|
|
258
|
+
return join([
|
|
259
|
+
video(p.video.label),
|
|
260
|
+
p.heading ? h(level, str(p.heading)) : '',
|
|
261
|
+
escapeText(str(p.body)),
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
/** The title a slide is known by: its own, else the layout's title prop, else the first heading. */
|
|
265
|
+
export function slideTitle(slide) {
|
|
266
|
+
const own = slide.title?.trim();
|
|
267
|
+
if (own)
|
|
268
|
+
return own;
|
|
269
|
+
const bag = slide;
|
|
270
|
+
if (Array.isArray(bag.blocks)) {
|
|
271
|
+
const first = bag.blocks.find((block) => block.type === 'heading');
|
|
272
|
+
return first ? str(first.text).trim() || null : null;
|
|
273
|
+
}
|
|
274
|
+
const props = (bag.props ?? {});
|
|
275
|
+
if (slide.layout === 'custom')
|
|
276
|
+
return null;
|
|
277
|
+
for (const key of ['title', 'heading', 'number']) {
|
|
278
|
+
const value = props[key];
|
|
279
|
+
if (typeof value === 'string' && value.trim())
|
|
280
|
+
return value.trim();
|
|
281
|
+
}
|
|
282
|
+
if (slide.layout === 'quote' && typeof props.quote === 'string') {
|
|
283
|
+
const quote = props.quote.trim().replace(/\s+/g, ' ');
|
|
284
|
+
return quote.length > 60 ? `${quote.slice(0, 59)}…` : quote;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function slideBody(slide, level) {
|
|
289
|
+
const bag = slide;
|
|
290
|
+
if (slide.layout === 'free')
|
|
291
|
+
return blocksText(bag.blocks, level);
|
|
292
|
+
if (slide.layout === 'component') {
|
|
293
|
+
const component = bag.component;
|
|
294
|
+
return blockText({ type: 'component', component: component.component, props: component.props }, level);
|
|
295
|
+
}
|
|
296
|
+
if (slide.layout === 'custom') {
|
|
297
|
+
const props = bag.props;
|
|
298
|
+
return join([
|
|
299
|
+
`*Custom slide ${inlineCode(str(props.component))}*`,
|
|
300
|
+
strings(props.data).join('\n'),
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
const render = LAYOUT_TEXT[slide.layout];
|
|
304
|
+
return render ? render(bag.props, level) : strings(bag.props).join('\n');
|
|
305
|
+
}
|
|
306
|
+
function listed(deck, includeHidden) {
|
|
307
|
+
const sectionOf = new Map();
|
|
308
|
+
for (const section of deck.outline?.sections ?? []) {
|
|
309
|
+
for (const id of section.slideIds) {
|
|
310
|
+
if (!sectionOf.has(id))
|
|
311
|
+
sectionOf.set(id, section.title === undefined
|
|
312
|
+
? { id: section.id }
|
|
313
|
+
: { id: section.id, title: section.title });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return deck.slides
|
|
317
|
+
.filter((slide) => includeHidden || !slide.hidden)
|
|
318
|
+
.map((slide, i) => ({ slide, number: i + 1, section: sectionOf.get(slide.id) ?? null }));
|
|
319
|
+
}
|
|
320
|
+
/** Never a valid section id (ids are lowercase slugs). */
|
|
321
|
+
const UNGROUPED = '#ungrouped';
|
|
322
|
+
/** The deck as a readable, inert Markdown document (one-way; see `deckToMarkdown` for the round-trip form). */
|
|
323
|
+
export function deckToText(deck, options = {}) {
|
|
324
|
+
const includeNotes = options.notes ?? true;
|
|
325
|
+
const slides = listed(deck, options.includeHidden ?? false);
|
|
326
|
+
const hasSections = slides.some((entry) => entry.section !== null);
|
|
327
|
+
const slideLevel = hasSections ? 3 : 2;
|
|
328
|
+
const parts = [
|
|
329
|
+
h(1, deck.outline?.title?.trim() || options.title?.trim() || deck.meta.title),
|
|
330
|
+
];
|
|
331
|
+
let current = null;
|
|
332
|
+
for (const { slide, number, section } of slides) {
|
|
333
|
+
if (hasSections && section && section.id !== current) {
|
|
334
|
+
current = section.id;
|
|
335
|
+
parts.push(h(2, section.title?.trim() || section.id));
|
|
336
|
+
}
|
|
337
|
+
else if (hasSections && !section && current !== null && current !== UNGROUPED) {
|
|
338
|
+
current = UNGROUPED;
|
|
339
|
+
parts.push(h(2, 'Other slides'));
|
|
340
|
+
}
|
|
341
|
+
const title = slideTitle(slide);
|
|
342
|
+
parts.push(join([
|
|
343
|
+
`${'#'.repeat(slideLevel)} ${number}. ${title ? escapeInline(title) : `Slide ${number}`}`,
|
|
344
|
+
slideBody(slide, slideLevel + 1),
|
|
345
|
+
includeNotes &&
|
|
346
|
+
slide.notes?.trim() &&
|
|
347
|
+
join([h(slideLevel + 1, 'Notes'), escapeText(slide.notes)]),
|
|
348
|
+
]));
|
|
349
|
+
}
|
|
350
|
+
return `${parts.join('\n\n')}\n`;
|
|
351
|
+
}
|
|
352
|
+
/** A title for the outline, with unsafe link targets dropped (see `neutralizeLinks`). */
|
|
353
|
+
const outlineTitle = (title) => (title === null ? null : neutralizeLinks(title));
|
|
354
|
+
/** One compact row per slide: id, layout and title (for authoring hints, MCP and agents). */
|
|
355
|
+
export function deckOutline(deck, options = {}) {
|
|
356
|
+
return {
|
|
357
|
+
title: outlineTitle(deck.outline?.title?.trim() || deck.meta.title.trim() || null),
|
|
358
|
+
slides: listed(deck, options.includeHidden ?? true).map(({ slide, number, section }) => ({
|
|
359
|
+
number,
|
|
360
|
+
id: slide.id,
|
|
361
|
+
layout: slide.layout,
|
|
362
|
+
title: outlineTitle(slideTitle(slide)),
|
|
363
|
+
section: section?.id ?? null,
|
|
364
|
+
hidden: slide.hidden === true,
|
|
365
|
+
})),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** The outline as plain lines: `3. pricing · comparison-table · Plans compared`. */
|
|
369
|
+
export function formatDeckOutline(outline) {
|
|
370
|
+
const clean = (text) => neutralizeLinks(text).replace(/\s+/g, ' ').trim();
|
|
371
|
+
return outline.slides
|
|
372
|
+
.map((slide) => `${slide.number}. ${slide.id} · ${slide.layout}${slide.title ? ` · ${clean(slide.title)}` : ''}${slide.hidden ? ' (hidden)' : ''}`)
|
|
373
|
+
.join('\n');
|
|
374
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Deck, DeckSlide } from '../model/deck.js';
|
|
2
|
+
import { type DeckDiagnostic } from '../schema/index.js';
|
|
3
|
+
export interface DeckMarkdownWriteResult {
|
|
4
|
+
markdown: string;
|
|
5
|
+
/** `md-unrepresentable` warnings: deck fields the Markdown could not carry. */
|
|
6
|
+
issues: DeckDiagnostic[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Write a validated deck as canonical deck Markdown, with `md-unrepresentable`
|
|
10
|
+
* warnings for deck-level fields the dialect cannot hold. Slides and blocks
|
|
11
|
+
* always round-trip (as Markdown when it reads back exactly, otherwise as JSON
|
|
12
|
+
* fences).
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeDeckMarkdown(deck: Deck): DeckMarkdownWriteResult;
|
|
15
|
+
/** A validated deck as canonical deck Markdown (see `writeDeckMarkdown` for the warnings). */
|
|
16
|
+
export declare function deckToMarkdown(deck: Deck): string;
|
|
17
|
+
export type { DeckSlide };
|