@tycoworks/tycoslide 0.11.3 → 0.12.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/SKILL.md +6 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -19
- package/dist/manifest.js +9 -24
- package/dist/markdown/deckCompiler.d.ts +6 -7
- package/dist/markdown/deckCompiler.js +50 -89
- package/dist/markdown/index.d.ts +2 -2
- package/dist/markdown/index.js +1 -1
- package/dist/markdown/schema/deckSchema.d.ts +10 -15
- package/dist/markdown/schema/deckSchema.js +13 -25
- package/dist/markdown/schema/themeConfigSchema.d.ts +2 -8
- package/dist/markdown/schema/themeConfigSchema.js +2 -10
- package/dist/markdown/types.d.ts +2 -34
- package/dist/markdown/types.js +0 -12
- package/package.json +3 -3
- package/syntax.md +12 -19
package/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ Before writing anything, read `manifest.json`. It contains:
|
|
|
40
40
|
- **assets** -- brand logos, client logos, illustrations, and icons (`description`)
|
|
41
41
|
|
|
42
42
|
A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
|
|
43
|
-
- **parameters** -- one value on a frontmatter line.
|
|
43
|
+
- **parameters** -- one value on a frontmatter line. Fill by putting a value under the parameter's key in the slide frontmatter.
|
|
44
44
|
- **slots** -- a multi-line region in the body. Accept types: `text`, `table`, `image`. Fill as a `::key::` region, using the slot's `key` from the manifest; a fenced code block routes to a `text` slot, a fenced mermaid block to an `image` slot.
|
|
45
45
|
|
|
46
46
|
A slot may accept more than one content type -- the manifest lists each slot's `accepts`, and the shape of the content you write selects which one.
|
|
@@ -89,7 +89,7 @@ A deck file has three parts:
|
|
|
89
89
|
|
|
90
90
|
### Slide frontmatter
|
|
91
91
|
|
|
92
|
-
Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters
|
|
92
|
+
Every slide must have a `layout:` key. All other frontmatter keys map 1:1 to the layout's **parameters**.
|
|
93
93
|
|
|
94
94
|
```yaml
|
|
95
95
|
---
|
|
@@ -100,13 +100,13 @@ subtitle: This Quarter
|
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
- `layout` is required and consumed by the compiler (not forwarded as content).
|
|
103
|
-
- All other frontmatter keys fill parameters: `title` fills the `title`
|
|
103
|
+
- All other frontmatter keys fill parameters: `title` fills the `title` parameter, `subtitle` fills the `subtitle` parameter, etc. A multi-line text shape surfaces as several keys (e.g. `name` + `jobTitle`); fill each as its own scalar line.
|
|
104
104
|
- Slots (accepting `text`, `table`, `image`) are filled by body regions, not frontmatter -- see below.
|
|
105
105
|
- A slide may also carry a `notes:` block in frontmatter -- plain-text speaker notes for the slide's notes page (see [syntax.md](syntax.md#speaker-notes)). It is slide-level metadata, not a parameter or slot.
|
|
106
106
|
|
|
107
107
|
### Body content, slots, and formatting
|
|
108
108
|
|
|
109
|
-
See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), the parameter/slot split (
|
|
109
|
+
See [syntax.md](syntax.md) for the full syntax reference: body content (paragraphs, bullets, nesting), inline formatting (bold, italic, strikethrough, underline, hyperlinks), named slots (`::key::` markers), and the parameter/slot split (slots accept: text, table, image).
|
|
110
110
|
|
|
111
111
|
### Build
|
|
112
112
|
|
|
@@ -143,7 +143,7 @@ Keep each slot's content to what its region comfortably holds. When content over
|
|
|
143
143
|
- **Don't restyle the layout** -- the theme owns all design; you only fill slots
|
|
144
144
|
- **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one.
|
|
145
145
|
- **Don't invent layout or asset names** -- only use what exists in the manifest
|
|
146
|
-
- **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image
|
|
146
|
+
- **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image slot you care about
|
|
147
147
|
|
|
148
148
|
---
|
|
149
149
|
|
|
@@ -159,7 +159,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
|
|
|
159
159
|
|-------|-----|
|
|
160
160
|
| `unknown layout "xyz"` | Check layout names in `manifest.json` |
|
|
161
161
|
| A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
|
|
162
|
-
| An image didn't swap / placeholder remains |
|
|
162
|
+
| An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `` from `manifest.json` |
|
|
163
163
|
| YAML parse error | Fix the YAML syntax in the slide's frontmatter |
|
|
164
164
|
| `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
|
|
165
165
|
| `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
|
package/dist/index.d.ts
CHANGED
|
@@ -36,4 +36,4 @@ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot,
|
|
|
36
36
|
export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
|
|
37
37
|
export { generateManifest } from "./manifest.js";
|
|
38
38
|
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
|
|
39
|
-
export { AcceptType, compileMarkdownDeck, loadThemeConfig,
|
|
39
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generate, SlotType, } from "./engine/index.js";
|
|
2
|
-
import { AcceptType,
|
|
2
|
+
import { AcceptType, } from "./markdown/types.js";
|
|
3
3
|
/**
|
|
4
4
|
* A frontmatter parameter always fills one physical shape on the layout's own
|
|
5
5
|
* slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
|
|
@@ -7,24 +7,15 @@ import { AcceptType, ParameterType, } from "./markdown/types.js";
|
|
|
7
7
|
* placeholder; only body slots with a transplant block carry a real frame.
|
|
8
8
|
*/
|
|
9
9
|
const NO_FRAME = { x: 0, y: 0, cx: 0, cy: 0 };
|
|
10
|
+
// A text shape carries no top-level key — its template placeholders are the keys.
|
|
11
|
+
// The compiler emits its expanded content under shapeName, so the engine slot is
|
|
12
|
+
// keyed by shapeName too.
|
|
10
13
|
function paramToEngineSlot(param, baseSlide) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
key: param.shapeName,
|
|
18
|
-
frame: NO_FRAME,
|
|
19
|
-
accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
|
|
20
|
-
};
|
|
21
|
-
case ParameterType.Image:
|
|
22
|
-
return {
|
|
23
|
-
key: param.key,
|
|
24
|
-
frame: NO_FRAME,
|
|
25
|
-
accepts: [{ type: SlotType.Image, sourceSlide: baseSlide, shapeName: param.shapeName }],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
14
|
+
return {
|
|
15
|
+
key: param.shapeName,
|
|
16
|
+
frame: NO_FRAME,
|
|
17
|
+
accepts: [{ type: SlotType.Template, sourceSlide: baseSlide, shapeName: param.shapeName }],
|
|
18
|
+
};
|
|
28
19
|
}
|
|
29
20
|
/**
|
|
30
21
|
* Project a body slot's real `accepts` to engine `Block[]` and pass its `frame`
|
|
@@ -129,4 +120,4 @@ export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from
|
|
|
129
120
|
// Authoring
|
|
130
121
|
export { generateManifest } from "./manifest.js";
|
|
131
122
|
// Markdown / Compiler
|
|
132
|
-
export { AcceptType, compileMarkdownDeck, loadThemeConfig,
|
|
123
|
+
export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
|
package/dist/manifest.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { templateKeys } from "./markdown/textTemplate.js";
|
|
2
|
-
import { ParameterType } from "./markdown/types.js";
|
|
3
2
|
/**
|
|
4
3
|
* Flatten a compiler parameter to the manifest entries advertised to AI authors.
|
|
5
|
-
* A
|
|
6
|
-
* flattens to one entry per key (shapeName/template stay manifest-internal).
|
|
7
|
-
* image parameter is a single key.
|
|
4
|
+
* A parameter has no top-level key — its template's placeholders are the keys, so
|
|
5
|
+
* it flattens to one entry per key (shapeName/template stay manifest-internal).
|
|
8
6
|
*/
|
|
9
7
|
function stripParameter(param) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return result;
|
|
17
|
-
});
|
|
18
|
-
case ParameterType.Image: {
|
|
19
|
-
const result = { key: param.key, type: param.type };
|
|
20
|
-
if (param.required)
|
|
21
|
-
result.required = true;
|
|
22
|
-
return [result];
|
|
23
|
-
}
|
|
24
|
-
}
|
|
8
|
+
return templateKeys(param.template).map((key) => {
|
|
9
|
+
const result = { key };
|
|
10
|
+
if (param.required)
|
|
11
|
+
result.required = true;
|
|
12
|
+
return result;
|
|
13
|
+
});
|
|
25
14
|
}
|
|
26
15
|
function stripSlot(slot) {
|
|
27
16
|
const result = { key: slot.key, accepts: slot.accepts.map((b) => b.type) };
|
|
@@ -53,10 +42,6 @@ export function generateManifest(config) {
|
|
|
53
42
|
assets[category][name] = manifestEntry;
|
|
54
43
|
}
|
|
55
44
|
}
|
|
56
|
-
const manifest = {
|
|
57
|
-
version: 1,
|
|
58
|
-
layouts,
|
|
59
|
-
assets,
|
|
60
|
-
};
|
|
45
|
+
const manifest = { layouts, assets };
|
|
61
46
|
return JSON.stringify(manifest, null, 2);
|
|
62
47
|
}
|
|
@@ -13,12 +13,11 @@ export declare function toImageFill(path: string, type: AssetType): ImageFill;
|
|
|
13
13
|
* code (Shiki) and rendered mermaid (PNG) — so the returned deck is
|
|
14
14
|
* engine-shaped, ready for `buildDeck`.
|
|
15
15
|
*
|
|
16
|
-
* `config.rootDir` is the base directory for resolving
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the code and mermaid compiles.
|
|
16
|
+
* `config.rootDir` is the base directory for resolving the asset catalog's
|
|
17
|
+
* relative image paths. When empty, image paths are returned unchanged — callers
|
|
18
|
+
* that already produce absolute paths (or don't need resolution, e.g. unit tests)
|
|
19
|
+
* rely on the pass-through. When set, relative paths are resolved to absolute via
|
|
20
|
+
* `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
|
|
21
|
+
* `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
|
|
23
22
|
*/
|
|
24
23
|
export declare function compileDeck(doc: ParsedDocument, config: CompilerConfig): Promise<CompilerDeck>;
|
|
@@ -3,7 +3,7 @@ import { ImageFit, SlotType } from "../engine/index.js";
|
|
|
3
3
|
import { parseSlotContent } from "./blocks/registry.js";
|
|
4
4
|
import { validateSlideFrontmatter } from "./schema/deckSchema.js";
|
|
5
5
|
import { templateKeys, templateToSegments } from "./textTemplate.js";
|
|
6
|
-
import { AssetType,
|
|
6
|
+
import { AssetType, RESERVED_KEY, } from "./types.js";
|
|
7
7
|
/** Map each semantic asset type to the engine's object-fit directive. */
|
|
8
8
|
const FIT_FOR = {
|
|
9
9
|
[AssetType.Icon]: ImageFit.ScaleDown,
|
|
@@ -37,7 +37,7 @@ function assertSlotRegion(slot, got, layoutName, slideNo, source) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Resolve a
|
|
40
|
+
* Resolve a catalog image path against the deck's root directory.
|
|
41
41
|
* When `rootDir` is empty, the path is returned unchanged so callers that
|
|
42
42
|
* already produce absolute paths (or callers that don't care about
|
|
43
43
|
* resolution) can opt out. Absolute paths always pass through.
|
|
@@ -73,12 +73,11 @@ function assertUniqueSlideNumbers(layouts) {
|
|
|
73
73
|
* be collision-free — otherwise a fill silently clobbers or throws a misleading
|
|
74
74
|
* error:
|
|
75
75
|
*
|
|
76
|
-
* - **Author keys** — what a frontmatter line addresses: every
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* overwrite each other in the content map.
|
|
76
|
+
* - **Author keys** — what a frontmatter line addresses: every parameter's placeholder
|
|
77
|
+
* keys must be distinct across the layout, so a line routes to exactly one parameter.
|
|
78
|
+
* - **Content keys** — what `step.content` is addressed by: every parameter's
|
|
79
|
+
* `shapeName` and every slot key must be distinct, so no two overwrite each other in
|
|
80
|
+
* the content map.
|
|
82
81
|
*
|
|
83
82
|
* Also rejects a required template parameter whose template has no keys — it declares
|
|
84
83
|
* no way to be filled, so `required` on it is unsatisfiable.
|
|
@@ -95,34 +94,25 @@ function validateLayout(layout) {
|
|
|
95
94
|
const claimContentKey = (key, owner) => {
|
|
96
95
|
if (contentKeys.has(key)) {
|
|
97
96
|
throw new Error(`Layout "${layout.name}": name "${key}" (${owner}) collides with another parameter or slot; ` +
|
|
98
|
-
"each
|
|
97
|
+
"each parameter's shape and each slot key must be distinct.");
|
|
99
98
|
}
|
|
100
99
|
contentKeys.add(key);
|
|
101
100
|
};
|
|
102
101
|
for (const param of layout.parameters) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
claimContentKey(param.key, "image parameter");
|
|
107
|
-
break;
|
|
108
|
-
case ParameterType.Template: {
|
|
109
|
-
const keys = templateKeys(param.template);
|
|
110
|
-
if (param.required && keys.length === 0) {
|
|
111
|
-
throw new Error(`Layout "${layout.name}": template parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
|
|
112
|
-
}
|
|
113
|
-
for (const key of keys)
|
|
114
|
-
claimAuthorKey(key, `template parameter "${param.shapeName}"`);
|
|
115
|
-
claimContentKey(param.shapeName, "template parameter");
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
102
|
+
const keys = templateKeys(param.template);
|
|
103
|
+
if (param.required && keys.length === 0) {
|
|
104
|
+
throw new Error(`Layout "${layout.name}": parameter "${param.shapeName}" is marked required but its template has no keys to fill.`);
|
|
118
105
|
}
|
|
106
|
+
for (const key of keys)
|
|
107
|
+
claimAuthorKey(key, `parameter "${param.shapeName}"`);
|
|
108
|
+
claimContentKey(param.shapeName, "parameter");
|
|
119
109
|
}
|
|
120
110
|
for (const slot of layout.slots) {
|
|
121
111
|
claimContentKey(slot.key, "slot");
|
|
122
112
|
}
|
|
123
113
|
}
|
|
124
|
-
async function compileStep(slide, config,
|
|
125
|
-
const { layouts
|
|
114
|
+
async function compileStep(slide, config, resolveAssetRef) {
|
|
115
|
+
const { layouts } = config;
|
|
126
116
|
const { frontmatter, slots, index } = slide;
|
|
127
117
|
// Slide numbers in errors are 1-based, matching how an author counts slides in
|
|
128
118
|
// the deck file. Derive it once: every message below, and the parse-time errors
|
|
@@ -148,44 +138,22 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
148
138
|
// layout/notes stripped first). The per-layout strict schema IS the unknown-key
|
|
149
139
|
// check — it fires before the resolution loop, so that loop only sees valid keys.
|
|
150
140
|
validateSlideFrontmatter(frontmatter, layoutDef, slideNo);
|
|
151
|
-
// Map each author-facing key to
|
|
152
|
-
//
|
|
153
|
-
// (run once per layout in compileDeck) has already proven these key spaces are
|
|
141
|
+
// Map each author-facing key to the parameter that declares it. validateLayout
|
|
142
|
+
// (run once per layout in compileDeck) has already proven these keys are
|
|
154
143
|
// collision-free, so a later lookup is unambiguous.
|
|
155
|
-
const templateParams = [];
|
|
156
|
-
const imageByKey = new Map();
|
|
157
144
|
const templateParamByKey = new Map();
|
|
158
145
|
for (const param of layoutDef.parameters) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
imageByKey.set(param.key, param);
|
|
162
|
-
break;
|
|
163
|
-
case ParameterType.Template:
|
|
164
|
-
templateParams.push(param);
|
|
165
|
-
for (const key of templateKeys(param.template))
|
|
166
|
-
templateParamByKey.set(key, param);
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
146
|
+
for (const key of templateKeys(param.template))
|
|
147
|
+
templateParamByKey.set(key, param);
|
|
169
148
|
}
|
|
170
149
|
const slotsByKey = new Map(layoutDef.slots.map((s) => [s.key, s]));
|
|
171
150
|
const content = {};
|
|
172
|
-
// Frontmatter lines fill
|
|
173
|
-
//
|
|
151
|
+
// Frontmatter lines fill template-parameter keys, gathered per parameter and
|
|
152
|
+
// expanded together once every line is read.
|
|
174
153
|
const valuesByTemplateParam = new Map();
|
|
175
154
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
176
155
|
if (key === RESERVED_KEY.LAYOUT || key === RESERVED_KEY.NOTES)
|
|
177
156
|
continue;
|
|
178
|
-
const image = imageByKey.get(key);
|
|
179
|
-
if (image) {
|
|
180
|
-
const imgPath = resolveImagePath(rootDir, String(value));
|
|
181
|
-
const assetType = assetTypeByPath.get(imgPath);
|
|
182
|
-
if (assetType === undefined) {
|
|
183
|
-
throw new Error(`Slide image "${image.key}": "${value}" has no asset-catalog entry, so no type. ` +
|
|
184
|
-
`Add it to the theme's assets with a type (icon | image | background).`);
|
|
185
|
-
}
|
|
186
|
-
content[image.key] = toImageFill(imgPath, assetType);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
157
|
const templateParam = templateParamByKey.get(key);
|
|
190
158
|
if (templateParam) {
|
|
191
159
|
let bucket = valuesByTemplateParam.get(templateParam);
|
|
@@ -196,17 +164,17 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
196
164
|
bucket.set(key, String(value));
|
|
197
165
|
}
|
|
198
166
|
// Unreachable: validateSlideFrontmatter (above) already rejected any key that is
|
|
199
|
-
//
|
|
167
|
+
// not a reserved key, so every key here routes to a parameter.
|
|
200
168
|
}
|
|
201
169
|
// Expand each template parameter whose keys were supplied. Filling any key fills the
|
|
202
170
|
// parameter as a whole — a missing key throws (fail-fast in templateToSegments). A
|
|
203
171
|
// parameter with no supplied keys stays designer-sample unless required. Content
|
|
204
172
|
// is keyed by shapeName: one parameter → one entry, regardless of key count.
|
|
205
|
-
for (const templateParam of
|
|
173
|
+
for (const templateParam of layoutDef.parameters) {
|
|
206
174
|
const supplied = valuesByTemplateParam.get(templateParam);
|
|
207
175
|
if (!supplied) {
|
|
208
176
|
if (templateParam.required) {
|
|
209
|
-
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires
|
|
177
|
+
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${templateParam.shapeName}" ` +
|
|
210
178
|
`(keys: ${templateKeys(templateParam.template).join(", ")}); none provided`);
|
|
211
179
|
}
|
|
212
180
|
continue;
|
|
@@ -231,17 +199,11 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
231
199
|
config,
|
|
232
200
|
layoutVariant: layoutDef.variant,
|
|
233
201
|
});
|
|
234
|
-
assertSlotRegion(slot, parsed.acceptType, layoutName,
|
|
202
|
+
assertSlotRegion(slot, parsed.acceptType, layoutName, slideNo, source);
|
|
235
203
|
content[name] = await parsed.fill();
|
|
236
204
|
}
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// enforced during expansion above.
|
|
240
|
-
for (const image of imageByKey.values()) {
|
|
241
|
-
if (image.required && content[image.key] === undefined) {
|
|
242
|
-
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires parameter "${image.key}"; none provided`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
205
|
+
// A required slot with no region throws with layout + key context. Required
|
|
206
|
+
// parameters are enforced during template expansion above.
|
|
245
207
|
for (const slot of layoutDef.slots) {
|
|
246
208
|
if (slot.required && content[slot.key] === undefined) {
|
|
247
209
|
throw new Error(`Slide ${slideNo}: layout "${layoutName}" requires slot "${slot.key}"; none provided`);
|
|
@@ -260,13 +222,12 @@ async function compileStep(slide, config, assetTypeByPath, resolveAssetRef) {
|
|
|
260
222
|
* code (Shiki) and rendered mermaid (PNG) — so the returned deck is
|
|
261
223
|
* engine-shaped, ready for `buildDeck`.
|
|
262
224
|
*
|
|
263
|
-
* `config.rootDir` is the base directory for resolving
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* the code and mermaid compiles.
|
|
225
|
+
* `config.rootDir` is the base directory for resolving the asset catalog's
|
|
226
|
+
* relative image paths. When empty, image paths are returned unchanged — callers
|
|
227
|
+
* that already produce absolute paths (or don't need resolution, e.g. unit tests)
|
|
228
|
+
* rely on the pass-through. When set, relative paths are resolved to absolute via
|
|
229
|
+
* `path.resolve(rootDir, path)`; absolute paths pass through. `config.codeTheme` /
|
|
230
|
+
* `config.mermaid` / `config.mermaidVariant` feed the code and mermaid compiles.
|
|
270
231
|
*/
|
|
271
232
|
export async function compileDeck(doc, config) {
|
|
272
233
|
const { layouts, rootDir, assets } = config;
|
|
@@ -284,18 +245,10 @@ export async function compileDeck(doc, config) {
|
|
|
284
245
|
assertUniqueSlideNumbers(layouts);
|
|
285
246
|
for (const layout of layouts)
|
|
286
247
|
validateLayout(layout);
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
for (const entry of Object.values(group)) {
|
|
292
|
-
assetTypeByPath.set(resolveImagePath(rootDir, entry.path), entry.type);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Resolve a body/`::name::` `$category.name` reference against the theme's
|
|
296
|
-
// curated asset catalog. Anchored ⇒ the whole ref is the reference or it is
|
|
297
|
-
// nothing; a found entry wraps through the same path→ImageFit mapping as a
|
|
298
|
-
// frontmatter image (`toImageFill`), so a body image has no second fit story.
|
|
248
|
+
// Resolve a `$category.name` reference against the theme's curated asset
|
|
249
|
+
// catalog. Anchored ⇒ the whole ref is the reference or it is nothing; a found
|
|
250
|
+
// entry wraps through `toImageFill`, which maps the asset's declared type to
|
|
251
|
+
// its ImageFit.
|
|
299
252
|
const resolveAssetRef = (ref) => {
|
|
300
253
|
const match = ASSET_REF_RE.exec(ref);
|
|
301
254
|
if (!match) {
|
|
@@ -304,9 +257,17 @@ export async function compileDeck(doc, config) {
|
|
|
304
257
|
const [, category, name] = match;
|
|
305
258
|
const entry = assets[category]?.[name];
|
|
306
259
|
if (!entry) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
260
|
+
// Every asset in the catalog is far too many to read (mz-slides has 126).
|
|
261
|
+
// A known category narrows it to that category's names, which is what the
|
|
262
|
+
// author is choosing between; an unknown one lists the categories instead.
|
|
263
|
+
const group = assets[category];
|
|
264
|
+
const available = group
|
|
265
|
+
? Object.keys(group)
|
|
266
|
+
.map((n) => `$${category}.${n}`)
|
|
267
|
+
.join(", ")
|
|
268
|
+
: Object.keys(assets)
|
|
269
|
+
.map((c) => `$${c}.*`)
|
|
270
|
+
.join(", ");
|
|
310
271
|
throw new Error(`Unknown asset reference "${ref}". Available: ${available}`);
|
|
311
272
|
}
|
|
312
273
|
return toImageFill(resolveImagePath(rootDir, entry.path), entry.type);
|
|
@@ -315,7 +276,7 @@ export async function compileDeck(doc, config) {
|
|
|
315
276
|
// bad asset ref, accept-type mismatch) fire before its own content is rendered.
|
|
316
277
|
const steps = [];
|
|
317
278
|
for (const slide of doc.slides) {
|
|
318
|
-
steps.push(await compileStep(slide, config,
|
|
279
|
+
steps.push(await compileStep(slide, config, resolveAssetRef));
|
|
319
280
|
}
|
|
320
281
|
return { theme: String(theme), steps };
|
|
321
282
|
}
|
package/dist/markdown/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export { parseRegion } from "./mdast.js";
|
|
|
9
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, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep,
|
|
13
|
-
export { AcceptType, AssetType,
|
|
12
|
+
export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, } from "./types.js";
|
|
13
|
+
export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
|
package/dist/markdown/index.js
CHANGED
|
@@ -11,4 +11,4 @@ export { parseInlineRuns } from "./inline.js";
|
|
|
11
11
|
export { parseRegion } from "./mdast.js";
|
|
12
12
|
export { loadThemeConfig, parseThemeConfig, ThemeConfigSchema } from "./schema/themeConfigSchema.js";
|
|
13
13
|
export { parseSlideDocument } from "./slideParser.js";
|
|
14
|
-
export { AcceptType, AssetType,
|
|
14
|
+
export { AcceptType, AssetType, RESERVED_KEY } from "./types.js";
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import type { CompilerLayout } from "../types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Per-layout frontmatter validation for a deck `.md`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* parameter contributes one field per `{key}` placeholder in its template (NOT its
|
|
10
|
-
* `shapeName`, which addresses `step.content`, not frontmatter).
|
|
4
|
+
* Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
|
|
5
|
+
* the schema is built dynamically from a layout's `parameters` using the same
|
|
6
|
+
* key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
|
|
7
|
+
* one field per `{key}` placeholder in its template, never its `shapeName`,
|
|
8
|
+
* which addresses `step.content` rather than frontmatter.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* later commits.
|
|
18
|
-
* The strict object IS the unknown-key check: a stray frontmatter key throws
|
|
19
|
-
* instead of being silently ignored.
|
|
10
|
+
* Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
|
|
11
|
+
* (values are `String()`-coerced downstream), and `.optional()` because
|
|
12
|
+
* `required` is per-parameter and enforced in `compileStep`, not "all a
|
|
13
|
+
* template's keys present". The strict object is the point: a stray frontmatter
|
|
14
|
+
* key throws instead of being silently ignored.
|
|
20
15
|
*/
|
|
21
16
|
export declare function deckFrontmatterSchema(layout: CompilerLayout): z.ZodObject<{
|
|
22
17
|
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { templateKeys } from "../textTemplate.js";
|
|
3
|
-
import {
|
|
3
|
+
import { RESERVED_KEY } from "../types.js";
|
|
4
4
|
import { strict } from "./strict.js";
|
|
5
5
|
/**
|
|
6
|
-
* Per-layout frontmatter validation for a deck `.md`.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* parameter contributes one field per `{key}` placeholder in its template (NOT its
|
|
12
|
-
* `shapeName`, which addresses `step.content`, not frontmatter).
|
|
6
|
+
* Per-layout frontmatter validation for a deck `.md`. Layouts are JSON data, so
|
|
7
|
+
* the schema is built dynamically from a layout's `parameters` using the same
|
|
8
|
+
* key-derivation the compiler uses (`deckCompiler.ts`): a parameter contributes
|
|
9
|
+
* one field per `{key}` placeholder in its template, never its `shapeName`,
|
|
10
|
+
* which addresses `step.content` rather than frontmatter.
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* later commits.
|
|
20
|
-
* The strict object IS the unknown-key check: a stray frontmatter key throws
|
|
21
|
-
* instead of being silently ignored.
|
|
12
|
+
* Every field is `z.coerce.string()` so a YAML number like `year: 2026` passes
|
|
13
|
+
* (values are `String()`-coerced downstream), and `.optional()` because
|
|
14
|
+
* `required` is per-parameter and enforced in `compileStep`, not "all a
|
|
15
|
+
* template's keys present". The strict object is the point: a stray frontmatter
|
|
16
|
+
* key throws instead of being silently ignored.
|
|
22
17
|
*/
|
|
23
18
|
export function deckFrontmatterSchema(layout) {
|
|
24
19
|
const shape = {};
|
|
25
20
|
for (const param of layout.parameters) {
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
21
|
+
for (const key of templateKeys(param.template)) {
|
|
22
|
+
shape[key] = z.coerce.string().optional();
|
|
35
23
|
}
|
|
36
24
|
}
|
|
37
25
|
return strict(shape);
|
|
@@ -9,17 +9,11 @@ export declare const ThemeConfigSchema: z.ZodObject<{
|
|
|
9
9
|
light: "light";
|
|
10
10
|
dark: "dark";
|
|
11
11
|
}>>;
|
|
12
|
-
parameters: z.ZodArray<z.
|
|
12
|
+
parameters: z.ZodArray<z.ZodObject<{
|
|
13
13
|
shapeName: z.ZodString;
|
|
14
14
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
-
type: z.ZodLiteral<"template">;
|
|
16
15
|
template: z.ZodString;
|
|
17
|
-
}, z.core.$strict
|
|
18
|
-
shapeName: z.ZodString;
|
|
19
|
-
required: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
-
key: z.ZodString;
|
|
21
|
-
type: z.ZodLiteral<"image">;
|
|
22
|
-
}, z.core.$strict>], "type">>;
|
|
16
|
+
}, z.core.$strict>>;
|
|
23
17
|
slots: z.ZodArray<z.ZodObject<{
|
|
24
18
|
key: z.ZodString;
|
|
25
19
|
accepts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname } from "node:path";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import { AcceptType, AssetType,
|
|
4
|
+
import { AcceptType, AssetType, Variant } from "../types.js";
|
|
5
5
|
import { strict } from "./strict.js";
|
|
6
6
|
/**
|
|
7
7
|
* Fail-fast runtime validation for a `theme.json`. The hand-written types in
|
|
@@ -63,19 +63,11 @@ const ThemeFontSchema = strict({
|
|
|
63
63
|
path: z.string(),
|
|
64
64
|
weight: z.number().optional(),
|
|
65
65
|
});
|
|
66
|
-
const
|
|
66
|
+
const ParameterSchema = strict({
|
|
67
67
|
shapeName: z.string(),
|
|
68
68
|
required: z.boolean().optional(),
|
|
69
|
-
type: z.literal(ParameterType.Template),
|
|
70
69
|
template: z.string(),
|
|
71
70
|
});
|
|
72
|
-
const ImageParamSchema = strict({
|
|
73
|
-
shapeName: z.string(),
|
|
74
|
-
required: z.boolean().optional(),
|
|
75
|
-
key: z.string(),
|
|
76
|
-
type: z.literal(ParameterType.Image),
|
|
77
|
-
});
|
|
78
|
-
const ParameterSchema = z.discriminatedUnion("type", [TemplateParamSchema, ImageParamSchema]);
|
|
79
71
|
// One arm per accept type, discriminated by `type`, mirroring `CompilerBlock`:
|
|
80
72
|
// `startAt` lives only on the text arm, and `bodyRows` only on the table arm (a
|
|
81
73
|
// table block MUST declare its repeatable-row range; text/image arms have no
|
package/dist/markdown/types.d.ts
CHANGED
|
@@ -27,18 +27,6 @@ export type AssetEntry = {
|
|
|
27
27
|
};
|
|
28
28
|
/** Two-level catalog: `{ category: { name: AssetEntry } }`. */
|
|
29
29
|
export type AssetCatalog = Record<string, Record<string, AssetEntry>>;
|
|
30
|
-
/**
|
|
31
|
-
* Discriminator for a layout's frontmatter *parameters* — inputs the author
|
|
32
|
-
* writes as a single `key: value` line. Two kinds: `Template` (fills a styled
|
|
33
|
-
* shape's runs via fillTemplate) and `Image` (a filesystem path filled via
|
|
34
|
-
* fillImage). Both share their value with the engine's `SlotType`, since a
|
|
35
|
-
* parameter maps straight to an engine slot with no resolution step.
|
|
36
|
-
*/
|
|
37
|
-
export declare const ParameterType: {
|
|
38
|
-
readonly Template: "template";
|
|
39
|
-
readonly Image: "image";
|
|
40
|
-
};
|
|
41
|
-
export type ParameterType = (typeof ParameterType)[keyof typeof ParameterType];
|
|
42
30
|
/**
|
|
43
31
|
* The engine content types a slot may `accept`. A subset of the engine's
|
|
44
32
|
* `SlotType`: `Template` is parameter-only, never a body block. There is no
|
|
@@ -143,14 +131,14 @@ export type CompilerDeck = {
|
|
|
143
131
|
steps: CompilerDeckStep[];
|
|
144
132
|
};
|
|
145
133
|
/**
|
|
146
|
-
*
|
|
134
|
+
* Parameter: a styled text shape filled by expanding a `template` into the
|
|
147
135
|
* shape's paragraphs via fillTemplate. The template is one string with `{key}`
|
|
148
136
|
* placeholders and newlines for line breaks (`"{lastname}, {firstname} -
|
|
149
137
|
* {company}"`, or `"{name}\n{jobTitle}"`); a shape's fillable keys are the
|
|
150
138
|
* placeholders in its template. The shape carries no top-level `key`; its
|
|
151
139
|
* placeholders are the keys the author fills in frontmatter.
|
|
152
140
|
*/
|
|
153
|
-
export type
|
|
141
|
+
export type CompilerParameter = {
|
|
154
142
|
shapeName: string;
|
|
155
143
|
/**
|
|
156
144
|
* Whether the parameter may be omitted from a slide. Optional (defaults to
|
|
@@ -159,29 +147,9 @@ export type CompilerTemplateParameter = {
|
|
|
159
147
|
* from the slide.
|
|
160
148
|
*/
|
|
161
149
|
required?: boolean;
|
|
162
|
-
type: typeof ParameterType.Template;
|
|
163
150
|
/** The shape's text as one template with `{key}` placeholders; newlines are line breaks. */
|
|
164
151
|
template: string;
|
|
165
152
|
};
|
|
166
|
-
/** Image parameter: one frontmatter path filled by fillImage. Sizing/crop
|
|
167
|
-
* behaviour comes from the resolved asset's `type`, not the slot. */
|
|
168
|
-
export type CompilerImageParameter = {
|
|
169
|
-
shapeName: string;
|
|
170
|
-
/**
|
|
171
|
-
* Whether the parameter may be omitted from a slide. Optional (defaults to
|
|
172
|
-
* false): a required one with no value causes the compiler to throw with
|
|
173
|
-
* layout + key names; an optional one left unfilled has its shape removed
|
|
174
|
-
* from the slide.
|
|
175
|
-
*/
|
|
176
|
-
required?: boolean;
|
|
177
|
-
key: string;
|
|
178
|
-
type: typeof ParameterType.Image;
|
|
179
|
-
};
|
|
180
|
-
/**
|
|
181
|
-
* Compiler-facing parameter. A layout's frontmatter inputs — each written as a
|
|
182
|
-
* single `key: value` line, resolved against the layout's `parameters` list.
|
|
183
|
-
*/
|
|
184
|
-
export type CompilerParameter = CompilerTemplateParameter | CompilerImageParameter;
|
|
185
153
|
/**
|
|
186
154
|
* Reserved keys in a deck's frontmatter — global (theme) and per-slide
|
|
187
155
|
* (layout). Exported so callers (e.g. cli.ts) reference the constants
|
package/dist/markdown/types.js
CHANGED
|
@@ -10,18 +10,6 @@ export const AssetType = {
|
|
|
10
10
|
Image: "image",
|
|
11
11
|
Background: "background",
|
|
12
12
|
};
|
|
13
|
-
// ── ParameterType discriminator (frontmatter, one value) ──────────────────────
|
|
14
|
-
/**
|
|
15
|
-
* Discriminator for a layout's frontmatter *parameters* — inputs the author
|
|
16
|
-
* writes as a single `key: value` line. Two kinds: `Template` (fills a styled
|
|
17
|
-
* shape's runs via fillTemplate) and `Image` (a filesystem path filled via
|
|
18
|
-
* fillImage). Both share their value with the engine's `SlotType`, since a
|
|
19
|
-
* parameter maps straight to an engine slot with no resolution step.
|
|
20
|
-
*/
|
|
21
|
-
export const ParameterType = {
|
|
22
|
-
Template: SlotType.Template,
|
|
23
|
-
Image: SlotType.Image,
|
|
24
|
-
};
|
|
25
13
|
// ── AcceptType discriminator (what a slot accepts) ────────────────────────────
|
|
26
14
|
/**
|
|
27
15
|
* The engine content types a slot may `accept`. A subset of the engine's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tycoworks/tycoslide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Create editable, on-brand PowerPoint slides from markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc --build",
|
|
19
|
-
"test": "tsc --build && node --test --experimental-transform-types test/**/*.test.ts",
|
|
20
|
-
"typecheck": "tsc --
|
|
19
|
+
"test": "tsc --build && tsc -p tsconfig.test.json && node --test --experimental-transform-types test/**/*.test.ts",
|
|
20
|
+
"typecheck": "tsc --build && tsc -p tsconfig.test.json",
|
|
21
21
|
"lint": "biome check .",
|
|
22
22
|
"lint:fix": "biome check --write .",
|
|
23
23
|
"format": "biome format --write ."
|
package/syntax.md
CHANGED
|
@@ -118,11 +118,6 @@ Fill a parameter by putting a value under its key in the slide's frontmatter.
|
|
|
118
118
|
jobTitle: CEO, Acme Corp
|
|
119
119
|
```
|
|
120
120
|
The engine substitutes each value into the run that carries its style, so if the designer made the name bold and the job title grey, the filled name stays bold and the filled title stays grey.
|
|
121
|
-
- **`image`** -- a picture placeholder. Set it in frontmatter with the image path (from an asset catalog entry, or an absolute path). How it is scaled and cropped is set by the **asset's `type`** in the catalog (`icon` never enlarges past native and never crops; `image` fits the whole picture without cropping; `background` fills the frame and center-crops).
|
|
122
|
-
```yaml
|
|
123
|
-
hero: assets/diagrams/architecture.png
|
|
124
|
-
```
|
|
125
|
-
|
|
126
121
|
### Body content shapes
|
|
127
122
|
|
|
128
123
|
Fill a slot by writing a `::key::` region in the body; the marker maps to the slot with that `key`. A slot's manifest entry lists which content types it `accepts` (`text`, `table`, `image`) -- write content whose shape matches one of them:
|
|
@@ -139,7 +134,13 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
|
|
|
139
134
|
````
|
|
140
135
|
The language tag (e.g. `sql`, `python`, `typescript`) is required -- it drives syntax highlighting, using the theme's `codeTheme` (set once in `theme.json`, not per slot). Colors are applied as native text runs in the output, not images.
|
|
141
136
|
- **table** (slots that accept `table`) -- a GFM table. Write it in the slot region between `|`-delimited headers and rows; cells inherit inline formatting (bold, italic, links).
|
|
142
|
-
- **image** (slots that accept `image`) -- a picture
|
|
137
|
+
- **image** (slots that accept `image`) -- a picture from the theme's asset catalog, written as ``:
|
|
138
|
+
```markdown
|
|
139
|
+
::logo::
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
```
|
|
143
|
+
The categories and names are listed in `manifest.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
|
|
143
144
|
|
|
144
145
|
---
|
|
145
146
|
|
|
@@ -200,20 +201,9 @@ The theme owns all styling. These directives are rejected at build time:
|
|
|
200
201
|
|
|
201
202
|
---
|
|
202
203
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
An image is a **parameter** -- one value (a path) on a frontmatter line. Reference it using the parameter key directly:
|
|
206
|
-
|
|
207
|
-
```yaml
|
|
208
|
-
---
|
|
209
|
-
layout: ImageSlide
|
|
210
|
-
title: Architecture Diagram
|
|
211
|
-
hero: assets/diagrams/architecture.png
|
|
212
|
-
---
|
|
213
|
-
```
|
|
204
|
+
## Layout declarations
|
|
214
205
|
|
|
215
206
|
Each parameter or slot in the layout definition may declare:
|
|
216
|
-
- **`type`** (parameters, required) -- `template` or `image`.
|
|
217
207
|
- **`accepts`** (slots, required) -- an array of `text`, `table`, `image`.
|
|
218
208
|
- **`required: true`** -- the slide has no usable default and the build fails if the parameter/slot has no value (e.g. team-member photos, icon-grid icons, the quote logo). If you don't have a suitable image, ask the user for one.
|
|
219
209
|
- **optional (the default)** -- a parameter or slot you leave unfilled is dropped from the slide (its shape is removed), so a layout with numbered slots (e.g. up to six sections, up to four stats) renders only the ones you fill.
|
|
@@ -309,6 +299,9 @@ Infrastructure:
|
|
|
309
299
|
---
|
|
310
300
|
layout: ImageSlide
|
|
311
301
|
title: Office Map
|
|
312
|
-
hero: assets/images/office-floor-plan.png
|
|
313
302
|
---
|
|
303
|
+
|
|
304
|
+
::hero::
|
|
305
|
+
|
|
306
|
+

|
|
314
307
|
```
|