@tycoworks/tycoslide 0.7.0 → 0.9.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.
Files changed (62) hide show
  1. package/README.md +6 -6
  2. package/SKILL.md +2 -1
  3. package/dist/cli.js +8 -107
  4. package/dist/engine/dom.d.ts +5 -0
  5. package/dist/engine/dom.js +19 -3
  6. package/dist/engine/fillers/filler.d.ts +21 -13
  7. package/dist/engine/fillers/filler.js +21 -27
  8. package/dist/engine/fillers/image.d.ts +46 -7
  9. package/dist/engine/fillers/image.js +78 -36
  10. package/dist/engine/fillers/table.d.ts +4 -3
  11. package/dist/engine/fillers/table.js +58 -9
  12. package/dist/engine/generate.d.ts +34 -18
  13. package/dist/engine/generate.js +234 -65
  14. package/dist/engine/index.d.ts +3 -2
  15. package/dist/engine/index.js +1 -1
  16. package/dist/engine/notes.d.ts +76 -0
  17. package/dist/engine/notes.js +313 -0
  18. package/dist/engine/types.d.ts +57 -24
  19. package/dist/engine/types.js +11 -7
  20. package/dist/index.d.ts +19 -25
  21. package/dist/index.js +65 -92
  22. package/dist/manifest.js +19 -29
  23. package/dist/markdown/blocks/code.d.ts +15 -0
  24. package/dist/markdown/blocks/code.js +50 -0
  25. package/dist/markdown/blocks/image.d.ts +2 -0
  26. package/dist/markdown/blocks/image.js +9 -0
  27. package/dist/markdown/blocks/mermaid.d.ts +15 -0
  28. package/dist/markdown/blocks/mermaid.js +227 -0
  29. package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
  30. package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
  31. package/dist/markdown/blocks/registry.d.ts +16 -0
  32. package/dist/markdown/blocks/registry.js +44 -0
  33. package/dist/markdown/blocks/table.d.ts +2 -0
  34. package/dist/markdown/blocks/table.js +23 -0
  35. package/dist/markdown/blocks/text.d.ts +12 -0
  36. package/dist/markdown/blocks/text.js +90 -0
  37. package/dist/markdown/deckCompiler.d.ts +18 -30
  38. package/dist/markdown/deckCompiler.js +167 -122
  39. package/dist/markdown/index.d.ts +11 -11
  40. package/dist/markdown/index.js +9 -8
  41. package/dist/markdown/inline.d.ts +26 -0
  42. package/dist/markdown/inline.js +136 -0
  43. package/dist/markdown/mdast.d.ts +25 -0
  44. package/dist/markdown/mdast.js +49 -0
  45. package/dist/markdown/schema/deckSchema.d.ts +30 -0
  46. package/dist/markdown/schema/deckSchema.js +51 -0
  47. package/dist/markdown/schema/strict.d.ts +9 -0
  48. package/dist/markdown/schema/strict.js +18 -0
  49. package/dist/markdown/schema/themeConfigSchema.d.ts +99 -0
  50. package/dist/markdown/schema/themeConfigSchema.js +145 -0
  51. package/dist/markdown/types.d.ts +184 -137
  52. package/dist/markdown/types.js +30 -19
  53. package/package.json +7 -3
  54. package/syntax.md +25 -5
  55. package/dist/markdown/parsers.d.ts +0 -32
  56. package/dist/markdown/parsers.js +0 -233
  57. package/dist/markdown/resolvers/code.d.ts +0 -17
  58. package/dist/markdown/resolvers/code.js +0 -44
  59. package/dist/markdown/resolvers/mermaid.d.ts +0 -14
  60. package/dist/markdown/resolvers/mermaid.js +0 -89
  61. package/dist/markdown/resolvers/resolver.d.ts +0 -42
  62. package/dist/markdown/resolvers/resolver.js +0 -52
@@ -1,13 +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";
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, 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";
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";
@@ -1,13 +1,14 @@
1
1
  import { compileDeck } from "./deckCompiler.js";
2
2
  import { parseSlideDocument } from "./slideParser.js";
3
- export function compileMarkdownDeck(source, layouts, rootDir = "") {
3
+ export function compileMarkdownDeck(source, config) {
4
4
  const doc = parseSlideDocument(source);
5
- return compileDeck(doc, layouts, rootDir);
5
+ return compileDeck(doc, config);
6
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";
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 { CompilerSlotType, FenceType, ParameterType } from "./types.js";
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
+ }
@@ -0,0 +1,25 @@
1
+ import type { Root } from "mdast";
2
+ /** mdast node type discriminators we handle. Backing const so switch/if cases
3
+ * reference named tokens rather than raw magic strings. Exported so the
4
+ * per-kind block handlers in `blocks/` recognize their node against the same
5
+ * tokens the inline walk uses. */
6
+ export declare const MdastType: {
7
+ readonly Text: "text";
8
+ readonly InlineCode: "inlineCode";
9
+ readonly Strong: "strong";
10
+ readonly Emphasis: "emphasis";
11
+ readonly Delete: "delete";
12
+ readonly Insert: "insert";
13
+ readonly Link: "link";
14
+ readonly Break: "break";
15
+ readonly Paragraph: "paragraph";
16
+ readonly Heading: "heading";
17
+ readonly Code: "code";
18
+ readonly Image: "image";
19
+ readonly List: "list";
20
+ readonly Table: "table";
21
+ };
22
+ /** Parse a single line of markdown into a raw mdast tree. */
23
+ export declare function parseInline(text: string): Root;
24
+ /** Parse a whole body/`::name::` region into a real mdast tree. */
25
+ export declare function parseRegion(text: string): Root;
@@ -0,0 +1,49 @@
1
+ import remarkGfm from "remark-gfm";
2
+ import remarkIns from "remark-ins";
3
+ import remarkParse from "remark-parse";
4
+ import { unified } from "unified";
5
+ /** mdast node type discriminators we handle. Backing const so switch/if cases
6
+ * reference named tokens rather than raw magic strings. Exported so the
7
+ * per-kind block handlers in `blocks/` recognize their node against the same
8
+ * tokens the inline walk uses. */
9
+ export const MdastType = {
10
+ Text: "text",
11
+ InlineCode: "inlineCode",
12
+ Strong: "strong",
13
+ Emphasis: "emphasis",
14
+ Delete: "delete",
15
+ Insert: "insert",
16
+ Link: "link",
17
+ Break: "break",
18
+ Paragraph: "paragraph",
19
+ Heading: "heading",
20
+ Code: "code",
21
+ Image: "image",
22
+ List: "list",
23
+ Table: "table",
24
+ };
25
+ // remark-parse establishes the processor as Processor<Root>; remarkGfm and
26
+ // remarkIns extend the parser but don't transform the tree shape, so
27
+ // runSync's output is still a Root. Unified's TailTree generic defaults to
28
+ // undefined and widens runSync's return to the base Node, hence the narrowing
29
+ // casts below — kept to a single hop, no double-cast. Shared by both entry
30
+ // points: neither plugin adds a transform phase, so `.parse()` alone already
31
+ // yields the final tree (gfm tables/delete, ins nodes included); `runSync` is
32
+ // only needed by the inline path below.
33
+ const processor = unified().use(remarkParse).use(remarkGfm).use(remarkIns);
34
+ // ══════════════════════════════════════════════════════════════════════════════
35
+ // INLINE / PROSE PARSING
36
+ // ══════════════════════════════════════════════════════════════════════════════
37
+ /** Parse a single line of markdown into a raw mdast tree. */
38
+ export function parseInline(text) {
39
+ return processor.runSync(processor.parse(text));
40
+ }
41
+ // ══════════════════════════════════════════════════════════════════════════════
42
+ // BLOCK / REGION PARSING
43
+ // ══════════════════════════════════════════════════════════════════════════════
44
+ // Region splitting (::name::) happens above the slot, so a region is plain
45
+ // GFM+ins markdown — `.parse()` alone is enough, no `runSync` needed here.
46
+ /** Parse a whole body/`::name::` region into a real mdast tree. */
47
+ export function parseRegion(text) {
48
+ return processor.parse(text);
49
+ }
@@ -0,0 +1,30 @@
1
+ import * as z from "zod";
2
+ import type { CompilerLayout } from "../types.js";
3
+ /**
4
+ * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
5
+ * every slide's frontmatter against a Zod schema carried by the layout program;
6
+ * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
7
+ * declared `parameters` using the same key-derivation the compiler already uses
8
+ * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
9
+ * parameter contributes one field per `{key}` placeholder in its template (NOT its
10
+ * `shapeName`, which addresses `step.content`, not frontmatter).
11
+ *
12
+ * Commit-1 scope is unknown-key detection ONLY — zero behavior change:
13
+ * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
14
+ * YAML number like `year: 2026` must still pass — plain `z.string()` would regress)
15
+ * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
16
+ * "all a template's keys present"). Value-typing and required-encoding are deliberate
17
+ * later commits.
18
+ * The strict object IS the unknown-key check that used to be imperative in
19
+ * `compileStep`; a stray frontmatter key throws instead of being silently ignored.
20
+ */
21
+ export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObject<{
22
+ [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
23
+ }, z.core.$strict>;
24
+ /**
25
+ * Validate one slide's frontmatter against its layout, throwing a fail-fast error
26
+ * prefixed with the slide index (naming the unknown key + the valid set, via the
27
+ * shared `strict` "Valid keys: …" formatter). Reserved keys (`layout`, `notes`) are
28
+ * slide-level metadata, not parameters, so they are stripped before `safeParse`.
29
+ */
30
+ export declare function validateSlideFrontmatter(frontmatter: Record<string, unknown>, layout: CompilerLayout, slideIdx: number): void;
@@ -0,0 +1,51 @@
1
+ import * as z from "zod";
2
+ import { templateKeys } from "../textTemplate.js";
3
+ import { ParameterType, RESERVED_KEY } from "../types.js";
4
+ import { strict } from "./strict.js";
5
+ /**
6
+ * Per-layout frontmatter validation for a deck `.md`. The old tycoslide validated
7
+ * every slide's frontmatter against a Zod schema carried by the layout program;
8
+ * current layouts are JSON DATA, so the schema is BUILT dynamically from a layout's
9
+ * declared `parameters` using the same key-derivation the compiler already uses
10
+ * (`deckCompiler.ts`): an image parameter contributes its `key`; a template
11
+ * parameter contributes one field per `{key}` placeholder in its template (NOT its
12
+ * `shapeName`, which addresses `step.content`, not frontmatter).
13
+ *
14
+ * Commit-1 scope is unknown-key detection ONLY — zero behavior change:
15
+ * - Every field is `z.coerce.string()` (values are `String()`-coerced today, so a
16
+ * YAML number like `year: 2026` must still pass — plain `z.string()` would regress)
17
+ * and `.optional()` (`required` is per-parameter, enforced in `compileStep`, not
18
+ * "all a template's keys present"). Value-typing and required-encoding are deliberate
19
+ * later commits.
20
+ * The strict object IS the unknown-key check that used to be imperative in
21
+ * `compileStep`; a stray frontmatter key throws instead of being silently ignored.
22
+ */
23
+ export function deckFrontmatterSchema(layout) {
24
+ const shape = {};
25
+ for (const param of layout.parameters) {
26
+ switch (param.type) {
27
+ case ParameterType.Image:
28
+ shape[param.key] = z.coerce.string().optional();
29
+ break;
30
+ case ParameterType.Template:
31
+ for (const key of templateKeys(param.template)) {
32
+ shape[key] = z.coerce.string().optional();
33
+ }
34
+ break;
35
+ }
36
+ }
37
+ return strict(shape);
38
+ }
39
+ /**
40
+ * Validate one slide's frontmatter against its layout, throwing a fail-fast error
41
+ * prefixed with the slide index (naming the unknown key + the valid set, via the
42
+ * shared `strict` "Valid keys: …" formatter). Reserved keys (`layout`, `notes`) are
43
+ * slide-level metadata, not parameters, so they are stripped before `safeParse`.
44
+ */
45
+ export function validateSlideFrontmatter(frontmatter, layout, slideIdx) {
46
+ const { [RESERVED_KEY.LAYOUT]: _layout, [RESERVED_KEY.NOTES]: _notes, ...params } = frontmatter;
47
+ const result = deckFrontmatterSchema(layout).safeParse(params);
48
+ if (!result.success) {
49
+ throw new Error(`Slide ${slideIdx}: ${z.prettifyError(result.error)}`);
50
+ }
51
+ }
@@ -0,0 +1,9 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * A strict object whose unrecognized-key error also lists the valid key set,
4
+ * matching the deck frontmatter validators' "Valid keys: …" style
5
+ * (`deckCompiler.ts`). The `error` callback only rewrites `unrecognized_keys`
6
+ * issues; returning `undefined` falls back to Zod's default message for every
7
+ * other issue code.
8
+ */
9
+ export declare function strict<T extends z.ZodRawShape>(shape: T): z.ZodObject<{ -readonly [P in keyof T]: T[P]; }, z.core.$strict>;
@@ -0,0 +1,18 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * A strict object whose unrecognized-key error also lists the valid key set,
4
+ * matching the deck frontmatter validators' "Valid keys: …" style
5
+ * (`deckCompiler.ts`). The `error` callback only rewrites `unrecognized_keys`
6
+ * issues; returning `undefined` falls back to Zod's default message for every
7
+ * other issue code.
8
+ */
9
+ export function strict(shape) {
10
+ return z.strictObject(shape, {
11
+ error: (issue) => {
12
+ if (issue.code === "unrecognized_keys") {
13
+ return `Unknown key(s): ${issue.keys.join(", ")}. Valid keys: ${Object.keys(shape).join(", ")}`;
14
+ }
15
+ return undefined;
16
+ },
17
+ });
18
+ }
@@ -0,0 +1,99 @@
1
+ import * as z from "zod";
2
+ import { type CompilerConfig, type CompilerThemeConfig } from "../types.js";
3
+ export declare const ThemeConfigSchema: z.ZodObject<{
4
+ layouts: z.ZodArray<z.ZodObject<{
5
+ name: z.ZodString;
6
+ slideNumber: z.ZodNumber;
7
+ description: z.ZodOptional<z.ZodString>;
8
+ whenToUse: z.ZodOptional<z.ZodString>;
9
+ whenNotToUse: z.ZodOptional<z.ZodString>;
10
+ parameters: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
11
+ shapeName: z.ZodString;
12
+ limit: z.ZodOptional<z.ZodObject<{
13
+ maxChars: z.ZodOptional<z.ZodNumber>;
14
+ maxLines: z.ZodOptional<z.ZodNumber>;
15
+ maxItems: z.ZodOptional<z.ZodNumber>;
16
+ }, z.core.$strict>>;
17
+ required: z.ZodOptional<z.ZodBoolean>;
18
+ type: z.ZodLiteral<"template">;
19
+ template: z.ZodString;
20
+ }, z.core.$strict>, z.ZodObject<{
21
+ shapeName: z.ZodString;
22
+ required: z.ZodOptional<z.ZodBoolean>;
23
+ key: z.ZodString;
24
+ type: z.ZodLiteral<"image">;
25
+ }, z.core.$strict>], "type">>;
26
+ slots: z.ZodArray<z.ZodObject<{
27
+ key: z.ZodString;
28
+ accepts: z.ZodArray<z.ZodObject<{
29
+ type: z.ZodEnum<{
30
+ text: "text";
31
+ table: "table";
32
+ image: "image";
33
+ }>;
34
+ sourceSlide: z.ZodNumber;
35
+ shapeName: z.ZodString;
36
+ startAt: z.ZodOptional<z.ZodNumber>;
37
+ }, z.core.$strict>>;
38
+ frame: z.ZodOptional<z.ZodObject<{
39
+ x: z.ZodNumber;
40
+ y: z.ZodNumber;
41
+ cx: z.ZodNumber;
42
+ cy: z.ZodNumber;
43
+ }, z.core.$strict>>;
44
+ limit: z.ZodOptional<z.ZodObject<{
45
+ maxChars: z.ZodOptional<z.ZodNumber>;
46
+ maxLines: z.ZodOptional<z.ZodNumber>;
47
+ maxItems: z.ZodOptional<z.ZodNumber>;
48
+ }, z.core.$strict>>;
49
+ required: z.ZodOptional<z.ZodBoolean>;
50
+ }, z.core.$strict>>;
51
+ }, z.core.$strict>>;
52
+ assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
53
+ path: z.ZodString;
54
+ type: z.ZodEnum<{
55
+ image: "image";
56
+ icon: "icon";
57
+ background: "background";
58
+ }>;
59
+ description: z.ZodString;
60
+ whenToUse: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strict>>>;
62
+ template: z.ZodString;
63
+ outputDir: z.ZodOptional<z.ZodString>;
64
+ mermaid: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
65
+ primary: z.ZodString;
66
+ primaryContrast: z.ZodString;
67
+ text: z.ZodString;
68
+ line: z.ZodString;
69
+ surface: z.ZodString;
70
+ surfaceBorder: z.ZodString;
71
+ fontFamily: z.ZodString;
72
+ accents: z.ZodArray<z.ZodString>;
73
+ accentOpacity: z.ZodNumber;
74
+ accentTextColor: z.ZodString;
75
+ groupCornerRadius: z.ZodNumber;
76
+ }, z.core.$strict>>>;
77
+ fonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
78
+ family: z.ZodString;
79
+ path: z.ZodString;
80
+ weight: z.ZodOptional<z.ZodNumber>;
81
+ }, z.core.$strict>>>;
82
+ codeTheme: z.ZodOptional<z.ZodString>;
83
+ mermaidVariant: z.ZodOptional<z.ZodString>;
84
+ }, z.core.$strict>;
85
+ /**
86
+ * Parse+validate a raw `theme.json` value into a `CompilerThemeConfig`, throwing
87
+ * a fail-fast error (naming the source file and, for unknown keys, the valid set)
88
+ * on any structural violation. This is the ONE loader both the CLI and the
89
+ * programmatic entry route through, so every caller validates identically.
90
+ */
91
+ export declare function parseThemeConfig(raw: unknown, sourcePath: string): CompilerThemeConfig;
92
+ /**
93
+ * Read, validate, and root a `theme.json` at `absPath` into a `CompilerConfig`.
94
+ * The single loader the CLI and programmatic callers share: file read/JSON
95
+ * failures fail fast with the path; structural failures go through
96
+ * `parseThemeConfig`. `rootDir` (the config's directory) is attached AFTER
97
+ * validation — it is not a JSON field.
98
+ */
99
+ export declare function loadThemeConfig(absPath: string): CompilerConfig;
@@ -0,0 +1,145 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, dirname } from "node:path";
3
+ import * as z from "zod";
4
+ import { AcceptType, AssetType, ParameterType } from "../types.js";
5
+ import { strict } from "./strict.js";
6
+ /**
7
+ * Fail-fast runtime validation for a `theme.json`. The hand-written types in
8
+ * `types.ts` stay canonical (they carry load-bearing doc-comments and export the
9
+ * const-object enums used as runtime values); this schema is declared alongside
10
+ * and bound to them by a compile-time drift guard (`_drift` below). The guard
11
+ * catches required-field and type drift in either direction, but — because TS
12
+ * structural assignability is lenient about ADDITIVE optional fields — it will
13
+ * NOT catch a schema that's missing an optional field the hand-written type
14
+ * declares. The `fullTheme()` fixture in `themeConfigSchema.test.ts`, which
15
+ * populates every optional field, is the runtime backstop for that gap.
16
+ *
17
+ * Every object is a `strictObject` so an unknown key throws instead of being
18
+ * silently dropped — strictness does NOT propagate, so each nested object is
19
+ * independently strict. The two exceptions are the `AssetCatalog` and `mermaid`
20
+ * records, whose keys are user-defined category/asset/variant names (a
21
+ * `z.record`, open by design); strictness lands on their leaf entries.
22
+ */
23
+ // Reuse the const-object enums as runtime values — no third copy of the literals.
24
+ const acceptTypeSchema = z.enum(Object.values(AcceptType));
25
+ const assetTypeSchema = z.enum(Object.values(AssetType));
26
+ // Re-declared here (not imported from the engine) so the schema layer never
27
+ // depends on the engine — mirrors engine `Frame`, guarded by `_drift`.
28
+ const FrameSchema = strict({
29
+ x: z.number(),
30
+ y: z.number(),
31
+ cx: z.number(),
32
+ cy: z.number(),
33
+ });
34
+ const LimitSchema = strict({
35
+ maxChars: z.number().optional(),
36
+ maxLines: z.number().optional(),
37
+ maxItems: z.number().optional(),
38
+ });
39
+ const AssetEntrySchema = strict({
40
+ path: z.string(),
41
+ type: assetTypeSchema,
42
+ description: z.string(),
43
+ whenToUse: z.string().optional(),
44
+ });
45
+ // AssetCatalog: `{ category: { name: AssetEntry } }`. The two record levels are
46
+ // OPEN (names are user-defined); only the leaf entry is strict.
47
+ const AssetCatalogSchema = z.record(z.string(), z.record(z.string(), AssetEntrySchema));
48
+ const MermaidVariantSchema = strict({
49
+ primary: z.string(),
50
+ primaryContrast: z.string(),
51
+ text: z.string(),
52
+ line: z.string(),
53
+ surface: z.string(),
54
+ surfaceBorder: z.string(),
55
+ fontFamily: z.string(),
56
+ accents: z.array(z.string()),
57
+ accentOpacity: z.number(),
58
+ accentTextColor: z.string(),
59
+ groupCornerRadius: z.number(),
60
+ });
61
+ // MermaidConfig: `Record<variantName, MermaidVariant>` — open keys, strict leaf.
62
+ const MermaidConfigSchema = z.record(z.string(), MermaidVariantSchema);
63
+ const ThemeFontSchema = strict({
64
+ family: z.string(),
65
+ path: z.string(),
66
+ weight: z.number().optional(),
67
+ });
68
+ const TemplateParamSchema = strict({
69
+ shapeName: z.string(),
70
+ limit: LimitSchema.optional(),
71
+ required: z.boolean().optional(),
72
+ type: z.literal(ParameterType.Template),
73
+ template: z.string(),
74
+ });
75
+ const ImageParamSchema = strict({
76
+ shapeName: z.string(),
77
+ required: z.boolean().optional(),
78
+ key: z.string(),
79
+ type: z.literal(ParameterType.Image),
80
+ });
81
+ const ParameterSchema = z.discriminatedUnion("type", [TemplateParamSchema, ImageParamSchema]);
82
+ const BlockSchema = strict({
83
+ type: acceptTypeSchema,
84
+ sourceSlide: z.number(),
85
+ shapeName: z.string(),
86
+ startAt: z.number().optional(),
87
+ });
88
+ const SlotSchema = strict({
89
+ key: z.string(),
90
+ accepts: z.array(BlockSchema),
91
+ frame: FrameSchema.optional(),
92
+ limit: LimitSchema.optional(),
93
+ required: z.boolean().optional(),
94
+ });
95
+ const LayoutSchema = strict({
96
+ name: z.string(),
97
+ slideNumber: z.number(),
98
+ description: z.string().optional(),
99
+ whenToUse: z.string().optional(),
100
+ whenNotToUse: z.string().optional(),
101
+ parameters: z.array(ParameterSchema),
102
+ slots: z.array(SlotSchema),
103
+ });
104
+ export const ThemeConfigSchema = strict({
105
+ layouts: z.array(LayoutSchema),
106
+ assets: AssetCatalogSchema,
107
+ template: z.string(),
108
+ outputDir: z.string().optional(),
109
+ mermaid: MermaidConfigSchema.optional(),
110
+ fonts: z.array(ThemeFontSchema).optional(),
111
+ codeTheme: z.string().optional(),
112
+ mermaidVariant: z.string().optional(),
113
+ });
114
+ const _drift = true;
115
+ void _drift;
116
+ /**
117
+ * Parse+validate a raw `theme.json` value into a `CompilerThemeConfig`, throwing
118
+ * a fail-fast error (naming the source file and, for unknown keys, the valid set)
119
+ * on any structural violation. This is the ONE loader both the CLI and the
120
+ * programmatic entry route through, so every caller validates identically.
121
+ */
122
+ export function parseThemeConfig(raw, sourcePath) {
123
+ const r = ThemeConfigSchema.safeParse(raw);
124
+ if (!r.success) {
125
+ throw new Error(`${basename(sourcePath)}: invalid theme config\n${z.prettifyError(r.error)}`);
126
+ }
127
+ return r.data;
128
+ }
129
+ /**
130
+ * Read, validate, and root a `theme.json` at `absPath` into a `CompilerConfig`.
131
+ * The single loader the CLI and programmatic callers share: file read/JSON
132
+ * failures fail fast with the path; structural failures go through
133
+ * `parseThemeConfig`. `rootDir` (the config's directory) is attached AFTER
134
+ * validation — it is not a JSON field.
135
+ */
136
+ export function loadThemeConfig(absPath) {
137
+ let raw;
138
+ try {
139
+ raw = JSON.parse(readFileSync(absPath, "utf-8"));
140
+ }
141
+ catch {
142
+ throw new Error(`Config file not found or invalid JSON: ${absPath}`);
143
+ }
144
+ return { ...parseThemeConfig(raw, absPath), rootDir: dirname(absPath) };
145
+ }