@tycoworks/tycoslide 0.8.0 → 0.10.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/README.md +12 -9
- package/SKILL.md +6 -7
- package/dist/cli.js +39 -59
- package/dist/engine/fillers/filler.d.ts +21 -13
- package/dist/engine/fillers/filler.js +21 -24
- package/dist/engine/generate.d.ts +23 -17
- package/dist/engine/generate.js +157 -70
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/types.d.ts +39 -9
- package/dist/index.d.ts +15 -21
- package/dist/index.js +63 -88
- package/dist/manifest.js +17 -25
- package/dist/markdown/blocks/code.d.ts +17 -0
- package/dist/markdown/blocks/code.js +65 -0
- package/dist/markdown/blocks/image.d.ts +2 -0
- package/dist/markdown/blocks/image.js +9 -0
- package/dist/markdown/blocks/mermaid.d.ts +15 -0
- package/dist/markdown/blocks/mermaid.js +227 -0
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
- package/dist/markdown/blocks/registry.d.ts +16 -0
- package/dist/markdown/blocks/registry.js +44 -0
- package/dist/markdown/blocks/table.d.ts +2 -0
- package/dist/markdown/blocks/table.js +23 -0
- package/dist/markdown/blocks/text.d.ts +12 -0
- package/dist/markdown/blocks/text.js +90 -0
- package/dist/markdown/deckCompiler.d.ts +17 -20
- package/dist/markdown/deckCompiler.js +143 -113
- package/dist/markdown/index.d.ts +11 -11
- package/dist/markdown/index.js +9 -8
- package/dist/markdown/inline.d.ts +26 -0
- package/dist/markdown/inline.js +136 -0
- package/dist/markdown/mdast.d.ts +25 -0
- package/dist/markdown/mdast.js +49 -0
- package/dist/markdown/schema/deckSchema.d.ts +30 -0
- package/dist/markdown/schema/deckSchema.js +51 -0
- package/dist/markdown/schema/strict.d.ts +9 -0
- package/dist/markdown/schema/strict.js +18 -0
- package/dist/markdown/schema/themeConfigSchema.d.ts +106 -0
- package/dist/markdown/schema/themeConfigSchema.js +147 -0
- package/dist/markdown/types.d.ts +194 -134
- package/dist/markdown/types.js +34 -23
- package/dist/skillZip.d.ts +17 -0
- package/dist/skillZip.js +35 -0
- package/package.json +7 -3
- package/syntax.md +1 -1
- package/dist/markdown/parsers.d.ts +0 -32
- package/dist/markdown/parsers.js +0 -233
- package/dist/markdown/resolvers/code.d.ts +0 -17
- package/dist/markdown/resolvers/code.js +0 -44
- package/dist/markdown/resolvers/mermaid.d.ts +0 -14
- package/dist/markdown/resolvers/mermaid.js +0 -81
- package/dist/markdown/resolvers/resolver.d.ts +0 -42
- package/dist/markdown/resolvers/resolver.js +0 -52
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import { type ImageFill } from "../engine/index.js";
|
|
2
2
|
import type { ParsedDocument } from "./slideParser.js";
|
|
3
|
-
import {
|
|
4
|
-
export declare function toImageFill(path: string, type: AssetType): ImageFill;
|
|
3
|
+
import { AssetType, type CompilerConfig, type CompilerDeck } from "./types.js";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Wrap an absolute image path as an ImageFill, expanding the resolved asset
|
|
6
|
+
* `type` into the engine's scaling constraints. Callers resolve the path (see
|
|
7
|
+
* `resolveImagePath`) and the type (from the catalog) first.
|
|
9
8
|
*/
|
|
10
|
-
export declare
|
|
11
|
-
readonly LAYOUT: "layout";
|
|
12
|
-
readonly BODY: "body";
|
|
13
|
-
readonly OUTPUT: "output";
|
|
14
|
-
readonly THEME: "theme";
|
|
15
|
-
readonly NOTES: "notes";
|
|
16
|
-
};
|
|
9
|
+
export declare function toImageFill(path: string, type: AssetType): ImageFill;
|
|
17
10
|
/**
|
|
18
|
-
* Compile a parsed deck document against a
|
|
11
|
+
* Compile a parsed deck document against a theme `config`. Each slide's content
|
|
12
|
+
* is compiled straight into engine fills — prose/tables/images plus highlighted
|
|
13
|
+
* code (Shiki) and rendered mermaid (PNG) — so the returned deck is
|
|
14
|
+
* engine-shaped, ready for `buildDeck`.
|
|
19
15
|
*
|
|
20
|
-
* `rootDir`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
16
|
+
* `config.rootDir` is the base directory for resolving relative image paths
|
|
17
|
+
* declared in the deck's frontmatter or named slots. When empty, image paths are
|
|
18
|
+
* returned unchanged — callers that already produce absolute paths (or don't need
|
|
19
|
+
* resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
|
|
20
|
+
* are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
|
|
21
|
+
* through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` /
|
|
22
|
+
* `config.outputDir` feed the code and mermaid compiles.
|
|
26
23
|
*/
|
|
27
|
-
export declare function compileDeck(doc: ParsedDocument,
|
|
24
|
+
export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
|
|
@@ -1,86 +1,41 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { ImageFit, SlotType } from "../engine/index.js";
|
|
3
|
+
import { parseSlotContent } from "./blocks/registry.js";
|
|
4
|
+
import { validateSlideFrontmatter } from "./schema/deckSchema.js";
|
|
5
5
|
import { templateKeys, templateToSegments } from "./textTemplate.js";
|
|
6
|
-
import { AssetType,
|
|
7
|
-
/**
|
|
8
|
-
* Wrap an absolute image path as an ImageFill, expanding the resolved asset
|
|
9
|
-
* `type` into the engine's scaling constraints. Callers resolve the path (see
|
|
10
|
-
* `resolveImagePath`) and the type (from the catalog) first.
|
|
11
|
-
*/
|
|
6
|
+
import { AssetType, ParameterType, RESERVED_KEY, } from "./types.js";
|
|
12
7
|
/** Map each semantic asset type to the engine's object-fit directive. */
|
|
13
8
|
const FIT_FOR = {
|
|
14
9
|
[AssetType.Icon]: ImageFit.ScaleDown,
|
|
15
10
|
[AssetType.Image]: ImageFit.Contain,
|
|
16
11
|
[AssetType.Background]: ImageFit.Cover,
|
|
17
12
|
};
|
|
18
|
-
export function toImageFill(path, type) {
|
|
19
|
-
return { type: SlotType.Image, path, fit: FIT_FOR[type] };
|
|
20
|
-
}
|
|
21
13
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
14
|
+
* Wrap an absolute image path as an ImageFill, expanding the resolved asset
|
|
15
|
+
* `type` into the engine's scaling constraints. Callers resolve the path (see
|
|
16
|
+
* `resolveImagePath`) and the type (from the catalog) first.
|
|
25
17
|
*/
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
BODY: "body",
|
|
29
|
-
OUTPUT: "output",
|
|
30
|
-
THEME: "theme",
|
|
31
|
-
NOTES: "notes",
|
|
32
|
-
};
|
|
33
|
-
const CODE_FENCE_RE = /^```(\w+)\n([\s\S]*?)```\s*$/;
|
|
34
|
-
function toTextFill(text) {
|
|
35
|
-
const paragraphs = text
|
|
36
|
-
.split(/\r?\n/)
|
|
37
|
-
.filter((line) => line.trim() !== "")
|
|
38
|
-
.map(parseStyledParagraph);
|
|
39
|
-
return { paragraphs };
|
|
40
|
-
}
|
|
41
|
-
function parseSlotContent(text) {
|
|
42
|
-
const fence = CODE_FENCE_RE.exec(text.trim());
|
|
43
|
-
if (fence) {
|
|
44
|
-
if (fence[1] === FenceType.Mermaid) {
|
|
45
|
-
const block = { type: FenceType.Mermaid, definition: fence[2].replace(/\n$/, "") };
|
|
46
|
-
return block;
|
|
47
|
-
}
|
|
48
|
-
const block = { type: FenceType.Code, language: fence[1], source: fence[2].replace(/\n$/, "") };
|
|
49
|
-
return block;
|
|
50
|
-
}
|
|
51
|
-
const table = parseGfmTable(text);
|
|
52
|
-
if (table)
|
|
53
|
-
return table;
|
|
54
|
-
return toTextFill(text);
|
|
18
|
+
export function toImageFill(path, type) {
|
|
19
|
+
return { type: SlotType.Image, path, fit: FIT_FOR[type] };
|
|
55
20
|
}
|
|
21
|
+
const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
|
|
22
|
+
// Anchored whole-field reference: the entire value is `$category.name` or it is
|
|
23
|
+
// not a reference at all. Anchored ⇒ no escaping concerns.
|
|
24
|
+
const ASSET_REF_RE = /^\$([a-zA-Z]\w*)\.([a-zA-Z]\w*)$/;
|
|
56
25
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
const REGION_MATCHERS = {
|
|
63
|
-
[CompilerSlotType.Text]: FILLERS[SlotType.Text].matches,
|
|
64
|
-
[CompilerSlotType.Table]: FILLERS[SlotType.Table].matches,
|
|
65
|
-
[CompilerSlotType.Code]: RESOLVERS[FenceType.Code].matches,
|
|
66
|
-
[CompilerSlotType.Mermaid]: RESOLVERS[FenceType.Mermaid].matches,
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Assert that a region's parsed block matches the slot's declared type. Called
|
|
70
|
-
* after `parseSlotContent` narrows a body/`::name::` region into a MarkdownBlock.
|
|
71
|
-
* Keys off the CompilerSlotType discriminator via `REGION_MATCHERS`.
|
|
26
|
+
* Assert that a region's parsed block folds to a type the slot `accepts`. The
|
|
27
|
+
* folded type comes straight from `parseSlotContent` (which returns it beside
|
|
28
|
+
* the block — no re-probe). A slot may accept several types (text/table/image);
|
|
29
|
+
* the author's markdown shape selects one. A type the slot does not accept fails
|
|
30
|
+
* fast, naming the layout, slot, the type it got, and the types the slot accepts.
|
|
72
31
|
*/
|
|
73
|
-
function assertSlotRegion(slot,
|
|
74
|
-
if (!
|
|
75
|
-
|
|
32
|
+
function assertSlotRegion(slot, got, layoutName, slideIdx, source) {
|
|
33
|
+
if (!slot.accepts.some((b) => b.type === got)) {
|
|
34
|
+
const accepted = slot.accepts.map((b) => b.type).join(", ");
|
|
35
|
+
throw new Error(`Slide ${slideIdx}: layout "${layoutName}" slot "${slot.key}" does not accept ${got} content ` +
|
|
36
|
+
`(from ${source}); it accepts: ${accepted}.`);
|
|
76
37
|
}
|
|
77
38
|
}
|
|
78
|
-
function isImageParameter(param) {
|
|
79
|
-
return param.type === ParameterType.Image;
|
|
80
|
-
}
|
|
81
|
-
function isTemplateParameter(param) {
|
|
82
|
-
return param.type === ParameterType.Template;
|
|
83
|
-
}
|
|
84
39
|
/**
|
|
85
40
|
* Resolve a user-supplied image path against the deck's root directory.
|
|
86
41
|
* When `rootDir` is empty, the path is returned unchanged so callers that
|
|
@@ -92,6 +47,26 @@ function resolveImagePath(rootDir, path) {
|
|
|
92
47
|
return path;
|
|
93
48
|
return resolve(rootDir, path);
|
|
94
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Assert no two layouts sample the same base slide. In the sampled-composition
|
|
52
|
+
* model one layout is one sampled base slide's region-arrangement, so a shared
|
|
53
|
+
* `slideNumber` means the old single-content-type welding leaked through — two
|
|
54
|
+
* layouts on one physical slide that should be one multi-`accepts` layout.
|
|
55
|
+
* `sourceSlide` inside a slot's `accepts` block is a distinct concept (a
|
|
56
|
+
* specimen source for one content type) and is legitimately reused across
|
|
57
|
+
* layouts, so it is deliberately not checked here.
|
|
58
|
+
*/
|
|
59
|
+
function assertUniqueSlideNumbers(layouts) {
|
|
60
|
+
const layoutNameBySlideNumber = new Map();
|
|
61
|
+
for (const layout of layouts) {
|
|
62
|
+
const existing = layoutNameBySlideNumber.get(layout.slideNumber);
|
|
63
|
+
if (existing !== undefined) {
|
|
64
|
+
throw new Error(`Layouts "${existing}" and "${layout.name}" share slideNumber ${layout.slideNumber}; ` +
|
|
65
|
+
"each layout must sample a distinct base slide (one layout = one slide, with slots that accept multiple content types).");
|
|
66
|
+
}
|
|
67
|
+
layoutNameBySlideNumber.set(layout.slideNumber, layout.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
95
70
|
/**
|
|
96
71
|
* Validate a layout's key spaces once, independent of any slide, so every later
|
|
97
72
|
* frontmatter lookup and content-map write is unambiguous. Two spaces must each
|
|
@@ -125,25 +100,29 @@ function validateLayout(layout) {
|
|
|
125
100
|
contentKeys.add(key);
|
|
126
101
|
};
|
|
127
102
|
for (const param of layout.parameters) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
103
|
+
switch (param.type) {
|
|
104
|
+
case ParameterType.Image:
|
|
105
|
+
claimAuthorKey(param.key, "image parameter");
|
|
106
|
+
claimContentKey(param.key, "image parameter");
|
|
107
|
+
break;
|
|
108
|
+
case ParameterType.Template: {
|
|
109
|
+
const keys = templateKeys(param.template);
|
|
110
|
+
if (param.required && keys.length === 0) {
|
|
111
|
+
throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
|
|
112
|
+
}
|
|
113
|
+
for (const key of keys)
|
|
114
|
+
claimAuthorKey(key, `template parameter "${param.shapeName}"`);
|
|
115
|
+
claimContentKey(param.shapeName, "template parameter");
|
|
116
|
+
break;
|
|
136
117
|
}
|
|
137
|
-
for (const key of keys)
|
|
138
|
-
claimAuthorKey(key, `template parameter "${param.shapeName}"`);
|
|
139
|
-
claimContentKey(param.shapeName, "template parameter");
|
|
140
118
|
}
|
|
141
119
|
}
|
|
142
120
|
for (const slot of layout.slots) {
|
|
143
121
|
claimContentKey(slot.key, "slot");
|
|
144
122
|
}
|
|
145
123
|
}
|
|
146
|
-
function compileStep(slide,
|
|
124
|
+
async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
125
|
+
const { layouts, rootDir } = config;
|
|
147
126
|
const { frontmatter, body, slots, index } = slide;
|
|
148
127
|
const layout = frontmatter[RESERVED_KEY.LAYOUT];
|
|
149
128
|
if (layout === undefined) {
|
|
@@ -161,20 +140,27 @@ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
|
|
|
161
140
|
const known = layouts.map((l) => l.name).join(", ");
|
|
162
141
|
throw new Error(`Slide ${index}: unknown layout "${layoutName}". Available layouts: ${known}`);
|
|
163
142
|
}
|
|
143
|
+
// Reject any frontmatter key not declared by this layout's parameters (reserved
|
|
144
|
+
// layout/notes stripped first). The per-layout strict schema IS the unknown-key
|
|
145
|
+
// check — it fires before the resolution loop, so that loop only sees valid keys.
|
|
146
|
+
validateSlideFrontmatter(frontmatter, layoutDef, index);
|
|
164
147
|
// Map each author-facing key to its owning parameter: template keys → the template
|
|
165
148
|
// parameter that declares them, image keys → the image parameter. validateLayout
|
|
166
149
|
// (run once per layout in compileDeck) has already proven these key spaces are
|
|
167
150
|
// collision-free, so a later lookup is unambiguous.
|
|
168
|
-
const templateParams =
|
|
151
|
+
const templateParams = [];
|
|
169
152
|
const imageByKey = new Map();
|
|
170
153
|
const templateParamByKey = new Map();
|
|
171
154
|
for (const param of layoutDef.parameters) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
155
|
+
switch (param.type) {
|
|
156
|
+
case ParameterType.Image:
|
|
157
|
+
imageByKey.set(param.key, param);
|
|
158
|
+
break;
|
|
159
|
+
case ParameterType.Template:
|
|
160
|
+
templateParams.push(param);
|
|
161
|
+
for (const key of templateKeys(param.template))
|
|
162
|
+
templateParamByKey.set(key, param);
|
|
163
|
+
break;
|
|
178
164
|
}
|
|
179
165
|
}
|
|
180
166
|
const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
|
|
@@ -204,10 +190,9 @@ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
|
|
|
204
190
|
valuesByTemplateParam.set(templateParam, bucket);
|
|
205
191
|
}
|
|
206
192
|
bucket.set(key, String(value));
|
|
207
|
-
continue;
|
|
208
193
|
}
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
// Unreachable: validateSlideFrontmatter (above) already rejected any key that is
|
|
195
|
+
// neither an image key nor a template key, so every key here routes to a parameter.
|
|
211
196
|
}
|
|
212
197
|
// Expand each template parameter whose keys were supplied. Filling any key fills the
|
|
213
198
|
// parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
|
|
@@ -232,9 +217,19 @@ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
|
|
|
232
217
|
if (!bodySlot) {
|
|
233
218
|
throw new Error(`Slide ${index}: layout "${layoutName}" does not accept body content. Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
234
219
|
}
|
|
235
|
-
const parsedBody = parseSlotContent(body
|
|
236
|
-
|
|
237
|
-
|
|
220
|
+
const parsedBody = parseSlotContent(body, {
|
|
221
|
+
resolveAssetRef,
|
|
222
|
+
layoutName,
|
|
223
|
+
slideIdx: index,
|
|
224
|
+
source: "body content",
|
|
225
|
+
config,
|
|
226
|
+
layoutVariant: layoutDef.variant,
|
|
227
|
+
});
|
|
228
|
+
// Validate the slot accepts this region's type BEFORE running the (possibly
|
|
229
|
+
// expensive — Shiki, Playwright) fill: a mismatched region fails fast without
|
|
230
|
+
// spinning up a renderer.
|
|
231
|
+
assertSlotRegion(bodySlot, parsedBody.acceptType, layoutName, index, "body content");
|
|
232
|
+
content[RESERVED_KEY.BODY] = await parsedBody.fill();
|
|
238
233
|
}
|
|
239
234
|
// `::name::` regions resolve against the layout's slots.
|
|
240
235
|
for (const [name, text] of Object.entries(slots)) {
|
|
@@ -243,9 +238,17 @@ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
|
|
|
243
238
|
throw new Error(`Slide ${index}: unknown slot "::${name}::" in layout "${layoutName}". ` +
|
|
244
239
|
`Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
245
240
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
const source = `::${name}::`;
|
|
242
|
+
const parsed = parseSlotContent(text, {
|
|
243
|
+
resolveAssetRef,
|
|
244
|
+
layoutName,
|
|
245
|
+
slideIdx: index,
|
|
246
|
+
source,
|
|
247
|
+
config,
|
|
248
|
+
layoutVariant: layoutDef.variant,
|
|
249
|
+
});
|
|
250
|
+
assertSlotRegion(slot, parsed.acceptType, layoutName, index, source);
|
|
251
|
+
content[name] = await parsed.fill();
|
|
249
252
|
}
|
|
250
253
|
// Required image parameters (missing frontmatter key) and required slots
|
|
251
254
|
// (missing region) throw with layout + key context. Required template parameters are
|
|
@@ -260,26 +263,29 @@ function compileStep(slide, layouts, rootDir, assetTypeByPath) {
|
|
|
260
263
|
throw new Error(`Slide ${index}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// StyledParagraph[] / ImageFill before the engine sees the deck.
|
|
266
|
+
// Content values are already engine fills — code fences highlighted to TextFill
|
|
267
|
+
// and mermaid fences rendered to ImageFill by their handler's `compile`.
|
|
266
268
|
const step = { layout: layoutName, content };
|
|
267
269
|
if (notes !== undefined)
|
|
268
270
|
step.notes = notes;
|
|
269
271
|
return step;
|
|
270
272
|
}
|
|
271
|
-
const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
|
|
272
273
|
/**
|
|
273
|
-
* Compile a parsed deck document against a
|
|
274
|
+
* Compile a parsed deck document against a theme `config`. Each slide's content
|
|
275
|
+
* is compiled straight into engine fills — prose/tables/images plus highlighted
|
|
276
|
+
* code (Shiki) and rendered mermaid (PNG) — so the returned deck is
|
|
277
|
+
* engine-shaped, ready for `buildDeck`.
|
|
274
278
|
*
|
|
275
|
-
* `rootDir`
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
279
|
+
* `config.rootDir` is the base directory for resolving relative image paths
|
|
280
|
+
* declared in the deck's frontmatter or named slots. When empty, image paths are
|
|
281
|
+
* returned unchanged — callers that already produce absolute paths (or don't need
|
|
282
|
+
* resolution, e.g. unit tests) rely on the pass-through. When set, relative paths
|
|
283
|
+
* are resolved to absolute via `path.resolve(rootDir, path)`; absolute paths pass
|
|
284
|
+
* through. `config.codeTheme` / `config.mermaid` / `config.mermaidVariant` /
|
|
285
|
+
* `config.outputDir` feed the code and mermaid compiles.
|
|
281
286
|
*/
|
|
282
|
-
export function compileDeck(doc,
|
|
287
|
+
export async function compileDeck(doc, config) {
|
|
288
|
+
const { layouts, rootDir, assets } = config;
|
|
283
289
|
const theme = doc.global[RESERVED_KEY.THEME];
|
|
284
290
|
if (theme === undefined) {
|
|
285
291
|
throw new Error(`Missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
|
|
@@ -288,8 +294,10 @@ export function compileDeck(doc, layouts, rootDir = "", assets = {}) {
|
|
|
288
294
|
if (unknownGlobal.length > 0) {
|
|
289
295
|
throw new Error(`Unknown key(s) in global frontmatter: ${unknownGlobal.join(", ")}. Valid keys: ${[...KNOWN_GLOBAL_KEYS].join(", ")}`);
|
|
290
296
|
}
|
|
291
|
-
// Validate
|
|
292
|
-
//
|
|
297
|
+
// Validate the layout list as a whole (one pass), then each layout's key
|
|
298
|
+
// spaces up front (once per layout), so a broken theme fails fast regardless
|
|
299
|
+
// of which layouts this deck's slides use.
|
|
300
|
+
assertUniqueSlideNumbers(layouts);
|
|
293
301
|
for (const layout of layouts)
|
|
294
302
|
validateLayout(layout);
|
|
295
303
|
// Index each catalog asset's resolved path → its declared type, so an image
|
|
@@ -300,10 +308,32 @@ export function compileDeck(doc, layouts, rootDir = "", assets = {}) {
|
|
|
300
308
|
assetTypeByPath.set(resolveImagePath(rootDir, entry.path), entry.type);
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
// Resolve a body/`::name::` `$category.name` reference against the theme's
|
|
312
|
+
// curated asset catalog. Anchored ⇒ the whole ref is the reference or it is
|
|
313
|
+
// nothing; a found entry wraps through the same path→ImageFit mapping as a
|
|
314
|
+
// frontmatter image (`toImageFill`), so a body image has no second fit story.
|
|
315
|
+
const resolveAssetRef = (ref) => {
|
|
316
|
+
const match = ASSET_REF_RE.exec(ref);
|
|
317
|
+
if (!match) {
|
|
318
|
+
throw new Error(`Asset reference "${ref}" must be in the form $category.name (e.g. $logos.primary).`);
|
|
319
|
+
}
|
|
320
|
+
const [, category, name] = match;
|
|
321
|
+
const entry = assets[category]?.[name];
|
|
322
|
+
if (!entry) {
|
|
323
|
+
const available = Object.entries(assets)
|
|
324
|
+
.flatMap(([cat, group]) => Object.keys(group).map((n) => `$${cat}.${n}`))
|
|
325
|
+
.join(", ");
|
|
326
|
+
throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
|
|
327
|
+
}
|
|
328
|
+
return toImageFill(resolveImagePath(rootDir, entry.path), entry.type);
|
|
306
329
|
};
|
|
330
|
+
// Slides compile in order: a slide's structural errors (unknown layout/key,
|
|
331
|
+
// bad asset ref, accept-type mismatch) fire before its own content is rendered.
|
|
332
|
+
const steps = [];
|
|
333
|
+
for (const slide of doc.slides) {
|
|
334
|
+
steps.push(await compileStep(slide, config, assetTypeByPath, resolveAssetRef));
|
|
335
|
+
}
|
|
336
|
+
const deck = { theme: String(theme), steps };
|
|
307
337
|
const output = doc.global[RESERVED_KEY.OUTPUT];
|
|
308
338
|
if (output !== undefined) {
|
|
309
339
|
deck.output = String(output);
|
package/dist/markdown/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function compileMarkdownDeck(source: string,
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
1
|
+
import type { CompilerConfig, CompilerDeck } from "./types.js";
|
|
2
|
+
export declare function compileMarkdownDeck(source: string, config: CompilerConfig): Promise<CompilerDeck>;
|
|
3
|
+
export { highlightCode } from "./blocks/code.js";
|
|
4
|
+
export type { MermaidConfig, MermaidVariant } from "./blocks/mermaidTheme.js";
|
|
5
|
+
export { type BlockContext, type BlockHandler, parseSlotContent } from "./blocks/registry.js";
|
|
6
|
+
export { compileDeck } from "./deckCompiler.js";
|
|
7
|
+
export { parseInlineRuns } from "./inline.js";
|
|
8
|
+
export { parseRegion } from "./mdast.js";
|
|
9
|
+
export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
|
|
10
10
|
export type { ParsedDocument, RawSlide } from "./slideParser.js";
|
|
11
11
|
export { parseSlideDocument } from "./slideParser.js";
|
|
12
|
-
export type { AssetCatalog, AssetEntry,
|
|
13
|
-
export {
|
|
12
|
+
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerParameter, CompilerSlot, CompilerTemplateParameter, CompilerThemeConfig, EngineFill, Limit, } from "./types.js";
|
|
13
|
+
export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
|
package/dist/markdown/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { compileDeck } from "./deckCompiler.js";
|
|
2
2
|
import { parseSlideDocument } from "./slideParser.js";
|
|
3
|
-
export function compileMarkdownDeck(source,
|
|
3
|
+
export function compileMarkdownDeck(source, config) {
|
|
4
4
|
const doc = parseSlideDocument(source);
|
|
5
|
-
return compileDeck(doc,
|
|
5
|
+
return compileDeck(doc, config);
|
|
6
6
|
}
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
7
|
+
export { highlightCode } from "./blocks/code.js";
|
|
8
|
+
export { parseSlotContent } from "./blocks/registry.js";
|
|
9
|
+
export { compileDeck } from "./deckCompiler.js";
|
|
10
|
+
export { parseInlineRuns } from "./inline.js";
|
|
11
|
+
export { parseRegion } from "./mdast.js";
|
|
12
|
+
export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
|
|
12
13
|
export { parseSlideDocument } from "./slideParser.js";
|
|
13
|
-
export {
|
|
14
|
+
export { AcceptType, AssetType, ParameterType, RESERVED_KEY } from "./types.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PhrasingContent } from "mdast";
|
|
2
|
+
import type { TextRun } from "../engine/index.js";
|
|
3
|
+
/** The subset of MdastType values that are PhrasingContent (dispatched by walkPhrasing). */
|
|
4
|
+
export declare const PHRASING_TYPES: ReadonlySet<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Parse inline markdown formatting in a single line of text into TextRun arrays.
|
|
7
|
+
* Handles **bold**, *italic*, ***bold italic***, ~~strikethrough~~, ++underline++,
|
|
8
|
+
* [link](url), and `inline code`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseInlineRuns(text: string): TextRun[];
|
|
11
|
+
/**
|
|
12
|
+
* Inline formatting carried down the phrasing walk. `breakAsNewline` is the one
|
|
13
|
+
* block-vs-inline knob: in block aggregation a markdown hard `break` node must
|
|
14
|
+
* split the paragraph, so the walk emits it as a `"\n"` run (which the block
|
|
15
|
+
* text splitter then breaks on); the single-line `parseInlineRuns` path leaves
|
|
16
|
+
* it unset, so a hard break stays a space.
|
|
17
|
+
*/
|
|
18
|
+
export interface InlineState {
|
|
19
|
+
bold?: boolean;
|
|
20
|
+
italic?: boolean;
|
|
21
|
+
strikethrough?: boolean;
|
|
22
|
+
underline?: boolean;
|
|
23
|
+
link?: string;
|
|
24
|
+
breakAsNewline?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function walkPhrasingChildren(children: PhrasingContent[], state: InlineState): TextRun[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { MdastType, parseInline } from "./mdast.js";
|
|
2
|
+
/** The subset of MdastType values that are PhrasingContent (dispatched by walkPhrasing). */
|
|
3
|
+
export const PHRASING_TYPES = new Set([
|
|
4
|
+
MdastType.Text,
|
|
5
|
+
MdastType.InlineCode,
|
|
6
|
+
MdastType.Strong,
|
|
7
|
+
MdastType.Emphasis,
|
|
8
|
+
MdastType.Delete,
|
|
9
|
+
MdastType.Insert,
|
|
10
|
+
MdastType.Link,
|
|
11
|
+
MdastType.Break,
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* Parse inline markdown formatting in a single line of text into TextRun arrays.
|
|
15
|
+
* Handles **bold**, *italic*, ***bold italic***, ~~strikethrough~~, ++underline++,
|
|
16
|
+
* [link](url), and `inline code`.
|
|
17
|
+
*/
|
|
18
|
+
export function parseInlineRuns(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return [{ text: "" }];
|
|
21
|
+
// Fast path: no formatting characters means plain text.
|
|
22
|
+
if (!text.includes("*") && !text.includes("[") && !text.includes("`") && !text.includes("~") && !text.includes("+")) {
|
|
23
|
+
return [{ text }];
|
|
24
|
+
}
|
|
25
|
+
const tree = parseInline(text);
|
|
26
|
+
const runs = walkInlineRoot(tree, {});
|
|
27
|
+
return runs.length > 0 ? runs : [{ text: "" }];
|
|
28
|
+
}
|
|
29
|
+
function makeRun(text, state) {
|
|
30
|
+
const run = { text };
|
|
31
|
+
if (state.bold)
|
|
32
|
+
run.bold = true;
|
|
33
|
+
if (state.italic)
|
|
34
|
+
run.italic = true;
|
|
35
|
+
if (state.strikethrough)
|
|
36
|
+
run.strikethrough = true;
|
|
37
|
+
if (state.underline)
|
|
38
|
+
run.underline = true;
|
|
39
|
+
if (state.link)
|
|
40
|
+
run.link = state.link;
|
|
41
|
+
return run;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Walk a `PhrasingContent` node, converting inline formatting into TextRun[].
|
|
45
|
+
*
|
|
46
|
+
* The `node.type` case strings are the mdast library's own discriminators;
|
|
47
|
+
* TypeScript narrows each case to the concrete node interface (Text, Strong,
|
|
48
|
+
* Emphasis, …) so misspellings and dropped cases are compile errors, not
|
|
49
|
+
* silent fallthroughs. `remark-ins` augments PhrasingContentMap with
|
|
50
|
+
* "insert", so that case narrows to the Insert node type.
|
|
51
|
+
*/
|
|
52
|
+
function walkPhrasing(node, state) {
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case MdastType.Text:
|
|
55
|
+
return [makeRun(node.value, state)];
|
|
56
|
+
case MdastType.InlineCode:
|
|
57
|
+
return [makeRun(node.value, state)];
|
|
58
|
+
case MdastType.Strong:
|
|
59
|
+
return walkPhrasingChildren(node.children, { ...state, bold: true });
|
|
60
|
+
case MdastType.Emphasis:
|
|
61
|
+
return walkPhrasingChildren(node.children, { ...state, italic: true });
|
|
62
|
+
case MdastType.Delete:
|
|
63
|
+
return walkPhrasingChildren(node.children, { ...state, strikethrough: true });
|
|
64
|
+
case MdastType.Insert:
|
|
65
|
+
return walkPhrasingChildren(node.children, { ...state, underline: true });
|
|
66
|
+
case MdastType.Link:
|
|
67
|
+
return walkPhrasingChildren(node.children, { ...state, link: node.url });
|
|
68
|
+
case MdastType.Break:
|
|
69
|
+
// A markdown hard break. In block aggregation it splits the paragraph
|
|
70
|
+
// (emit a "\n" run the splitter breaks on); on the single-line inline
|
|
71
|
+
// path it collapses to a space.
|
|
72
|
+
return [makeRun(state.breakAsNewline ? "\n" : " ", state)];
|
|
73
|
+
default:
|
|
74
|
+
// Any other phrasing node with a literal `value` (footnote references,
|
|
75
|
+
// etc.) — emit its text if it has one, otherwise nothing.
|
|
76
|
+
if ("value" in node && typeof node.value === "string") {
|
|
77
|
+
return [makeRun(node.value, state)];
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function walkPhrasingChildren(children, state) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const child of children)
|
|
85
|
+
out.push(...walkPhrasing(child, state));
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Walk the root's children, entering each block-level container's phrasing. */
|
|
89
|
+
function walkInlineRoot(root, state) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const child of root.children)
|
|
92
|
+
out.push(...walkBlock(child, state));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Enter a block-level node. remarkParse on a single line produces a `Root`
|
|
97
|
+
* whose children include block-level nodes (Paragraph, most commonly), which
|
|
98
|
+
* in turn hold phrasing content. Anything unexpected (Heading, Blockquote,
|
|
99
|
+
* List, …) recurses via its phrasing-holding children when applicable.
|
|
100
|
+
*/
|
|
101
|
+
function walkBlock(node, state) {
|
|
102
|
+
switch (node.type) {
|
|
103
|
+
case MdastType.Paragraph:
|
|
104
|
+
return walkPhrasingChildren(node.children, state);
|
|
105
|
+
case MdastType.Heading:
|
|
106
|
+
return walkPhrasingChildren(node.children, state);
|
|
107
|
+
default:
|
|
108
|
+
// Fall back: some block nodes carry phrasing content among their
|
|
109
|
+
// children. Recurse into typed children when the shape is known;
|
|
110
|
+
// otherwise return nothing rather than guess.
|
|
111
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const child of node.children) {
|
|
114
|
+
out.push(...walkUnknown(child, state));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Handle a child of unknown shape: dispatch to phrasing- or block-level walker
|
|
123
|
+
* based on the node's type discriminator. Preserves discriminated-union
|
|
124
|
+
* narrowing by explicitly re-checking the type against known mdast unions.
|
|
125
|
+
*/
|
|
126
|
+
function walkUnknown(node, state) {
|
|
127
|
+
if (typeof node !== "object" || node === null || !("type" in node))
|
|
128
|
+
return [];
|
|
129
|
+
const typed = node;
|
|
130
|
+
// Phrasing-content types we handle directly:
|
|
131
|
+
if (PHRASING_TYPES.has(typed.type))
|
|
132
|
+
return walkPhrasing(node, state);
|
|
133
|
+
// Otherwise treat as a block-content node; walkBlock will recurse or
|
|
134
|
+
// return nothing.
|
|
135
|
+
return walkBlock(node, state);
|
|
136
|
+
}
|