@tycoworks/tycoslide 0.7.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 +73 -0
- package/SKILL.md +249 -0
- package/bin/tycoslide.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +197 -0
- package/dist/engine/dom.d.ts +92 -0
- package/dist/engine/dom.js +354 -0
- package/dist/engine/fillers/filler.d.ts +22 -0
- package/dist/engine/fillers/filler.js +53 -0
- package/dist/engine/fillers/image.d.ts +19 -0
- package/dist/engine/fillers/image.js +105 -0
- package/dist/engine/fillers/table.d.ts +21 -0
- package/dist/engine/fillers/table.js +62 -0
- package/dist/engine/fillers/template.d.ts +27 -0
- package/dist/engine/fillers/template.js +221 -0
- package/dist/engine/fillers/text.d.ts +28 -0
- package/dist/engine/fillers/text.js +29 -0
- package/dist/engine/generate.d.ts +51 -0
- package/dist/engine/generate.js +161 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/types.d.ts +128 -0
- package/dist/engine/types.js +22 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +146 -0
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js +88 -0
- package/dist/markdown/deckCompiler.d.ts +36 -0
- package/dist/markdown/deckCompiler.js +289 -0
- package/dist/markdown/index.d.ts +13 -0
- package/dist/markdown/index.js +13 -0
- package/dist/markdown/parsers.d.ts +32 -0
- package/dist/markdown/parsers.js +233 -0
- package/dist/markdown/resolvers/code.d.ts +17 -0
- package/dist/markdown/resolvers/code.js +44 -0
- package/dist/markdown/resolvers/mermaid.d.ts +14 -0
- package/dist/markdown/resolvers/mermaid.js +89 -0
- package/dist/markdown/resolvers/mermaidTheme.d.ts +27 -0
- package/dist/markdown/resolvers/mermaidTheme.js +113 -0
- package/dist/markdown/resolvers/resolver.d.ts +42 -0
- package/dist/markdown/resolvers/resolver.js +52 -0
- package/dist/markdown/slideParser.d.ts +14 -0
- package/dist/markdown/slideParser.js +196 -0
- package/dist/markdown/textTemplate.d.ts +15 -0
- package/dist/markdown/textTemplate.js +75 -0
- package/dist/markdown/types.d.ts +239 -0
- package/dist/markdown/types.js +40 -0
- package/package.json +41 -0
- package/syntax.md +291 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Config, type ThemeConfig } from "./engine/index.js";
|
|
2
|
+
import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig, type ResolvedCompilerDeck } from "./markdown/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run every compiler-owned resolver over `deck` (highlight code fences,
|
|
5
|
+
* render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
|
|
6
|
+
* values are narrowed to the engine's `TextFill | TableFill | ImageFill |
|
|
7
|
+
* TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
|
|
8
|
+
* caller passes the returned value straight to `generate()` with no cast.
|
|
9
|
+
*
|
|
10
|
+
* Fails fast if `deck.output` is missing: downstream `generate()` requires it,
|
|
11
|
+
* and the CLI populates it before calling `buildDeck`; a programmatic caller
|
|
12
|
+
* that forgot to set it hits the error here instead of a confusing engine-side
|
|
13
|
+
* failure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveDeck(deck: CompilerDeck, config: CompilerConfig): Promise<ResolvedCompilerDeck>;
|
|
16
|
+
/**
|
|
17
|
+
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
18
|
+
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
19
|
+
* The compiler's `assets` catalog is intentionally NOT forwarded: the engine
|
|
20
|
+
* is ignorant of theme-level asset metadata; the compiler resolves any asset
|
|
21
|
+
* reference to a filesystem path and wraps it as an ImageFill in
|
|
22
|
+
* `step.content` before this projection runs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Project a CompilerConfig down to the engine's Config shape. Explicit
|
|
27
|
+
* projection at the boundary where compiler-only fields stop mattering.
|
|
28
|
+
*/
|
|
29
|
+
export declare function toEngineConfig(config: CompilerConfig): Config;
|
|
30
|
+
/**
|
|
31
|
+
* End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
|
|
32
|
+
* PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
|
|
33
|
+
* `ResolvedCompilerDeck` — structurally equivalent to the engine's `Deck` —
|
|
34
|
+
* then hand it to the engine's primitives-only `generate()`. No cast required:
|
|
35
|
+
* the narrowing happens at the type level via `resolveDeck`.
|
|
36
|
+
*
|
|
37
|
+
* Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
|
|
38
|
+
* post-write cleanup is needed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig): Promise<void>;
|
|
41
|
+
export type { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
|
|
42
|
+
export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
43
|
+
export type { ManifestOptions } from "./manifest.js";
|
|
44
|
+
export { generateManifest } from "./manifest.js";
|
|
45
|
+
export type { AssetCatalog, AssetEntry, CodeFence, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, MarkdownBlock, MermaidConfig, MermaidFence, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
46
|
+
export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { generate, SlotType, } from "./engine/index.js";
|
|
2
|
+
import { isFence, resolveFences } from "./markdown/resolvers/resolver.js";
|
|
3
|
+
import { CompilerSlotType, ParameterType, } from "./markdown/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Narrow a resolved content map to the engine-shaped value union. Runs after
|
|
6
|
+
* `resolveFences`, so no CodeFence or MermaidFence should remain — a leftover is
|
|
7
|
+
* a resolver bug and throws. Every surviving value is already an engine fill
|
|
8
|
+
* (TextFill / TableFill / ImageFill / TemplateFill), so no unwrapping is needed.
|
|
9
|
+
*/
|
|
10
|
+
function narrowContent(content) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, value] of Object.entries(content)) {
|
|
13
|
+
if (isFence(value)) {
|
|
14
|
+
throw new Error(`resolveDeck: slot "${key}" still holds an unresolved ${value.type} block ` +
|
|
15
|
+
"after resolvers ran. This is a resolver bug.");
|
|
16
|
+
}
|
|
17
|
+
out[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run every compiler-owned resolver over `deck` (highlight code fences,
|
|
23
|
+
* render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
|
|
24
|
+
* values are narrowed to the engine's `TextFill | TableFill | ImageFill |
|
|
25
|
+
* TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
|
|
26
|
+
* caller passes the returned value straight to `generate()` with no cast.
|
|
27
|
+
*
|
|
28
|
+
* Fails fast if `deck.output` is missing: downstream `generate()` requires it,
|
|
29
|
+
* and the CLI populates it before calling `buildDeck`; a programmatic caller
|
|
30
|
+
* that forgot to set it hits the error here instead of a confusing engine-side
|
|
31
|
+
* failure.
|
|
32
|
+
*/
|
|
33
|
+
export async function resolveDeck(deck, config) {
|
|
34
|
+
await resolveFences(deck, config);
|
|
35
|
+
if (deck.output === undefined) {
|
|
36
|
+
throw new Error('resolveDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
theme: deck.theme,
|
|
40
|
+
output: deck.output,
|
|
41
|
+
steps: deck.steps.map((step) => {
|
|
42
|
+
const resolvedStep = { layout: step.layout };
|
|
43
|
+
if (step.content)
|
|
44
|
+
resolvedStep.content = narrowContent(step.content);
|
|
45
|
+
return resolvedStep;
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Project a CompilerParameter or CompilerSlot down to the engine's flat Slot.
|
|
51
|
+
* Parameters (Template, Image) map straight to their engine equivalent; compiler-
|
|
52
|
+
* only slot types (Code, Mermaid) map to Text / Image since their resolved
|
|
53
|
+
* StyledParagraph[] / ImageFill content is filled by the corresponding engine
|
|
54
|
+
* primitive once the compiler is done.
|
|
55
|
+
*
|
|
56
|
+
* The discriminated unions narrow per-variant fields, so the projection is a
|
|
57
|
+
* straight switch over all six type values — no runtime "wrong field on wrong
|
|
58
|
+
* type" checks; TypeScript enforces the invariants at authoring time.
|
|
59
|
+
*/
|
|
60
|
+
function toEngineSlot(slot) {
|
|
61
|
+
switch (slot.type) {
|
|
62
|
+
case ParameterType.Template:
|
|
63
|
+
// A text shape carries no top-level key — its template placeholders are the keys. The
|
|
64
|
+
// compiler emits its expanded content under shapeName, so the engine slot
|
|
65
|
+
// is keyed by shapeName too.
|
|
66
|
+
return { key: slot.shapeName, shapeName: slot.shapeName, type: SlotType.Template };
|
|
67
|
+
case ParameterType.Image:
|
|
68
|
+
return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
|
|
69
|
+
case CompilerSlotType.Text: {
|
|
70
|
+
const result = { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
|
|
71
|
+
if (slot.startAt !== undefined)
|
|
72
|
+
result.startAt = slot.startAt;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
case CompilerSlotType.Table: {
|
|
76
|
+
const result = { key: slot.key, shapeName: slot.shapeName, type: SlotType.Table };
|
|
77
|
+
if (slot.columns !== undefined)
|
|
78
|
+
result.columns = slot.columns;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
case CompilerSlotType.Code:
|
|
82
|
+
// Highlighter resolves the code fence into StyledParagraph[]; engine
|
|
83
|
+
// fills it via fillText.
|
|
84
|
+
return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
|
|
85
|
+
case CompilerSlotType.Mermaid:
|
|
86
|
+
// Mermaid renderer produces a PNG (ImageFill); engine fills it via
|
|
87
|
+
// fillImage. The fit lives on the ImageFill, not the engine Slot.
|
|
88
|
+
return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function toEngineLayout(layout) {
|
|
92
|
+
return {
|
|
93
|
+
name: layout.name,
|
|
94
|
+
slideNumber: layout.slideNumber,
|
|
95
|
+
description: layout.description,
|
|
96
|
+
whenToUse: layout.whenToUse,
|
|
97
|
+
whenNotToUse: layout.whenNotToUse,
|
|
98
|
+
slots: [...layout.parameters.map(toEngineSlot), ...layout.slots.map(toEngineSlot)],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
|
|
103
|
+
* Fields are copied cell-by-cell so the boundary is explicit — no casts.
|
|
104
|
+
* The compiler's `assets` catalog is intentionally NOT forwarded: the engine
|
|
105
|
+
* is ignorant of theme-level asset metadata; the compiler resolves any asset
|
|
106
|
+
* reference to a filesystem path and wraps it as an ImageFill in
|
|
107
|
+
* `step.content` before this projection runs.
|
|
108
|
+
*/
|
|
109
|
+
export function toEngineThemeConfig(config) {
|
|
110
|
+
const result = {
|
|
111
|
+
layouts: config.layouts.map(toEngineLayout),
|
|
112
|
+
template: config.template,
|
|
113
|
+
};
|
|
114
|
+
if (config.outputDir !== undefined)
|
|
115
|
+
result.outputDir = config.outputDir;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Project a CompilerConfig down to the engine's Config shape. Explicit
|
|
120
|
+
* projection at the boundary where compiler-only fields stop mattering.
|
|
121
|
+
*/
|
|
122
|
+
export function toEngineConfig(config) {
|
|
123
|
+
return {
|
|
124
|
+
...toEngineThemeConfig(config),
|
|
125
|
+
rootDir: config.rootDir,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
|
|
130
|
+
* PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
|
|
131
|
+
* `ResolvedCompilerDeck` — structurally equivalent to the engine's `Deck` —
|
|
132
|
+
* then hand it to the engine's primitives-only `generate()`. No cast required:
|
|
133
|
+
* the narrowing happens at the type level via `resolveDeck`.
|
|
134
|
+
*
|
|
135
|
+
* Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
|
|
136
|
+
* post-write cleanup is needed.
|
|
137
|
+
*/
|
|
138
|
+
export async function buildDeck(deck, config) {
|
|
139
|
+
const resolved = await resolveDeck(deck, config);
|
|
140
|
+
await generate(resolved, toEngineConfig(config));
|
|
141
|
+
}
|
|
142
|
+
// Engine — primitives-only public surface.
|
|
143
|
+
export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
144
|
+
export { generateManifest } from "./manifest.js";
|
|
145
|
+
// Markdown / Compiler
|
|
146
|
+
export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { templateKeys } from "./markdown/textTemplate.js";
|
|
2
|
+
import { CompilerSlotType, ParameterType } from "./markdown/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flatten a compiler parameter to the manifest entries advertised to AI authors.
|
|
5
|
+
* A template parameter has no top-level key — its template's keys are the keys, so it
|
|
6
|
+
* flattens to one entry per key (shapeName/template stay manifest-internal). An
|
|
7
|
+
* image parameter is a single key.
|
|
8
|
+
*
|
|
9
|
+
* A template parameter's `limit` is deliberately NOT projected here: it measures the
|
|
10
|
+
* expanded run text (parameter-level), so surfacing it per key would misrepresent
|
|
11
|
+
* it as per-key. How to advertise a parameter-level limit is a Phase 2 decision;
|
|
12
|
+
* until then it stays manifest-internal.
|
|
13
|
+
*/
|
|
14
|
+
function stripParameter(param) {
|
|
15
|
+
switch (param.type) {
|
|
16
|
+
case ParameterType.Template:
|
|
17
|
+
return templateKeys(param.template).map((key) => {
|
|
18
|
+
const result = { key, type: param.type };
|
|
19
|
+
if (param.required)
|
|
20
|
+
result.required = true;
|
|
21
|
+
return result;
|
|
22
|
+
});
|
|
23
|
+
case ParameterType.Image: {
|
|
24
|
+
const result = { key: param.key, type: param.type, fit: param.fit };
|
|
25
|
+
if (param.required)
|
|
26
|
+
result.required = true;
|
|
27
|
+
if (param.limit)
|
|
28
|
+
result.limit = param.limit;
|
|
29
|
+
return [result];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function stripSlot(slot) {
|
|
34
|
+
const result = { key: slot.key, type: slot.type };
|
|
35
|
+
if (slot.required)
|
|
36
|
+
result.required = true;
|
|
37
|
+
if (slot.limit)
|
|
38
|
+
result.limit = slot.limit;
|
|
39
|
+
switch (slot.type) {
|
|
40
|
+
case CompilerSlotType.Text:
|
|
41
|
+
// startAt is a fill hint, not a manifest surface concern.
|
|
42
|
+
break;
|
|
43
|
+
case CompilerSlotType.Table:
|
|
44
|
+
if (slot.columns !== undefined)
|
|
45
|
+
result.columns = slot.columns;
|
|
46
|
+
break;
|
|
47
|
+
case CompilerSlotType.Code:
|
|
48
|
+
result.codeTheme = slot.codeTheme;
|
|
49
|
+
break;
|
|
50
|
+
case CompilerSlotType.Mermaid:
|
|
51
|
+
result.mermaidVariant = slot.mermaidVariant;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
export function generateManifest(config, options) {
|
|
57
|
+
const layouts = config.layouts.map((layout) => ({
|
|
58
|
+
name: layout.name,
|
|
59
|
+
slideNumber: layout.slideNumber,
|
|
60
|
+
description: layout.description,
|
|
61
|
+
whenToUse: layout.whenToUse,
|
|
62
|
+
whenNotToUse: layout.whenNotToUse,
|
|
63
|
+
parameters: layout.parameters.flatMap(stripParameter),
|
|
64
|
+
slots: layout.slots.map(stripSlot),
|
|
65
|
+
}));
|
|
66
|
+
const assets = {};
|
|
67
|
+
for (const [category, entries] of Object.entries(config.assets)) {
|
|
68
|
+
assets[category] = {};
|
|
69
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
70
|
+
const manifestEntry = {
|
|
71
|
+
path: entry.path,
|
|
72
|
+
description: entry.description,
|
|
73
|
+
};
|
|
74
|
+
if (entry.whenToUse)
|
|
75
|
+
manifestEntry.whenToUse = entry.whenToUse;
|
|
76
|
+
assets[category][name] = manifestEntry;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const manifest = {
|
|
80
|
+
version: 1,
|
|
81
|
+
layouts,
|
|
82
|
+
assets,
|
|
83
|
+
build: {
|
|
84
|
+
command: options.build.command,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return JSON.stringify(manifest, null, 2);
|
|
88
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FitMode, type ImageFill } from "../engine/index.js";
|
|
2
|
+
import type { ParsedDocument } from "./slideParser.js";
|
|
3
|
+
import { type CompilerDeck, type CompilerImageParameter, type CompilerLayout } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a resolved image path as an ImageFill using the parameter's declared
|
|
6
|
+
* fit. `param` is narrowed to `CompilerImageParameter` — only image parameters
|
|
7
|
+
* carry a raw path. Mermaid slots never reach here (their content flows as
|
|
8
|
+
* fences through MermaidResolver, which wraps the rendered PNG directly).
|
|
9
|
+
*
|
|
10
|
+
* `path` must be absolute. Callers are responsible for resolution (see
|
|
11
|
+
* `resolve(rootDir, ...)` in the compiler / cli.ts).
|
|
12
|
+
*/
|
|
13
|
+
export declare function toImageFill(param: CompilerImageParameter, path: string): ImageFill;
|
|
14
|
+
/**
|
|
15
|
+
* Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
|
|
16
|
+
* (layout, body). Exported so callers (e.g. cli.ts) reference the constants
|
|
17
|
+
* instead of literal strings.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RESERVED_KEY: {
|
|
20
|
+
readonly LAYOUT: "layout";
|
|
21
|
+
readonly BODY: "body";
|
|
22
|
+
readonly OUTPUT: "output";
|
|
23
|
+
readonly THEME: "theme";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Compile a parsed deck document against a set of layouts.
|
|
27
|
+
*
|
|
28
|
+
* `rootDir` (optional) is the base directory for resolving relative image
|
|
29
|
+
* paths declared in the deck's frontmatter or named slots. When omitted (or
|
|
30
|
+
* empty), image paths are returned unchanged — callers that already produce
|
|
31
|
+
* absolute paths (or callers that don't need resolution, e.g. unit tests)
|
|
32
|
+
* can rely on the pass-through. When provided, relative paths are resolved
|
|
33
|
+
* to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
|
|
34
|
+
*/
|
|
35
|
+
export declare function compileDeck(doc: ParsedDocument, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
|
|
36
|
+
export { FitMode };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { FILLERS, FitMode, SlotType } from "../engine/index.js";
|
|
3
|
+
import { parseGfmTable, parseStyledParagraph } from "./parsers.js";
|
|
4
|
+
import { RESOLVERS } from "./resolvers/resolver.js";
|
|
5
|
+
import { templateKeys, templateToSegments } from "./textTemplate.js";
|
|
6
|
+
import { CompilerSlotType, FenceType, ParameterType, } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Wrap a resolved image path as an ImageFill using the parameter's declared
|
|
9
|
+
* fit. `param` is narrowed to `CompilerImageParameter` — only image parameters
|
|
10
|
+
* carry a raw path. Mermaid slots never reach here (their content flows as
|
|
11
|
+
* fences through MermaidResolver, which wraps the rendered PNG directly).
|
|
12
|
+
*
|
|
13
|
+
* `path` must be absolute. Callers are responsible for resolution (see
|
|
14
|
+
* `resolve(rootDir, ...)` in the compiler / cli.ts).
|
|
15
|
+
*/
|
|
16
|
+
export function toImageFill(param, path) {
|
|
17
|
+
return { type: SlotType.Image, path, fit: param.fit };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reserved keys in a deck's frontmatter — global (theme, output) and per-slide
|
|
21
|
+
* (layout, body). Exported so callers (e.g. cli.ts) reference the constants
|
|
22
|
+
* instead of literal strings.
|
|
23
|
+
*/
|
|
24
|
+
export const RESERVED_KEY = {
|
|
25
|
+
LAYOUT: "layout",
|
|
26
|
+
BODY: "body",
|
|
27
|
+
OUTPUT: "output",
|
|
28
|
+
THEME: "theme",
|
|
29
|
+
};
|
|
30
|
+
const CODE_FENCE_RE = /^```(\w+)\n([\s\S]*?)```\s*$/;
|
|
31
|
+
function toTextFill(text) {
|
|
32
|
+
const paragraphs = text
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.filter((line) => line.trim() !== "")
|
|
35
|
+
.map(parseStyledParagraph);
|
|
36
|
+
return { paragraphs };
|
|
37
|
+
}
|
|
38
|
+
function parseSlotContent(text) {
|
|
39
|
+
const fence = CODE_FENCE_RE.exec(text.trim());
|
|
40
|
+
if (fence) {
|
|
41
|
+
if (fence[1] === FenceType.Mermaid) {
|
|
42
|
+
const block = { type: FenceType.Mermaid, definition: fence[2].replace(/\n$/, "") };
|
|
43
|
+
return block;
|
|
44
|
+
}
|
|
45
|
+
const block = { type: FenceType.Code, language: fence[1], source: fence[2].replace(/\n$/, "") };
|
|
46
|
+
return block;
|
|
47
|
+
}
|
|
48
|
+
const table = parseGfmTable(text);
|
|
49
|
+
if (table)
|
|
50
|
+
return table;
|
|
51
|
+
return toTextFill(text);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Matcher for each CompilerSlotType, composed from the engine's `FILLERS` (text,
|
|
55
|
+
* table) and the compiler's `RESOLVERS` (code, mermaid) — no raw guards imported
|
|
56
|
+
* here, so a slot's expected shape always tracks whatever those registries say a
|
|
57
|
+
* fill/fence looks like.
|
|
58
|
+
*/
|
|
59
|
+
const REGION_MATCHERS = {
|
|
60
|
+
[CompilerSlotType.Text]: FILLERS[SlotType.Text].matches,
|
|
61
|
+
[CompilerSlotType.Table]: FILLERS[SlotType.Table].matches,
|
|
62
|
+
[CompilerSlotType.Code]: RESOLVERS[FenceType.Code].matches,
|
|
63
|
+
[CompilerSlotType.Mermaid]: RESOLVERS[FenceType.Mermaid].matches,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Assert that a region's parsed block matches the slot's declared type. Called
|
|
67
|
+
* after `parseSlotContent` narrows a body/`::name::` region into a MarkdownBlock.
|
|
68
|
+
* Keys off the CompilerSlotType discriminator via `REGION_MATCHERS`.
|
|
69
|
+
*/
|
|
70
|
+
function assertSlotRegion(slot, block, slideIdx, source) {
|
|
71
|
+
if (!REGION_MATCHERS[slot.type](block)) {
|
|
72
|
+
throw new Error(`Slide ${slideIdx}: slot "${slot.key}" (type "${slot.type}") got wrong content shape from ${source}.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isImageParameter(param) {
|
|
76
|
+
return param.type === ParameterType.Image;
|
|
77
|
+
}
|
|
78
|
+
function isTemplateParameter(param) {
|
|
79
|
+
return param.type === ParameterType.Template;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a user-supplied image path against the deck's root directory.
|
|
83
|
+
* When `rootDir` is empty, the path is returned unchanged so callers that
|
|
84
|
+
* already produce absolute paths (or callers that don't care about
|
|
85
|
+
* resolution) can opt out. Absolute paths always pass through.
|
|
86
|
+
*/
|
|
87
|
+
function resolveImagePath(rootDir, path) {
|
|
88
|
+
if (!rootDir || path.startsWith("/"))
|
|
89
|
+
return path;
|
|
90
|
+
return resolve(rootDir, path);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate a layout's key spaces once, independent of any slide, so every later
|
|
94
|
+
* frontmatter lookup and content-map write is unambiguous. Two spaces must each
|
|
95
|
+
* be collision-free — otherwise a fill silently clobbers or throws a misleading
|
|
96
|
+
* error:
|
|
97
|
+
*
|
|
98
|
+
* - **Author keys** — what a frontmatter line addresses: every template-parameter key
|
|
99
|
+
* and every image-parameter key must be distinct, so a line routes to exactly
|
|
100
|
+
* one parameter.
|
|
101
|
+
* - **Content keys** — what `step.content` is addressed by: every image key, every
|
|
102
|
+
* template parameter's `shapeName`, and every slot key must be distinct, so no two
|
|
103
|
+
* overwrite each other in the content map.
|
|
104
|
+
*
|
|
105
|
+
* Also rejects a required template parameter whose template has no keys — it declares
|
|
106
|
+
* no way to be filled, so `required` on it is unsatisfiable.
|
|
107
|
+
*/
|
|
108
|
+
function validateLayout(layout) {
|
|
109
|
+
const authorKeys = new Set();
|
|
110
|
+
const contentKeys = new Set();
|
|
111
|
+
const claimAuthorKey = (key, owner) => {
|
|
112
|
+
if (authorKeys.has(key)) {
|
|
113
|
+
throw new Error(`Layout "${layout.name}": key "${key}" (${owner}) is declared twice; a frontmatter key fills exactly one parameter.`);
|
|
114
|
+
}
|
|
115
|
+
authorKeys.add(key);
|
|
116
|
+
};
|
|
117
|
+
const claimContentKey = (key, owner) => {
|
|
118
|
+
if (contentKeys.has(key)) {
|
|
119
|
+
throw new Error(`Layout "${layout.name}": name "${key}" (${owner}) collides with another parameter or slot; ` +
|
|
120
|
+
"each template parameter's shape, image key, and slot key must be distinct.");
|
|
121
|
+
}
|
|
122
|
+
contentKeys.add(key);
|
|
123
|
+
};
|
|
124
|
+
for (const param of layout.parameters) {
|
|
125
|
+
if (isImageParameter(param)) {
|
|
126
|
+
claimAuthorKey(param.key, "image parameter");
|
|
127
|
+
claimContentKey(param.key, "image parameter");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const keys = templateKeys(param.template);
|
|
131
|
+
if (param.required && keys.length === 0) {
|
|
132
|
+
throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
|
|
133
|
+
}
|
|
134
|
+
for (const key of keys)
|
|
135
|
+
claimAuthorKey(key, `template parameter "${param.shapeName}"`);
|
|
136
|
+
claimContentKey(param.shapeName, "template parameter");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const slot of layout.slots) {
|
|
140
|
+
claimContentKey(slot.key, "slot");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function compileStep(slide, layouts, rootDir) {
|
|
144
|
+
const { frontmatter, body, slots, index } = slide;
|
|
145
|
+
const layout = frontmatter[RESERVED_KEY.LAYOUT];
|
|
146
|
+
if (layout === undefined) {
|
|
147
|
+
throw new Error(`Slide ${index}: missing required "${RESERVED_KEY.LAYOUT}" in frontmatter`);
|
|
148
|
+
}
|
|
149
|
+
const layoutName = String(layout);
|
|
150
|
+
const layoutDef = layouts.find((l) => l.name === layoutName);
|
|
151
|
+
if (!layoutDef) {
|
|
152
|
+
const known = layouts.map((l) => l.name).join(", ");
|
|
153
|
+
throw new Error(`Slide ${index}: unknown layout "${layoutName}". Available layouts: ${known}`);
|
|
154
|
+
}
|
|
155
|
+
// Map each author-facing key to its owning parameter: template keys → the template
|
|
156
|
+
// parameter that declares them, image keys → the image parameter. validateLayout
|
|
157
|
+
// (run once per layout in compileDeck) has already proven these key spaces are
|
|
158
|
+
// collision-free, so a later lookup is unambiguous.
|
|
159
|
+
const templateParams = layoutDef.parameters.filter(isTemplateParameter);
|
|
160
|
+
const imageByKey = new Map();
|
|
161
|
+
const templateParamByKey = new Map();
|
|
162
|
+
for (const param of layoutDef.parameters) {
|
|
163
|
+
if (isImageParameter(param)) {
|
|
164
|
+
imageByKey.set(param.key, param);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const key of templateKeys(param.template))
|
|
168
|
+
templateParamByKey.set(key, param);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
|
|
172
|
+
const content = {};
|
|
173
|
+
// Frontmatter lines fill image parameters (by key) or template-parameter keys
|
|
174
|
+
// (gathered per parameter, expanded together once every line is read).
|
|
175
|
+
const valuesByTemplateParam = new Map();
|
|
176
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
177
|
+
if (key === RESERVED_KEY.LAYOUT)
|
|
178
|
+
continue;
|
|
179
|
+
const image = imageByKey.get(key);
|
|
180
|
+
if (image) {
|
|
181
|
+
content[image.key] = toImageFill(image, resolveImagePath(rootDir, String(value)));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const templateParam = templateParamByKey.get(key);
|
|
185
|
+
if (templateParam) {
|
|
186
|
+
let bucket = valuesByTemplateParam.get(templateParam);
|
|
187
|
+
if (!bucket) {
|
|
188
|
+
bucket = new Map();
|
|
189
|
+
valuesByTemplateParam.set(templateParam, bucket);
|
|
190
|
+
}
|
|
191
|
+
bucket.set(key, String(value));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const validKeys = [...templateParamByKey.keys(), ...imageByKey.keys()].join(", ");
|
|
195
|
+
throw new Error(`Slide ${index}: unknown key "${key}" in layout "${layoutName}". Valid parameters: ${validKeys}`);
|
|
196
|
+
}
|
|
197
|
+
// Expand each template parameter whose keys were supplied. Filling any key fills the
|
|
198
|
+
// parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
|
|
199
|
+
// parameter with no supplied keys stays designer-sample unless required. Content
|
|
200
|
+
// is keyed by shapeName: one parameter → one entry, regardless of key count.
|
|
201
|
+
for (const templateParam of templateParams) {
|
|
202
|
+
const supplied = valuesByTemplateParam.get(templateParam);
|
|
203
|
+
if (!supplied) {
|
|
204
|
+
if (templateParam.required) {
|
|
205
|
+
throw new Error(`Slide ${index}: layout "${layoutName}" requires template parameter "${templateParam.shapeName}" ` +
|
|
206
|
+
`(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
content[templateParam.shapeName] = {
|
|
211
|
+
lines: templateToSegments(templateParam.template, supplied, templateParam.shapeName),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// The default body region resolves against the layout's body slot.
|
|
215
|
+
if (body.trim()) {
|
|
216
|
+
const bodySlot = slotsByKey.get(RESERVED_KEY.BODY);
|
|
217
|
+
if (!bodySlot) {
|
|
218
|
+
throw new Error(`Slide ${index}: layout "${layoutName}" does not accept body content. Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
const parsedBody = parseSlotContent(body);
|
|
221
|
+
assertSlotRegion(bodySlot, parsedBody, index, "body content");
|
|
222
|
+
content[RESERVED_KEY.BODY] = parsedBody;
|
|
223
|
+
}
|
|
224
|
+
// `::name::` regions resolve against the layout's slots.
|
|
225
|
+
for (const [name, text] of Object.entries(slots)) {
|
|
226
|
+
const slot = slotsByKey.get(name);
|
|
227
|
+
if (!slot) {
|
|
228
|
+
throw new Error(`Slide ${index}: unknown slot "::${name}::" in layout "${layoutName}". ` +
|
|
229
|
+
`Valid slots: ${[...slotsByKey.keys()].join(", ")}`);
|
|
230
|
+
}
|
|
231
|
+
const block = parseSlotContent(text);
|
|
232
|
+
assertSlotRegion(slot, block, index, `::${name}::`);
|
|
233
|
+
content[name] = block;
|
|
234
|
+
}
|
|
235
|
+
// Required image parameters (missing frontmatter key) and required slots
|
|
236
|
+
// (missing region) throw with layout + key context. Required template parameters are
|
|
237
|
+
// enforced during expansion above.
|
|
238
|
+
for (const image of imageByKey.values()) {
|
|
239
|
+
if (image.required && content[image.key] === undefined) {
|
|
240
|
+
throw new Error(`Slide ${index}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const slot of layoutDef.slots) {
|
|
244
|
+
if (slot.required && content[slot.key] === undefined) {
|
|
245
|
+
throw new Error(`Slide ${index}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// CompilerDeckStep.content values are MarkdownBlock — CodeFence and
|
|
249
|
+
// MermaidFence are legal in transit until the resolvers narrow them into
|
|
250
|
+
// StyledParagraph[] / ImageFill before the engine sees the deck.
|
|
251
|
+
return { layout: layoutName, content };
|
|
252
|
+
}
|
|
253
|
+
const KNOWN_GLOBAL_KEYS = new Set([RESERVED_KEY.THEME, RESERVED_KEY.OUTPUT]);
|
|
254
|
+
/**
|
|
255
|
+
* Compile a parsed deck document against a set of layouts.
|
|
256
|
+
*
|
|
257
|
+
* `rootDir` (optional) is the base directory for resolving relative image
|
|
258
|
+
* paths declared in the deck's frontmatter or named slots. When omitted (or
|
|
259
|
+
* empty), image paths are returned unchanged — callers that already produce
|
|
260
|
+
* absolute paths (or callers that don't need resolution, e.g. unit tests)
|
|
261
|
+
* can rely on the pass-through. When provided, relative paths are resolved
|
|
262
|
+
* to absolute via `path.resolve(rootDir, path)`; absolute paths pass through.
|
|
263
|
+
*/
|
|
264
|
+
export function compileDeck(doc, layouts, rootDir = "") {
|
|
265
|
+
const theme = doc.global[RESERVED_KEY.THEME];
|
|
266
|
+
if (theme === undefined) {
|
|
267
|
+
throw new Error(`Missing required "${RESERVED_KEY.THEME}" in global frontmatter`);
|
|
268
|
+
}
|
|
269
|
+
const unknownGlobal = Object.keys(doc.global).filter((k) => !KNOWN_GLOBAL_KEYS.has(k));
|
|
270
|
+
if (unknownGlobal.length > 0) {
|
|
271
|
+
throw new Error(`Unknown key(s) in global frontmatter: ${unknownGlobal.join(", ")}. Valid keys: ${[...KNOWN_GLOBAL_KEYS].join(", ")}`);
|
|
272
|
+
}
|
|
273
|
+
// Validate every layout's key spaces up front (once per layout), so a broken
|
|
274
|
+
// theme fails fast regardless of which layouts this deck's slides use.
|
|
275
|
+
for (const layout of layouts)
|
|
276
|
+
validateLayout(layout);
|
|
277
|
+
const deck = {
|
|
278
|
+
theme: String(theme),
|
|
279
|
+
steps: doc.slides.map((slide) => compileStep(slide, layouts, rootDir)),
|
|
280
|
+
};
|
|
281
|
+
const output = doc.global[RESERVED_KEY.OUTPUT];
|
|
282
|
+
if (output !== undefined) {
|
|
283
|
+
deck.output = String(output);
|
|
284
|
+
}
|
|
285
|
+
return deck;
|
|
286
|
+
}
|
|
287
|
+
// Re-export FitMode so callers that build ImageFills by hand can import it
|
|
288
|
+
// through the compiler surface without reaching into the engine.
|
|
289
|
+
export { FitMode };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompilerDeck, CompilerLayout } from "./types.js";
|
|
2
|
+
export declare function compileMarkdownDeck(source: string, layouts: CompilerLayout[], rootDir?: string): CompilerDeck;
|
|
3
|
+
export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
|
|
4
|
+
export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
|
|
5
|
+
export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
|
|
6
|
+
export { isMermaidBlock, MermaidResolver } from "./resolvers/mermaid.js";
|
|
7
|
+
export type { MermaidConfig, MermaidVariant } from "./resolvers/mermaidTheme.js";
|
|
8
|
+
export type { ResolveContext, Resolver } from "./resolvers/resolver.js";
|
|
9
|
+
export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
|
|
10
|
+
export type { ParsedDocument, RawSlide } from "./slideParser.js";
|
|
11
|
+
export { parseSlideDocument } from "./slideParser.js";
|
|
12
|
+
export type { AssetCatalog, AssetEntry, CodeFence, CompilerCodeSlot, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerImageParameter, CompilerLayout, CompilerMermaidSlot, CompilerParameter, CompilerSlot, CompilerTableSlot, CompilerTemplateParameter, CompilerTextSlot, CompilerThemeConfig, MarkdownBlock, MermaidFence, ResolvedCompilerDeck, ResolvedCompilerDeckStep, } from "./types.js";
|
|
13
|
+
export { CompilerSlotType, FenceType, ParameterType } from "./types.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { compileDeck } from "./deckCompiler.js";
|
|
2
|
+
import { parseSlideDocument } from "./slideParser.js";
|
|
3
|
+
export function compileMarkdownDeck(source, layouts, rootDir = "") {
|
|
4
|
+
const doc = parseSlideDocument(source);
|
|
5
|
+
return compileDeck(doc, layouts, rootDir);
|
|
6
|
+
}
|
|
7
|
+
export { compileDeck, RESERVED_KEY } from "./deckCompiler.js";
|
|
8
|
+
export { parseGfmTable, parseInlineRuns, parseProseLine, parseStyledParagraph } from "./parsers.js";
|
|
9
|
+
export { CodeResolver, highlightCode, isCodeBlock } from "./resolvers/code.js";
|
|
10
|
+
export { isMermaidBlock, MermaidResolver } from "./resolvers/mermaid.js";
|
|
11
|
+
export { isFence, RESOLVERS, resolveFences } from "./resolvers/resolver.js";
|
|
12
|
+
export { parseSlideDocument } from "./slideParser.js";
|
|
13
|
+
export { CompilerSlotType, FenceType, ParameterType } from "./types.js";
|