slidemind 0.2.1 → 0.2.3
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/.agents/plugins/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +9 -1
- package/{CLAUDE.md → AGENTS.md} +88 -17
- package/README.md +87 -39
- package/assets/app-icon.png +0 -0
- package/bin/slidemind.mjs +335 -100
- package/package.json +4 -2
- package/skills/create-slides/SKILL.md +15 -15
- package/skills/search/SKILL.md +6 -6
- package/src/bridge/client.mjs +11 -10
- package/src/bridge/config.mjs +2 -2
- package/src/bridge/{pair.mjs → http-json.mjs} +9 -24
- package/src/bridge/setup-claim.mjs +127 -0
- package/src/builders/build.mjs +3 -3
- package/src/builders/resolve-skill.mjs +66 -6
- package/src/installers/claude.mjs +24 -0
- package/src/installers/codex.mjs +60 -4
- package/src/installers/lib.mjs +49 -19
- package/src/installers/opencode.mjs +16 -0
- package/src/installers/tui.mjs +21 -10
- package/src/pptx/fill/engine.mjs +6 -3
- package/src/pptx/fill/freeform.mjs +36 -22
- package/src/pptx/fill/parse-slides.mjs +56 -22
- package/src/pptx/fill/xml.mjs +1 -1
package/src/installers/lib.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const CURSOR_SHOW = "\x1b[?25h";
|
|
|
113
113
|
// uniform.
|
|
114
114
|
function startWordmarkAnimation() {
|
|
115
115
|
const lines = wordmarkFrame(0);
|
|
116
|
-
|
|
116
|
+
tui.out("\n" + lines.join("\n") + "\n\n");
|
|
117
117
|
return () => {};
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -125,6 +125,30 @@ export function isForce(flags) {
|
|
|
125
125
|
return flags.includes("--force") || flags.includes("-F");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Is `name` an executable on the user's PATH? Used as the second half of
|
|
129
|
+
// harness detection: a coding agent's config dir (~/.claude, ~/.codex,
|
|
130
|
+
// ~/.config/opencode) is created by USE, not by install, so a freshly installed
|
|
131
|
+
// agent that has never been launched has no directory at all — its binary is
|
|
132
|
+
// the only evidence it exists. Resolved by walking PATH rather than shelling
|
|
133
|
+
// out to which/where, so it costs nothing and behaves the same on Windows.
|
|
134
|
+
export function binaryOnPath(name) {
|
|
135
|
+
const exts =
|
|
136
|
+
process.platform === "win32"
|
|
137
|
+
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
|
|
138
|
+
: [""];
|
|
139
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
140
|
+
if (!dir) continue;
|
|
141
|
+
for (const ext of exts) {
|
|
142
|
+
try {
|
|
143
|
+
if (fs.statSync(path.join(dir, name + ext)).isFile()) return true;
|
|
144
|
+
} catch {
|
|
145
|
+
/* not there / unreadable — keep looking */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
export function expandHome(p) {
|
|
129
153
|
if (p.startsWith("~/")) return path.join(homeDir(), p.slice(2));
|
|
130
154
|
return p;
|
|
@@ -234,6 +258,12 @@ let _startedAt = 0;
|
|
|
234
258
|
let _stopWordmark = null;
|
|
235
259
|
|
|
236
260
|
export const ui = {
|
|
261
|
+
// Suppress ALL decorative output. `slidemind setup --json` runs the real
|
|
262
|
+
// installers but owns stdout for its machine-readable payload, so the rail
|
|
263
|
+
// would corrupt the very thing the caller is parsing.
|
|
264
|
+
silence(value = true) {
|
|
265
|
+
tui.setSilent(value);
|
|
266
|
+
},
|
|
237
267
|
// `wordmark: false` skips painting the wordmark — used when the caller
|
|
238
268
|
// (e.g. the install.sh bootstrap) already printed it, so we only emit the
|
|
239
269
|
// `v… · action` intro line and continue the rail.
|
|
@@ -276,12 +306,12 @@ export const ui = {
|
|
|
276
306
|
// Manual emission so we can animate the title in place. Symbol
|
|
277
307
|
// sits at col 1; " " padding lands the title at col 4.
|
|
278
308
|
tui.bumpRow(1);
|
|
279
|
-
|
|
309
|
+
tui.out(tui.rail() + "\n");
|
|
280
310
|
tui.bumpRow(1);
|
|
281
|
-
|
|
311
|
+
tui.out(SYM.step + " ");
|
|
282
312
|
await tui.shimmer(title, { startCol: 4, restColor: (s) => s });
|
|
283
|
-
if (sub)
|
|
284
|
-
|
|
313
|
+
if (sub) tui.out(" " + brand.slate(tildify(sub)));
|
|
314
|
+
tui.out("\n");
|
|
285
315
|
},
|
|
286
316
|
step({ status = "ok", label, value, note }) {
|
|
287
317
|
const symbol =
|
|
@@ -328,15 +358,15 @@ export const ui = {
|
|
|
328
358
|
|
|
329
359
|
const boxLine = (content) => {
|
|
330
360
|
const pad = Math.max(0, inner - visibleLength(content));
|
|
331
|
-
|
|
361
|
+
tui.out(slate("│") + content + " ".repeat(pad) + slate("│") + "\n");
|
|
332
362
|
};
|
|
333
363
|
|
|
334
|
-
|
|
364
|
+
tui.out("\n");
|
|
335
365
|
|
|
336
366
|
// Top: ╭─ installed ────╮
|
|
337
367
|
const labelText = " installed ";
|
|
338
368
|
const fillerLen = Math.max(2, inner - 1 - labelText.length);
|
|
339
|
-
|
|
369
|
+
tui.out(
|
|
340
370
|
slate("╭─") +
|
|
341
371
|
dim(labelText) +
|
|
342
372
|
slate("─".repeat(fillerLen)) +
|
|
@@ -359,10 +389,10 @@ export const ui = {
|
|
|
359
389
|
boxLine(" " + dim(stats));
|
|
360
390
|
|
|
361
391
|
// Bottom: ╰────────╯
|
|
362
|
-
|
|
392
|
+
tui.out(slate("╰" + "─".repeat(inner) + "╯") + "\n");
|
|
363
393
|
|
|
364
|
-
|
|
365
|
-
|
|
394
|
+
tui.out("\n");
|
|
395
|
+
tui.out(`${SYM.success} ${brand.bold(action)}\n`);
|
|
366
396
|
|
|
367
397
|
// Stop the wordmark animation last — that resets the scroll region
|
|
368
398
|
// and shows the cursor. The wordmark itself stays painted at the
|
|
@@ -401,16 +431,16 @@ export const ui = {
|
|
|
401
431
|
// Reserve two rows: rail spacer + the active spinner line. Both
|
|
402
432
|
// count toward the gradient row counter.
|
|
403
433
|
tui.bumpRow(1);
|
|
404
|
-
|
|
434
|
+
tui.out(tui.rail() + "\n");
|
|
405
435
|
tui.bumpRow(1);
|
|
406
|
-
|
|
436
|
+
tui.out(
|
|
407
437
|
`${brand.techBlue40(SPINNER_FRAMES[0])} ${fmtStep({ label, value, note })}`,
|
|
408
438
|
);
|
|
409
439
|
let frame = 0;
|
|
410
440
|
const timer = setInterval(() => {
|
|
411
441
|
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
412
|
-
|
|
413
|
-
|
|
442
|
+
tui.out("\r\x1b[2K");
|
|
443
|
+
tui.out(
|
|
414
444
|
`${brand.techBlue40(SPINNER_FRAMES[frame])} ${fmtStep({ label, value, note })}`,
|
|
415
445
|
);
|
|
416
446
|
}, 80);
|
|
@@ -418,15 +448,15 @@ export const ui = {
|
|
|
418
448
|
const result = await fn();
|
|
419
449
|
const time = Date.now() - start;
|
|
420
450
|
clearInterval(timer);
|
|
421
|
-
|
|
422
|
-
|
|
451
|
+
tui.out("\r\x1b[2K");
|
|
452
|
+
tui.out(
|
|
423
453
|
`${SYM.success} ${fmtStep({ label, value, note: finalNote ?? note, time })}\n`,
|
|
424
454
|
);
|
|
425
455
|
return result;
|
|
426
456
|
} catch (err) {
|
|
427
457
|
clearInterval(timer);
|
|
428
|
-
|
|
429
|
-
|
|
458
|
+
tui.out("\r\x1b[2K");
|
|
459
|
+
tui.out(
|
|
430
460
|
`${SYM.error} ${fmtStep({ label, value, note: "failed" })}\n`,
|
|
431
461
|
);
|
|
432
462
|
throw err;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import {
|
|
20
|
+
binaryOnPath,
|
|
20
21
|
expandHome,
|
|
21
22
|
installDir,
|
|
22
23
|
isForce,
|
|
@@ -117,6 +118,21 @@ function removeLegacyArtifacts() {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// See claude.mjs `detect()`. opencode has no slidemind plugin channel, so the
|
|
122
|
+
// skills can only ever come from here.
|
|
123
|
+
export function detect() {
|
|
124
|
+
const root = opencodeRoot();
|
|
125
|
+
const configured = fs.existsSync(root);
|
|
126
|
+
const onPath = binaryOnPath("opencode");
|
|
127
|
+
return {
|
|
128
|
+
root,
|
|
129
|
+
configured,
|
|
130
|
+
onPath,
|
|
131
|
+
present: configured || onPath,
|
|
132
|
+
pluginInstalled: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
120
136
|
export async function install({ packageRoot, flags }) {
|
|
121
137
|
const link = isLinkMode(flags);
|
|
122
138
|
const force = isForce(flags);
|
package/src/installers/tui.mjs
CHANGED
|
@@ -23,6 +23,17 @@ const FADE_ROWS = 28; // rail finishes fading after this many emitted rows
|
|
|
23
23
|
|
|
24
24
|
let _row = 0;
|
|
25
25
|
|
|
26
|
+
// Every terminal write in the installer UI funnels through here, so a caller
|
|
27
|
+
// whose stdout carries machine-readable output (`slidemind setup --json`) can
|
|
28
|
+
// silence the decorative rail without each writer knowing about it.
|
|
29
|
+
let _silent = false;
|
|
30
|
+
export function setSilent(value) {
|
|
31
|
+
_silent = Boolean(value);
|
|
32
|
+
}
|
|
33
|
+
export function out(text) {
|
|
34
|
+
if (!_silent) process.stdout.write(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
// Linear interpolate between #9999FF (techBlue40) and #6C7D8C (slate).
|
|
27
38
|
// Top of the rail is bright tech-blue, fading to brand slate as the
|
|
28
39
|
// install progresses — feels like the deck's tech-blue gradation card.
|
|
@@ -52,7 +63,7 @@ export function bumpRow(n = 1) {
|
|
|
52
63
|
|
|
53
64
|
export function intro(text) {
|
|
54
65
|
_row = 0;
|
|
55
|
-
|
|
66
|
+
out("\n" + paint(_row, "┌") + " " + text + "\n");
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
// Each message starts with its own gradient-coloured rail spacer, then
|
|
@@ -60,28 +71,28 @@ export function intro(text) {
|
|
|
60
71
|
// (◆ ● ▲ ■ etc.); when omitted we fall back to a continuation rail.
|
|
61
72
|
export function message(text, { symbol } = {}) {
|
|
62
73
|
_row += 1;
|
|
63
|
-
|
|
74
|
+
out(paint(_row, "│") + "\n");
|
|
64
75
|
_row += 1;
|
|
65
76
|
const sym = symbol ?? paint(_row, "│");
|
|
66
|
-
|
|
77
|
+
out(sym + " " + text + "\n");
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
export function outro(text) {
|
|
70
81
|
_row += 1;
|
|
71
|
-
|
|
82
|
+
out(paint(_row, "│") + "\n");
|
|
72
83
|
_row += 1;
|
|
73
|
-
|
|
84
|
+
out(paint(_row, "└") + " " + text + "\n\n");
|
|
74
85
|
_row = 0;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
// Reserve the next two rows for a spin step: emits the rail spacer,
|
|
78
89
|
// returns the row index the spinner is written on. Callers should
|
|
79
|
-
// `
|
|
90
|
+
// `out(...)` the spinner content themselves (without a
|
|
80
91
|
// trailing `\n`) so they can overwrite it via `\r`. When the operation
|
|
81
92
|
// completes they call `commitSpinRow()` after writing the final line.
|
|
82
93
|
export function reserveSpinRows() {
|
|
83
94
|
_row += 1;
|
|
84
|
-
|
|
95
|
+
out(paint(_row, "│") + "\n");
|
|
85
96
|
_row += 1;
|
|
86
97
|
return _row;
|
|
87
98
|
}
|
|
@@ -96,7 +107,7 @@ const ANIMATE = !NO_COLOR && process.stdout.isTTY;
|
|
|
96
107
|
export async function shimmer(text, { startCol = 1, restColor }) {
|
|
97
108
|
const final = brand.bold(restColor(text));
|
|
98
109
|
if (!ANIMATE) {
|
|
99
|
-
|
|
110
|
+
out(final);
|
|
100
111
|
return;
|
|
101
112
|
}
|
|
102
113
|
const len = text.length;
|
|
@@ -117,9 +128,9 @@ export async function shimmer(text, { startCol = 1, restColor }) {
|
|
|
117
128
|
else painter = (s) => brand.dim(brand.slate(s));
|
|
118
129
|
line += brand.bold(painter(text[i]));
|
|
119
130
|
}
|
|
120
|
-
|
|
131
|
+
out(`\x1b[${startCol}G\x1b[K` + line);
|
|
121
132
|
if (f < frames) await new Promise((r) => setTimeout(r, frameMs));
|
|
122
133
|
}
|
|
123
134
|
// Final settle.
|
|
124
|
-
|
|
135
|
+
out(`\x1b[${startCol}G\x1b[K` + final);
|
|
125
136
|
}
|
package/src/pptx/fill/engine.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The fill-template deck builder — the host-free `build-pptx` path for
|
|
2
|
-
//
|
|
2
|
+
// template packs (mode: "fill-template" in the pack's skill.json).
|
|
3
3
|
//
|
|
4
4
|
// Assembly direction: the template .pptx is cloned WHOLE (masters, layouts,
|
|
5
5
|
// media, tags, embeddings), then only declared shapes inside the clone are
|
|
@@ -74,7 +74,7 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
74
74
|
problems.push(err.message);
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
const layout = layoutNumber(parsed.component, locale);
|
|
77
|
+
const layout = layoutNumber(parsed.component, locale, pack.components);
|
|
78
78
|
if (!layout) {
|
|
79
79
|
problems.push(`${file}: <${parsed.component}> is a freeform (create-slides) slide with no ` +
|
|
80
80
|
`rendered artifact next to it — 'slidemind build-pptx' renders slides/${file.replace(/\.jsx$/, "")}.pptx ` +
|
|
@@ -190,6 +190,9 @@ function loadPack(skill) {
|
|
|
190
190
|
fields,
|
|
191
191
|
repeats: read("repeats.json", {}),
|
|
192
192
|
policy: skill.manifest.policy ?? {},
|
|
193
|
+
// The pack's layout-component naming contract (see parse-slides.mjs).
|
|
194
|
+
// skillDir rides along so a bad pattern names the pack that shipped it.
|
|
195
|
+
components: { ...(skill.manifest.components ?? {}), skillDir: skill.skillDir },
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -283,7 +286,7 @@ function checkIdentities(sequence, warnings, problems) {
|
|
|
283
286
|
problems.push(`WP count mismatch: slide 10 declares ${wpItems.length} work packages but there are ${details.length} WP-detail files (slide 11)`);
|
|
284
287
|
}
|
|
285
288
|
// Every WP detail must carry FULL bullet lists — a section left empty (or a
|
|
286
|
-
// one-liner) is a defect in a client
|
|
289
|
+
// one-liner) is a defect in a client deliverable, not a style choice.
|
|
287
290
|
details.forEach((d, i) => {
|
|
288
291
|
for (const section of ["services", "assumptions", "deliverables"]) {
|
|
289
292
|
const lines = asLines(d.props?.[section]);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// In-deck freeform slides for fill-template decks.
|
|
2
2
|
//
|
|
3
|
-
// Contract: a
|
|
3
|
+
// Contract: a fill-template deck's slides/ may carry create-slides freeform JSX
|
|
4
4
|
// (NN-name.jsx composing the design bundle's layouts) directly next to the
|
|
5
5
|
// master-backed slides — no sibling deck. The rendered native artifact lives
|
|
6
6
|
// beside its source as NN-name.pptx; the fill engine inserts that artifact at
|
|
7
7
|
// the filename position like any native slide. This module keeps the
|
|
8
8
|
// artifacts fresh: stale or missing renders go through the add-in's render
|
|
9
|
-
// path (the ONLY host-dependent step of a
|
|
9
|
+
// path (the ONLY host-dependent step of such a build) and are folded to
|
|
10
10
|
// pruned one-slide packages. Unchanged freeform slides rebuild fully offline.
|
|
11
11
|
|
|
12
12
|
import crypto from "node:crypto";
|
|
@@ -22,22 +22,26 @@ import { layoutNumber } from "./parse-slides.mjs";
|
|
|
22
22
|
|
|
23
23
|
const SIDE_CAR = ".freeform-renders.json";
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
// Some templates fix a slide's title (the master prints it, so a freeform
|
|
26
|
+
// replacement must keep it or the deck stops reading like the master). WHICH
|
|
27
|
+
// slides, and in which locale, is the pack's business — it declares them in
|
|
28
|
+
// skill.json as policy.fixedTitles:
|
|
29
|
+
//
|
|
30
|
+
// "fixedTitles": {
|
|
31
|
+
// "en": [ { "stem": "agenda", "title": "Agenda" } ],
|
|
32
|
+
// "de": [ { "stem": "agenda|ablauf", "title": "Ablauf" } ]
|
|
33
|
+
// }
|
|
34
|
+
//
|
|
35
|
+
// `stem` is a case-insensitive regex matched against the slide file's stem.
|
|
36
|
+
// A pack that declares none simply gets no title warnings.
|
|
37
|
+
function fixedTitleRules(policy, locale) {
|
|
38
|
+
const all = policy?.fixedTitles ?? {};
|
|
39
|
+
return all[locale] ?? all.en ?? [];
|
|
40
|
+
}
|
|
37
41
|
|
|
38
|
-
function isFreeformSource(src) {
|
|
42
|
+
function isFreeformSource(src, components) {
|
|
39
43
|
const comp = src.match(/<([A-Z][A-Za-z0-9]*)/)?.[1];
|
|
40
|
-
return Boolean(comp) && layoutNumber(comp, "en") === null;
|
|
44
|
+
return Boolean(comp) && layoutNumber(comp, "en", components) === null;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function contentHash(parts) {
|
|
@@ -53,9 +57,10 @@ function slideText(bytes) {
|
|
|
53
57
|
return [...(xml ?? "").matchAll(/<a:t>([^<]*)<\/a:t>/g)].map((m) => m[1]).join("\n");
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
function fixedTitleWarning(stem, bytes, locale) {
|
|
57
|
-
const
|
|
58
|
-
|
|
60
|
+
function fixedTitleWarning(stem, bytes, locale, policy) {
|
|
61
|
+
const rule = fixedTitleRules(policy, locale).find((r) =>
|
|
62
|
+
new RegExp(r.stem, "i").test(stem),
|
|
63
|
+
);
|
|
59
64
|
if (!rule) return null;
|
|
60
65
|
if (slideText(bytes).includes(rule.title)) return null;
|
|
61
66
|
return (
|
|
@@ -69,7 +74,14 @@ function fixedTitleWarning(stem, bytes, locale) {
|
|
|
69
74
|
* `{ rendered, warnings }`; throws with the offending file named when a stale
|
|
70
75
|
* render is needed but the paired backend cannot render.
|
|
71
76
|
*/
|
|
72
|
-
export async function ensureFreeformRenders({
|
|
77
|
+
export async function ensureFreeformRenders({
|
|
78
|
+
deckDir,
|
|
79
|
+
locale = "en",
|
|
80
|
+
srcDir,
|
|
81
|
+
components,
|
|
82
|
+
policy = {},
|
|
83
|
+
onLog = () => {},
|
|
84
|
+
}) {
|
|
73
85
|
const slidesDir = path.join(deckDir, "slides");
|
|
74
86
|
const jsxFiles = fs.existsSync(slidesDir)
|
|
75
87
|
? fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx")).sort()
|
|
@@ -77,7 +89,9 @@ export async function ensureFreeformRenders({ deckDir, locale = "en", srcDir, on
|
|
|
77
89
|
const items = [];
|
|
78
90
|
for (const file of jsxFiles) {
|
|
79
91
|
const src = fs.readFileSync(path.join(slidesDir, file), "utf8");
|
|
80
|
-
if (isFreeformSource(src))
|
|
92
|
+
if (isFreeformSource(src, components)) {
|
|
93
|
+
items.push({ file, stem: file.replace(/\.jsx$/, ""), src });
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
const warnings = [];
|
|
83
97
|
if (items.length === 0) return { rendered: 0, warnings };
|
|
@@ -149,7 +163,7 @@ export async function ensureFreeformRenders({ deckDir, locale = "en", srcDir, on
|
|
|
149
163
|
for (const it of items) {
|
|
150
164
|
const target = path.join(slidesDir, `${it.stem}.pptx`);
|
|
151
165
|
if (!fs.existsSync(target)) continue;
|
|
152
|
-
const warn = fixedTitleWarning(it.stem, fs.readFileSync(target), locale);
|
|
166
|
+
const warn = fixedTitleWarning(it.stem, fs.readFileSync(target), locale, policy);
|
|
153
167
|
if (warn) warnings.push(warn);
|
|
154
168
|
}
|
|
155
169
|
return { rendered: stale.length, warnings };
|
|
@@ -1,27 +1,61 @@
|
|
|
1
|
-
// Parser for
|
|
2
|
-
// one default export returning exactly one <
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Parser for fill-template slide files. Their JSX is declarative by contract —
|
|
2
|
+
// one default export returning exactly one <SomeLayout …/> element whose props
|
|
3
|
+
// are data literals. That constraint is what makes a real JSX runtime
|
|
4
|
+
// unnecessary: attributes are scanned textually and brace expressions are
|
|
5
|
+
// evaluated as plain JS literals in an empty vm sandbox.
|
|
6
|
+
//
|
|
7
|
+
// The component NAMES are the pack's business, not this engine's: a pack ships
|
|
8
|
+
// its own layouts under its own naming scheme, so it declares the scheme in
|
|
9
|
+
// skill.json and this file only applies it.
|
|
6
10
|
|
|
7
11
|
import vm from "node:vm";
|
|
8
12
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
// Compiled once per distinct pattern — layoutNumber runs per slide file.
|
|
14
|
+
const patternCache = new Map();
|
|
15
|
+
function compile(pattern, which, skillDir) {
|
|
16
|
+
let re = patternCache.get(pattern);
|
|
17
|
+
if (re) return re;
|
|
18
|
+
try {
|
|
19
|
+
re = new RegExp(pattern);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`pack ${skillDir ?? "(unknown)"}: skill.json components.${which} is not a ` +
|
|
23
|
+
`valid regular expression (${err.message})`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
patternCache.set(pattern, re);
|
|
27
|
+
return re;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Map a component name → the template slide number it fills, or null when the
|
|
31
|
+
// component is not master-backed at all (i.e. a freeform slide).
|
|
32
|
+
//
|
|
33
|
+
// `components` comes from the pack's skill.json and declares two patterns:
|
|
34
|
+
// canonical — locale-neutral spelling; capture group 1 is the slide number
|
|
35
|
+
// localized — optional generated spelling; group 1 is the locale, group 2
|
|
36
|
+
// the slide number (so a locale mismatch can be reported)
|
|
37
|
+
export function layoutNumber(component, locale, components) {
|
|
38
|
+
if (!components || !components.canonical) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"fill-template pack declares no components.canonical pattern in " +
|
|
41
|
+
"skill.json — the pack owns its layout naming contract and must " +
|
|
42
|
+
"declare it, e.g. { \"components\": { \"canonical\": \"^MyLayout(\\\\d{2})$\" } }",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
let m = component.match(compile(components.canonical, "canonical", components.skillDir));
|
|
14
46
|
if (m) return { no: Number(m[1]) };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
if (components.localized) {
|
|
48
|
+
m = component.match(compile(components.localized, "localized", components.skillDir));
|
|
49
|
+
if (m) {
|
|
50
|
+
const compLocale = String(m[1]).toLowerCase();
|
|
51
|
+
return {
|
|
52
|
+
no: Number(m[2]),
|
|
53
|
+
warn:
|
|
54
|
+
compLocale === locale
|
|
55
|
+
? `use the canonical alias for slide ${Number(m[2])}`
|
|
56
|
+
: `component locale "${compLocale}" does not match deck locale "${locale}"`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
25
59
|
}
|
|
26
60
|
return null;
|
|
27
61
|
}
|
|
@@ -81,7 +115,7 @@ function evalLiteral(expr, file) {
|
|
|
81
115
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
82
116
|
} catch (err) {
|
|
83
117
|
throw new Error(`${file}: cannot evaluate prop expression (${err.message}). ` +
|
|
84
|
-
`
|
|
118
|
+
`Slide props must be data literals — strings, numbers, arrays, objects.`);
|
|
85
119
|
}
|
|
86
120
|
}
|
|
87
121
|
|
|
@@ -136,7 +170,7 @@ export function parseSlideFile(source, file) {
|
|
|
136
170
|
const inner = closeIdx > 0 ? src.slice(i + 1, closeIdx).trim() : "";
|
|
137
171
|
if (inner) {
|
|
138
172
|
throw new Error(
|
|
139
|
-
`${file}: <${component}> has children. Master-backed
|
|
173
|
+
`${file}: <${component}> has children. Master-backed slides are ` +
|
|
140
174
|
`self-closing props-only elements; freeform content belongs to create-slides.`,
|
|
141
175
|
);
|
|
142
176
|
}
|
package/src/pptx/fill/xml.mjs
CHANGED
|
@@ -30,7 +30,7 @@ const stripHighlight = (rPr) =>
|
|
|
30
30
|
|
|
31
31
|
const SHAPE_TAGS = ["sp", "pic", "cxnSp", "graphicFrame"];
|
|
32
32
|
|
|
33
|
-
// Top-level shape blocks of one kind.
|
|
33
|
+
// Top-level shape blocks of one kind. Template masters keep all editable
|
|
34
34
|
// shapes at spTree top level (verified by inventory); grouped shapes are not
|
|
35
35
|
// addressed by the pack metadata.
|
|
36
36
|
export function* blocks(xml, tag) {
|