slidemind 0.3.20 → 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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- 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,639 @@
|
|
|
1
|
+
// A declared set, written as one component. The reader names the organisms a
|
|
2
|
+
// concept repeats (`spec.repeats`, concept-repeats.ts) and repeat-groups.mjs
|
|
3
|
+
// builds each from one template — one size, one inner layout. The transcriber
|
|
4
|
+
// still writes every member out in full; this pass folds a BUILT set into
|
|
5
|
+
//
|
|
6
|
+
// const Repeat1 = ({ x, y, id, textId, text }) => (<>
|
|
7
|
+
// <B id={id} x={x} y={y} w={248} h={112} fillVar="var(--bg-2)" shape="rounded" />
|
|
8
|
+
// <T id={textId} x={x + 72} y={y + 16} w={160} role="body">{text}</T>
|
|
9
|
+
// </>);
|
|
10
|
+
// <Repeat1 x={0} y={0} id="c1" textId="t1" text={<>Plan</>} />
|
|
11
|
+
//
|
|
12
|
+
// so the design agent changes the set's look once and a member's words at its
|
|
13
|
+
// call site (workflow/authoring.md → Repeated organisms are components).
|
|
14
|
+
//
|
|
15
|
+
// Exact or not at all. A set folds only when
|
|
16
|
+
// - every member draws the same parts (same helpers, same order),
|
|
17
|
+
// - each part sits at one offset from its member's origin in every member,
|
|
18
|
+
// or at an offset its call site passes (a taller text lowers its stack;
|
|
19
|
+
// parts that move together share one prop),
|
|
20
|
+
// - a drawn figure is the first member's, moved to the member's place
|
|
21
|
+
// (or, drawn differently, its member's own at the call site),
|
|
22
|
+
// - instantiating the template gives back each member's own lines, and
|
|
23
|
+
// - no element painted between a member's first and last part overlaps one
|
|
24
|
+
// of its parts (an instance paints its parts together, so that element
|
|
25
|
+
// would change sides).
|
|
26
|
+
// Anything else stays as drawn, with the set's comment. Lines stay outside the
|
|
27
|
+
// component as static directives; ids stay the spec's, so settle, connectors,
|
|
28
|
+
// assets and the preservation reader (src/lib/inline-components.mjs) keep
|
|
29
|
+
// addressing every part.
|
|
30
|
+
//
|
|
31
|
+
// Pure. `SLIDEMIND_ABLATE=foldSets` turns it off for A/B runs.
|
|
32
|
+
import { correspond, repeatGroups } from "./repeat-groups.mjs";
|
|
33
|
+
|
|
34
|
+
const ablated = () => (process.env.SLIDEMIND_ABLATE ?? "").split(",").includes("foldSets");
|
|
35
|
+
const INDENT = " ";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// One emitted line: `<Tag a="v" b={…} flag>children</Tag>` or `<Tag … />`.
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/** End (exclusive) of the braced value opening at `i`. */
|
|
42
|
+
function braceEnd(s, i) {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (let k = i; k < s.length; k++) {
|
|
45
|
+
const c = s[k];
|
|
46
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
47
|
+
for (k++; k < s.length && s[k] !== c; k++) if (s[k] === "\\") k++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (c === "{") depth++;
|
|
51
|
+
else if (c === "}" && --depth === 0) return k + 1;
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A bare line directive (not a group of them, whose children sit in its frame). */
|
|
57
|
+
const BARE_LINE = /^\s*<div (?:data-connector|data-sm-line)\b(?!-group)/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `{ tag, attrs: [{ name, raw }], children, selfClose }`, or null for anything
|
|
61
|
+
* else. A bare line directive is no part: its ends are static by contract
|
|
62
|
+
* (lint-connectors.ts), so it stays outside the component.
|
|
63
|
+
*/
|
|
64
|
+
export function tokenizeLine(line) {
|
|
65
|
+
const s = line.trim();
|
|
66
|
+
const head = /^<([A-Za-z][\w]*)(?=[\s/>])/.exec(s);
|
|
67
|
+
if (!head || BARE_LINE.test(s)) return null;
|
|
68
|
+
const tag = head[1];
|
|
69
|
+
const attrs = [];
|
|
70
|
+
let k = head[0].length;
|
|
71
|
+
for (;;) {
|
|
72
|
+
while (s[k] === " ") k++;
|
|
73
|
+
if (s.startsWith("/>", k)) {
|
|
74
|
+
return k + 2 === s.length ? { tag, attrs, children: null, selfClose: true } : null;
|
|
75
|
+
}
|
|
76
|
+
if (s[k] === ">") {
|
|
77
|
+
k++;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const name = /^[A-Za-z_][\w-]*/.exec(s.slice(k))?.[0];
|
|
81
|
+
if (!name) return null;
|
|
82
|
+
k += name.length;
|
|
83
|
+
if (s[k] !== "=") {
|
|
84
|
+
attrs.push({ name, raw: null });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
k++;
|
|
88
|
+
let end;
|
|
89
|
+
if (s[k] === '"') end = s.indexOf('"', k + 1) + 1;
|
|
90
|
+
else if (s[k] === "{") end = braceEnd(s, k);
|
|
91
|
+
else return null;
|
|
92
|
+
if (end <= k) return null;
|
|
93
|
+
attrs.push({ name, raw: s.slice(k, end) });
|
|
94
|
+
k = end;
|
|
95
|
+
}
|
|
96
|
+
const close = `</${tag}>`;
|
|
97
|
+
if (!s.endsWith(close)) return null;
|
|
98
|
+
return { tag, attrs, children: s.slice(k, s.length - close.length), selfClose: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderLine(tag, attrs, children, selfClose) {
|
|
102
|
+
const head = [tag, ...attrs.map(({ name, raw }) => (raw === null ? name : `${name}=${raw}`))].join(" ");
|
|
103
|
+
return selfClose ? `<${head} />` : `<${head}>${children}</${tag}>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `<X …></X>` and `<X … />` draw the same element. */
|
|
107
|
+
const sameElement = (a, b) => a.replace(/><\/[A-Za-z]\w*>$/, " />") === b.replace(/><\/[A-Za-z]\w*>$/, " />");
|
|
108
|
+
|
|
109
|
+
const numberOf = (raw) => (raw && /^\{-?\d+(?:\.\d+)?(?:e[+-]?\d+)?\}$/.test(raw) ? Number(raw.slice(1, -1)) : null);
|
|
110
|
+
const offsetExpr = (axis, d) => (d === 0 ? axis : d > 0 ? `${axis} + ${d}` : `${axis} - ${-d}`);
|
|
111
|
+
const camel = (s) => String(s).replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")).replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
112
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Every member's attribute names in one order that keeps each member's own:
|
|
116
|
+
* a name only some members carry (`nowrap` on one text) goes right after the
|
|
117
|
+
* name it follows there, so the template gives that member back as written.
|
|
118
|
+
*/
|
|
119
|
+
function attrOrder(lists) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const attrs of lists) {
|
|
122
|
+
attrs.forEach((a, i) => {
|
|
123
|
+
if (!out.includes(a.name)) out.splice(i ? out.indexOf(attrs[i - 1].name) + 1 : 0, 0, a.name);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A difference without float noise (`75.7`, not `75.69999999999999`). */
|
|
130
|
+
const tidy = (n) => Number(n.toFixed(4));
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// A drawn figure's geometry: `<Figure circles={…} outline={…} mirror={…}
|
|
134
|
+
// holes={…}>`, object literals as figure-sketch.mjs figureJsxAttrs writes them.
|
|
135
|
+
// In a template every coordinate is written from the member's x/y
|
|
136
|
+
// (`{x: x + 182.5,y: y + 35}`); the inliner computes it back to the literal.
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
const GEOMETRY = new Set(["circles", "outline", "mirror", "holes"]);
|
|
140
|
+
const lit = (v) => JSON.stringify(v).replace(/"([A-Za-z_][A-Za-z0-9_]*)":/g, "$1: ");
|
|
141
|
+
|
|
142
|
+
/** An attribute's object literal, or undefined when it is not one. */
|
|
143
|
+
function literalOf(raw) {
|
|
144
|
+
if (!raw?.startsWith("{") || !raw.endsWith("}")) return undefined;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(raw.slice(1, -1).replace(/([{,[]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:/g, '$1"$2":'));
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every leaf of a geometry attribute in order: `{ path, value, axis }` (axis null: a radius, an angle, a flag). */
|
|
153
|
+
function geometryLeaves(attr, root) {
|
|
154
|
+
const out = [];
|
|
155
|
+
const axisOf = (path) => {
|
|
156
|
+
const last = path[path.length - 1];
|
|
157
|
+
if (attr === "outline" || attr === "holes") return last === "x" || last === "y" ? last : null;
|
|
158
|
+
if (attr === "circles") return last === 0 ? "x" : last === 1 ? "y" : null;
|
|
159
|
+
if (attr === "mirror" && path[0] === "through") return last === 0 ? "x" : last === 1 ? "y" : null;
|
|
160
|
+
if (attr === "mirror" && path[0] === "at") return root.axis === "vertical" ? "x" : root.axis === "horizontal" ? "y" : null;
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
const walk = (v, path) => {
|
|
164
|
+
if (v && typeof v === "object") for (const [key, child] of Object.entries(v)) walk(child, [...path, Array.isArray(v) ? Number(key) : key]);
|
|
165
|
+
else out.push({ path, value: v, axis: typeof v === "number" ? axisOf(path) : null });
|
|
166
|
+
};
|
|
167
|
+
walk(root, []);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A figure part's place: its outline's first point. */
|
|
172
|
+
function figurePlace(t) {
|
|
173
|
+
const p = literalOf(t.attrs.find((a) => a.name === "outline")?.raw)?.[0];
|
|
174
|
+
return { x: Number.isFinite(p?.x) ? p.x : null, y: Number.isFinite(p?.y) ? p.y : null };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Geometry for the paint-order guard
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function rectIndex(spec, measured) {
|
|
182
|
+
const out = new Map();
|
|
183
|
+
for (const e of spec.elements ?? []) out.set(e.id, { x: e.x, y: e.y, w: e.w, h: e.h });
|
|
184
|
+
for (const c of spec.connectors ?? []) {
|
|
185
|
+
const pts = (c.points ?? c.path?.points ?? []).map((p) => (Array.isArray(p) ? { x: p[0], y: p[1] } : p)).filter((p) => Number.isFinite(p?.x));
|
|
186
|
+
if (c.id && pts.length) {
|
|
187
|
+
const x = Math.min(...pts.map((p) => p.x));
|
|
188
|
+
const y = Math.min(...pts.map((p) => p.y));
|
|
189
|
+
out.set(c.id, { x, y, w: Math.max(...pts.map((p) => p.x)) - x, h: Math.max(...pts.map((p) => p.y)) - y });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const [id, m] of measured ?? []) if (Number.isFinite(m?.x)) out.set(id, { x: m.x, y: m.y, w: m.w, h: m.h });
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const overlaps = (a, b, pad) =>
|
|
197
|
+
a.x - pad < b.x + b.w && b.x - pad < a.x + a.w && a.y - pad < b.y + b.h && b.y - pad < a.y + a.h;
|
|
198
|
+
|
|
199
|
+
/** The point ends of a line directive's own attributes, as a box. */
|
|
200
|
+
function directiveBox(line) {
|
|
201
|
+
const pts = [...line.matchAll(/data-(?:from|to)="(-?[\d.]+),(-?[\d.]+)"/g)].map((m) => ({ x: Number(m[1]), y: Number(m[2]) }));
|
|
202
|
+
if (pts.length < 2) return null;
|
|
203
|
+
const x = Math.min(...pts.map((p) => p.x));
|
|
204
|
+
const y = Math.min(...pts.map((p) => p.y));
|
|
205
|
+
return { x, y, w: Math.max(...pts.map((p) => p.x)) - x, h: Math.max(...pts.map((p) => p.y)) - y };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// One set, from a plan: `{ id, comment, members: [{ lines, ids }], prefixes,
|
|
210
|
+
// names? }` — per member the line of each template part and the id it owns
|
|
211
|
+
// (part 0 first), per part the prefix its props are named by, and (a refold)
|
|
212
|
+
// the prop names the set already had.
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/** A part's box: measured, else the spec's, else its own literal x/y/w/h. */
|
|
216
|
+
function boxOf(id, line, rects) {
|
|
217
|
+
const known = rects.get(id);
|
|
218
|
+
if (known) return known;
|
|
219
|
+
const t = tokenizeLine(line);
|
|
220
|
+
const n = (name) => numberOf(t?.attrs.find((a) => a.name === name)?.raw);
|
|
221
|
+
const [x, y, w, h] = ["x", "y", "w", "h"].map(n);
|
|
222
|
+
return [x, y, w, h].every((v) => v !== null) ? { x, y, w, h } : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The plan with its parts in paint order — the template paints them in one run,
|
|
227
|
+
* so it must paint them in the order the slide did — or a reason when members
|
|
228
|
+
* disagree about that order.
|
|
229
|
+
*/
|
|
230
|
+
function inPaintOrder(plan) {
|
|
231
|
+
const first = plan.members[0].lines;
|
|
232
|
+
const order = first.map((_, j) => j).sort((a, b) => first[a] - first[b]);
|
|
233
|
+
for (const m of plan.members) {
|
|
234
|
+
const lines = order.map((j) => m.lines[j]);
|
|
235
|
+
if (lines.some((v, i) => i > 0 && v < lines[i - 1])) return { why: "its members paint their parts in different orders" };
|
|
236
|
+
}
|
|
237
|
+
const moved = new Map(order.map((j, k) => [j, k]));
|
|
238
|
+
const names = plan.names && new Map([...plan.names].map(([key, v]) => [key.replace(/^\d+/, (j) => String(moved.get(Number(j)))), v]));
|
|
239
|
+
return {
|
|
240
|
+
...plan,
|
|
241
|
+
names,
|
|
242
|
+
prefixes: order.map((j) => plan.prefixes[j]),
|
|
243
|
+
members: plan.members.map((m) => ({ ...m, lines: order.map((j) => m.lines[j]), ids: order.map((j) => m.ids[j]) })),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function foldPlan(lines, given, n, rects) {
|
|
248
|
+
if (!given.members.length || given.members.some((m) => m.lines.length !== given.members[0].lines.length)) return { why: "its members draw different parts" };
|
|
249
|
+
const plan = inPaintOrder(given);
|
|
250
|
+
if (plan.why) return plan;
|
|
251
|
+
const members = plan.members;
|
|
252
|
+
const count = members[0].lines.length;
|
|
253
|
+
if (!count) return { why: "its members draw different parts" };
|
|
254
|
+
const tokens = members.map((m) => m.lines.map((i) => tokenizeLine(lines[i])));
|
|
255
|
+
if (tokens.some((list) => list.some((t) => !t))) return { why: "a part is not a helper line" };
|
|
256
|
+
for (let j = 0; j < count; j++) {
|
|
257
|
+
if (new Set(tokens.map((list) => list[j].tag)).size > 1) return { why: "its members draw different parts" };
|
|
258
|
+
}
|
|
259
|
+
// Where each part is drawn: its x/y, a figure its outline's first point.
|
|
260
|
+
const places = tokens.map((list) =>
|
|
261
|
+
list.map((t) => {
|
|
262
|
+
if (t.tag === "Figure") return figurePlace(t);
|
|
263
|
+
const at = (name) => (t.attrs.some((a) => a.name === name) ? numberOf(t.attrs.find((a) => a.name === name).raw) ?? NaN : null);
|
|
264
|
+
return { x: at("x"), y: at("y") };
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
// A member's origin is where its first part is drawn.
|
|
268
|
+
const origins = places.map((list) => list[0]);
|
|
269
|
+
if (origins.some((o) => !Number.isFinite(o.x) || !Number.isFinite(o.y))) return { why: "its first part has no literal place" };
|
|
270
|
+
|
|
271
|
+
const usedNames = new Set(["x", "y"]);
|
|
272
|
+
const unique = (name) => {
|
|
273
|
+
let out = name;
|
|
274
|
+
for (let k = 2; usedNames.has(out); k++) out = `${name}${k}`;
|
|
275
|
+
usedNames.add(out);
|
|
276
|
+
return out;
|
|
277
|
+
};
|
|
278
|
+
const named = (j, attr, fallback) => unique(plan.names?.get(`${j}:${attr}`) ?? fallback);
|
|
279
|
+
const props = []; // [{ name, part, attr | "children" | offset axis, values? }]
|
|
280
|
+
|
|
281
|
+
// Each part's place from its member's origin, per axis: one offset every
|
|
282
|
+
// member shares, or — where a member's content set it apart (a taller text
|
|
283
|
+
// lowers its stack) — an offset prop at the call site, one per group of
|
|
284
|
+
// parts that move together.
|
|
285
|
+
const offsets = places[0].map(() => ({})); // [j][axis] = { prop, values, c }
|
|
286
|
+
for (const axis of ["x", "y"]) {
|
|
287
|
+
const leaders = new Map(); // delta key -> leader part
|
|
288
|
+
for (let j = 0; j < count; j++) {
|
|
289
|
+
const v = places.map((list) => list[j][axis]);
|
|
290
|
+
if (v.every((n) => n === null)) continue;
|
|
291
|
+
if (v.some((n) => !Number.isFinite(n))) return { why: `a part has no literal ${axis}` };
|
|
292
|
+
const d = v.map((n, k) => n - origins[k][axis]);
|
|
293
|
+
const shared = [tidy(d[0]), d[0]].find((c) => v.every((n, k) => origins[k][axis] + c === n));
|
|
294
|
+
if (shared !== undefined) {
|
|
295
|
+
offsets[j][axis] = { prop: null, c: shared };
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const key = d.map((n) => tidy(n - d[0])).join(",");
|
|
299
|
+
if (!leaders.has(key)) {
|
|
300
|
+
const values = d.map(tidy);
|
|
301
|
+
const prop = named(j, `@${axis}`, `${plan.prefixes[j] || "part"}${axis.toUpperCase()}`);
|
|
302
|
+
props.push({ name: prop, part: j, offset: axis, values });
|
|
303
|
+
leaders.set(key, { values, prop });
|
|
304
|
+
}
|
|
305
|
+
const { values, prop } = leaders.get(key);
|
|
306
|
+
const c = tidy(d[0] - values[0]);
|
|
307
|
+
if (v.some((n, k) => origins[k][axis] + values[k] + c !== n)) return { why: "its members' inner layouts differ" };
|
|
308
|
+
offsets[j][axis] = { prop, values, c };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** The expression for a coordinate `c` past part j's place on `axis`, and its value for member k. */
|
|
312
|
+
const coord = (j, axis, c) => {
|
|
313
|
+
const o = offsets[j][axis];
|
|
314
|
+
if (!o.prop) return { text: offsetExpr(axis, c), at: (k) => origins[k][axis] + c };
|
|
315
|
+
const tail = c === 0 ? "" : c > 0 ? ` + ${c}` : ` - ${-c}`;
|
|
316
|
+
return { text: `${axis} + ${o.prop}${tail}`, at: (k) => (c === 0 ? origins[k][axis] + o.values[k] : origins[k][axis] + o.values[k] + c) };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/** A figure attribute as the first member's, moved to each member's place (template text); null when it is not that. */
|
|
320
|
+
const movedFigure = (j, name, values) => {
|
|
321
|
+
const roots = values.map((a) => literalOf(a?.raw));
|
|
322
|
+
if (roots.some((r, k) => r === undefined || values[k].raw !== `{${lit(r)}}`)) return null;
|
|
323
|
+
const leaves = roots.map((r) => geometryLeaves(name, r));
|
|
324
|
+
if (leaves.some((l) => l.length !== leaves[0].length)) return null;
|
|
325
|
+
const root = structuredClone(roots[0]);
|
|
326
|
+
for (const [i, leaf] of leaves[0].entries()) {
|
|
327
|
+
if (leaves.some((l) => JSON.stringify(l[i].path) !== JSON.stringify(leaf.path))) return null;
|
|
328
|
+
if (!leaf.axis) {
|
|
329
|
+
if (leaves.some((l) => l[i].value !== leaf.value)) return null;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const o = offsets[j][leaf.axis];
|
|
333
|
+
if (!o) return null;
|
|
334
|
+
const e = coord(j, leaf.axis, tidy(leaf.value - origins[0][leaf.axis] - (o.prop ? o.values[0] : 0)));
|
|
335
|
+
if (leaves.some((l, k) => e.at(k) !== l[i].value)) return null;
|
|
336
|
+
let at = root;
|
|
337
|
+
for (const step of leaf.path.slice(0, -1)) at = at[step];
|
|
338
|
+
at[leaf.path.at(-1)] = `⟦${e.text}⟧`;
|
|
339
|
+
}
|
|
340
|
+
return `{${lit(root).replace(/"⟦([^⟧]*)⟧"/g, "$1")}}`;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Template: per part, per attribute — literal when every member agrees.
|
|
344
|
+
const template = [];
|
|
345
|
+
for (let j = 0; j < count; j++) {
|
|
346
|
+
const prefix = plan.prefixes[j];
|
|
347
|
+
const names = attrOrder(tokens.map((list) => list[j].attrs));
|
|
348
|
+
const attrs = [];
|
|
349
|
+
for (const name of names) {
|
|
350
|
+
const values = tokens.map((list) => list[j].attrs.find((a) => a.name === name));
|
|
351
|
+
if ((name === "x" || name === "y") && tokens[0][j].tag !== "Figure") {
|
|
352
|
+
attrs.push({ name, raw: `{${coord(j, name, offsets[j][name].c).text}}` });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// A figure every member repeats at its own place is the first one's,
|
|
356
|
+
// written from the member's x/y; one drawn differently is its member's.
|
|
357
|
+
const drawn = tokens[0][j].tag === "Figure" && GEOMETRY.has(name) ? movedFigure(j, name, values) : null;
|
|
358
|
+
if (drawn) {
|
|
359
|
+
attrs.push({ name, raw: drawn });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const same = name !== "id" && values.every((a) => a && a.raw === values[0].raw);
|
|
363
|
+
if (same) {
|
|
364
|
+
attrs.push({ name, raw: values[0].raw });
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const prop = named(j, name, name === "id" ? (prefix ? `${prefix}Id` : "id") : prefix ? `${prefix}${cap(camel(name))}` : camel(name));
|
|
368
|
+
props.push({ name: prop, part: j, attr: name });
|
|
369
|
+
attrs.push({ name, raw: `{${prop}}` });
|
|
370
|
+
}
|
|
371
|
+
const contents = tokens.map((list) => list[j].children ?? "");
|
|
372
|
+
let children = contents[0];
|
|
373
|
+
// A seated helper (`<I …/>` on its disc) is templated too, so its size
|
|
374
|
+
// and look are the set's; otherwise differing words ride in a slot. A
|
|
375
|
+
// chart rides whole in its member's slot: its data and styles are its own
|
|
376
|
+
// and stay literals at the call site (lint: `<Chart>` props must be
|
|
377
|
+
// literals — x113 QBR 1's eight KPI cards lifted `data`/`labels`/
|
|
378
|
+
// `seriesStyle` into props and every draft was refused).
|
|
379
|
+
const inner = contents.map((c) => (c.startsWith("<") ? tokenizeLine(c) : null));
|
|
380
|
+
if (contents.some((c) => c !== contents[0]) && inner.every((t) => t?.selfClose && t.tag === inner[0].tag && t.tag !== "Chart")) {
|
|
381
|
+
const idRaw = inner[0].attrs.find((a) => a.name === "id")?.raw;
|
|
382
|
+
const innerPrefix = plan.prefixFor?.(/^"(.*)"$/.exec(idRaw ?? "")?.[1]) ?? `${prefix || "inner"}Inner`;
|
|
383
|
+
const innerNames = attrOrder(inner.map((t) => t.attrs));
|
|
384
|
+
const innerAttrs = [];
|
|
385
|
+
for (const name of innerNames) {
|
|
386
|
+
const values = inner.map((t) => t.attrs.find((a) => a.name === name));
|
|
387
|
+
if (name !== "id" && values.every((a) => a && a.raw === values[0].raw)) {
|
|
388
|
+
innerAttrs.push({ name, raw: values[0].raw });
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const prop = named(j, `>${name}`, name === "id" ? `${innerPrefix}Id` : `${innerPrefix}${cap(camel(name))}`);
|
|
392
|
+
props.push({ name: prop, part: j, attr: name, inner: true });
|
|
393
|
+
innerAttrs.push({ name, raw: `{${prop}}` });
|
|
394
|
+
}
|
|
395
|
+
children = renderLine(inner[0].tag, innerAttrs, null, true);
|
|
396
|
+
} else if (contents.some((c) => c !== contents[0])) {
|
|
397
|
+
const text = tokens[0][j].tag === "T" || tokens[0][j].tag === "L";
|
|
398
|
+
const prop = named(j, "children", prefix ? `${prefix}${text ? "Text" : "Content"}` : text ? "text" : "content");
|
|
399
|
+
props.push({ name: prop, part: j, attr: "children" });
|
|
400
|
+
children = `{${prop}}`;
|
|
401
|
+
}
|
|
402
|
+
const selfClose = tokens.every((list) => list[j].selfClose) && !children;
|
|
403
|
+
template.push(renderLine(tokens[0][j].tag, attrs, children, selfClose));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// An offset prop only a drawing used that became its member's own is unused.
|
|
407
|
+
for (let i = props.length - 1; i >= 0; i--) {
|
|
408
|
+
if (props[i].offset && !template.some((t) => new RegExp(`\\b${props[i].name}\\b`).test(t))) props.splice(i, 1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Self-check: the template, filled with each member's own values, gives
|
|
412
|
+
// back that member's lines.
|
|
413
|
+
for (const [k, list] of tokens.entries()) {
|
|
414
|
+
for (let j = 0; j < count; j++) {
|
|
415
|
+
const own = list[j];
|
|
416
|
+
const attrs = [];
|
|
417
|
+
for (const a of tokenizeLine(template[j]).attrs) {
|
|
418
|
+
if ((a.name === "x" || a.name === "y") && own.tag !== "Figure") {
|
|
419
|
+
attrs.push({ name: a.name, raw: `{${coord(j, a.name, offsets[j][a.name].c).at(k)}}` });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// A figure's geometry was checked point by point against every member.
|
|
423
|
+
if (own.tag === "Figure" && GEOMETRY.has(a.name)) {
|
|
424
|
+
const value = own.attrs.find((o) => o.name === a.name);
|
|
425
|
+
if (value) attrs.push(value);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (!props.some((p) => p.part === j && !p.inner && p.attr === a.name)) attrs.push(a);
|
|
429
|
+
else {
|
|
430
|
+
const value = own.attrs.find((o) => o.name === a.name);
|
|
431
|
+
if (value) attrs.push(value);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let children = own.children ?? "";
|
|
435
|
+
if (props.some((p) => p.part === j && p.inner)) {
|
|
436
|
+
const ownInner = tokenizeLine(children);
|
|
437
|
+
const innerAttrs = [];
|
|
438
|
+
for (const a of tokenizeLine(tokenizeLine(template[j]).children).attrs) {
|
|
439
|
+
if (!props.some((p) => p.part === j && p.inner && p.attr === a.name)) innerAttrs.push(a);
|
|
440
|
+
else {
|
|
441
|
+
const value = ownInner?.attrs.find((o) => o.name === a.name);
|
|
442
|
+
if (value) innerAttrs.push(value);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
children = renderLine(ownInner?.tag ?? "", innerAttrs, null, true);
|
|
446
|
+
}
|
|
447
|
+
const rebuilt = renderLine(own.tag, attrs, children, own.selfClose);
|
|
448
|
+
if (!sameElement(rebuilt, lines[members[k].lines[j]].trim())) return { why: "the template does not give back a member" };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Paint order: an instance paints its parts together, where its first one
|
|
453
|
+
// was. A part painted AFTER some element now paints before it, so the two
|
|
454
|
+
// must not overlap; parts painted before it keep their side.
|
|
455
|
+
for (const m of members) {
|
|
456
|
+
const lo = Math.min(...m.lines);
|
|
457
|
+
const hi = Math.max(...m.lines);
|
|
458
|
+
const own = new Set(m.lines);
|
|
459
|
+
for (let i = lo + 1; i < hi; i++) {
|
|
460
|
+
// Another member's part counts too: it moves to its own call site.
|
|
461
|
+
if (own.has(i) || !/^\s*</.test(lines[i])) continue;
|
|
462
|
+
const later = m.lines.flatMap((at, j) => (at > i ? [boxOf(m.ids[j], lines[at], rects)] : []));
|
|
463
|
+
const ids = [...lines[i].matchAll(/\bid="([^"]+)"/g)].map((x) => x[1]);
|
|
464
|
+
// A free line's ends are literals on its own line: the source's truth,
|
|
465
|
+
// even after a move no render has measured yet.
|
|
466
|
+
const box = (BARE_LINE.test(lines[i]) ? directiveBox(lines[i]) : null) ?? ids.map((id) => boxOf(id, lines[i], rects)).find(Boolean);
|
|
467
|
+
if (!box || later.some((b) => !b)) return { why: "an element painted between its parts has no known place" };
|
|
468
|
+
const pad = /\bshadow\b/.test(lines[i]) || m.lines.some((x) => x > i && /\bshadow\b/.test(lines[x])) ? 16 : 2;
|
|
469
|
+
if (later.some((b) => overlaps(b, box, pad))) return { why: "an element painted between its parts overlaps it" };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The call sites, in the order members are declared.
|
|
474
|
+
const name = `Repeat${n}`;
|
|
475
|
+
const calls = tokens.map((list, k) => {
|
|
476
|
+
const attrs = [`x={${origins[k].x}}`, `y={${origins[k].y}}`];
|
|
477
|
+
for (const p of props) {
|
|
478
|
+
if (p.offset) {
|
|
479
|
+
attrs.push(`${p.name}={${p.values[k]}}`);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const own = p.inner ? tokenizeLine(list[p.part].children) : list[p.part];
|
|
483
|
+
if (p.attr === "children" && !p.inner) {
|
|
484
|
+
// Plain words travel as a string; markup, entities and quotes as a fragment.
|
|
485
|
+
if (own.children) attrs.push(/[<>{}&"\n]/.test(own.children) ? `${p.name}={<>${own.children}</>}` : `${p.name}="${own.children}"`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const value = own.attrs.find((a) => a.name === p.attr);
|
|
489
|
+
if (!value) continue;
|
|
490
|
+
attrs.push(value.raw === null ? p.name : `${p.name}=${value.raw}`);
|
|
491
|
+
}
|
|
492
|
+
return `${INDENT}<${name} ${attrs.join(" ")} />`;
|
|
493
|
+
});
|
|
494
|
+
const params = ["x", "y", ...props.map((p) => p.name)].join(", ");
|
|
495
|
+
const declaration = [plan.comment, ` const ${name} = ({ ${params} }) => (<>`, ...template.map((t) => ` ${t}`), " </>);"];
|
|
496
|
+
return { members, calls, declaration };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Index every element line by the ids it carries. */
|
|
500
|
+
function linesById(lines) {
|
|
501
|
+
const lineOf = new Map();
|
|
502
|
+
lines.forEach((line, i) => {
|
|
503
|
+
if (!line.startsWith(`${INDENT}<`)) return;
|
|
504
|
+
for (const m of line.matchAll(/\bid="([^"]+)"/g)) if (!lineOf.has(m[1])) lineOf.set(m[1], i);
|
|
505
|
+
});
|
|
506
|
+
return lineOf;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Fold the plans that hold; everything else stays as written. */
|
|
510
|
+
function applyPlans(src, plans, rects, dropComment) {
|
|
511
|
+
const lines = String(src).split("\n");
|
|
512
|
+
const kept = [];
|
|
513
|
+
const folds = [];
|
|
514
|
+
// A line belongs to ONE component. A set drawn inside another set's members
|
|
515
|
+
// (the rows of each card, when the cards themselves fold) would claim lines
|
|
516
|
+
// the outer fold already collapsed into its call sites — and lose them (x101
|
|
517
|
+
// PM 2: twelve row texts gone). It stays flat, with its comment.
|
|
518
|
+
const claimed = new Set();
|
|
519
|
+
for (const plan of plans) {
|
|
520
|
+
if (plan.why) {
|
|
521
|
+
kept.push({ id: plan.id, why: plan.why });
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (plan.members.some((m) => m.lines.some((i) => claimed.has(i)))) {
|
|
525
|
+
kept.push({ id: plan.id, why: "its parts are drawn inside another set's component" });
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const result = foldPlan(lines, plan, folds.length + 1, rects);
|
|
529
|
+
if (result.why) kept.push({ id: plan.id, why: result.why });
|
|
530
|
+
else {
|
|
531
|
+
folds.push({ plan, ...result });
|
|
532
|
+
for (const m of result.members) for (const i of m.lines) claimed.add(i);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!folds.length) return { src, folded: [], kept };
|
|
536
|
+
// Each member's parts collapse into its call site, at its first part.
|
|
537
|
+
const replaced = new Map();
|
|
538
|
+
const dropped = new Set();
|
|
539
|
+
for (const fold of folds) {
|
|
540
|
+
fold.members.forEach((m, k) => {
|
|
541
|
+
const first = Math.min(...m.lines);
|
|
542
|
+
replaced.set(first, fold.calls[k]);
|
|
543
|
+
for (const i of m.lines) if (i !== first) dropped.add(i);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const body = [];
|
|
547
|
+
lines.forEach((line, i) => {
|
|
548
|
+
if (dropped.has(i) || dropComment(line, folds)) return;
|
|
549
|
+
body.push(replaced.get(i) ?? line);
|
|
550
|
+
});
|
|
551
|
+
const at = body.findIndex((line) => line === " return (");
|
|
552
|
+
if (at < 0) return { src, folded: [], kept: [...kept, ...folds.map((f) => ({ id: f.plan.id, why: "the slide has no return to declare it before" }))] };
|
|
553
|
+
body.splice(at, 0, ...folds.flatMap((f) => f.declaration));
|
|
554
|
+
return { src: body.join("\n"), folded: folds.map((f) => f.plan.id), kept };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Fold every built set of a transcribed slide into a local component.
|
|
559
|
+
* @returns {{ src: string, folded: string[], kept: Array<{ id: string, why: string }> }}
|
|
560
|
+
*/
|
|
561
|
+
export function foldSets(src, spec, measured = null) {
|
|
562
|
+
const groups = repeatGroups(spec ?? {}).filter((g) => g.built);
|
|
563
|
+
if (ablated() || !groups.length) return { src, folded: [], kept: [] };
|
|
564
|
+
const lineOf = linesById(String(src).split("\n"));
|
|
565
|
+
const elements = spec.elements ?? [];
|
|
566
|
+
const plans = groups.map((group) => {
|
|
567
|
+
const kids = correspond(elements, group.members);
|
|
568
|
+
const members = group.members.map((m, i) => {
|
|
569
|
+
const lines = [];
|
|
570
|
+
const owners = [];
|
|
571
|
+
for (const e of [...(m.virtual ? [] : [m]), ...(kids?.[i] ?? [])]) {
|
|
572
|
+
const at = lineOf.get(e.id);
|
|
573
|
+
if (at === undefined || lines.includes(at)) continue;
|
|
574
|
+
lines.push(at);
|
|
575
|
+
owners.push(e);
|
|
576
|
+
}
|
|
577
|
+
return { lines, ids: owners.map((e) => e.id), owners };
|
|
578
|
+
});
|
|
579
|
+
const prefixes = members[0].owners.map((e, j) => (j === 0 && !group.members[0].virtual ? "" : camel(e.role ?? e.kind ?? `part${j}`)));
|
|
580
|
+
const prefixFor = (id) => {
|
|
581
|
+
const e = elements.find((x) => x.id === id);
|
|
582
|
+
return e ? camel(e.role ?? e.kind ?? "inner") : null;
|
|
583
|
+
};
|
|
584
|
+
const comment = ` // ${group.id}: ${group.members.map((m) => m.id).join(", ")} are one organism (${group.arrangement}${group.hub ? ` around ${group.hub.id}` : ""}) — its look is drawn once here; each member is a call site below.`;
|
|
585
|
+
return { id: group.id, comment, members, prefixes, prefixFor };
|
|
586
|
+
});
|
|
587
|
+
const ids = groups.map((g) => g.id);
|
|
588
|
+
const setComment = new RegExp(`^\\s*\\{/\\* (${ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):`);
|
|
589
|
+
return applyPlans(src, plans, rectIndex(spec, measured), (line, folds) =>
|
|
590
|
+
setComment.test(line) && folds.some((f) => line.includes(`{/* ${f.plan.id}:`)),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Fold a flat source the way `folded` was folded: its `Repeat<n>` components
|
|
596
|
+
* name the sets, their call sites the members and part ids, their templates
|
|
597
|
+
* the prop names. For a source that was inlined to be rewritten line by line
|
|
598
|
+
* (the bounded repair) and must go back to its components where that is still
|
|
599
|
+
* exact. `flat` is assumed to carry no declarations of its own.
|
|
600
|
+
*/
|
|
601
|
+
export function refoldLike(folded, flat, measured = null) {
|
|
602
|
+
if (ablated()) return { src: flat, folded: [], kept: [] };
|
|
603
|
+
const srcLines = String(folded).split("\n");
|
|
604
|
+
const lineOf = linesById(String(flat).split("\n"));
|
|
605
|
+
const plans = [];
|
|
606
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
607
|
+
const head = /^ {2}const (Repeat\d+) = \(\{ [^}]* \}\) => \(<>$/.exec(srcLines[i]);
|
|
608
|
+
if (!head) continue;
|
|
609
|
+
const comment = /^ {2}\/\/ /.test(srcLines[i - 1] ?? "") ? srcLines[i - 1] : ` // ${head[1]}`;
|
|
610
|
+
const template = [];
|
|
611
|
+
const raws = [];
|
|
612
|
+
for (i++; i < srcLines.length && srcLines[i] !== " </>);"; i++) {
|
|
613
|
+
template.push(tokenizeLine(srcLines[i]));
|
|
614
|
+
raws.push(srcLines[i]);
|
|
615
|
+
}
|
|
616
|
+
if (template.some((t) => !t)) continue;
|
|
617
|
+
const prop = (raw) => /^\{([A-Za-z_$][\w$]*)\}$/.exec(raw ?? "")?.[1] ?? null;
|
|
618
|
+
const idProps = template.map((t) => prop(t.attrs.find((a) => a.name === "id")?.raw));
|
|
619
|
+
const names = new Map();
|
|
620
|
+
template.forEach((t, j) => {
|
|
621
|
+
for (const a of t.attrs) if (prop(a.raw) && a.name !== "x" && a.name !== "y") names.set(`${j}:${a.name}`, prop(a.raw));
|
|
622
|
+
if (prop(t.children)) names.set(`${j}:children`, prop(t.children));
|
|
623
|
+
// An offset prop (`y={y + titleY}`, a figure's `{x: x + figX + 4}`).
|
|
624
|
+
for (const m of raws[j].matchAll(/\b([xy]) \+ ([A-Za-z_$][\w$]*)/g)) if (!names.has(`${j}:@${m[1]}`)) names.set(`${j}:@${m[1]}`, m[2]);
|
|
625
|
+
const seated = t.children?.startsWith("<") ? tokenizeLine(t.children) : null;
|
|
626
|
+
for (const a of seated?.attrs ?? []) if (prop(a.raw)) names.set(`${j}:>${a.name}`, prop(a.raw));
|
|
627
|
+
});
|
|
628
|
+
const prefixes = idProps.map((p) => (p && p !== "id" ? p.replace(/Id$/, "") : ""));
|
|
629
|
+
const calls = srcLines.map(tokenizeLine).filter((t) => t?.tag === head[1]);
|
|
630
|
+
const members = calls.map((call) => {
|
|
631
|
+
const ids = idProps.map((p) => /^"([^"]+)"$/.exec(call.attrs.find((a) => a.name === p)?.raw ?? "")?.[1] ?? null);
|
|
632
|
+
return { ids, lines: ids.map((id) => (id === null ? undefined : lineOf.get(id))) };
|
|
633
|
+
});
|
|
634
|
+
const missing = idProps.includes(null) || members.some((m) => m.lines.includes(undefined));
|
|
635
|
+
plans.push({ id: head[1], comment, names, prefixes, members, ...(missing ? { why: "a part is gone from the source" } : {}) });
|
|
636
|
+
}
|
|
637
|
+
if (!plans.length) return { src: flat, folded: [], kept: [] };
|
|
638
|
+
return applyPlans(flat, plans, rectIndex({}, measured), () => false);
|
|
639
|
+
}
|
|
@@ -17,6 +17,7 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const GATE_SESSION_PATH = path.join(__dirname, "gate-session.js");
|
|
20
|
+
const GATE_SHELL_PATH = path.join(__dirname, "gate-shell.js");
|
|
20
21
|
|
|
21
22
|
// Tab-reuse timing contract, shared by both gates and injected into their
|
|
22
23
|
// overlays as CFG.pollMs / CFG.pollTtlMs:
|
|
@@ -204,14 +205,18 @@ export function stripModuleSyntax(src) {
|
|
|
204
205
|
.replace(/^export\s+/gm, "");
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
// gate-session.js as
|
|
208
|
-
//
|
|
209
|
-
//
|
|
208
|
+
// gate-session.js + gate-shell.js as one browser script exposing
|
|
209
|
+
// window.__SLIDEMIND_GATE__ (the ledger/round machinery) and
|
|
210
|
+
// window.__SLIDEMIND_SHELL__ (the shared chrome). Both gate pages inline this
|
|
211
|
+
// BEFORE their overlay script — the overlays read both at boot.
|
|
210
212
|
export function bundleGateSession() {
|
|
211
213
|
return (
|
|
212
214
|
"(function () {\n" +
|
|
213
215
|
stripModuleSyntax(fs.readFileSync(GATE_SESSION_PATH, "utf8")) +
|
|
214
|
-
"\nwindow.__SLIDEMIND_GATE__ = { createGateSession: createGateSession };\n})();\n"
|
|
216
|
+
"\nwindow.__SLIDEMIND_GATE__ = { createGateSession: createGateSession };\n})();\n" +
|
|
217
|
+
"(function () {\n" +
|
|
218
|
+
stripModuleSyntax(fs.readFileSync(GATE_SHELL_PATH, "utf8")) +
|
|
219
|
+
"\nwindow.__SLIDEMIND_SHELL__ = { createGateShell: createGateShell, ICONS: ICONS, el: el };\n})();\n"
|
|
215
220
|
);
|
|
216
221
|
}
|
|
217
222
|
|