@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RawSlide {
|
|
2
|
+
index: number;
|
|
3
|
+
frontmatter: Record<string, unknown>;
|
|
4
|
+
body: string;
|
|
5
|
+
slots: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedDocument {
|
|
8
|
+
global: Record<string, unknown>;
|
|
9
|
+
slides: RawSlide[];
|
|
10
|
+
}
|
|
11
|
+
export declare function parseSlideDocument(source: string): ParsedDocument;
|
|
12
|
+
export declare class FrontmatterParseError extends Error {
|
|
13
|
+
constructor(slideIndex: number, yamlSource: string, cause: unknown);
|
|
14
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
// ============================================
|
|
3
|
+
// PUBLIC API
|
|
4
|
+
// ============================================
|
|
5
|
+
export function parseSlideDocument(source) {
|
|
6
|
+
const { global, rest } = extractGlobalFrontmatter(source);
|
|
7
|
+
const rawSlides = splitIntoSlides(rest);
|
|
8
|
+
const slides = [];
|
|
9
|
+
for (const raw of rawSlides) {
|
|
10
|
+
slides.push(buildSlide(slides.length, raw.frontmatter, raw.content));
|
|
11
|
+
}
|
|
12
|
+
return { global, slides };
|
|
13
|
+
}
|
|
14
|
+
// ============================================
|
|
15
|
+
// GLOBAL FRONTMATTER
|
|
16
|
+
// ============================================
|
|
17
|
+
const GLOBAL_FM_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
18
|
+
function extractGlobalFrontmatter(source) {
|
|
19
|
+
const match = source.match(GLOBAL_FM_RE);
|
|
20
|
+
if (!match)
|
|
21
|
+
return { global: {}, rest: source };
|
|
22
|
+
const parsed = parseYaml(match[1]);
|
|
23
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
24
|
+
return {
|
|
25
|
+
global: parsed,
|
|
26
|
+
rest: source.slice(match[0].length),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (parsed !== null && parsed !== undefined) {
|
|
30
|
+
throw new Error(`Global frontmatter must be a YAML mapping (key: value pairs), got ${Array.isArray(parsed) ? "array" : typeof parsed}.`);
|
|
31
|
+
}
|
|
32
|
+
return { global: {}, rest: source };
|
|
33
|
+
}
|
|
34
|
+
const SEPARATOR_RE = /^---[ \t]*$/;
|
|
35
|
+
const CODE_FENCE_OPEN_RE = /^(`{3,}|~{3,})/;
|
|
36
|
+
/**
|
|
37
|
+
* States:
|
|
38
|
+
* - BODY: accumulating body content lines for current slide
|
|
39
|
+
* - FM: accumulating frontmatter lines (between --- pair)
|
|
40
|
+
*
|
|
41
|
+
* Transitions:
|
|
42
|
+
* - BODY + `---` -> flush current slide, start new slide, enter FM state
|
|
43
|
+
* - FM + `---` -> close frontmatter, enter BODY state
|
|
44
|
+
* - FM + blank line (before any non-blank FM line) -> no frontmatter, enter BODY
|
|
45
|
+
* - EOF while in FM -> unterminated frontmatter, treat accumulated lines as body
|
|
46
|
+
*/
|
|
47
|
+
function splitIntoSlides(text) {
|
|
48
|
+
const lines = text.split(/\r?\n/);
|
|
49
|
+
const slides = [];
|
|
50
|
+
let inFM = false;
|
|
51
|
+
let fmStarted = false;
|
|
52
|
+
let fmClosed = false;
|
|
53
|
+
let fmLines = [];
|
|
54
|
+
let bodyLines = [];
|
|
55
|
+
let inCodeFence = false;
|
|
56
|
+
let codeFenceChar = "";
|
|
57
|
+
let codeFenceLen = 0;
|
|
58
|
+
function flushSlide() {
|
|
59
|
+
const fm = fmLines.join("\n").trim();
|
|
60
|
+
const body = bodyLines.join("\n").trim();
|
|
61
|
+
if (fm || body) {
|
|
62
|
+
slides.push({ frontmatter: fm, content: body });
|
|
63
|
+
}
|
|
64
|
+
fmLines = [];
|
|
65
|
+
bodyLines = [];
|
|
66
|
+
}
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (!inCodeFence) {
|
|
69
|
+
const fenceMatch = line.match(CODE_FENCE_OPEN_RE);
|
|
70
|
+
if (fenceMatch) {
|
|
71
|
+
inCodeFence = true;
|
|
72
|
+
codeFenceChar = fenceMatch[1][0];
|
|
73
|
+
codeFenceLen = fenceMatch[1].length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (trimmed.length >= codeFenceLen && trimmed === codeFenceChar.repeat(trimmed.length)) {
|
|
79
|
+
inCodeFence = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const isSeparator = !inCodeFence && SEPARATOR_RE.test(line);
|
|
83
|
+
if (isSeparator) {
|
|
84
|
+
if (inFM) {
|
|
85
|
+
inFM = false;
|
|
86
|
+
fmClosed = true;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (fmLines.length > 0 && !fmClosed) {
|
|
90
|
+
bodyLines = [...fmLines, ...bodyLines];
|
|
91
|
+
fmLines = [];
|
|
92
|
+
}
|
|
93
|
+
flushSlide();
|
|
94
|
+
inFM = true;
|
|
95
|
+
fmStarted = false;
|
|
96
|
+
fmClosed = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (inFM) {
|
|
100
|
+
if (!fmStarted && line.trim() === "") {
|
|
101
|
+
inFM = false;
|
|
102
|
+
bodyLines.push(line);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
fmStarted = true;
|
|
106
|
+
fmLines.push(line);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
bodyLines.push(line);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Unterminated FM at EOF -> treat as body content
|
|
114
|
+
if (fmLines.length > 0 && !fmClosed) {
|
|
115
|
+
bodyLines = [...fmLines, ...bodyLines];
|
|
116
|
+
fmLines = [];
|
|
117
|
+
}
|
|
118
|
+
flushSlide();
|
|
119
|
+
return slides;
|
|
120
|
+
}
|
|
121
|
+
// ============================================
|
|
122
|
+
// SLIDE BUILDER
|
|
123
|
+
// ============================================
|
|
124
|
+
function buildSlide(index, fmString, rawContent) {
|
|
125
|
+
const frontmatter = parseFrontmatter(fmString, index);
|
|
126
|
+
const { defaultSlot, slots } = extractSlots(rawContent);
|
|
127
|
+
return { index, frontmatter, body: defaultSlot, slots };
|
|
128
|
+
}
|
|
129
|
+
export class FrontmatterParseError extends Error {
|
|
130
|
+
constructor(slideIndex, yamlSource, cause) {
|
|
131
|
+
const preview = yamlSource.length > 80 ? `${yamlSource.slice(0, 80)}...` : yamlSource;
|
|
132
|
+
super(`Invalid YAML in slide ${slideIndex} frontmatter:\n${preview}`);
|
|
133
|
+
this.name = "FrontmatterParseError";
|
|
134
|
+
this.cause = cause;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function parseFrontmatter(yaml, slideIndex) {
|
|
138
|
+
if (!yaml)
|
|
139
|
+
return {};
|
|
140
|
+
try {
|
|
141
|
+
const result = parseYaml(yaml);
|
|
142
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
throw new FrontmatterParseError(slideIndex, yaml, err);
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Slide ${slideIndex + 1}: frontmatter must be a YAML mapping (key: value pairs), got ${Array.isArray(parseYaml(yaml)) ? "array" : typeof parseYaml(yaml)}.`);
|
|
150
|
+
}
|
|
151
|
+
// ============================================
|
|
152
|
+
// CONTENT SLOTS
|
|
153
|
+
// ============================================
|
|
154
|
+
const SLOT_LINE_RE = /^::(\w+)::[ \t]*$/;
|
|
155
|
+
function extractSlots(content) {
|
|
156
|
+
const lines = content.split(/\r?\n/);
|
|
157
|
+
let currentSlot = null;
|
|
158
|
+
const slotLines = new Map([[null, []]]);
|
|
159
|
+
let inCodeFence = false;
|
|
160
|
+
let codeFenceChar = "";
|
|
161
|
+
let codeFenceLen = 0;
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
if (!inCodeFence) {
|
|
164
|
+
const fenceMatch = line.match(CODE_FENCE_OPEN_RE);
|
|
165
|
+
if (fenceMatch) {
|
|
166
|
+
inCodeFence = true;
|
|
167
|
+
codeFenceChar = fenceMatch[1][0];
|
|
168
|
+
codeFenceLen = fenceMatch[1].length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
if (trimmed.length >= codeFenceLen && trimmed === codeFenceChar.repeat(trimmed.length)) {
|
|
174
|
+
inCodeFence = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const slotMatch = !inCodeFence && line.match(SLOT_LINE_RE);
|
|
178
|
+
if (slotMatch) {
|
|
179
|
+
currentSlot = slotMatch[1];
|
|
180
|
+
if (!slotLines.has(currentSlot)) {
|
|
181
|
+
slotLines.set(currentSlot, []);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
slotLines.get(currentSlot).push(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const defaultSlot = slotLines.get(null).join("\n").trim();
|
|
189
|
+
const slots = {};
|
|
190
|
+
for (const [name, sLines] of slotLines) {
|
|
191
|
+
if (name !== null) {
|
|
192
|
+
slots[name] = sLines.join("\n").trim();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { defaultSlot, slots };
|
|
196
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TemplateSegment } from "../engine/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The unique keys named by a template's placeholders, in first-seen order. A key
|
|
4
|
+
* repeated in the template appears once — it is filled once, everywhere it occurs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function templateKeys(template: string): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Parse a template into per-line segment lists: split on newlines, then tokenize
|
|
9
|
+
* each line into an ordered list of literal and variable segments. Variables
|
|
10
|
+
* carry their substituted value (escapes collapse: `{{` → `{`, `}}` → `}`).
|
|
11
|
+
* Fails fast — a placeholder with no supplied value throws, naming the shape and
|
|
12
|
+
* the key, since a partially-filled text parameter is an authoring error.
|
|
13
|
+
* `shapeName` is the target PPTX shape's name, used only for the error message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function templateToSegments(template: string, values: Map<string, string>, shapeName: string): TemplateSegment[][];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-template parsing. A text parameter's `template` is a single string with
|
|
3
|
+
* `{key}` placeholders; newlines in it are line breaks. Filling substitutes the
|
|
4
|
+
* author's values; the substituted result is written into the shape's existing
|
|
5
|
+
* runs by the engine. Lives in the compiler — the engine never parses `{}`.
|
|
6
|
+
*
|
|
7
|
+
* Syntax:
|
|
8
|
+
* - `{name}` — a placeholder for the key `name`; `name` matches `[A-Za-z0-9_]+`.
|
|
9
|
+
* - `{{` / `}}` — an escaped literal `{` / `}`.
|
|
10
|
+
* - A `{` not followed by a valid name + `}` is a literal brace, left as-is.
|
|
11
|
+
*/
|
|
12
|
+
// Order matters: escapes (`{{`, `}}`) are tried before a placeholder so
|
|
13
|
+
// `{{name}}` reads as a literal-brace-wrapped `name`, not a `{name}` placeholder.
|
|
14
|
+
const TOKEN_RE = /\{\{|\}\}|\{([A-Za-z0-9_]+)\}/g;
|
|
15
|
+
/**
|
|
16
|
+
* The unique keys named by a template's placeholders, in first-seen order. A key
|
|
17
|
+
* repeated in the template appears once — it is filled once, everywhere it occurs.
|
|
18
|
+
*/
|
|
19
|
+
export function templateKeys(template) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const keys = [];
|
|
22
|
+
for (const match of template.matchAll(TOKEN_RE)) {
|
|
23
|
+
const name = match[1];
|
|
24
|
+
if (name !== undefined && !seen.has(name)) {
|
|
25
|
+
seen.add(name);
|
|
26
|
+
keys.push(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return keys;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a template into per-line segment lists: split on newlines, then tokenize
|
|
33
|
+
* each line into an ordered list of literal and variable segments. Variables
|
|
34
|
+
* carry their substituted value (escapes collapse: `{{` → `{`, `}}` → `}`).
|
|
35
|
+
* Fails fast — a placeholder with no supplied value throws, naming the shape and
|
|
36
|
+
* the key, since a partially-filled text parameter is an authoring error.
|
|
37
|
+
* `shapeName` is the target PPTX shape's name, used only for the error message.
|
|
38
|
+
*/
|
|
39
|
+
export function templateToSegments(template, values, shapeName) {
|
|
40
|
+
return template.split("\n").map((line) => lineToSegments(line, values, shapeName));
|
|
41
|
+
}
|
|
42
|
+
function lineToSegments(line, values, shapeName) {
|
|
43
|
+
const segments = [];
|
|
44
|
+
const pushLiteral = (text) => {
|
|
45
|
+
if (text === "")
|
|
46
|
+
return;
|
|
47
|
+
const last = segments[segments.length - 1];
|
|
48
|
+
if (last?.kind === "literal")
|
|
49
|
+
last.text += text;
|
|
50
|
+
else
|
|
51
|
+
segments.push({ kind: "literal", text });
|
|
52
|
+
};
|
|
53
|
+
let lastIndex = 0;
|
|
54
|
+
for (const match of line.matchAll(TOKEN_RE)) {
|
|
55
|
+
pushLiteral(line.slice(lastIndex, match.index));
|
|
56
|
+
const token = match[0];
|
|
57
|
+
const name = match[1];
|
|
58
|
+
if (token === "{{") {
|
|
59
|
+
pushLiteral("{");
|
|
60
|
+
}
|
|
61
|
+
else if (token === "}}") {
|
|
62
|
+
pushLiteral("}");
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const value = values.get(name);
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
throw new Error(`Text parameter "${shapeName}": no value supplied for key "${name}" (placeholder "{${name}}")`);
|
|
68
|
+
}
|
|
69
|
+
segments.push({ kind: "variable", key: name, value });
|
|
70
|
+
}
|
|
71
|
+
lastIndex = match.index + token.length;
|
|
72
|
+
}
|
|
73
|
+
pushLiteral(line.slice(lastIndex));
|
|
74
|
+
return segments;
|
|
75
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { type FitMode, type ImageFill, type TableFill, type TemplateFill, type TextFill } from "../engine/index.js";
|
|
2
|
+
import type { MermaidConfig } from "./resolvers/mermaidTheme.js";
|
|
3
|
+
/**
|
|
4
|
+
* A theme's declaration of a reusable image asset. Purely compiler-facing —
|
|
5
|
+
* the engine never sees this type; the compiler resolves an entry's `path`
|
|
6
|
+
* to a fully-qualified filesystem path and wraps it as an ImageFill before
|
|
7
|
+
* handing the deck to the engine.
|
|
8
|
+
*/
|
|
9
|
+
export type AssetEntry = {
|
|
10
|
+
path: string;
|
|
11
|
+
description: string;
|
|
12
|
+
whenToUse?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Two-level catalog: `{ category: { name: AssetEntry } }`. */
|
|
15
|
+
export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
|
|
16
|
+
/**
|
|
17
|
+
* Discriminator strings for the compiler-internal fence shapes produced by
|
|
18
|
+
* deckCompiler.ts. `Code` and `Mermaid` are resolved away entirely (syntax
|
|
19
|
+
* highlighting → TextFill, PNG rendering → ImageFill), so neither reaches the
|
|
20
|
+
* engine. Prose has no FenceType: it becomes a `TextFill` (`{ paragraphs }`),
|
|
21
|
+
* one of the engine's own fill shapes.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FenceType: {
|
|
24
|
+
readonly Code: "code";
|
|
25
|
+
readonly Mermaid: "mermaid";
|
|
26
|
+
};
|
|
27
|
+
export type FenceType = (typeof FenceType)[keyof typeof FenceType];
|
|
28
|
+
/**
|
|
29
|
+
* Discriminator for a layout's frontmatter *parameters* — inputs the author
|
|
30
|
+
* writes as a single `key: value` line. Two kinds: `Template` (fills a styled
|
|
31
|
+
* shape's runs via fillTemplate) and `Image` (a filesystem path filled via
|
|
32
|
+
* fillImage). Both share their value with the engine's `SlotType`, since a
|
|
33
|
+
* parameter maps straight to an engine slot with no resolution step.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ParameterType: {
|
|
36
|
+
readonly Template: "template";
|
|
37
|
+
readonly Image: "image";
|
|
38
|
+
};
|
|
39
|
+
export type ParameterType = (typeof ParameterType)[keyof typeof ParameterType];
|
|
40
|
+
/**
|
|
41
|
+
* Compiler-facing slot type for a layout's *slots* — multi-line body regions
|
|
42
|
+
* (the default body region, or a `::name::` region). Narrowed to four kinds;
|
|
43
|
+
* `Template` and `Image` left this list to become parameters. `Code` and `Mermaid`
|
|
44
|
+
* are compiler-only, resolved (syntax highlighting, mermaid PNG rendering)
|
|
45
|
+
* before the engine sees the deck. Required on every CompilerSlot — no silent
|
|
46
|
+
* default.
|
|
47
|
+
*/
|
|
48
|
+
export declare const CompilerSlotType: {
|
|
49
|
+
readonly Text: "text";
|
|
50
|
+
readonly Table: "table";
|
|
51
|
+
readonly Code: "code";
|
|
52
|
+
readonly Mermaid: "mermaid";
|
|
53
|
+
};
|
|
54
|
+
export type CompilerSlotType = (typeof CompilerSlotType)[keyof typeof CompilerSlotType];
|
|
55
|
+
/**
|
|
56
|
+
* A fenced code block awaiting syntax highlighting. Produced by deckCompiler,
|
|
57
|
+
* consumed by CodeResolver. Never crosses the engine boundary.
|
|
58
|
+
*/
|
|
59
|
+
export type CodeFence = {
|
|
60
|
+
type: typeof FenceType.Code;
|
|
61
|
+
language: string;
|
|
62
|
+
source: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A mermaid code fence awaiting rendering. Produced by deckCompiler, consumed
|
|
66
|
+
* by MermaidResolver — the resulting PNG is wrapped as an ImageFill in
|
|
67
|
+
* step.content, so the engine never learns mermaid exists.
|
|
68
|
+
*/
|
|
69
|
+
export type MermaidFence = {
|
|
70
|
+
type: typeof FenceType.Mermaid;
|
|
71
|
+
definition: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The full set of value shapes a slot may hold during compilation, before code
|
|
75
|
+
* fences are highlighted (→ TextFill) and mermaid fences are rendered
|
|
76
|
+
* (→ ImageFill). CodeFence, MermaidFence, and ImageFill carry a `.type`
|
|
77
|
+
* discriminator; the three engine fills without one — TextFill, TableFill,
|
|
78
|
+
* TemplateFill — are identified structurally by their signature field.
|
|
79
|
+
*/
|
|
80
|
+
export type MarkdownBlock = TextFill | TableFill | CodeFence | MermaidFence | ImageFill | TemplateFill;
|
|
81
|
+
/**
|
|
82
|
+
* A DeckStep as compileDeck produces it — content values may still be
|
|
83
|
+
* unresolved (CodeFence, MermaidFence) at this point. `resolveFences` narrows
|
|
84
|
+
* these into StyledParagraph[] / ImageFill before the engine sees the deck.
|
|
85
|
+
*/
|
|
86
|
+
export type CompilerDeckStep = {
|
|
87
|
+
layout: string;
|
|
88
|
+
content?: Record<string, MarkdownBlock>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The intermediate deck shape produced by compileDeck. buildDeck runs the
|
|
92
|
+
* resolvers and hands the resulting engine-shaped Deck to generate().
|
|
93
|
+
*/
|
|
94
|
+
export type CompilerDeck = {
|
|
95
|
+
theme: string;
|
|
96
|
+
output?: string;
|
|
97
|
+
steps: CompilerDeckStep[];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A DeckStep whose content values have been narrowed post-resolution — no
|
|
101
|
+
* CodeFence or MermaidFence remains, only shapes the engine understands.
|
|
102
|
+
* Structurally equivalent to the engine's DeckStep.
|
|
103
|
+
*/
|
|
104
|
+
export type ResolvedCompilerDeckStep = {
|
|
105
|
+
layout: string;
|
|
106
|
+
content?: Record<string, TextFill | TableFill | ImageFill | TemplateFill>;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* A CompilerDeck whose steps have been passed through the resolvers via
|
|
110
|
+
* `resolveFences`. Structurally equivalent to the engine's Deck — deck
|
|
111
|
+
* resolution returns this shape so `generate` can consume it without any cast.
|
|
112
|
+
*/
|
|
113
|
+
export type ResolvedCompilerDeck = {
|
|
114
|
+
theme: string;
|
|
115
|
+
output: string;
|
|
116
|
+
steps: ResolvedCompilerDeckStep[];
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Fields common to every text shape, image parameter, and slot. A text shape is
|
|
120
|
+
* addressed by `shapeName` and owns a `template` (its placeholders are the
|
|
121
|
+
* fillable keys); image parameters and slots additionally carry a single `key`.
|
|
122
|
+
*/
|
|
123
|
+
type CompilerShapeBase = {
|
|
124
|
+
shapeName: string;
|
|
125
|
+
limit?: {
|
|
126
|
+
maxChars?: number;
|
|
127
|
+
maxLines?: number;
|
|
128
|
+
maxItems?: number;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Whether the parameter/slot may be omitted from a slide. Optional (defaults
|
|
132
|
+
* to false): a required one with no value causes the compiler to throw with
|
|
133
|
+
* layout + key names.
|
|
134
|
+
*/
|
|
135
|
+
required?: boolean;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Shape base plus a single `key` — the addressing model for image parameters
|
|
139
|
+
* and every slot. Template parameters do NOT extend this: their fillable keys are
|
|
140
|
+
* the placeholders in their `template`, not a single top-level key.
|
|
141
|
+
*/
|
|
142
|
+
type CompilerSlotBase = CompilerShapeBase & {
|
|
143
|
+
key: string;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Template parameter: a styled text shape filled by expanding a `template` into the
|
|
147
|
+
* shape's paragraphs via fillTemplate. The template is one string with `{key}`
|
|
148
|
+
* placeholders and newlines for line breaks (`"{lastname}, {firstname} -
|
|
149
|
+
* {company}"`, or `"{name}\n{jobTitle}"`); a shape's fillable keys are the
|
|
150
|
+
* placeholders in its template. The shape carries no top-level `key`; its
|
|
151
|
+
* placeholders are the keys the author fills in frontmatter.
|
|
152
|
+
*/
|
|
153
|
+
export type CompilerTemplateParameter = CompilerShapeBase & {
|
|
154
|
+
type: typeof ParameterType.Template;
|
|
155
|
+
/** The shape's text as one template with `{key}` placeholders; newlines are line breaks. */
|
|
156
|
+
template: string;
|
|
157
|
+
};
|
|
158
|
+
/** Image parameter: one frontmatter path filled by fillImage. */
|
|
159
|
+
export type CompilerImageParameter = CompilerSlotBase & {
|
|
160
|
+
type: typeof ParameterType.Image;
|
|
161
|
+
/** How the picture scales inside its frame (required — no silent default). */
|
|
162
|
+
fit: FitMode;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Compiler-facing parameter. A layout's frontmatter inputs — each written as a
|
|
166
|
+
* single `key: value` line, resolved against the layout's `parameters` list.
|
|
167
|
+
*/
|
|
168
|
+
export type CompilerParameter = CompilerTemplateParameter | CompilerImageParameter;
|
|
169
|
+
/** Multi-paragraph body filled by fillText (specimen-style rebuild). */
|
|
170
|
+
export type CompilerTextSlot = CompilerSlotBase & {
|
|
171
|
+
type: typeof CompilerSlotType.Text;
|
|
172
|
+
/** Leave the first N specimen paragraphs untouched. */
|
|
173
|
+
startAt?: number;
|
|
174
|
+
};
|
|
175
|
+
/** Table shape backed by an `<a:tbl>` with header + data specimen rows. */
|
|
176
|
+
export type CompilerTableSlot = CompilerSlotBase & {
|
|
177
|
+
type: typeof CompilerSlotType.Table;
|
|
178
|
+
/** Enforced column count. */
|
|
179
|
+
columns?: number;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Text slot that consumes a fenced code block. The resolver (`CodeResolver`)
|
|
183
|
+
* uses `codeTheme` to Shiki-highlight the source into StyledParagraph[]; the
|
|
184
|
+
* engine sees the projected Text slot.
|
|
185
|
+
*/
|
|
186
|
+
export type CompilerCodeSlot = CompilerSlotBase & {
|
|
187
|
+
type: typeof CompilerSlotType.Code;
|
|
188
|
+
/** Shiki theme id (required — no silent default). */
|
|
189
|
+
codeTheme: string;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Slot that consumes a mermaid code fence. The resolver (`MermaidResolver`)
|
|
193
|
+
* uses `mermaidVariant` to select colors and renders a PNG; the engine sees the
|
|
194
|
+
* projected Image slot with `contain` fit. Mermaid slots do NOT declare `fit` —
|
|
195
|
+
* mermaid diagrams are always contained.
|
|
196
|
+
*/
|
|
197
|
+
export type CompilerMermaidSlot = CompilerSlotBase & {
|
|
198
|
+
type: typeof CompilerSlotType.Mermaid;
|
|
199
|
+
/** Name of the mermaid variant (required — no silent default). */
|
|
200
|
+
mermaidVariant: string;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Compiler-facing slot. Discriminated union on `type` — each variant declares
|
|
204
|
+
* ONLY the fields legal for that slot type. A layout's body regions (the
|
|
205
|
+
* default region, or a `::name::` region), resolved against the layout's
|
|
206
|
+
* `slots` list. Adds markdown-flavored concepts (limit hints, code-fence theme
|
|
207
|
+
* selection, mermaid variant naming) on top of the engine's minimal Slot.
|
|
208
|
+
* Compiler-only types (Code, Mermaid) are projected down to engine types (Text,
|
|
209
|
+
* Image) at the engine boundary before the engine sees the layout.
|
|
210
|
+
*/
|
|
211
|
+
export type CompilerSlot = CompilerTextSlot | CompilerTableSlot | CompilerCodeSlot | CompilerMermaidSlot;
|
|
212
|
+
export type CompilerLayout = {
|
|
213
|
+
name: string;
|
|
214
|
+
slideNumber: number;
|
|
215
|
+
description: string;
|
|
216
|
+
whenToUse: string;
|
|
217
|
+
whenNotToUse: string;
|
|
218
|
+
/** Frontmatter inputs (template, image) — one value per `key: value` line. */
|
|
219
|
+
parameters: CompilerParameter[];
|
|
220
|
+
/** Body regions (text, table, code, mermaid) — the body or `::name::` regions. */
|
|
221
|
+
slots: CompilerSlot[];
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Compiler-facing theme configuration. Mirrors the engine's ThemeConfig but
|
|
225
|
+
* carries markdown-flavored fields (mermaid variants) that live outside the
|
|
226
|
+
* engine's awareness. Fields are declared explicitly rather than
|
|
227
|
+
* inherit-and-omit, so the boundary is visible.
|
|
228
|
+
*/
|
|
229
|
+
export type CompilerThemeConfig = {
|
|
230
|
+
layouts: CompilerLayout[];
|
|
231
|
+
assets: AssetCatalog;
|
|
232
|
+
template: string;
|
|
233
|
+
outputDir?: string;
|
|
234
|
+
mermaid?: MermaidConfig;
|
|
235
|
+
};
|
|
236
|
+
export type CompilerConfig = CompilerThemeConfig & {
|
|
237
|
+
rootDir: string;
|
|
238
|
+
};
|
|
239
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SlotType, } from "../engine/index.js";
|
|
2
|
+
// ── FenceType discriminator ───────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Discriminator strings for the compiler-internal fence shapes produced by
|
|
5
|
+
* deckCompiler.ts. `Code` and `Mermaid` are resolved away entirely (syntax
|
|
6
|
+
* highlighting → TextFill, PNG rendering → ImageFill), so neither reaches the
|
|
7
|
+
* engine. Prose has no FenceType: it becomes a `TextFill` (`{ paragraphs }`),
|
|
8
|
+
* one of the engine's own fill shapes.
|
|
9
|
+
*/
|
|
10
|
+
export const FenceType = {
|
|
11
|
+
Code: "code",
|
|
12
|
+
Mermaid: "mermaid",
|
|
13
|
+
};
|
|
14
|
+
// ── ParameterType discriminator (frontmatter, one value) ──────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Discriminator for a layout's frontmatter *parameters* — inputs the author
|
|
17
|
+
* writes as a single `key: value` line. Two kinds: `Template` (fills a styled
|
|
18
|
+
* shape's runs via fillTemplate) and `Image` (a filesystem path filled via
|
|
19
|
+
* fillImage). Both share their value with the engine's `SlotType`, since a
|
|
20
|
+
* parameter maps straight to an engine slot with no resolution step.
|
|
21
|
+
*/
|
|
22
|
+
export const ParameterType = {
|
|
23
|
+
Template: SlotType.Template,
|
|
24
|
+
Image: SlotType.Image,
|
|
25
|
+
};
|
|
26
|
+
// ── CompilerSlotType discriminator (body region, multi-line) ──────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Compiler-facing slot type for a layout's *slots* — multi-line body regions
|
|
29
|
+
* (the default body region, or a `::name::` region). Narrowed to four kinds;
|
|
30
|
+
* `Template` and `Image` left this list to become parameters. `Code` and `Mermaid`
|
|
31
|
+
* are compiler-only, resolved (syntax highlighting, mermaid PNG rendering)
|
|
32
|
+
* before the engine sees the deck. Required on every CompilerSlot — no silent
|
|
33
|
+
* default.
|
|
34
|
+
*/
|
|
35
|
+
export const CompilerSlotType = {
|
|
36
|
+
Text: SlotType.Text,
|
|
37
|
+
Table: SlotType.Table,
|
|
38
|
+
Code: FenceType.Code,
|
|
39
|
+
Mermaid: FenceType.Mermaid,
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tycoworks/tycoslide",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tycoslide": "bin/tycoslide.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"SKILL.md",
|
|
14
|
+
"syntax.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc --build",
|
|
18
|
+
"test": "tsc --build && node --test --experimental-transform-types test/**/*.test.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"lint:fix": "biome check --write .",
|
|
22
|
+
"format": "biome format --write ."
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^15.0.0",
|
|
26
|
+
"image-size": "^2.0.2",
|
|
27
|
+
"pptx-automizer": "^0.8.2",
|
|
28
|
+
"remark-gfm": "^4.0.1",
|
|
29
|
+
"remark-ins": "^1.2.5",
|
|
30
|
+
"remark-parse": "^11.0.0",
|
|
31
|
+
"shiki": "^4.3.0",
|
|
32
|
+
"unified": "^11.0.0",
|
|
33
|
+
"yaml": "^2.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.5.1",
|
|
37
|
+
"@types/node": "^22.20.0",
|
|
38
|
+
"@xmldom/xmldom": "^0.9.10",
|
|
39
|
+
"typescript": "^5.8.0"
|
|
40
|
+
}
|
|
41
|
+
}
|