@classic-homes/slides 0.2.1 → 0.4.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 +80 -23
- package/dist/chunk-JFUJ47TP.js +107 -0
- package/dist/chunk-JFUJ47TP.js.map +1 -0
- package/dist/{chunk-MYY5GZC2.js → chunk-L7G3VH7K.js} +37 -7
- package/dist/chunk-L7G3VH7K.js.map +1 -0
- package/dist/{chunk-7FROOJCY.js → chunk-PTTLL6PQ.js} +2503 -300
- package/dist/chunk-PTTLL6PQ.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/echarts-render-2QVSBZ6V.js +50 -0
- package/dist/echarts-render-2QVSBZ6V.js.map +1 -0
- package/dist/index.d.ts +154 -4
- package/dist/index.js +66 -12
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DGMQTIQI.js → mcp-UX32IP2J.js} +17 -14
- package/dist/mcp-UX32IP2J.js.map +1 -0
- package/dist/node.d.ts +4 -2
- package/dist/node.js +2 -2
- package/dist/render-CU0sXO2y.d.ts +604 -0
- package/package.json +3 -1
- package/dist/chunk-33EPZMT6.js +0 -69
- package/dist/chunk-33EPZMT6.js.map +0 -1
- package/dist/chunk-7FROOJCY.js.map +0 -1
- package/dist/chunk-MYY5GZC2.js.map +0 -1
- package/dist/chunk-VONVGYRL.js +0 -52
- package/dist/chunk-VONVGYRL.js.map +0 -1
- package/dist/mcp-DGMQTIQI.js.map +0 -1
- package/dist/render-DTz7tLKM.d.ts +0 -149
package/README.md
CHANGED
|
@@ -63,30 +63,72 @@ Inline text supports `**bold**`, `*highlight*` (brand accent), `` `code` `` and
|
|
|
63
63
|
Run `classic-slides layouts` for the list, `classic-slides layouts <name>` for
|
|
64
64
|
every field plus an example.
|
|
65
65
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `title`
|
|
69
|
-
|
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `split`
|
|
73
|
-
|
|
|
74
|
-
| `stats` | 2–6 numbers worth remembering |
|
|
75
|
-
| `comparison` | 2–3 columns side by side |
|
|
76
|
-
| `journey` | Numbered agenda map (uses `journey.steps`) |
|
|
77
|
-
| `photos` | A grid of photos: team, products, homes |
|
|
78
|
-
| `image` | One big image or diagram |
|
|
79
|
-
| `walkthrough` | Ordered stages, one slide each with a progress rail |
|
|
80
|
-
| `quote` | A testimonial or pull quote |
|
|
81
|
-
| `qr` | Send people to a URL with a scannable code |
|
|
82
|
-
| `contacts` | Who to call |
|
|
66
|
+
| Group | Layouts |
|
|
67
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| Openers & dividers | `title`, `section`, `statement`, `journey` |
|
|
69
|
+
| Text | `bullets`, `cards`, `comparison`, `quote` |
|
|
70
|
+
| Data | `stats` (with trends + sparklines), `chart`, `table` |
|
|
71
|
+
| Diagrams | `flow`, `timeline`, `cycle`, `hierarchy`, `matrix`, `tiers` (funnel / pyramid) |
|
|
72
|
+
| Media | `split`, `photos`, `image` (with numbered hotspots), `compare` (before/after), `walkthrough`, `video` (YouTube/Vimeo), `embed` (any https page) |
|
|
73
|
+
| People & places | `qr`, `contacts` |
|
|
83
74
|
|
|
84
75
|
Every slide also accepts `track` (value/red, build/teal, neutral/navy),
|
|
85
|
-
`background` (paper or a solid brand colour), `
|
|
86
|
-
decks with `journey.steps` — `step`,
|
|
76
|
+
`background` (paper or a solid brand colour), `backgroundImage` + `overlay`,
|
|
77
|
+
`notes`, `time`, `id`, `hidden`, and — in decks with `journey.steps` — `step`,
|
|
78
|
+
`stepDone` and `showJourney`. `bullets`, `cards`, `flow` and `timeline` take
|
|
79
|
+
`build: true` to reveal one item per click.
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
### Charts
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
- layout: chart
|
|
85
|
+
title: Closings *doubled* since spring
|
|
86
|
+
chart:
|
|
87
|
+
type: column # bar, stacked-column, stacked-bar, line, area, pie, donut, funnel, gauge, scatter, radar
|
|
88
|
+
labels: [Q1, Q2, Q3, Q4]
|
|
89
|
+
series: [{ name: Closings, values: [42, 61, 88, 94] }]
|
|
90
|
+
unit: homes
|
|
91
|
+
highlight: Q4
|
|
92
|
+
insights: [Q4 best quarter on record]
|
|
93
|
+
source: Sales ops
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Charts are rendered to static SVG with ECharts (server-side rendering, themed
|
|
97
|
+
from `@classic-homes/charts-core`), so they work offline and in print. ECharts
|
|
98
|
+
is loaded only when a deck has charts.
|
|
99
|
+
|
|
100
|
+
### Video and embeds
|
|
101
|
+
|
|
102
|
+
`video` takes YouTube/Vimeo links (privacy-enhanced players); `embed` takes any
|
|
103
|
+
https page (maps, dashboards, 3D tours, forms) in a sandboxed frame. Both load
|
|
104
|
+
only while their slide is shown, and fall back to a poster/screenshot with a QR
|
|
105
|
+
code offline, in print and in PDF.
|
|
106
|
+
|
|
107
|
+
## Brand profiles and deck settings
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
brand:
|
|
111
|
+
profile: pentrust # admin-managed profile; default classic-homes
|
|
112
|
+
name: PenTrust Lending # optional overrides: name, tagline, logo, logoOnDark, favicon, footer
|
|
113
|
+
settings:
|
|
114
|
+
slideNumbers: true
|
|
115
|
+
logo: all # title | all | none
|
|
116
|
+
confidential: Internal
|
|
117
|
+
autoAdvance: 15 # kiosk mode (also ?kiosk=15 on the URL)
|
|
118
|
+
loop: true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Profiles (`BrandProfile`) carry name, logos, favicon, footer and an optional
|
|
122
|
+
palette; `validateBrandProfile()` enforces WCAG AA contrast. Pass them to
|
|
123
|
+
`renderDeck(deck, { brands })` / `validateDeck(deck, { brands })`. Authors can
|
|
124
|
+
override words and images, never colours.
|
|
125
|
+
|
|
126
|
+
## Presenting
|
|
127
|
+
|
|
128
|
+
Arrow keys / Space advance (and step through reveals) · **N** notes · **P**
|
|
129
|
+
presenter window (current + next slide, notes, timer, clock) · **O** overview ·
|
|
130
|
+
**F** fullscreen · `#slide-7` deep links · `?kiosk=15` loops unattended. Print
|
|
131
|
+
to PDF gives one 16:9 page per slide with media fallbacks.
|
|
90
132
|
|
|
91
133
|
## With an AI agent (MCP)
|
|
92
134
|
|
|
@@ -117,8 +159,23 @@ import { buildDeckFile, fileImageResolver } from '@classic-homes/slides/node';
|
|
|
117
159
|
const { html } = await renderDeck(deck, { resolveImage: (src) => lookUp(src) });
|
|
118
160
|
```
|
|
119
161
|
|
|
120
|
-
The core entry has no Node dependencies (it runs in Workers);
|
|
121
|
-
file-based image loading and `buildDeckFile`.
|
|
162
|
+
The core entry has no Node dependencies (it runs in Workers and browsers);
|
|
163
|
+
`/node` adds file-based image loading and `buildDeckFile`. For editors,
|
|
164
|
+
`layoutEditorInfo()` / `deckEditorFields()` describe every field as a widget
|
|
165
|
+
(with `hint`s for chart, grid, url, video-url and point pickers),
|
|
166
|
+
`blankSlide(layout)` gives a valid starting slide, and `renderDeck(deck,
|
|
167
|
+
{ includeHidden: true, initialSlide })` plus the `postMessage` API drive a
|
|
168
|
+
live preview. Fields flagged `advanced` (slide id, speaking time, accent
|
|
169
|
+
colour) belong behind an "Advanced" section.
|
|
170
|
+
|
|
171
|
+
Click-to-edit: render the preview with `editorMarkers: true` and, inside an
|
|
172
|
+
iframe, clicking a title, bullet or picture posts
|
|
173
|
+
`{ type: 'classic-slides:select', index, path }` to the parent (`index` is the
|
|
174
|
+
deck slide, `path` the field such as `['items', 2, 'text']`, or `null` for the
|
|
175
|
+
slide itself). Send `{ type: 'classic-slides:highlight', index, path }` to
|
|
176
|
+
outline the field being edited. Never turn it on for shared or exported decks.
|
|
177
|
+
`layoutGalleryDeck()` returns one deck with every layout's starting slide, for
|
|
178
|
+
rendering a layout picker's thumbnails in one pass.
|
|
122
179
|
|
|
123
180
|
Rendered HTML embeds the deck source (minus image data), so
|
|
124
181
|
`extractDeckSource(html)` recovers the editable deck.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeckSchema,
|
|
3
|
+
LAYOUTS,
|
|
4
|
+
LAYOUT_CATEGORIES,
|
|
5
|
+
baseSlide,
|
|
6
|
+
getLayout
|
|
7
|
+
} from "./chunk-PTTLL6PQ.js";
|
|
8
|
+
|
|
9
|
+
// src/catalog.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
12
|
+
function listLayouts() {
|
|
13
|
+
return LAYOUTS.map((l) => ({
|
|
14
|
+
name: l.name,
|
|
15
|
+
category: LAYOUT_CATEGORIES[l.name],
|
|
16
|
+
description: l.description,
|
|
17
|
+
whenToUse: l.whenToUse
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function describeLayout(name) {
|
|
21
|
+
const layout = getLayout(name);
|
|
22
|
+
if (!layout) return void 0;
|
|
23
|
+
const schema = z.object({ layout: z.literal(layout.name), ...layout.shape, ...baseSlide });
|
|
24
|
+
return {
|
|
25
|
+
name: layout.name,
|
|
26
|
+
description: layout.description,
|
|
27
|
+
whenToUse: layout.whenToUse,
|
|
28
|
+
defaults: layout.defaults ?? {},
|
|
29
|
+
schema: zodToJsonSchema(schema, { $refStrategy: "none" }),
|
|
30
|
+
example: layout.example
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function deckJsonSchema() {
|
|
34
|
+
return zodToJsonSchema(DeckSchema, { name: "ClassicDeck", $refStrategy: "none" });
|
|
35
|
+
}
|
|
36
|
+
var AUTHORING_GUIDE = `# Writing a Classic Homes deck
|
|
37
|
+
|
|
38
|
+
A deck is a YAML (or JSON) file: deck-level fields plus a list of slides. Each slide
|
|
39
|
+
picks a **layout** and fills its fields. Layouts own all styling \u2014 there is no way to
|
|
40
|
+
set colours, fonts or sizes directly, which is what keeps every deck on-brand.
|
|
41
|
+
|
|
42
|
+
## Principles
|
|
43
|
+
- **One idea per slide.** The headline states the point. Detail goes in \`notes\`.
|
|
44
|
+
- **Short copy.** Titles under ~70 characters, bullets a few words, at most 6 bullets.
|
|
45
|
+
\`validate\` warns when a slide gets crowded \u2014 split it rather than shrinking text.
|
|
46
|
+
- **Speaker notes on every content slide.** Presenters press N (or P for presenter view).
|
|
47
|
+
- **Real photos over decoration.** Every image needs \`alt\` text.
|
|
48
|
+
- **Tracks** colour the accent: \`value\` (red, default) for the homeowner/value story,
|
|
49
|
+
\`build\` (teal) for construction/process, \`neutral\` (navy) for framing slides.
|
|
50
|
+
- **Backgrounds**: \`paper\` (default) for content; solid \`navy\`/\`red\`/\`teal\`/\`plum\`/\`grey\`/\`stone\`
|
|
51
|
+
for dividers. \`backgroundImage\` + \`overlay\` puts a photo behind title/section/statement slides.
|
|
52
|
+
|
|
53
|
+
## Picking a layout
|
|
54
|
+
- **Numbers:** \`stats\` (2\u20136 headline figures, optional trend + sparkline), \`chart\` (a real chart \u2014
|
|
55
|
+
put the takeaway in the title), \`table\` (exact figures people compare).
|
|
56
|
+
- **Processes & structure:** \`flow\` (ordered steps with arrows), \`timeline\` (dated milestones, phases),
|
|
57
|
+
\`cycle\` (things that repeat), \`hierarchy\` (org chart / tree), \`matrix\` (2\xD72), \`tiers\`
|
|
58
|
+
(funnel or pyramid), \`walkthrough\` (photo per stage), \`journey\` (deck agenda).
|
|
59
|
+
- **Media:** \`split\` (text + photos), \`photos\`, \`image\` (with numbered \`hotspots\` for floor plans
|
|
60
|
+
and site maps), \`compare\` (before/after slider), \`video\` (YouTube/Vimeo), \`embed\` (any https page:
|
|
61
|
+
maps, dashboards, 3D tours, forms).
|
|
62
|
+
- **Words:** \`statement\`, \`bullets\`, \`cards\`, \`comparison\`, \`quote\`.
|
|
63
|
+
- **Wrap-up:** \`qr\` (send people somewhere), \`contacts\`, closing \`statement\`.
|
|
64
|
+
|
|
65
|
+
## Charts
|
|
66
|
+
\`chart.labels\` are the categories; each series has one value per label. Pie, donut, funnel and
|
|
67
|
+
gauge use the first series; gauge needs \`max\`; scatter uses two series as x and y. Use
|
|
68
|
+
\`highlight\` to emphasise one category and \`format\`/\`unit\` instead of typing symbols into numbers.
|
|
69
|
+
|
|
70
|
+
## Video & embeds
|
|
71
|
+
They need internet while presenting. Offline, in print and in PDF they show a poster (or
|
|
72
|
+
\`fallback\` screenshot) with a QR code. Some sites refuse to be framed \u2014 the "Open \u2197" link
|
|
73
|
+
always works; add a \`fallback\` image for anything critical.
|
|
74
|
+
|
|
75
|
+
## Brand
|
|
76
|
+
\`brand.profile\` picks who the deck speaks for (default \`classic-homes\`). You may override
|
|
77
|
+
\`name\`, \`tagline\`, \`logo\`, \`logoOnDark\`, \`favicon\` and \`footer\`; colours come from the profile.
|
|
78
|
+
\`settings\` controls slide numbers, logo placement, a confidential label and kiosk auto-advance.
|
|
79
|
+
|
|
80
|
+
## Presenting
|
|
81
|
+
Arrow keys / Space advance (and step through \`build: true\` reveals). N notes \xB7 P presenter window
|
|
82
|
+
(current + next slide, notes, timer) \xB7 O overview \xB7 F fullscreen \xB7 \`?kiosk=15\` on the URL loops
|
|
83
|
+
unattended. \`hidden: true\` skips a slide without deleting it.
|
|
84
|
+
|
|
85
|
+
## Inline text
|
|
86
|
+
\`**bold**\`, \`*highlight*\` (accent colour), \`\\\`code\\\`\`, \`[label](https://\u2026)\`. No HTML.
|
|
87
|
+
|
|
88
|
+
## Journey decks
|
|
89
|
+
Set \`journey.steps\` at deck level to get a numbered agenda. Then:
|
|
90
|
+
- a \`journey\` slide shows the full map,
|
|
91
|
+
- \`step: 3\` on any slide draws the vertical STEP rail,
|
|
92
|
+
- \`showJourney: true\` adds the compact "you are here" strip (great on \`section\` slides).
|
|
93
|
+
|
|
94
|
+
## Typical shape
|
|
95
|
+
1. \`title\`
|
|
96
|
+
2. \`statement\` or \`journey\` (why we're here / the agenda)
|
|
97
|
+
3. per part: \`section\` \u2192 2\u20135 content slides
|
|
98
|
+
4. \`qr\` (where to go next) \u2192 \`contacts\` \u2192 closing \`statement\`
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
listLayouts,
|
|
103
|
+
describeLayout,
|
|
104
|
+
deckJsonSchema,
|
|
105
|
+
AUTHORING_GUIDE
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=chunk-JFUJ47TP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["import { z } from 'zod';\nimport { zodToJsonSchema } from 'zod-to-json-schema';\nimport {\n LAYOUT_CATEGORIES,\n LAYOUTS,\n getLayout,\n type LayoutCategory,\n type LayoutName,\n} from './layouts/index.js';\nimport { baseSlide } from './schema/common.js';\nimport { DeckSchema } from './schema/deck.js';\n\nexport interface LayoutSummary {\n name: string;\n category: LayoutCategory;\n description: string;\n whenToUse: string;\n}\n\nexport function listLayouts(): LayoutSummary[] {\n return LAYOUTS.map((l) => ({\n name: l.name,\n category: LAYOUT_CATEGORIES[l.name as LayoutName],\n description: l.description,\n whenToUse: l.whenToUse,\n }));\n}\n\n/** Full description of one layout: fields as JSON Schema plus a working example. */\nexport function describeLayout(name: string) {\n const layout = getLayout(name);\n if (!layout) return undefined;\n const schema = z.object({ layout: z.literal(layout.name), ...layout.shape, ...baseSlide });\n return {\n name: layout.name,\n description: layout.description,\n whenToUse: layout.whenToUse,\n defaults: layout.defaults ?? {},\n schema: zodToJsonSchema(schema, { $refStrategy: 'none' }),\n example: layout.example,\n };\n}\n\n/** JSON Schema for a whole deck (editor tooling, `$schema` in deck files). */\nexport function deckJsonSchema() {\n return zodToJsonSchema(DeckSchema, { name: 'ClassicDeck', $refStrategy: 'none' });\n}\n\n/** Authoring guidance shared by the CLI, MCP server and docs. */\nexport const AUTHORING_GUIDE = `# Writing a Classic Homes deck\n\nA deck is a YAML (or JSON) file: deck-level fields plus a list of slides. Each slide\npicks a **layout** and fills its fields. Layouts own all styling — there is no way to\nset colours, fonts or sizes directly, which is what keeps every deck on-brand.\n\n## Principles\n- **One idea per slide.** The headline states the point. Detail goes in \\`notes\\`.\n- **Short copy.** Titles under ~70 characters, bullets a few words, at most 6 bullets.\n \\`validate\\` warns when a slide gets crowded — split it rather than shrinking text.\n- **Speaker notes on every content slide.** Presenters press N (or P for presenter view).\n- **Real photos over decoration.** Every image needs \\`alt\\` text.\n- **Tracks** colour the accent: \\`value\\` (red, default) for the homeowner/value story,\n \\`build\\` (teal) for construction/process, \\`neutral\\` (navy) for framing slides.\n- **Backgrounds**: \\`paper\\` (default) for content; solid \\`navy\\`/\\`red\\`/\\`teal\\`/\\`plum\\`/\\`grey\\`/\\`stone\\`\n for dividers. \\`backgroundImage\\` + \\`overlay\\` puts a photo behind title/section/statement slides.\n\n## Picking a layout\n- **Numbers:** \\`stats\\` (2–6 headline figures, optional trend + sparkline), \\`chart\\` (a real chart —\n put the takeaway in the title), \\`table\\` (exact figures people compare).\n- **Processes & structure:** \\`flow\\` (ordered steps with arrows), \\`timeline\\` (dated milestones, phases),\n \\`cycle\\` (things that repeat), \\`hierarchy\\` (org chart / tree), \\`matrix\\` (2×2), \\`tiers\\`\n (funnel or pyramid), \\`walkthrough\\` (photo per stage), \\`journey\\` (deck agenda).\n- **Media:** \\`split\\` (text + photos), \\`photos\\`, \\`image\\` (with numbered \\`hotspots\\` for floor plans\n and site maps), \\`compare\\` (before/after slider), \\`video\\` (YouTube/Vimeo), \\`embed\\` (any https page:\n maps, dashboards, 3D tours, forms).\n- **Words:** \\`statement\\`, \\`bullets\\`, \\`cards\\`, \\`comparison\\`, \\`quote\\`.\n- **Wrap-up:** \\`qr\\` (send people somewhere), \\`contacts\\`, closing \\`statement\\`.\n\n## Charts\n\\`chart.labels\\` are the categories; each series has one value per label. Pie, donut, funnel and\ngauge use the first series; gauge needs \\`max\\`; scatter uses two series as x and y. Use\n\\`highlight\\` to emphasise one category and \\`format\\`/\\`unit\\` instead of typing symbols into numbers.\n\n## Video & embeds\nThey need internet while presenting. Offline, in print and in PDF they show a poster (or\n\\`fallback\\` screenshot) with a QR code. Some sites refuse to be framed — the \"Open ↗\" link\nalways works; add a \\`fallback\\` image for anything critical.\n\n## Brand\n\\`brand.profile\\` picks who the deck speaks for (default \\`classic-homes\\`). You may override\n\\`name\\`, \\`tagline\\`, \\`logo\\`, \\`logoOnDark\\`, \\`favicon\\` and \\`footer\\`; colours come from the profile.\n\\`settings\\` controls slide numbers, logo placement, a confidential label and kiosk auto-advance.\n\n## Presenting\nArrow keys / Space advance (and step through \\`build: true\\` reveals). N notes · P presenter window\n(current + next slide, notes, timer) · O overview · F fullscreen · \\`?kiosk=15\\` on the URL loops\nunattended. \\`hidden: true\\` skips a slide without deleting it.\n\n## Inline text\n\\`**bold**\\`, \\`*highlight*\\` (accent colour), \\`\\\\\\`code\\\\\\`\\`, \\`[label](https://…)\\`. No HTML.\n\n## Journey decks\nSet \\`journey.steps\\` at deck level to get a numbered agenda. Then:\n- a \\`journey\\` slide shows the full map,\n- \\`step: 3\\` on any slide draws the vertical STEP rail,\n- \\`showJourney: true\\` adds the compact \"you are here\" strip (great on \\`section\\` slides).\n\n## Typical shape\n1. \\`title\\`\n2. \\`statement\\` or \\`journey\\` (why we're here / the agenda)\n3. per part: \\`section\\` → 2–5 content slides\n4. \\`qr\\` (where to go next) → \\`contacts\\` → closing \\`statement\\`\n`;\n"],"mappings":";;;;;;;;;AAAA,SAAS,SAAS;AAClB,SAAS,uBAAuB;AAkBzB,SAAS,cAA+B;AAC7C,SAAO,QAAQ,IAAI,CAAC,OAAO;AAAA,IACzB,MAAM,EAAE;AAAA,IACR,UAAU,kBAAkB,EAAE,IAAkB;AAAA,IAChD,aAAa,EAAE;AAAA,IACf,WAAW,EAAE;AAAA,EACf,EAAE;AACJ;AAGO,SAAS,eAAe,MAAc;AAC3C,QAAM,SAAS,UAAU,IAAI;AAC7B,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,OAAO,IAAI,GAAG,GAAG,OAAO,OAAO,GAAG,UAAU,CAAC;AACzF,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,aAAa,OAAO;AAAA,IACpB,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO,YAAY,CAAC;AAAA,IAC9B,QAAQ,gBAAgB,QAAQ,EAAE,cAAc,OAAO,CAAC;AAAA,IACxD,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,iBAAiB;AAC/B,SAAO,gBAAgB,YAAY,EAAE,MAAM,eAAe,cAAc,OAAO,CAAC;AAClF;AAGO,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}
|
|
@@ -2,11 +2,38 @@ import {
|
|
|
2
2
|
parseDeckSource,
|
|
3
3
|
renderDeck,
|
|
4
4
|
stringifyDeck
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-PTTLL6PQ.js";
|
|
6
6
|
|
|
7
7
|
// src/node/index.ts
|
|
8
8
|
import { readFile, stat, writeFile } from "fs/promises";
|
|
9
|
-
import { dirname, extname, isAbsolute, join, resolve } from "path";
|
|
9
|
+
import { dirname as dirname2, extname, isAbsolute, join, resolve as resolve2 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/node/paths.ts
|
|
12
|
+
import { lstatSync, realpathSync } from "fs";
|
|
13
|
+
import { dirname, relative, resolve, sep } from "path";
|
|
14
|
+
function confinedPath(root, path) {
|
|
15
|
+
const canonicalRoot = realpathSync(root);
|
|
16
|
+
const absolute = resolve(path);
|
|
17
|
+
let ancestor = absolute;
|
|
18
|
+
for (; ; ) {
|
|
19
|
+
try {
|
|
20
|
+
const canonical = resolve(realpathSync(ancestor), relative(ancestor, absolute));
|
|
21
|
+
if (canonical !== canonicalRoot && !canonical.startsWith(canonicalRoot + sep)) {
|
|
22
|
+
throw new Error(`Path "${path}" is outside the deck root ${root}.`);
|
|
23
|
+
}
|
|
24
|
+
return canonical;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT" || ancestor === dirname(ancestor))
|
|
27
|
+
throw error;
|
|
28
|
+
if (lstatSync(ancestor, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
29
|
+
throw new Error(`Cannot follow dangling symlink "${ancestor}".`);
|
|
30
|
+
}
|
|
31
|
+
ancestor = dirname(ancestor);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/node/index.ts
|
|
10
37
|
var MIME = {
|
|
11
38
|
".jpg": "image/jpeg",
|
|
12
39
|
".jpeg": "image/jpeg",
|
|
@@ -32,7 +59,8 @@ function fileImageResolver(baseDir, options = {}) {
|
|
|
32
59
|
warn(`${src} is ${(buf2.length / 1024).toFixed(0)} KB; consider a smaller copy.`);
|
|
33
60
|
return `data:${type2};base64,${buf2.toString("base64")}`;
|
|
34
61
|
}
|
|
35
|
-
const
|
|
62
|
+
const candidate = isAbsolute(src) ? src : join(baseDir, src);
|
|
63
|
+
const path = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;
|
|
36
64
|
const type = MIME[extname(path).toLowerCase()];
|
|
37
65
|
if (!type)
|
|
38
66
|
throw new Error(`Unsupported image type for "${src}". Use jpg, png, webp, gif, svg or avif.`);
|
|
@@ -55,22 +83,24 @@ async function saveDeckFile(path, deck) {
|
|
|
55
83
|
await writeFile(path, text);
|
|
56
84
|
}
|
|
57
85
|
async function buildDeckFile(path, options = {}) {
|
|
58
|
-
const abs =
|
|
86
|
+
const abs = options.rootDir ? confinedPath(options.rootDir, resolve2(path)) : resolve2(path);
|
|
59
87
|
const deck = await loadDeckFile(abs);
|
|
60
88
|
const result = await renderDeck(deck, {
|
|
61
89
|
...options,
|
|
62
|
-
resolveImage: fileImageResolver(
|
|
90
|
+
resolveImage: fileImageResolver(dirname2(abs), options)
|
|
63
91
|
});
|
|
64
|
-
const
|
|
92
|
+
const candidate = options.out ? resolve2(options.out) : abs.replace(/\.(ya?ml|json)$/i, "") + ".html";
|
|
93
|
+
const out = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;
|
|
65
94
|
await writeFile(out, result.html);
|
|
66
95
|
return { ...result, out, bytes: (await stat(out)).size };
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
export {
|
|
99
|
+
confinedPath,
|
|
70
100
|
IMAGE_WARN_BYTES,
|
|
71
101
|
fileImageResolver,
|
|
72
102
|
loadDeckFile,
|
|
73
103
|
saveDeckFile,
|
|
74
104
|
buildDeckFile
|
|
75
105
|
};
|
|
76
|
-
//# sourceMappingURL=chunk-
|
|
106
|
+
//# sourceMappingURL=chunk-L7G3VH7K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node/index.ts","../src/node/paths.ts"],"sourcesContent":["/** Node-only helpers: read deck files, inline local images, write HTML. */\nimport { readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, extname, isAbsolute, join, resolve } from 'node:path';\nimport { confinedPath } from './paths.js';\nimport { parseDeckSource, stringifyDeck } from '../parse.js';\nimport {\n renderDeck,\n type ImageResolver,\n type RenderOptions,\n type RenderResult,\n} from '../render.js';\n\nconst MIME: Record<string, string> = {\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.webp': 'image/webp',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.avif': 'image/avif',\n};\n\n/** Largest single image we inline without warning. Big photos bloat the deck file. */\nexport const IMAGE_WARN_BYTES = 800 * 1024;\n\nexport interface FileResolverOptions {\n /** Restrict local image reads and build paths to this root (used by the MCP server). */\n rootDir?: string;\n /** Fetch https images and inline them so the deck works offline. Default true. */\n inlineRemote?: boolean;\n onWarning?: (message: string) => void;\n}\n\n/** Resolves image paths relative to the deck file and inlines them as data URIs. */\nexport function fileImageResolver(\n baseDir: string,\n options: FileResolverOptions = {}\n): ImageResolver {\n const warn = options.onWarning ?? (() => {});\n return async (src) => {\n if (src.startsWith('data:')) return src;\n if (/^https?:/.test(src)) {\n if (options.inlineRemote === false) return src;\n const res = await fetch(src);\n if (!res.ok) throw new Error(`Image ${src} returned HTTP ${res.status}`);\n const type = res.headers.get('content-type') ?? 'application/octet-stream';\n const buf = Buffer.from(await res.arrayBuffer());\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider a smaller copy.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n }\n const candidate = isAbsolute(src) ? src : join(baseDir, src);\n const path = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;\n const type = MIME[extname(path).toLowerCase()];\n if (!type)\n throw new Error(`Unsupported image type for \"${src}\". Use jpg, png, webp, gif, svg or avif.`);\n let buf: Buffer;\n try {\n buf = await readFile(path);\n } catch {\n throw new Error(`Image not found: \"${src}\" (looked in ${path})`);\n }\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider resizing to ~2000px wide.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n };\n}\n\nexport async function loadDeckFile(path: string): Promise<unknown> {\n return parseDeckSource(await readFile(path, 'utf8'));\n}\n\nexport async function saveDeckFile(path: string, deck: unknown): Promise<void> {\n const text = path.endsWith('.json') ? JSON.stringify(deck, null, 2) + '\\n' : stringifyDeck(deck);\n await writeFile(path, text);\n}\n\nexport interface BuildOptions extends Omit<RenderOptions, 'resolveImage'>, FileResolverOptions {\n out?: string;\n}\n\n/** Renders a deck file to a self-contained HTML file next to it (or at `out`). */\nexport async function buildDeckFile(\n path: string,\n options: BuildOptions = {}\n): Promise<RenderResult & { out: string; bytes: number }> {\n const abs = options.rootDir ? confinedPath(options.rootDir, resolve(path)) : resolve(path);\n const deck = await loadDeckFile(abs);\n const result = await renderDeck(deck, {\n ...options,\n resolveImage: fileImageResolver(dirname(abs), options),\n });\n const candidate = options.out\n ? resolve(options.out)\n : abs.replace(/\\.(ya?ml|json)$/i, '') + '.html';\n const out = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;\n await writeFile(out, result.html);\n return { ...result, out, bytes: (await stat(out)).size };\n}\n\nexport { renderDeck };\n","/** Canonical containment for existing files and new output files. */\nimport { lstatSync, realpathSync } from 'node:fs';\nimport { dirname, relative, resolve, sep } from 'node:path';\n\nexport function confinedPath(root: string, path: string): string {\n const canonicalRoot = realpathSync(root);\n const absolute = resolve(path);\n let ancestor = absolute;\n for (;;) {\n try {\n const canonical = resolve(realpathSync(ancestor), relative(ancestor, absolute));\n if (canonical !== canonicalRoot && !canonical.startsWith(canonicalRoot + sep)) {\n throw new Error(`Path \"${path}\" is outside the deck root ${root}.`);\n }\n return canonical;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || ancestor === dirname(ancestor))\n throw error;\n if (lstatSync(ancestor, { throwIfNoEntry: false })?.isSymbolicLink()) {\n throw new Error(`Cannot follow dangling symlink \"${ancestor}\".`);\n }\n ancestor = dirname(ancestor);\n }\n }\n}\n"],"mappings":"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","names":["dirname","resolve","type","buf","resolve","dirname"]}
|