slidemind 0.3.20 → 0.4.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
// Inline the local components a slide declares, so a reader that only knows
|
|
2
|
+
// literal elements sees every element a component instance draws.
|
|
3
|
+
//
|
|
4
|
+
// A repeated organism is one local component and one call site per member
|
|
5
|
+
// (workflow/authoring.md → Repeated organisms are components):
|
|
6
|
+
//
|
|
7
|
+
// const Repeat1 = ({ x, y, id, textId, text }) => (<>
|
|
8
|
+
// <B id={id} x={x} y={y} w={248} h={112} />
|
|
9
|
+
// <T id={textId} x={x + 72} y={y + 16} w={160}>{text}</T>
|
|
10
|
+
// </>);
|
|
11
|
+
// <Repeat1 x={0} y={0} id="c1" textId="t1" text={<><P>Plan</P></>} />
|
|
12
|
+
//
|
|
13
|
+
// Readers of slide SOURCE — the preservation reader, the bounded repair —
|
|
14
|
+
// address elements by literal ids and literal geometry. `inlineComponents`
|
|
15
|
+
// rewrites each call site of a COMPOSITE component (its template calls other
|
|
16
|
+
// components) into that template with the call site's literal props
|
|
17
|
+
// substituted, and blanks the declaration once no call site is left. Leaf
|
|
18
|
+
// helpers (`B`, `T`, …: only lowercase DOM inside) stay call sites: their
|
|
19
|
+
// call-site attributes ARE the element, which is how every reader reads them.
|
|
20
|
+
//
|
|
21
|
+
// `preserveLines` (default true): an inlined call site keeps its line count
|
|
22
|
+
// and a blanked declaration leaves empty lines, so a reported line still points
|
|
23
|
+
// into the file the author wrote. `preserveLines: false` keeps the template's
|
|
24
|
+
// own line breaks (one element per line, at the call site's indentation) — the
|
|
25
|
+
// shape the bounded repair rewrites line by line.
|
|
26
|
+
//
|
|
27
|
+
// Pure and import-free: byte-mirrored into the add-in (edit-contracts group).
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// A small JS/JSX scanner — enough to find where an expression, a parenthesised
|
|
31
|
+
// body or an element ends without being fooled by quotes in JSX text.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** End (exclusive) of the string, template literal or comment opening at `i`; `i` when none does. */
|
|
35
|
+
function skipLiteral(src, i) {
|
|
36
|
+
const c = src[i];
|
|
37
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
38
|
+
const nl = src.indexOf("\n", i);
|
|
39
|
+
return nl < 0 ? src.length : nl;
|
|
40
|
+
}
|
|
41
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
42
|
+
const close = src.indexOf("*/", i + 2);
|
|
43
|
+
return close < 0 ? src.length : close + 2;
|
|
44
|
+
}
|
|
45
|
+
if (c === '"' || c === "'") {
|
|
46
|
+
for (let k = i + 1; k < src.length; k++) {
|
|
47
|
+
if (src[k] === "\\") k++;
|
|
48
|
+
else if (src[k] === c) return k + 1;
|
|
49
|
+
else if (src[k] === "\n") return i + 1; // never closed: one character
|
|
50
|
+
}
|
|
51
|
+
return i + 1;
|
|
52
|
+
}
|
|
53
|
+
if (c === "`") {
|
|
54
|
+
for (let k = i + 1; k < src.length; k++) {
|
|
55
|
+
if (src[k] === "\\") k++;
|
|
56
|
+
else if (src[k] === "`") return k + 1;
|
|
57
|
+
else if (src[k] === "$" && src[k + 1] === "{") k = scanBalanced(src, k + 1, "{", "}") - 1;
|
|
58
|
+
}
|
|
59
|
+
return src.length;
|
|
60
|
+
}
|
|
61
|
+
return i;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Does a JSX element start at `i` (as opposed to a less-than)? */
|
|
65
|
+
function jsxStartsAt(src, i) {
|
|
66
|
+
if (src[i] !== "<" || !/[A-Za-z>]/.test(src[i + 1] ?? "")) return false;
|
|
67
|
+
let k = i - 1;
|
|
68
|
+
while (k >= 0 && /\s/.test(src[k])) k--;
|
|
69
|
+
if (k < 0 || "(,=:?&|{[>".includes(src[k])) return true;
|
|
70
|
+
return /\breturn$/.test(src.slice(Math.max(0, k - 6), k + 1));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** End (exclusive) of the bracketed run opening at `i` (`src[i] === open`). */
|
|
74
|
+
function scanBalanced(src, i, open, close) {
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (let k = i; k < src.length; k++) {
|
|
77
|
+
const end = skipLiteral(src, k);
|
|
78
|
+
if (end !== k) {
|
|
79
|
+
k = end - 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const c = src[k];
|
|
83
|
+
if (c === open) depth++;
|
|
84
|
+
else if (c === close) {
|
|
85
|
+
depth--;
|
|
86
|
+
if (depth === 0) return k + 1;
|
|
87
|
+
} else if (c === "<" && jsxStartsAt(src, k)) {
|
|
88
|
+
k = scanElement(src, k).end - 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return src.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const skipSpace = (src, k) => {
|
|
95
|
+
while (k < src.length && /\s/.test(src[k])) k++;
|
|
96
|
+
return k;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The element opening at `i`: name ("" for a fragment), attributes and
|
|
101
|
+
* children, each with its span in `src`.
|
|
102
|
+
*/
|
|
103
|
+
function scanElement(src, i) {
|
|
104
|
+
const el = { start: i, name: "", attrs: [], children: [], selfClose: false, end: src.length };
|
|
105
|
+
let k = i + 1;
|
|
106
|
+
el.name = /^[A-Za-z][\w.:-]*/.exec(src.slice(k, k + 128))?.[0] ?? "";
|
|
107
|
+
k += el.name.length;
|
|
108
|
+
for (;;) {
|
|
109
|
+
k = skipSpace(src, k);
|
|
110
|
+
if (k >= src.length) return el;
|
|
111
|
+
if (src[k] === "/" && src[k + 1] === ">") {
|
|
112
|
+
el.selfClose = true;
|
|
113
|
+
el.end = k + 2;
|
|
114
|
+
return el;
|
|
115
|
+
}
|
|
116
|
+
if (src[k] === ">") {
|
|
117
|
+
k++;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (src[k] === "{") {
|
|
121
|
+
const end = scanBalanced(src, k, "{", "}");
|
|
122
|
+
el.attrs.push({ name: null, kind: "spread", code: src.slice(k + 1, end - 1), start: k, end });
|
|
123
|
+
k = end;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const name = /^[A-Za-z_][\w:.-]*/.exec(src.slice(k, k + 128))?.[0];
|
|
127
|
+
if (!name) {
|
|
128
|
+
k++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const start = k;
|
|
132
|
+
k += name.length;
|
|
133
|
+
let j = skipSpace(src, k);
|
|
134
|
+
if (src[j] !== "=") {
|
|
135
|
+
el.attrs.push({ name, kind: "flag", start, end: k });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
j = skipSpace(src, j + 1);
|
|
139
|
+
if (src[j] === '"' || src[j] === "'") {
|
|
140
|
+
const close = src.indexOf(src[j], j + 1);
|
|
141
|
+
const end = close < 0 ? src.length : close + 1;
|
|
142
|
+
el.attrs.push({ name, kind: "string", value: src.slice(j + 1, end - 1), start, end });
|
|
143
|
+
k = end;
|
|
144
|
+
} else if (src[j] === "{") {
|
|
145
|
+
const end = scanBalanced(src, j, "{", "}");
|
|
146
|
+
el.attrs.push({ name, kind: "expr", code: src.slice(j + 1, end - 1), start, end });
|
|
147
|
+
k = end;
|
|
148
|
+
} else {
|
|
149
|
+
k = j;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
el.contentStart = k;
|
|
153
|
+
while (k < src.length) {
|
|
154
|
+
if (src[k] === "<" && src[k + 1] === "/") {
|
|
155
|
+
const close = src.indexOf(">", k);
|
|
156
|
+
el.contentEnd = k;
|
|
157
|
+
el.end = close < 0 ? src.length : close + 1;
|
|
158
|
+
return el;
|
|
159
|
+
}
|
|
160
|
+
if (src[k] === "<" && /[A-Za-z>]/.test(src[k + 1] ?? "")) {
|
|
161
|
+
const child = scanElement(src, k);
|
|
162
|
+
el.children.push({ kind: "el", el: child, start: k, end: child.end });
|
|
163
|
+
k = child.end;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (src[k] === "{") {
|
|
167
|
+
const end = scanBalanced(src, k, "{", "}");
|
|
168
|
+
el.children.push({ kind: "expr", code: src.slice(k + 1, end - 1), start: k, end });
|
|
169
|
+
k = end;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let j = k + 1;
|
|
173
|
+
while (j < src.length && src[j] !== "<" && src[j] !== "{") j++;
|
|
174
|
+
el.children.push({ kind: "text", raw: src.slice(k, j), start: k, end: j });
|
|
175
|
+
k = j;
|
|
176
|
+
}
|
|
177
|
+
el.contentEnd = src.length;
|
|
178
|
+
return el;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Declarations
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const LITERAL_RE = /^(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|-?\d+(?:\.\d+)?|true|false|null)$/;
|
|
186
|
+
|
|
187
|
+
/** A literal's value: `{ t: "str" | "num" | "bool" | "undef", v }`, else null. */
|
|
188
|
+
function literalValue(code) {
|
|
189
|
+
const s = String(code).trim();
|
|
190
|
+
if (!LITERAL_RE.test(s)) return null;
|
|
191
|
+
if (s === "true" || s === "false") return { t: "bool", v: s === "true" };
|
|
192
|
+
if (s === "null") return { t: "undef", v: undefined };
|
|
193
|
+
if (s[0] === '"' || s[0] === "'") {
|
|
194
|
+
try {
|
|
195
|
+
return { t: "str", v: JSON.parse(s[0] === "'" ? `"${s.slice(1, -1).replace(/\\'/g, "'").replace(/"/g, '\\"')}"` : s) };
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { t: "num", v: Number(s) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Top-level comma split (outside brackets, strings and JSX). */
|
|
204
|
+
function splitTopLevel(src) {
|
|
205
|
+
const out = [];
|
|
206
|
+
let depth = 0;
|
|
207
|
+
let from = 0;
|
|
208
|
+
for (let k = 0; k < src.length; k++) {
|
|
209
|
+
const end = skipLiteral(src, k);
|
|
210
|
+
if (end !== k) {
|
|
211
|
+
k = end - 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const c = src[k];
|
|
215
|
+
if ("([{".includes(c)) depth++;
|
|
216
|
+
else if (")]}".includes(c)) depth--;
|
|
217
|
+
else if (c === "," && depth === 0) {
|
|
218
|
+
out.push(src.slice(from, k));
|
|
219
|
+
from = k + 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
out.push(src.slice(from));
|
|
223
|
+
return out.map((s) => s.trim()).filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** `({ a, b = 1, c: d })` → [{ prop, local, fallback }], or null when not one destructured object. */
|
|
227
|
+
function readParams(paramsSrc) {
|
|
228
|
+
let s = paramsSrc.trim().replace(/^\(/, "").replace(/\)$/, "").trim();
|
|
229
|
+
s = s.replace(/=\s*\{\s*\}\s*$/, "").trim();
|
|
230
|
+
if (!s) return [];
|
|
231
|
+
if (s[0] !== "{" || scanBalanced(s, 0, "{", "}") !== s.length) return null;
|
|
232
|
+
const params = [];
|
|
233
|
+
for (const entry of splitTopLevel(s.slice(1, -1))) {
|
|
234
|
+
const m = /^([A-Za-z_$][\w$]*)(?:\s*:\s*([A-Za-z_$][\w$]*))?(?:\s*=\s*([\s\S]+))?$/.exec(entry);
|
|
235
|
+
if (!m) return null;
|
|
236
|
+
params.push({ prop: m[1], local: m[2] ?? m[1], fallback: m[3] === undefined ? null : literalValue(m[3]) });
|
|
237
|
+
}
|
|
238
|
+
return params;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The JSX a body returns: `(<…>)`, `<…>`, or `{ … return (<…>); }`; its element. */
|
|
242
|
+
function bodyElement(src, k) {
|
|
243
|
+
k = skipSpace(src, k);
|
|
244
|
+
if (src[k] === "{") {
|
|
245
|
+
const end = scanBalanced(src, k, "{", "}");
|
|
246
|
+
const ret = /\breturn\b\s*/g;
|
|
247
|
+
ret.lastIndex = k;
|
|
248
|
+
const m = ret.exec(src);
|
|
249
|
+
if (!m || m.index > end) return null;
|
|
250
|
+
const el = bodyElement(src, m.index + m[0].length);
|
|
251
|
+
return el && { ...el, end };
|
|
252
|
+
}
|
|
253
|
+
if (src[k] === "(") {
|
|
254
|
+
const end = scanBalanced(src, k, "(", ")");
|
|
255
|
+
const inner = skipSpace(src, k + 1);
|
|
256
|
+
if (src[inner] !== "<") return null;
|
|
257
|
+
return { element: scanElement(src, inner), end };
|
|
258
|
+
}
|
|
259
|
+
if (src[k] === "<") {
|
|
260
|
+
const element = scanElement(src, k);
|
|
261
|
+
return { element, end: element.end };
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Local PascalCase components: name → { params, element, start, end } (declaration span). */
|
|
267
|
+
function readComponents(src) {
|
|
268
|
+
const out = new Map();
|
|
269
|
+
const arrow = /\b(?:const|let)\s+([A-Z][\w$]*)\s*=\s*(?=\()/g;
|
|
270
|
+
for (const m of src.matchAll(arrow)) {
|
|
271
|
+
const open = m.index + m[0].length;
|
|
272
|
+
const paramsEnd = scanBalanced(src, open, "(", ")");
|
|
273
|
+
const k = skipSpace(src, paramsEnd);
|
|
274
|
+
if (src.slice(k, k + 2) !== "=>") continue;
|
|
275
|
+
const body = bodyElement(src, k + 2);
|
|
276
|
+
if (!body) continue;
|
|
277
|
+
let end = skipSpace(src, body.end);
|
|
278
|
+
end = src[end] === ";" ? end + 1 : body.end;
|
|
279
|
+
out.set(m[1], { params: readParams(src.slice(open, paramsEnd)), element: body.element, start: m.index, end });
|
|
280
|
+
}
|
|
281
|
+
const fn = /\bfunction\s+([A-Z][\w$]*)\s*(?=\()/g;
|
|
282
|
+
for (const m of src.matchAll(fn)) {
|
|
283
|
+
const open = m.index + m[0].length;
|
|
284
|
+
const paramsEnd = scanBalanced(src, open, "(", ")");
|
|
285
|
+
const body = bodyElement(src, paramsEnd);
|
|
286
|
+
if (!body) continue;
|
|
287
|
+
out.set(m[1], { params: readParams(src.slice(open, paramsEnd)), element: body.element, start: m.index, end: body.end });
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function walkElements(el, visit) {
|
|
293
|
+
visit(el);
|
|
294
|
+
for (const child of el.children) if (child.kind === "el") walkElements(child.el, visit);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const isPascal = (name) => /^[A-Z]/.test(name);
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Substitution
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/** Evaluate `a + "-t"`, `x + 16`, `y - 4`, a literal or a bare prop; null when it is anything else. */
|
|
304
|
+
function evaluate(code, env) {
|
|
305
|
+
const tokens = [];
|
|
306
|
+
const re = /\s*(?:("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(-?\d+(?:\.\d+)?)|([A-Za-z_$][\w$]*)|([+-]))/y;
|
|
307
|
+
const s = String(code).trim();
|
|
308
|
+
re.lastIndex = 0;
|
|
309
|
+
while (re.lastIndex < s.length) {
|
|
310
|
+
const at = re.lastIndex;
|
|
311
|
+
const m = re.exec(s);
|
|
312
|
+
if (!m || m.index !== at) return null;
|
|
313
|
+
if (m[1]) tokens.push(literalValue(m[1]));
|
|
314
|
+
else if (m[2]) tokens.push({ t: "num", v: Number(m[2]) });
|
|
315
|
+
else if (m[3]) {
|
|
316
|
+
if (m[3] === "true" || m[3] === "false") tokens.push({ t: "bool", v: m[3] === "true" });
|
|
317
|
+
else if (!env.has(m[3])) return null;
|
|
318
|
+
else tokens.push(env.get(m[3]));
|
|
319
|
+
} else tokens.push({ op: m[4] });
|
|
320
|
+
}
|
|
321
|
+
if (!tokens.length || tokens[0]?.op) return null;
|
|
322
|
+
let acc = tokens[0];
|
|
323
|
+
for (let k = 1; k < tokens.length; k += 2) {
|
|
324
|
+
const op = tokens[k]?.op;
|
|
325
|
+
const rhs = tokens[k + 1];
|
|
326
|
+
if (!op || !rhs || rhs.op) return null;
|
|
327
|
+
if (["jsx", "code", "undef"].includes(acc.t) || ["jsx", "code", "undef"].includes(rhs.t)) return null;
|
|
328
|
+
if (op === "+" && (acc.t === "str" || rhs.t === "str")) acc = { t: "str", v: `${acc.v}${rhs.v}` };
|
|
329
|
+
else if (acc.t === "num" && rhs.t === "num") acc = { t: "num", v: op === "+" ? acc.v + rhs.v : acc.v - rhs.v };
|
|
330
|
+
else return null;
|
|
331
|
+
}
|
|
332
|
+
return acc;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Replace prop names inside an expression we cannot evaluate (not keys, not members, not in strings). */
|
|
336
|
+
function substituteText(code, env) {
|
|
337
|
+
let out = "";
|
|
338
|
+
for (let k = 0; k < code.length; ) {
|
|
339
|
+
const end = skipLiteral(code, k);
|
|
340
|
+
if (end !== k) {
|
|
341
|
+
out += code.slice(k, end);
|
|
342
|
+
k = end;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(code.slice(k));
|
|
346
|
+
if (!m) {
|
|
347
|
+
out += code[k++];
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const word = m[0];
|
|
351
|
+
const before = code.slice(0, k).trimEnd().slice(-1);
|
|
352
|
+
const after = code.slice(k + word.length).trimStart()[0];
|
|
353
|
+
const value = env.get(word);
|
|
354
|
+
if (value && before !== "." && after !== ":") {
|
|
355
|
+
out += value.t === "str" ? JSON.stringify(value.v) : value.t === "jsx" || value.t === "code" ? `(${value.code})` : String(value.v);
|
|
356
|
+
} else out += word;
|
|
357
|
+
k += word.length;
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Compute every `220 + 182.5 - 4` a substitution left as a whole value (a
|
|
364
|
+
* figure's `{x: x + 182.5}` once `x` is known), left to right as JS does — so
|
|
365
|
+
* readers get the literal the transcriber writes. A chain next to `*`, `/` or
|
|
366
|
+
* a call is left alone.
|
|
367
|
+
*/
|
|
368
|
+
function foldNumbers(code) {
|
|
369
|
+
const num = String.raw`-?\d+(?:\.\d+)?(?:e[+-]?\d+)?`;
|
|
370
|
+
const chain = new RegExp(String.raw`(^|[:,[({]\s*)(${num}(?:\s*[+-]\s*${num})+)(?=\s*(?:[,\]})]|$))`, "g");
|
|
371
|
+
// Strings and comments masked, so no number inside one is touched.
|
|
372
|
+
let masked = "";
|
|
373
|
+
for (let k = 0; k < code.length; ) {
|
|
374
|
+
const end = skipLiteral(code, k);
|
|
375
|
+
if (end !== k) {
|
|
376
|
+
masked += "_".repeat(end - k);
|
|
377
|
+
k = end;
|
|
378
|
+
} else masked += code[k++];
|
|
379
|
+
}
|
|
380
|
+
let out = "";
|
|
381
|
+
let last = 0;
|
|
382
|
+
for (const m of masked.matchAll(chain)) {
|
|
383
|
+
const at = m.index + m[1].length;
|
|
384
|
+
const terms = [...m[2].matchAll(new RegExp(String.raw`([+-]?)\s*(${num})`, "g"))];
|
|
385
|
+
let acc = Number(`${terms[0][1]}${terms[0][2]}`);
|
|
386
|
+
for (const t of terms.slice(1)) acc = t[1] === "-" ? acc - Number(t[2]) : acc + Number(t[2]);
|
|
387
|
+
out += code.slice(last, at) + String(acc);
|
|
388
|
+
last = at + m[2].length;
|
|
389
|
+
}
|
|
390
|
+
return out + code.slice(last);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const fragmentInner = (code) => {
|
|
394
|
+
const s = code.trim();
|
|
395
|
+
return s.startsWith("<>") && s.endsWith("</>") ? s.slice(2, -3) : s;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
function serializeAttr(name, value) {
|
|
399
|
+
if (value.t === "undef" || (value.t === "bool" && !value.v)) return null;
|
|
400
|
+
if (value.t === "bool") return name;
|
|
401
|
+
if (value.t === "str") return /["\n]/.test(value.v) ? `${name}={${JSON.stringify(value.v)}}` : `${name}="${value.v}"`;
|
|
402
|
+
if (value.t === "num") return `${name}={${value.v}}`;
|
|
403
|
+
return `${name}={${value.code}}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The template element rendered with `env` substituted. */
|
|
407
|
+
function instantiate(src, el, env) {
|
|
408
|
+
const head = [el.name];
|
|
409
|
+
for (const attr of el.attrs) {
|
|
410
|
+
if (attr.kind !== "expr") {
|
|
411
|
+
head.push(src.slice(attr.start, attr.end));
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const value = evaluate(attr.code, env);
|
|
415
|
+
if (value) {
|
|
416
|
+
const text = serializeAttr(attr.name, value);
|
|
417
|
+
if (text) head.push(text);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
head.push(`${attr.name}={${foldNumbers(substituteText(attr.code, env))}}`);
|
|
421
|
+
}
|
|
422
|
+
const open = `<${head.join(" ")}`;
|
|
423
|
+
if (el.selfClose) return `${open} />`;
|
|
424
|
+
let body = "";
|
|
425
|
+
for (const child of el.children) {
|
|
426
|
+
if (child.kind === "text") body += child.raw;
|
|
427
|
+
else if (child.kind === "el") body += instantiate(src, child.el, env);
|
|
428
|
+
else body += instantiateExpr(child.code, env);
|
|
429
|
+
}
|
|
430
|
+
return `${open}>${body}</${el.name}>`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** A child expression: a slot (`{text}`), a guarded part (`{discId && <B …/>}`), or text. */
|
|
434
|
+
function instantiateExpr(code, env) {
|
|
435
|
+
const bare = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(code)?.[1];
|
|
436
|
+
if (bare && env.has(bare)) {
|
|
437
|
+
const value = env.get(bare);
|
|
438
|
+
if (value.t === "jsx") return fragmentInner(value.code);
|
|
439
|
+
if (value.t === "undef" || value.t === "bool") return "";
|
|
440
|
+
if (value.t === "code") return `{${value.code}}`;
|
|
441
|
+
return value.t === "str" && !/[<>{}&]/.test(value.v) ? value.v : `{${JSON.stringify(value.v)}}`;
|
|
442
|
+
}
|
|
443
|
+
const guard = /^\s*([A-Za-z_$][\w$]*)\s*&&\s*\(?\s*(<[\s\S]*?>)\s*\)?\s*$/.exec(code);
|
|
444
|
+
if (guard && env.has(guard[1])) {
|
|
445
|
+
const value = env.get(guard[1]);
|
|
446
|
+
if (value.t === "undef" || (value.t === "bool" && !value.v) || (value.t === "str" && !value.v)) return "";
|
|
447
|
+
const at = code.indexOf("<", guard.index + guard[1].length);
|
|
448
|
+
return instantiate(code, scanElement(code, at), env);
|
|
449
|
+
}
|
|
450
|
+
return `{${substituteText(code, env)}}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* An object or array literal that names nothing outside itself
|
|
455
|
+
* (`{ borderWidth: "var(--bw-hairline)" }`): every word is a key or a literal.
|
|
456
|
+
*/
|
|
457
|
+
function selfContained(code) {
|
|
458
|
+
const s = code.trim();
|
|
459
|
+
if (!/^[{[]/.test(s)) return false;
|
|
460
|
+
let out = "";
|
|
461
|
+
for (let k = 0; k < s.length; ) {
|
|
462
|
+
const end = skipLiteral(s, k);
|
|
463
|
+
if (end !== k) {
|
|
464
|
+
out += s[k] === "`" && s.slice(k, end).includes("${") ? "@" : '""';
|
|
465
|
+
k = end;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
out += s[k++];
|
|
469
|
+
}
|
|
470
|
+
if (/@|=>|\.\.\./.test(out)) return false;
|
|
471
|
+
for (const m of out.matchAll(/[A-Za-z_$][\w$]*/g)) {
|
|
472
|
+
if (/^(?:true|false|null|undefined)$/.test(m[0])) continue;
|
|
473
|
+
if (/^\s*:/.test(out.slice(m.index + m[0].length))) continue;
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** The call site's props, or a reason (a spread) when they cannot be read. */
|
|
480
|
+
function callProps(call, params) {
|
|
481
|
+
const env = new Map();
|
|
482
|
+
for (const attr of call.attrs) {
|
|
483
|
+
if (attr.kind === "spread") return { reason: "spread_props" };
|
|
484
|
+
if (attr.name === "key") continue;
|
|
485
|
+
if (attr.kind === "flag") env.set(attr.name, { t: "bool", v: true });
|
|
486
|
+
else if (attr.kind === "string") env.set(attr.name, { t: "str", v: attr.value });
|
|
487
|
+
else {
|
|
488
|
+
const code = attr.code.trim();
|
|
489
|
+
const literal = literalValue(code);
|
|
490
|
+
if (literal) env.set(attr.name, literal);
|
|
491
|
+
else if (code.startsWith("<")) env.set(attr.name, { t: "jsx", code });
|
|
492
|
+
// Any other expression travels as written: it is evaluated where the
|
|
493
|
+
// call site was, so the instance renders the same. Ids are literal by
|
|
494
|
+
// contract (lint-components.ts), so the readers still find every part.
|
|
495
|
+
else env.set(attr.name, { t: "code", code });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Locals: the template reads props by their local names, defaults included.
|
|
499
|
+
const locals = new Map();
|
|
500
|
+
for (const p of params) {
|
|
501
|
+
const value = env.get(p.prop) ?? p.fallback ?? { t: "undef", v: undefined };
|
|
502
|
+
locals.set(p.local, value);
|
|
503
|
+
}
|
|
504
|
+
return { env: locals };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const lineCount = (s) => s.split("\n").length - 1;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Is the call site at `at` an expression (`=> <X/>`, `{<X/>}`, `(<X/>)`) rather
|
|
511
|
+
* than a JSX child? An expression holds ONE element, so a fragment template
|
|
512
|
+
* keeps its `<>…</>` there; as a child, its parts are spliced in directly.
|
|
513
|
+
*/
|
|
514
|
+
function inExpression(src, at) {
|
|
515
|
+
let k = at - 1;
|
|
516
|
+
while (k >= 0 && /\s/.test(src[k])) k--;
|
|
517
|
+
if (src[k] === ">") return src[k - 1] === "=";
|
|
518
|
+
return src[k] !== "}";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The declaration's span widened to its whole lines when nothing else shares
|
|
523
|
+
* them — with the `//` comment lines right above it, which describe it.
|
|
524
|
+
*/
|
|
525
|
+
function wholeLines(src, start, end) {
|
|
526
|
+
let lineStart = src.lastIndexOf("\n", start - 1) + 1;
|
|
527
|
+
const lineEnd = src.indexOf("\n", end);
|
|
528
|
+
const tail = lineEnd < 0 ? src.length : lineEnd;
|
|
529
|
+
if (src.slice(lineStart, start).trim() || src.slice(end, tail).trim()) return { start, end };
|
|
530
|
+
while (lineStart > 0) {
|
|
531
|
+
const prev = src.lastIndexOf("\n", lineStart - 2) + 1;
|
|
532
|
+
if (!/^\s*\/\//.test(src.slice(prev, lineStart - 1))) break;
|
|
533
|
+
lineStart = prev;
|
|
534
|
+
}
|
|
535
|
+
return { start: lineStart, end: lineEnd < 0 ? src.length : lineEnd + 1 };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Inline every call site of a composite local component.
|
|
540
|
+
* @param {string} src
|
|
541
|
+
* @param {{ preserveLines?: boolean }} [options]
|
|
542
|
+
* @returns {{ src: string, unreadable: Array<{ component: string, line: number, reason: string }>, inlined: number }}
|
|
543
|
+
*/
|
|
544
|
+
export function inlineComponents(src, { preserveLines = true } = {}) {
|
|
545
|
+
let out = String(src ?? "");
|
|
546
|
+
const unreadable = [];
|
|
547
|
+
let inlined = 0;
|
|
548
|
+
// A composite inside a composite unrolls one level per pass.
|
|
549
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
550
|
+
const composites = new Map();
|
|
551
|
+
// The slide itself (the default export) is where call sites live, never a template.
|
|
552
|
+
const slideName = /\bexport\s+default\s+(?:function\s+)?([A-Z][\w$]*)/.exec(out)?.[1];
|
|
553
|
+
for (const [name, c] of readComponents(out)) {
|
|
554
|
+
if (name === slideName) continue;
|
|
555
|
+
let calls = false;
|
|
556
|
+
walkElements(c.element, (e) => {
|
|
557
|
+
if (isPascal(e.name) && e.name !== name) calls = true;
|
|
558
|
+
});
|
|
559
|
+
if (calls) composites.set(name, c);
|
|
560
|
+
}
|
|
561
|
+
if (!composites.size) break;
|
|
562
|
+
const insideTemplate = (at) => [...composites.values()].some((c) => at >= c.start && at < c.end);
|
|
563
|
+
// A composite another template still calls keeps its declaration this pass.
|
|
564
|
+
const nested = new Set();
|
|
565
|
+
for (const c of composites.values()) {
|
|
566
|
+
walkElements(c.element, (e) => {
|
|
567
|
+
if (composites.has(e.name)) nested.add(e.name);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const edits = [];
|
|
571
|
+
const called = new Set();
|
|
572
|
+
const kept = new Set();
|
|
573
|
+
const tag = new RegExp(`<(${[...composites.keys()].join("|")})(?=[\\s/>])`, "g");
|
|
574
|
+
for (const m of out.matchAll(tag)) {
|
|
575
|
+
if (insideTemplate(m.index)) continue;
|
|
576
|
+
const c = composites.get(m[1]);
|
|
577
|
+
const call = scanElement(out, m.index);
|
|
578
|
+
const line = lineCount(out.slice(0, m.index)) + 1;
|
|
579
|
+
called.add(m[1]);
|
|
580
|
+
const props = c.params ? callProps(call, c.params) : { reason: "unsupported_params" };
|
|
581
|
+
if (!props.env) {
|
|
582
|
+
if (!unreadable.some((u) => u.component === m[1] && u.line === line)) {
|
|
583
|
+
unreadable.push({ component: m[1], line, reason: props.reason });
|
|
584
|
+
}
|
|
585
|
+
kept.add(m[1]);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
let text = instantiate(out, c.element, props.env);
|
|
589
|
+
if (!c.element.name && !inExpression(out, m.index)) text = text.replace(/^<>/, "").replace(/<\/>$/, "");
|
|
590
|
+
if (preserveLines) {
|
|
591
|
+
text = text.replace(/\s*\n\s*/g, " ").trim() + "\n".repeat(lineCount(out.slice(call.start, call.end)));
|
|
592
|
+
} else {
|
|
593
|
+
const indent = /^[ \t]*/.exec(out.slice(out.lastIndexOf("\n", m.index - 1) + 1))[0];
|
|
594
|
+
text = text
|
|
595
|
+
.split("\n")
|
|
596
|
+
.map((l) => l.trim())
|
|
597
|
+
.filter(Boolean)
|
|
598
|
+
.join(`\n${indent}`);
|
|
599
|
+
}
|
|
600
|
+
edits.push({ start: call.start, end: call.end, text });
|
|
601
|
+
inlined++;
|
|
602
|
+
}
|
|
603
|
+
for (const [name, c] of composites) {
|
|
604
|
+
if (!called.has(name) || kept.has(name) || nested.has(name)) continue;
|
|
605
|
+
const span = preserveLines ? c : wholeLines(out, c.start, c.end);
|
|
606
|
+
edits.push({ start: span.start, end: span.end, text: preserveLines ? "\n".repeat(lineCount(out.slice(c.start, c.end))) : "" });
|
|
607
|
+
}
|
|
608
|
+
if (!edits.length) break;
|
|
609
|
+
edits.sort((a, b) => b.start - a.start);
|
|
610
|
+
for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
611
|
+
}
|
|
612
|
+
return { src: out, unreadable, inlined };
|
|
613
|
+
}
|