@tycoworks/tycoslide 0.8.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.
- package/README.md +1 -1
- package/dist/cli.js +5 -15
- package/dist/engine/fillers/filler.d.ts +21 -13
- package/dist/engine/fillers/filler.js +21 -24
- package/dist/engine/generate.d.ts +23 -17
- package/dist/engine/generate.js +157 -70
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/types.d.ts +39 -9
- package/dist/index.d.ts +15 -21
- package/dist/index.js +63 -88
- package/dist/manifest.js +17 -25
- package/dist/markdown/blocks/code.d.ts +15 -0
- package/dist/markdown/blocks/code.js +50 -0
- package/dist/markdown/blocks/image.d.ts +2 -0
- package/dist/markdown/blocks/image.js +9 -0
- package/dist/markdown/blocks/mermaid.d.ts +15 -0
- package/dist/markdown/blocks/mermaid.js +227 -0
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
- package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
- package/dist/markdown/blocks/registry.d.ts +16 -0
- package/dist/markdown/blocks/registry.js +44 -0
- package/dist/markdown/blocks/table.d.ts +2 -0
- package/dist/markdown/blocks/table.js +23 -0
- package/dist/markdown/blocks/text.d.ts +12 -0
- package/dist/markdown/blocks/text.js +90 -0
- package/dist/markdown/deckCompiler.d.ts +17 -20
- package/dist/markdown/deckCompiler.js +135 -113
- package/dist/markdown/index.d.ts +11 -11
- package/dist/markdown/index.js +9 -8
- package/dist/markdown/inline.d.ts +26 -0
- package/dist/markdown/inline.js +136 -0
- package/dist/markdown/mdast.d.ts +25 -0
- package/dist/markdown/mdast.js +49 -0
- package/dist/markdown/schema/deckSchema.d.ts +30 -0
- package/dist/markdown/schema/deckSchema.js +51 -0
- package/dist/markdown/schema/strict.d.ts +9 -0
- package/dist/markdown/schema/strict.js +18 -0
- package/dist/markdown/schema/themeConfigSchema.d.ts +99 -0
- package/dist/markdown/schema/themeConfigSchema.js +145 -0
- package/dist/markdown/types.d.ts +168 -135
- package/dist/markdown/types.js +23 -23
- package/package.json +6 -3
- package/dist/markdown/parsers.d.ts +0 -32
- package/dist/markdown/parsers.js +0 -233
- package/dist/markdown/resolvers/code.d.ts +0 -17
- package/dist/markdown/resolvers/code.js +0 -44
- package/dist/markdown/resolvers/mermaid.d.ts +0 -14
- package/dist/markdown/resolvers/mermaid.js +0 -81
- package/dist/markdown/resolvers/resolver.d.ts +0 -42
- package/dist/markdown/resolvers/resolver.js +0 -52
|
@@ -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
|
+
}
|