@wildwinter/expr-editor 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1621 @@
1
+ 'use strict';
2
+
3
+ var expr = require('@wildwinter/expr');
4
+
5
+ // src/ast.ts
6
+ var boolLit = (value) => ({ kind: "bool", value });
7
+ var numLit = (value) => ({ kind: "number", value });
8
+ var strLit = (value) => ({ kind: "string", value });
9
+ var scopedVar = (scope, name) => ({ kind: "scopedvar", scope, name: name.toLowerCase() });
10
+ var binary = (op, left, right) => ({ kind: "binary", op, left, right });
11
+ var notNode = (operand) => ({ kind: "unary", op: "not", operand });
12
+ var callNode = (name, args) => ({ kind: "call", name, args });
13
+ var flagDelta = (sign, name) => ({ kind: "flagdelta", sign, name });
14
+ var placeholderForOp = (op) => boolLit(op === "and");
15
+ var isPlaceholderForOp = (node, op) => node.kind === "bool" && node.value === (op === "and");
16
+ var COMPARISON = /* @__PURE__ */ new Set(["==", "!=", ">", ">=", "<", "<="]);
17
+ var isComparisonOp = (op) => COMPARISON.has(op);
18
+ function getNodeAt(ast, path) {
19
+ let node = ast;
20
+ for (let i = 0; i < path.length; i++) {
21
+ if (!node) return null;
22
+ const seg = path[i];
23
+ if (node.kind === "binary" && seg === "left") node = node.left;
24
+ else if (node.kind === "binary" && seg === "right") node = node.right;
25
+ else if (node.kind === "unary" && seg === "operand") node = node.operand;
26
+ else if (node.kind === "call" && seg === "args") {
27
+ node = node.args[path[++i]] ?? null;
28
+ } else return null;
29
+ }
30
+ return node;
31
+ }
32
+ function setNodeAt(ast, path, next) {
33
+ if (path.length === 0) return next;
34
+ const [seg, ...rest] = path;
35
+ if (ast.kind === "binary" && seg === "left") return { ...ast, left: setNodeAt(ast.left, rest, next) };
36
+ if (ast.kind === "binary" && seg === "right") return { ...ast, right: setNodeAt(ast.right, rest, next) };
37
+ if (ast.kind === "unary" && seg === "operand") return { ...ast, operand: setNodeAt(ast.operand, rest, next) };
38
+ if (ast.kind === "call" && seg === "args") {
39
+ const idx = rest[0];
40
+ const args = ast.args.map((a, i) => i === idx ? setNodeAt(a, rest.slice(1), next) : a);
41
+ return { ...ast, args };
42
+ }
43
+ throw new Error(`cannot descend into ${ast.kind} via '${String(seg)}'`);
44
+ }
45
+ function deleteAt(ast, path) {
46
+ if (path.length === 0) return null;
47
+ const last = path[path.length - 1];
48
+ if (typeof last === "number" && path[path.length - 2] === "args") {
49
+ const callPath = path.slice(0, -2);
50
+ const call = getNodeAt(ast, callPath);
51
+ if (call?.kind !== "call") return ast;
52
+ return setNodeAt(ast, callPath, { ...call, args: call.args.filter((_, i) => i !== last) });
53
+ }
54
+ const parentPath = path.slice(0, -1);
55
+ const parent = getNodeAt(ast, parentPath);
56
+ if (parent?.kind === "binary") {
57
+ const survivor = last === "left" ? parent.right : parent.left;
58
+ return setNodeAt(ast, parentPath, survivor);
59
+ }
60
+ if (parent?.kind === "unary") {
61
+ return setNodeAt(ast, parentPath, parent.operand);
62
+ }
63
+ return ast;
64
+ }
65
+ function insertSiblingClauseAt(ast, path, op, side, clause) {
66
+ const target = getNodeAt(ast, path);
67
+ if (!target) return ast;
68
+ const wrapped = side === "right" ? binary(op, target, clause) : binary(op, clause, target);
69
+ return setNodeAt(ast, path, wrapped);
70
+ }
71
+ function isWrappedInNot(ast, path) {
72
+ if (path[path.length - 1] !== "operand") return false;
73
+ const parent = getNodeAt(ast, path.slice(0, -1));
74
+ return parent?.kind === "unary" && parent.op === "not";
75
+ }
76
+ function wrapInNotAt(ast, path) {
77
+ const target = getNodeAt(ast, path);
78
+ return target ? setNodeAt(ast, path, notNode(target)) : ast;
79
+ }
80
+ function toggleNotAt(ast, path) {
81
+ if (isWrappedInNot(ast, path)) {
82
+ const inner = getNodeAt(ast, path);
83
+ return inner ? setNodeAt(ast, path.slice(0, -1), inner) : ast;
84
+ }
85
+ return wrapInNotAt(ast, path);
86
+ }
87
+ function findEnumPeer(ast, path) {
88
+ const last = path[path.length - 1];
89
+ if (last !== "left" && last !== "right") return null;
90
+ const parent = getNodeAt(ast, path.slice(0, -1));
91
+ if (parent?.kind !== "binary" || parent.op !== "==" && parent.op !== "!=") return null;
92
+ const other = last === "left" ? parent.right : parent.left;
93
+ return other.kind === "scopedvar" ? { scope: other.scope, name: other.name } : null;
94
+ }
95
+ function firstEmptyLeafPath(node, base = []) {
96
+ switch (node.kind) {
97
+ case "string":
98
+ return node.value === "" ? base : null;
99
+ case "flagdelta":
100
+ return node.name === "" ? base : null;
101
+ case "unary":
102
+ return firstEmptyLeafPath(node.operand, [...base, "operand"]);
103
+ case "binary":
104
+ return firstEmptyLeafPath(node.left, [...base, "left"]) ?? firstEmptyLeafPath(node.right, [...base, "right"]);
105
+ case "call": {
106
+ for (let i = 0; i < node.args.length; i++) {
107
+ const p = firstEmptyLeafPath(node.args[i], [...base, "args", i]);
108
+ if (p) return p;
109
+ }
110
+ return null;
111
+ }
112
+ default:
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // src/tree.ts
118
+ var isLogical = (n) => n != null && n.kind === "binary" && (n.op === "and" || n.op === "or");
119
+ function collectChain(node, path, op) {
120
+ if (node.kind === "binary" && node.op === op) {
121
+ return [
122
+ ...collectChain(node.left, [...path, "left"], op),
123
+ ...collectChain(node.right, [...path, "right"], op)
124
+ ];
125
+ }
126
+ return [astToTree(node, path)];
127
+ }
128
+ function containerRow(chain, chainPath, negated, path) {
129
+ return {
130
+ kind: "container",
131
+ op: chain.op,
132
+ negated,
133
+ path,
134
+ chainPath,
135
+ children: [
136
+ ...collectChain(chain.left, [...chainPath, "left"], chain.op),
137
+ ...collectChain(chain.right, [...chainPath, "right"], chain.op)
138
+ ]
139
+ };
140
+ }
141
+ function astToTree(node, path = []) {
142
+ if (node.kind === "unary" && node.op === "not" && isLogical(node.operand)) {
143
+ return containerRow(node.operand, [...path, "operand"], true, path);
144
+ }
145
+ if (isLogical(node)) return containerRow(node, path, false, path);
146
+ if (node.kind === "unary" && node.op === "not" && node.operand.kind === "binary" && isComparisonOp(node.operand.op)) {
147
+ const inner = node.operand;
148
+ return { kind: "comparison", left: inner.left, op: inner.op, right: inner.right, negated: true, path, contentPath: [...path, "operand"] };
149
+ }
150
+ if (node.kind === "binary" && isComparisonOp(node.op)) {
151
+ return { kind: "comparison", left: node.left, op: node.op, right: node.right, negated: false, path, contentPath: path };
152
+ }
153
+ if (node.kind === "unary" && node.op === "not") {
154
+ return { kind: "wrapped", node: node.operand, negated: true, path, contentPath: [...path, "operand"] };
155
+ }
156
+ return { kind: "wrapped", node, negated: false, path, contentPath: path };
157
+ }
158
+ function addChildToContainer(ast, chainPath, clause) {
159
+ const node = getNodeAt(ast, chainPath);
160
+ if (!node) return ast;
161
+ const op = isLogical(node) ? node.op : "and";
162
+ return setNodeAt(ast, chainPath, binary(op, node, clause));
163
+ }
164
+ function flipContainerOp(ast, chainPath, newOp) {
165
+ const node = getNodeAt(ast, chainPath);
166
+ if (!isLogical(node) || node.op === newOp) return ast;
167
+ const oldOp = node.op;
168
+ const flip = (n) => {
169
+ if (n.kind === "binary" && n.op === oldOp) return binary(newOp, flip(n.left), flip(n.right));
170
+ if (isPlaceholderForOp(n, oldOp)) return placeholderForOp(newOp);
171
+ return n;
172
+ };
173
+ return setNodeAt(ast, chainPath, flip(node));
174
+ }
175
+ function toggleContainerNot(ast, path) {
176
+ const node = getNodeAt(ast, path);
177
+ if (!node) return ast;
178
+ if (node.kind === "unary" && node.op === "not") return setNodeAt(ast, path, node.operand);
179
+ return setNodeAt(ast, path, { kind: "unary", op: "not", operand: node });
180
+ }
181
+ function buildSubGroupClause(parentOp, firstClause) {
182
+ const childOp = parentOp === "and" ? "or" : "and";
183
+ return binary(childOp, firstClause, placeholderForOp(childOp));
184
+ }
185
+ var chainNodes = (node, op) => node.kind === "binary" && node.op === op ? [...chainNodes(node.left, op), ...chainNodes(node.right, op)] : [node];
186
+ var buildChain = (op, nodes) => nodes.reduce((acc, n) => acc ? binary(op, acc, n) : n);
187
+ function redirectDeleteForPlaceholderSibling(ast, path) {
188
+ if (path.length === 0) return path;
189
+ const last = path[path.length - 1];
190
+ if (last !== "left" && last !== "right") return path;
191
+ const parentPath = path.slice(0, -1);
192
+ const parent = getNodeAt(ast, parentPath);
193
+ if (!isLogical(parent)) return path;
194
+ const sibling = last === "left" ? parent.right : parent.left;
195
+ return isPlaceholderForOp(sibling, parent.op) ? parentPath : path;
196
+ }
197
+ function moveChildInContainer(ast, chainPath, from, to) {
198
+ const node = getNodeAt(ast, chainPath);
199
+ if (!isLogical(node)) return ast;
200
+ const arr = chainNodes(node, node.op);
201
+ if (from < 0 || to < 0 || from >= arr.length || to >= arr.length || from === to) return ast;
202
+ const [moved] = arr.splice(from, 1);
203
+ arr.splice(to, 0, moved);
204
+ return setNodeAt(ast, chainPath, buildChain(node.op, arr));
205
+ }
206
+
207
+ // src/ops.ts
208
+ var BINARY_LABEL = {
209
+ and: "AND",
210
+ or: "OR",
211
+ "==": "is",
212
+ "!=": "\u2260",
213
+ ">": ">",
214
+ ">=": "\u2265",
215
+ "<": "<",
216
+ "<=": "\u2264",
217
+ "+": "+",
218
+ "-": "\u2212",
219
+ "*": "\xD7",
220
+ "/": "\xF7"
221
+ };
222
+ var UNARY_LABEL = { not: "NOT", neg: "\u2212" };
223
+ var COMPARISON_OPS = ["==", "!=", ">", ">=", "<", "<="];
224
+ var ARITHMETIC_OPS = ["+", "-", "*", "/"];
225
+ function opSwapGroup(op) {
226
+ if (COMPARISON_OPS.includes(op)) return COMPARISON_OPS;
227
+ if (ARITHMETIC_OPS.includes(op)) return ARITHMETIC_OPS;
228
+ return null;
229
+ }
230
+ var PREC = {
231
+ or: 1,
232
+ and: 2,
233
+ "==": 4,
234
+ "!=": 4,
235
+ ">": 4,
236
+ ">=": 4,
237
+ "<": 4,
238
+ "<=": 4,
239
+ "+": 5,
240
+ "-": 5,
241
+ "*": 6,
242
+ "/": 6
243
+ };
244
+ function needsParens(childOp, parentOp, side) {
245
+ if (PREC[childOp] < PREC[parentOp]) return true;
246
+ return PREC[childOp] === PREC[parentOp] && side === "right" && (parentOp === "-" || parentOp === "/");
247
+ }
248
+ function formatNumber(n) {
249
+ if (Number.isInteger(n)) return String(n);
250
+ return String(parseFloat(n.toFixed(12)));
251
+ }
252
+
253
+ // src/schema.ts
254
+ var refOf = (e, defaultScope) => e.scope === defaultScope ? `@${e.name}` : `@${e.scope}.${e.name}`;
255
+ var displayName = (e, defaultScope) => e.scope === defaultScope ? e.name : `${e.scope}.${e.name}`;
256
+ function filterCatalogue(entries, filter = {}) {
257
+ return entries.filter(
258
+ (e) => (!filter.acceptTypes || filter.acceptTypes.includes(e.type)) && (!filter.acceptScopes || filter.acceptScopes.includes(e.scope))
259
+ );
260
+ }
261
+ function searchCatalogue(entries, query, defaultScope) {
262
+ const q = query.trim().toLowerCase();
263
+ if (!q) return [...entries];
264
+ return entries.filter((e) => displayName(e, defaultScope).toLowerCase().includes(q) || (e.purpose ?? "").toLowerCase().includes(q));
265
+ }
266
+ function groupByScope(entries, scopeOrder = []) {
267
+ const byScope = /* @__PURE__ */ new Map();
268
+ for (const e of entries) {
269
+ const list = byScope.get(e.scope) ?? [];
270
+ list.push(e);
271
+ byScope.set(e.scope, list);
272
+ }
273
+ const rank = (s) => {
274
+ const i = scopeOrder.indexOf(s);
275
+ return i === -1 ? scopeOrder.length : i;
276
+ };
277
+ return [...byScope.keys()].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((scope) => ({ scope, entries: byScope.get(scope).slice().sort((a, b) => a.name.localeCompare(b.name)) }));
278
+ }
279
+ function lookup(entries, scope, name) {
280
+ const n = name.toLowerCase();
281
+ return entries.find((e) => e.scope === scope && e.name.toLowerCase() === n) ?? null;
282
+ }
283
+ var pathKey = (p) => p.join("/");
284
+ function validateSource(src, schema, dialect) {
285
+ const r = expr.parseAndValidate(src, schema, dialect);
286
+ const byPath = /* @__PURE__ */ new Map();
287
+ for (const issue of r.issues) {
288
+ const k = pathKey(issue.path);
289
+ const list = byPath.get(k) ?? [];
290
+ list.push(issue);
291
+ byPath.set(k, list);
292
+ }
293
+ const unparseable = r.issues.some((i) => i.kind === "unparseable");
294
+ return { ...r, byPath, unparseable };
295
+ }
296
+ var issuesAt = (byPath, path) => byPath.get(pathKey(path)) ?? [];
297
+
298
+ // src/dom.ts
299
+ function el(tag, cls, children) {
300
+ const node = document.createElement(tag);
301
+ if (cls) node.className = cls;
302
+ for (const c of children ?? []) {
303
+ if (c == null) continue;
304
+ node.append(typeof c === "string" ? document.createTextNode(c) : c);
305
+ }
306
+ return node;
307
+ }
308
+ function button(cls, label, onClick, title) {
309
+ const b = el("button", cls, [label]);
310
+ b.type = "button";
311
+ if (title) b.title = title;
312
+ b.addEventListener("click", onClick);
313
+ return b;
314
+ }
315
+ function openPopover(anchor, render, onClose) {
316
+ const pop = el("div", "exed-pop");
317
+ let closed = false;
318
+ const close = () => {
319
+ if (closed) return;
320
+ closed = true;
321
+ document.removeEventListener("pointerdown", onDown, true);
322
+ document.removeEventListener("keydown", onKey, true);
323
+ pop.remove();
324
+ onClose?.();
325
+ };
326
+ const onDown = (e) => {
327
+ const t = e.target;
328
+ if (!pop.contains(t) && !anchor.contains(t)) close();
329
+ };
330
+ const onKey = (e) => {
331
+ if (e.key === "Escape") {
332
+ e.stopPropagation();
333
+ close();
334
+ }
335
+ };
336
+ pop.append(render(close));
337
+ document.body.append(pop);
338
+ const a = anchor.getBoundingClientRect();
339
+ const ph = pop.getBoundingClientRect();
340
+ let top = a.bottom + 4;
341
+ if (top + ph.height > window.innerHeight - 8 && a.top - ph.height - 4 > 8) top = a.top - ph.height - 4;
342
+ let left = a.left;
343
+ if (left + ph.width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - 8 - ph.width);
344
+ pop.style.top = `${Math.round(top + window.scrollY)}px`;
345
+ pop.style.left = `${Math.round(left + window.scrollX)}px`;
346
+ setTimeout(() => {
347
+ document.addEventListener("pointerdown", onDown, true);
348
+ document.addEventListener("keydown", onKey, true);
349
+ }, 0);
350
+ return { el: pop, close };
351
+ }
352
+ function textField(opts) {
353
+ const wrap = el("div", "exed-field");
354
+ if (opts.caption) wrap.append(el("div", "exed-field-cap", [opts.caption]));
355
+ const input = el("input", "exed-input");
356
+ input.type = "text";
357
+ input.value = opts.initial ?? "";
358
+ if (opts.placeholder) input.placeholder = opts.placeholder;
359
+ const submit = button("exed-btn primary", opts.submitLabel ?? "Apply", () => commit());
360
+ const ok = () => opts.validate ? opts.validate(input.value) : true;
361
+ const sync = () => {
362
+ submit.disabled = !ok();
363
+ };
364
+ const commit = () => {
365
+ if (ok()) opts.onCommit(input.value);
366
+ };
367
+ input.addEventListener("input", sync);
368
+ input.addEventListener("keydown", (e) => {
369
+ if (e.key === "Enter") {
370
+ e.preventDefault();
371
+ e.stopPropagation();
372
+ commit();
373
+ }
374
+ });
375
+ sync();
376
+ wrap.append(el("div", "exed-field-row", [input, submit]));
377
+ setTimeout(() => input.focus(), 0);
378
+ return wrap;
379
+ }
380
+
381
+ // src/flat.ts
382
+ var FLAG_FNS = /* @__PURE__ */ new Set(["check_flags", "set_flags"]);
383
+ var TAG_ARG0_FNS = /* @__PURE__ */ new Set(["seen", "patter_seen", "visits", "patter_visits"]);
384
+ function pill(kind, label, onClick, opts = {}) {
385
+ const b = button(`exed-pill exed-pill-${kind}${opts.issue ? " exed-pill-err" : ""}`, label, () => onClick(b), opts.issue ?? opts.title);
386
+ if (opts.path) b.dataset["exedPath"] = opts.path.join("/");
387
+ return b;
388
+ }
389
+ var issueText = (ctx, path) => {
390
+ const list = issuesAt(ctx.byPath, path);
391
+ return list.length ? list.map((i) => i.message).join("; ") : void 0;
392
+ };
393
+ var replace = (ctx, path, next) => ctx.apply(setNodeAt(ctx.getAst(), path, next));
394
+ function renderNode(node, path, ctx, parentOp, side) {
395
+ const issue = issueText(ctx, path);
396
+ switch (node.kind) {
397
+ case "bool":
398
+ return pill("bool", node.value ? "true" : "false", (b) => boolEditor(ctx, path, node.value, b), { issue });
399
+ case "number":
400
+ return pill("number", formatNumber(node.value), (b) => numberEditor(ctx, path, node.value, b), { issue });
401
+ case "string": {
402
+ const isArg = path[path.length - 2] === "args";
403
+ const call = isArg ? getNodeAt(ctx.getAst(), path.slice(0, -2)) : null;
404
+ const isNodeRef = !!ctx.pickNode && call?.kind === "call" && TAG_ARG0_FNS.has(call.name) && path[path.length - 1] === 0;
405
+ if (isNodeRef) {
406
+ const openPick = (b) => ctx.pickNode(b, node.value, (id) => replace(ctx, path, strLit(id)));
407
+ return node.value === "" ? pill("placeholder", "pick a node\u2026", openPick, { issue, path }) : pill("node", ctx.nodeLabel?.(node.value) ?? node.value, openPick, { issue, title: node.value });
408
+ }
409
+ if (node.value === "")
410
+ return pill("placeholder", isArg ? "set a tag\u2026" : "set a value\u2026", (b) => stringEditor(ctx, path, node, b), { issue, path });
411
+ return pill(isArg ? "tag" : "string", node.value, (b) => stringEditor(ctx, path, node, b), { issue });
412
+ }
413
+ case "scopedvar": {
414
+ const entry = lookup(ctx.catalogue, node.scope, node.name);
415
+ const label = displayName({ scope: node.scope, name: node.name }, ctx.defaultScope);
416
+ return pill("var", label, (b) => variableEditor(ctx, path, node, b), { issue: issue ?? (entry ? void 0 : `Unknown property ${label}`) });
417
+ }
418
+ case "flagdelta":
419
+ return pill(node.sign === "+" ? "flagpos" : "flagneg", `${node.sign}${node.name || "flag"}`, (b) => flagEditor(ctx, path, node, b), { issue, path });
420
+ case "call": {
421
+ const row = el("span", "exed-call");
422
+ row.append(pill("func", node.name, (b) => callEditor(ctx, path, node, b), { issue }));
423
+ row.append(el("span", "exed-paren", ["("]));
424
+ node.args.forEach((arg, i) => {
425
+ if (i > 0) row.append(el("span", "exed-comma", [", "]));
426
+ row.append(renderNode(arg, [...path, "args", i], ctx));
427
+ });
428
+ row.append(el("span", "exed-paren", [")"]));
429
+ return row;
430
+ }
431
+ case "unary": {
432
+ const row = el("span", "exed-unary");
433
+ row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove" }));
434
+ row.append(renderNode(node.operand, [...path, "operand"], ctx));
435
+ return row;
436
+ }
437
+ case "binary": {
438
+ const row = el("span", "exed-binary");
439
+ const wrap = parentOp && needsParens(node.op, parentOp, side ?? "left");
440
+ if (wrap) row.append(el("span", "exed-paren", ["("]));
441
+ row.append(renderNode(node.left, [...path, "left"], ctx, node.op, "left"));
442
+ const swap = opSwapGroup(node.op);
443
+ const opPill = pill("op", BINARY_LABEL[node.op], (b) => {
444
+ if (swap) operatorEditor(ctx, path, node.op, swap, b);
445
+ }, { title: swap ? "swap operator" : void 0 });
446
+ if (!swap) opPill.classList.add("exed-op-structural");
447
+ row.append(opPill);
448
+ row.append(renderNode(node.right, [...path, "right"], ctx, node.op, "right"));
449
+ if (wrap) row.append(el("span", "exed-paren", [")"]));
450
+ return row;
451
+ }
452
+ }
453
+ }
454
+ var deleteOrUnwrapNot = (ctx, path, node) => (
455
+ // clicking the NOT pill strips it (keeps the operand)
456
+ setNodeAt(ctx.getAst(), path, node.operand)
457
+ );
458
+ function boolEditor(ctx, path, value, anchor) {
459
+ ctx.openPopover(anchor, (close) => {
460
+ const wrap = el("div", "exed-menu");
461
+ wrap.append(el("div", "exed-menu-head", ["Boolean value"]));
462
+ for (const v of [true, false]) {
463
+ wrap.append(button(`exed-opt${v === value ? " sel" : ""}`, String(v), () => {
464
+ replace(ctx, path, boolLit(v));
465
+ close();
466
+ }));
467
+ }
468
+ return wrap;
469
+ });
470
+ }
471
+ function numberEditor(ctx, path, value, anchor) {
472
+ ctx.openPopover(anchor, (close) => textField({
473
+ initial: formatNumber(value),
474
+ caption: "Number value",
475
+ placeholder: "0",
476
+ validate: (v) => v.trim() !== "" && Number.isFinite(Number(v)),
477
+ onCommit: (v) => {
478
+ replace(ctx, path, numLit(Number(v)));
479
+ close();
480
+ }
481
+ }));
482
+ }
483
+ function stringEditor(ctx, path, node, anchor) {
484
+ const peer = findEnumPeer(ctx.getAst(), path);
485
+ const enumEntry = peer ? lookup(ctx.catalogue, peer.scope, peer.name) : null;
486
+ ctx.openPopover(anchor, (close) => {
487
+ if (enumEntry?.enumValues?.length) {
488
+ const wrap = el("div", "exed-menu");
489
+ wrap.append(el("div", "exed-menu-head", [`${displayName(enumEntry, ctx.defaultScope)} value`]));
490
+ for (const v of enumEntry.enumValues) {
491
+ wrap.append(button(`exed-opt${v === node.value ? " sel" : ""}`, v, () => {
492
+ replace(ctx, path, strLit(v));
493
+ close();
494
+ }));
495
+ }
496
+ return wrap;
497
+ }
498
+ return textField({
499
+ initial: node.value,
500
+ caption: "Text value",
501
+ placeholder: "value",
502
+ onCommit: (v) => {
503
+ replace(ctx, path, strLit(v));
504
+ close();
505
+ }
506
+ });
507
+ });
508
+ }
509
+ function operatorEditor(ctx, path, current, group, anchor) {
510
+ ctx.openPopover(anchor, (close) => {
511
+ const wrap = el("div", "exed-menu");
512
+ wrap.append(el("div", "exed-menu-head", ["Operator"]));
513
+ for (const op of group) {
514
+ wrap.append(button(`exed-opt${op === current ? " sel" : ""}`, `${BINARY_LABEL[op]} ${op}`, () => {
515
+ const node = ctx.getAst();
516
+ const cur = setNodeAt(node, path, { ...getBinary(ctx, path), op });
517
+ ctx.apply(cur);
518
+ close();
519
+ }));
520
+ }
521
+ return wrap;
522
+ });
523
+ }
524
+ var getBinary = (ctx, path) => {
525
+ const n = getNodeAt(ctx.getAst(), path);
526
+ if (n?.kind !== "binary") throw new Error("expected binary");
527
+ return n;
528
+ };
529
+ function flagEditor(ctx, path, node, anchor) {
530
+ const callPath = path.slice(0, -2);
531
+ const call = getNodeAt(ctx.getAst(), callPath);
532
+ const flagsVar = call?.kind === "call" ? call.args[0] : void 0;
533
+ const entry = flagsVar?.kind === "scopedvar" ? lookup(ctx.catalogue, flagsVar.scope, flagsVar.name) : null;
534
+ const used = call?.kind === "call" ? call.args.filter((a, i) => a.kind === "flagdelta" && i !== path[path.length - 1]).map((a) => a.name) : [];
535
+ ctx.openPopover(anchor, (close) => {
536
+ const wrap = el("div", "exed-menu");
537
+ wrap.append(el("div", "exed-menu-head", ["Flag set?"]));
538
+ const signRow = el("div", "exed-field-row");
539
+ for (const s of ["+", "-"]) {
540
+ signRow.append(button(`exed-opt${s === node.sign ? " sel" : ""}`, s === "+" ? "+ set" : "\u2212 unset", () => {
541
+ replace(ctx, path, flagDelta(s, node.name));
542
+ close();
543
+ }));
544
+ }
545
+ wrap.append(signRow);
546
+ const names = (entry?.enumValues ?? []).filter((n) => !used.includes(n));
547
+ if (names.length) {
548
+ wrap.append(el("div", "exed-menu-head", ["Flag"]));
549
+ for (const n of names) wrap.append(button(`exed-opt${n === node.name ? " sel" : ""}`, n, () => {
550
+ replace(ctx, path, flagDelta(node.sign, n));
551
+ close();
552
+ }));
553
+ } else {
554
+ wrap.append(textField({ initial: node.name, caption: "Flag name", onCommit: (v) => {
555
+ replace(ctx, path, flagDelta(node.sign, v));
556
+ close();
557
+ } }));
558
+ }
559
+ return wrap;
560
+ });
561
+ }
562
+ function callEditor(ctx, path, node, anchor) {
563
+ ctx.openPopover(anchor, (close) => {
564
+ const wrap = el("div", "exed-menu");
565
+ wrap.append(el("div", "exed-menu-head", [node.name]));
566
+ wrap.append(el("div", "exed-hint", ["Edit the arguments by clicking each one."]));
567
+ if (FLAG_FNS.has(node.name)) {
568
+ wrap.append(button("exed-opt", "+ add flag", () => {
569
+ replace(ctx, path, { ...node, args: [...node.args, flagDelta("+", "")] });
570
+ close();
571
+ }));
572
+ }
573
+ wrap.append(button("exed-opt danger", "Delete", () => {
574
+ ctx.apply(deleteAt(ctx.getAst(), path));
575
+ close();
576
+ }));
577
+ return wrap;
578
+ });
579
+ }
580
+ function variableEditor(ctx, path, node, anchor) {
581
+ ctx.openPopover(anchor, (close) => propertyPicker(ctx, {
582
+ current: refOf(node, ctx.defaultScope),
583
+ onPick: (entry) => {
584
+ replace(ctx, path, scopedVar(entry.scope, entry.name));
585
+ close();
586
+ },
587
+ footer: button("exed-opt danger", "Delete", () => {
588
+ ctx.apply(deleteAt(ctx.getAst(), path));
589
+ close();
590
+ })
591
+ }));
592
+ }
593
+ function propertyPicker(ctx, opts) {
594
+ const wrap = el("div", "exed-picker");
595
+ const search = el("input", "exed-input");
596
+ search.type = "text";
597
+ search.placeholder = "Search properties\u2026";
598
+ const list = el("div", "exed-picker-list");
599
+ const pool = filterCatalogue(ctx.catalogue, { acceptTypes: opts.accept });
600
+ const draw = () => {
601
+ list.replaceChildren();
602
+ const groups = groupByScope(searchCatalogue(pool, search.value, ctx.defaultScope), ctx.scopeOrder);
603
+ if (!groups.length) {
604
+ list.append(el("div", "exed-hint", ["No matching properties."]));
605
+ return;
606
+ }
607
+ for (const g of groups) {
608
+ list.append(el("div", "exed-picker-scope", [g.scope]));
609
+ for (const e of g.entries) {
610
+ const label = displayName(e, ctx.defaultScope);
611
+ const sel = opts.current === refOf(e, ctx.defaultScope);
612
+ const row = button(`exed-opt${sel ? " sel" : ""}`, "", () => opts.onPick(e));
613
+ row.append(el("span", "exed-opt-name", [label]), el("span", "exed-opt-type", [e.type]));
614
+ if (e.purpose) row.append(el("span", "exed-opt-purpose", [e.purpose]));
615
+ list.append(row);
616
+ }
617
+ }
618
+ };
619
+ search.addEventListener("input", draw);
620
+ draw();
621
+ wrap.append(search, list);
622
+ if (opts.footer) wrap.append(el("div", "exed-picker-foot", [opts.footer]));
623
+ setTimeout(() => search.focus(), 0);
624
+ return wrap;
625
+ }
626
+
627
+ // src/clausewizard.ts
628
+ var EQUALITY = ["==", "!="];
629
+ var OP_WORD = {
630
+ "==": "equals",
631
+ "!=": "not equal to",
632
+ ">": "greater than",
633
+ ">=": "at least",
634
+ "<": "less than",
635
+ "<=": "at most"
636
+ };
637
+ var opButton = (o, onClick) => {
638
+ const b = button("exed-opt", "", onClick);
639
+ b.append(el("span", "exed-opt-name", [BINARY_LABEL[o]]), el("span", "exed-opt-purpose", [OP_WORD[o] ?? ""]));
640
+ return b;
641
+ };
642
+ var opsForType = (t) => t === "number" ? COMPARISON_OPS : EQUALITY;
643
+ var rhsTypesFor = (t) => t === "number" ? ["number"] : t === "boolean" ? ["boolean"] : t === "enum" ? ["enum", "string"] : ["string", "enum"];
644
+ function header(host, title, back, cancel) {
645
+ const h = el("div", "exed-vwiz-head");
646
+ if (back) h.append(button("exed-vwiz-back", "\u2190", back, "Back"));
647
+ else if (cancel) h.append(button("exed-vwiz-back", "\u2715", cancel, "Cancel"));
648
+ h.append(el("span", "exed-vwiz-title", [title]));
649
+ host.append(h);
650
+ }
651
+ var mono = (s) => el("span", "exed-vwiz-mono", [s]);
652
+ var pickCtxOf = (w) => ({ catalogue: w.catalogue, defaultScope: w.defaultScope, scopeOrder: w.scopeOrder });
653
+ function valueStep(host, w, lhs, op, back, cancel, commit) {
654
+ let mode = "value";
655
+ const draw = () => {
656
+ host.replaceChildren();
657
+ header(host, el("span", void 0, [mono(lhs.ref), " ", BINARY_LABEL[op], " \u2026"]), back, cancel);
658
+ const body = el("div", "exed-vwiz-body");
659
+ host.append(body);
660
+ const swap = () => button("exed-vwiz-other", mode === "value" ? "\u21A9 use a property instead" : "\u21A9 use a value instead", () => {
661
+ mode = mode === "value" ? "property" : "value";
662
+ draw();
663
+ });
664
+ const done = (rhs) => commit(binary(op, lhsVar(w, lhs.ref), rhs));
665
+ if (mode === "property") {
666
+ body.append(propertyPicker(pickCtxOf(w), { accept: rhsTypesFor(lhs.type), onPick: (e) => done(scopedVar(e.scope, e.name)) }));
667
+ body.append(swap());
668
+ return;
669
+ }
670
+ if (lhs.type === "boolean") {
671
+ const row = el("div", "exed-field-row");
672
+ row.append(button("exed-opt", "true", () => done(boolLit(true))), button("exed-opt", "false", () => done(boolLit(false))));
673
+ body.append(row);
674
+ } else if (lhs.type === "enum" && lhs.enumValues?.length) {
675
+ for (const v of lhs.enumValues) body.append(button("exed-opt", v, () => done(strLit(v))));
676
+ } else if (lhs.type === "number") {
677
+ body.append(textField({ caption: "Number", placeholder: "e.g. 3", validate: (v) => v.trim() !== "" && Number.isFinite(Number(v)), onCommit: (v) => done(numLit(Number(v))) }));
678
+ } else {
679
+ body.append(textField({ caption: "Text", placeholder: "e.g. autumn", onCommit: (v) => done(strLit(v)) }));
680
+ }
681
+ body.append(swap());
682
+ };
683
+ draw();
684
+ }
685
+ function lhsVar(w, ref) {
686
+ const bare = ref.replace(/^@/, "");
687
+ const dot = bare.indexOf(".");
688
+ return dot >= 0 ? scopedVar(bare.slice(0, dot), bare.slice(dot + 1)) : scopedVar(w.defaultScope, bare);
689
+ }
690
+ function comparisonWizard(host, w, commit, cancel) {
691
+ const pickLhs = () => {
692
+ host.replaceChildren();
693
+ header(host, "Pick a property to compare", void 0, cancel);
694
+ const body = el("div", "exed-vwiz-body");
695
+ host.append(body);
696
+ body.append(propertyPicker(pickCtxOf(w), {
697
+ accept: ["boolean", "number", "string", "enum"],
698
+ onPick: (e) => pickOp({ ref: refDisplay(w, e), type: e.type, ...e.enumValues ? { enumValues: e.enumValues } : {} })
699
+ }));
700
+ };
701
+ const pickOp = (lhs) => {
702
+ host.replaceChildren();
703
+ header(host, el("span", void 0, ["Operator for ", mono(lhs.ref)]), pickLhs, cancel);
704
+ const body = el("div", "exed-vwiz-body");
705
+ host.append(body);
706
+ for (const o of opsForType(lhs.type)) {
707
+ body.append(opButton(o, () => valueStep(host, w, lhs, o, () => pickOp(lhs), cancel, commit)));
708
+ }
709
+ };
710
+ pickLhs();
711
+ }
712
+ function booleanWizard(host, w, commit, cancel) {
713
+ host.replaceChildren();
714
+ header(host, "Pick a boolean property", void 0, cancel);
715
+ const body = el("div", "exed-vwiz-body");
716
+ host.append(body);
717
+ body.append(propertyPicker(pickCtxOf(w), { accept: ["boolean"], onPick: (e) => commit(scopedVar(e.scope, e.name)) }));
718
+ }
719
+ function checkFlagsWizard(host, w, commit, cancel) {
720
+ const pickProp = () => {
721
+ host.replaceChildren();
722
+ header(host, el("span", void 0, ["Flags property for ", mono("check_flags()")]), void 0, cancel);
723
+ const body = el("div", "exed-vwiz-body");
724
+ host.append(body);
725
+ body.append(propertyPicker(pickCtxOf(w), { accept: ["flags"], onPick: (e) => pickFlag(e) }));
726
+ };
727
+ const pickFlag = (prop) => {
728
+ let sign = "+";
729
+ const draw = () => {
730
+ host.replaceChildren();
731
+ header(host, el("span", void 0, ["Flag on ", mono(refDisplay(w, prop))]), pickProp, cancel);
732
+ const body = el("div", "exed-vwiz-body");
733
+ host.append(body);
734
+ const signRow = el("div", "exed-field-row");
735
+ signRow.append(
736
+ button(`exed-opt${sign === "+" ? " sel" : ""}`, "\uFF0B set", () => {
737
+ sign = "+";
738
+ draw();
739
+ }),
740
+ button(`exed-opt${sign === "-" ? " sel" : ""}`, "\uFF0D unset", () => {
741
+ sign = "-";
742
+ draw();
743
+ })
744
+ );
745
+ body.append(signRow);
746
+ const flags = prop.enumValues ?? [];
747
+ if (!flags.length) body.append(el("div", "exed-hint", [`${refDisplay(w, prop)} declares no flag values yet.`]));
748
+ for (const f of flags) body.append(button("exed-opt", f, () => commit(callNode("check_flags", [scopedVar(prop.scope, prop.name), flagDelta(sign, f)]))));
749
+ };
750
+ draw();
751
+ };
752
+ pickProp();
753
+ }
754
+ function randomWizard(host, w, commit, cancel) {
755
+ const pickRange = () => {
756
+ host.replaceChildren();
757
+ header(host, el("span", void 0, ["Range for ", mono("random(a, b)")]), void 0, cancel);
758
+ const body = el("div", "exed-vwiz-body");
759
+ host.append(body);
760
+ let a = "1";
761
+ body.append(textField({ caption: "Low (a)", initial: "1", placeholder: "e.g. 1", validate: numOk, onCommit: (v) => {
762
+ a = v;
763
+ } }));
764
+ body.append(textField({
765
+ caption: "High (b)",
766
+ initial: "6",
767
+ placeholder: "e.g. 6",
768
+ submitLabel: "Next \u2192",
769
+ validate: numOk,
770
+ onCommit: (b) => pickOp(Math.round(Number(a)), Math.round(Number(b)))
771
+ }));
772
+ };
773
+ const pickOp = (a, b) => {
774
+ host.replaceChildren();
775
+ header(host, el("span", void 0, ["Operator for ", mono(`random(${a}, ${b})`)]), pickRange, cancel);
776
+ const body = el("div", "exed-vwiz-body");
777
+ host.append(body);
778
+ for (const o of COMPARISON_OPS) {
779
+ body.append(opButton(o, () => pickValue(a, b, o)));
780
+ }
781
+ };
782
+ const pickValue = (a, b, op) => {
783
+ host.replaceChildren();
784
+ header(host, el("span", void 0, [mono(`random(${a}, ${b})`), ` ${BINARY_LABEL[op]} \u2026`]), () => pickOp(a, b), cancel);
785
+ const body = el("div", "exed-vwiz-body");
786
+ host.append(body);
787
+ body.append(textField({ caption: "Compare to", placeholder: "e.g. 1", validate: numOk, onCommit: (v) => commit(binary(op, callNode("random", [numLit(a), numLit(b)]), numLit(Number(v)))) }));
788
+ };
789
+ pickRange();
790
+ }
791
+ var numOk = (v) => v.trim() !== "" && Number.isFinite(Number(v));
792
+ function genericWizard(host, spec, commit, cancel) {
793
+ const values = [];
794
+ const stepAt = (i) => {
795
+ const step = spec.steps[i];
796
+ if (!step) return;
797
+ host.replaceChildren();
798
+ const back = i > 0 ? () => {
799
+ values.length = i - 1;
800
+ stepAt(i - 1);
801
+ } : void 0;
802
+ header(host, step.title, back, cancel);
803
+ const body = el("div", "exed-vwiz-body");
804
+ host.append(body);
805
+ const last = i === spec.steps.length - 1;
806
+ const done = (v) => {
807
+ values[i] = v;
808
+ if (last) commit(spec.build(values));
809
+ else stepAt(i + 1);
810
+ };
811
+ switch (step.kind) {
812
+ case "string":
813
+ body.append(textField({
814
+ caption: step.caption ?? "Text",
815
+ placeholder: step.placeholder,
816
+ submitLabel: last ? "Apply" : "Next \u2192",
817
+ validate: (v) => v.trim() !== "",
818
+ onCommit: (v) => done(v.trim())
819
+ }));
820
+ break;
821
+ case "number":
822
+ body.append(textField({
823
+ caption: step.caption ?? "Number",
824
+ placeholder: step.placeholder,
825
+ initial: step.initial !== void 0 ? String(step.initial) : void 0,
826
+ submitLabel: last ? "Apply" : "Next \u2192",
827
+ validate: numOk,
828
+ onCommit: (v) => done(Number(v))
829
+ }));
830
+ break;
831
+ case "op":
832
+ for (const o of step.ops ?? COMPARISON_OPS) body.append(opButton(o, () => done(o)));
833
+ break;
834
+ }
835
+ };
836
+ stepAt(0);
837
+ }
838
+ function refDisplay(w, e) {
839
+ return e.scope === w.defaultScope ? `@${e.name}` : `@${e.scope}.${e.name}`;
840
+ }
841
+
842
+ // src/treeview.ts
843
+ function requestFocusForInsert(ctx, clause, base) {
844
+ const rel = firstEmptyLeafPath(clause);
845
+ if (rel) ctx.requestFocus?.([...base, ...rel]);
846
+ }
847
+ function renderTree(ctx) {
848
+ const row = astToTree(ctx.getAst(), []);
849
+ const wrap = el("div", "exed-tree");
850
+ wrap.append(renderRow(ctx, row, { root: true }));
851
+ if (row.kind !== "container") wrap.append(rootAddBar(ctx));
852
+ return wrap;
853
+ }
854
+ function renderRow(ctx, row, env) {
855
+ if (row.kind === "container") return renderContainer(ctx, row, env);
856
+ return renderLeaf(ctx, row, env);
857
+ }
858
+ function notToggle(ctx, path) {
859
+ return button("exed-rowbtn", "NOT", () => ctx.apply(toggleContainerNot(ctx.getAst(), path)), "toggle NOT");
860
+ }
861
+ function rowActions(ctx, row, env) {
862
+ const acts = el("div", "exed-rowacts");
863
+ acts.append(notToggle(ctx, row.path));
864
+ if (env.chainPath && env.count != null && env.index != null) {
865
+ if (env.index > 0) acts.append(button("exed-rowbtn", "\u2191", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index - 1)), "move up"));
866
+ if (env.index < env.count - 1) acts.append(button("exed-rowbtn", "\u2193", () => ctx.apply(moveChildInContainer(ctx.getAst(), env.chainPath, env.index, env.index + 1)), "move down"));
867
+ }
868
+ acts.append(button("exed-rowbtn danger", "\u2715", () => {
869
+ if (env.root) {
870
+ ctx.apply(null);
871
+ return;
872
+ }
873
+ const target = redirectDeleteForPlaceholderSibling(ctx.getAst(), row.path);
874
+ ctx.apply(deleteAt(ctx.getAst(), target));
875
+ }, "delete"));
876
+ return acts;
877
+ }
878
+ function renderLeaf(ctx, row, env) {
879
+ if (row.kind === "wrapped" && (isPlaceholderForOp(row.node, "and") || isPlaceholderForOp(row.node, "or"))) {
880
+ return placeholderRow(ctx, row.path);
881
+ }
882
+ const line = el("div", "exed-row");
883
+ if (row.negated) line.append(el("span", "exed-not", ["NOT"]));
884
+ const content = row.kind === "comparison" ? renderNode(nodeAtContent(ctx, row.contentPath), row.contentPath, ctx) : renderNode(row.node, row.contentPath, ctx);
885
+ line.append(el("span", "exed-rowcontent", [content]));
886
+ line.append(rowActions(ctx, row, env));
887
+ return line;
888
+ }
889
+ var nodeAtContent = (ctx, path) => {
890
+ return getNodeAt(ctx.getAst(), path);
891
+ };
892
+ function renderContainer(ctx, row, env) {
893
+ const box = el("div", `exed-group exed-group-${row.op}`);
894
+ const head = el("div", "exed-grouphead");
895
+ if (row.negated) head.append(el("span", "exed-not", ["NOT"]));
896
+ const label = row.op === "and" ? "ALL OF THESE:" : "ANY OF THESE:";
897
+ if (env.root) {
898
+ head.append(button("exed-flip", label, () => ctx.apply(flipContainerOp(ctx.getAst(), row.chainPath, row.op === "and" ? "or" : "and")), "switch AND / OR"));
899
+ } else {
900
+ head.append(el("span", "exed-grouplabel", [label]));
901
+ }
902
+ const headActs = el("div", "exed-rowacts");
903
+ headActs.append(notToggle(ctx, row.path));
904
+ if (!env.root) headActs.append(button("exed-rowbtn danger", "\u2715", () => ctx.apply(deleteAt(ctx.getAst(), row.path)), "delete group"));
905
+ head.append(headActs);
906
+ box.append(head);
907
+ const body = el("div", "exed-groupbody");
908
+ row.children.forEach((child, i) => {
909
+ const childRow = el("div", "exed-child");
910
+ childRow.append(el("span", "exed-connector", [i === 0 ? "" : row.op.toUpperCase()]));
911
+ childRow.append(renderRow(ctx, child, { index: i, count: row.children.length, chainPath: row.chainPath }));
912
+ body.append(childRow);
913
+ });
914
+ body.append(addBar(ctx, row.chainPath, row.op));
915
+ box.append(body);
916
+ return box;
917
+ }
918
+ function placeholderRow(ctx, path) {
919
+ const b = button("exed-placeholder", "Click to add condition", (e) => {
920
+ clauseMenu(ctx, e.currentTarget, (node) => {
921
+ requestFocusForInsert(ctx, node, path);
922
+ ctx.apply(replaceAt(ctx, path, node));
923
+ });
924
+ });
925
+ return b;
926
+ }
927
+ function addBar(ctx, chainPath, op) {
928
+ const bar = el("div", "exed-addbar");
929
+ bar.append(button("exed-add", "+ Add condition", (e) => {
930
+ clauseMenu(ctx, e.currentTarget, (node) => {
931
+ requestFocusForInsert(ctx, node, [...chainPath, "right"]);
932
+ ctx.apply(addChildToContainer(ctx.getAst(), chainPath, node));
933
+ });
934
+ }));
935
+ bar.append(button("exed-add", `+ Add ${op === "and" ? "OR" : "AND"} group`, () => {
936
+ ctx.apply(addChildToContainer(ctx.getAst(), chainPath, buildSubGroupClause(op, seedClause(ctx))));
937
+ }, "add a nested group"));
938
+ return bar;
939
+ }
940
+ function rootAddBar(ctx) {
941
+ const bar = el("div", "exed-addbar");
942
+ bar.append(button("exed-add", "+ Add condition", (e) => {
943
+ clauseMenu(ctx, e.currentTarget, (node) => {
944
+ requestFocusForInsert(ctx, node, ["right"]);
945
+ ctx.apply(addChildToContainer(ctx.getAst(), [], node));
946
+ });
947
+ }));
948
+ bar.append(button("exed-add", "+ Add group", () => {
949
+ ctx.apply(addChildToContainer(ctx.getAst(), [], buildSubGroupClause("and", seedClause(ctx))));
950
+ }));
951
+ return bar;
952
+ }
953
+ function seedClause(ctx) {
954
+ const first = ctx.catalogue[0];
955
+ if (!first) return { kind: "bool", value: true };
956
+ if (first.type === "boolean") return scopedVar(first.scope, first.name);
957
+ return binary("==", scopedVar(first.scope, first.name), first.type === "number" ? { kind: "number", value: 0 } : strLit(""));
958
+ }
959
+ function clauseMenu(ctx, anchor, onPick) {
960
+ ctx.openPopover(anchor, (close) => {
961
+ const wrap = el("div", "exed-menu");
962
+ wrap.append(el("div", "exed-menu-head", ["Add a clause"]));
963
+ const add = (label, hint, make, disabled = false) => {
964
+ const b = button(`exed-opt${disabled ? " disabled" : ""}`, "", () => {
965
+ if (disabled) return;
966
+ make();
967
+ close();
968
+ });
969
+ if (disabled) b.disabled = true;
970
+ b.append(el("span", "exed-opt-name", [label]));
971
+ if (hint) b.append(el("span", "exed-opt-purpose", [hint]));
972
+ wrap.append(b);
973
+ };
974
+ const wctx = { catalogue: ctx.catalogue, scopeOrder: ctx.scopeOrder, defaultScope: ctx.defaultScope };
975
+ const launch = (run) => {
976
+ ctx.openPopover(anchor, (close2) => {
977
+ const host = el("div", "exed-vwiz");
978
+ run(host, wctx, (node) => {
979
+ onPick(node);
980
+ close2();
981
+ }, close2);
982
+ return host;
983
+ });
984
+ };
985
+ const addFn = (fn) => add(fn.label, fn.hint, () => {
986
+ if (fn.wizard === "check_flags") launch(checkFlagsWizard);
987
+ else if (fn.wizard === "random") launch(randomWizard);
988
+ else if (fn.wizard && typeof fn.wizard === "object") {
989
+ const spec = fn.wizard;
990
+ launch((host, _w, commit, cancel) => genericWizard(host, spec, commit, cancel));
991
+ } else onPick(fn.build());
992
+ }, !!fn.disabled);
993
+ const flagFns = ctx.functions.filter((f) => f.name === "check_flags");
994
+ const otherFns = ctx.functions.filter((f) => f.name !== "check_flags");
995
+ flagFns.forEach(addFn);
996
+ add("Property comparison", "a property vs a value", () => launch(comparisonWizard));
997
+ add("Property is true", "a boolean property on its own", () => launch(booleanWizard));
998
+ otherFns.forEach(addFn);
999
+ return wrap;
1000
+ });
1001
+ }
1002
+ var replaceAt = (ctx, path, node) => setNodeAt(ctx.getAst(), path, node);
1003
+
1004
+ // src/mount.ts
1005
+ function mountExpressionEditor(host, opts) {
1006
+ const defaultScope = opts.dialect.defaultScope;
1007
+ let src = opts.value ?? "";
1008
+ let raw = opts.text ?? false;
1009
+ let activePopover = null;
1010
+ let editing = false;
1011
+ let pendingFocus = null;
1012
+ host.classList.add("exed-root");
1013
+ const emit = (next) => {
1014
+ src = next;
1015
+ opts.onChange(src);
1016
+ render();
1017
+ };
1018
+ const toSrc = (ast) => ast ? expr.unparse(ast, { defaultScope }) : "";
1019
+ const setEditing = (on) => {
1020
+ if (on === editing) return;
1021
+ editing = on;
1022
+ opts.onEditingChange?.(on);
1023
+ };
1024
+ const closePopover = () => {
1025
+ activePopover?.close();
1026
+ activePopover = null;
1027
+ };
1028
+ function buildCtx(ast) {
1029
+ const v = validateSource(src, opts.schema, opts.dialect);
1030
+ return {
1031
+ schema: opts.schema,
1032
+ dialect: opts.dialect,
1033
+ defaultScope,
1034
+ catalogue: opts.catalogue,
1035
+ scopeOrder: opts.scopeOrder ?? [],
1036
+ functions: opts.functions ?? [],
1037
+ byPath: v.byPath,
1038
+ getAst: () => ast,
1039
+ apply: (next) => emit(toSrc(next)),
1040
+ openPopover: (anchor, r) => {
1041
+ closePopover();
1042
+ const pop = openPopover(anchor, r, () => {
1043
+ if (activePopover === pop) activePopover = null;
1044
+ setEditing(false);
1045
+ });
1046
+ activePopover = pop;
1047
+ setEditing(true);
1048
+ },
1049
+ requestFocus: (p) => {
1050
+ pendingFocus = p;
1051
+ },
1052
+ ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1053
+ ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
1054
+ };
1055
+ }
1056
+ function render() {
1057
+ closePopover();
1058
+ host.replaceChildren();
1059
+ const v = validateSource(src, opts.schema, opts.dialect);
1060
+ if (raw || v.unparseable) {
1061
+ host.append(rawArea());
1062
+ } else if (!src.trim() || !v.ast) {
1063
+ host.append(emptyState());
1064
+ } else {
1065
+ const ctx = buildCtx(v.ast);
1066
+ const body = el("div", "exed-body");
1067
+ if ((opts.mode ?? "tree") === "flat") {
1068
+ const flat = el("div", "exed-flat", [renderNode(v.ast, [], ctx)]);
1069
+ if (opts.addTerm) flat.append(addTermControl(ctx));
1070
+ body.append(flat);
1071
+ } else body.append(renderTree(ctx));
1072
+ host.append(body);
1073
+ }
1074
+ if ((opts.messages ?? true) && (!v.unparseable || src.trim())) host.append(messages(v.issues));
1075
+ if (pendingFocus) {
1076
+ const key = pendingFocus.join("/");
1077
+ pendingFocus = null;
1078
+ host.querySelector(`[data-exed-path="${key}"]`)?.click();
1079
+ }
1080
+ }
1081
+ function rawArea() {
1082
+ const ta = el("textarea", "exed-raw");
1083
+ ta.value = src;
1084
+ ta.placeholder = "@gold > 0 and @met_anna";
1085
+ ta.rows = 2;
1086
+ ta.addEventListener("input", () => {
1087
+ src = ta.value;
1088
+ opts.onChange(src);
1089
+ msgHost.replaceChildren(messages(validateSource(src, opts.schema, opts.dialect).issues));
1090
+ });
1091
+ const msgHost = el("div", "exed-rawmsg");
1092
+ const wrap = el("div", "exed-rawwrap", [ta, msgHost]);
1093
+ return wrap;
1094
+ }
1095
+ function emptyState() {
1096
+ const wrap = el("div", "exed-empty");
1097
+ wrap.append(el("span", "exed-pill exed-pill-always", [opts.nullLabel ?? "always"]));
1098
+ const ctx = buildCtx(boolLit(true));
1099
+ wrap.append(button("exed-add", "+ Add your first condition", (e) => {
1100
+ clauseMenu(ctx, e.currentTarget, (node) => {
1101
+ requestFocusForInsert(ctx, node, []);
1102
+ emit(toSrc(node));
1103
+ });
1104
+ }));
1105
+ return wrap;
1106
+ }
1107
+ function addTermControl(ctx) {
1108
+ const boolean = opts.addTerm === "boolean";
1109
+ const ops = boolean ? ["and", "or"] : ARITHMETIC_OPS;
1110
+ const operandType = boolean ? "boolean" : "number";
1111
+ return button("exed-add exed-addterm", "+ term", (e) => {
1112
+ const anchor = e.currentTarget;
1113
+ let op = ops[0];
1114
+ ctx.openPopover(anchor, (close) => {
1115
+ const wrap = el("div", "exed-menu");
1116
+ wrap.append(el("div", "exed-menu-head", ["Add term"]));
1117
+ const commit = (operand) => {
1118
+ ctx.apply(binary(op, ctx.getAst(), operand));
1119
+ close();
1120
+ };
1121
+ const opRow = el("div", "exed-field-row");
1122
+ const drawOps = () => {
1123
+ opRow.replaceChildren();
1124
+ for (const o of ops) opRow.append(button(`exed-opt${o === op ? " sel" : ""}`, BINARY_LABEL[o], () => {
1125
+ op = o;
1126
+ drawOps();
1127
+ }));
1128
+ };
1129
+ drawOps();
1130
+ wrap.append(opRow);
1131
+ if (boolean) {
1132
+ const row = el("div", "exed-field-row");
1133
+ row.append(button("exed-opt", "true", () => commit(boolLit(true))), button("exed-opt", "false", () => commit(boolLit(false))));
1134
+ wrap.append(row);
1135
+ } else {
1136
+ wrap.append(textField({
1137
+ caption: "by",
1138
+ placeholder: "e.g. 1",
1139
+ validate: (v) => v.trim() !== "" && Number.isFinite(Number(v)),
1140
+ onCommit: (v) => commit(numLit(Number(v)))
1141
+ }));
1142
+ }
1143
+ wrap.append(button("exed-opt", "\u2026or a property", () => {
1144
+ ctx.openPopover(anchor, (c2) => propertyPicker(ctx, { accept: [operandType], onPick: (en) => {
1145
+ commit(scopedVar(en.scope, en.name));
1146
+ c2();
1147
+ } }));
1148
+ }));
1149
+ return wrap;
1150
+ });
1151
+ }, boolean ? "add a logical term" : "add an arithmetic term");
1152
+ }
1153
+ function messages(issues) {
1154
+ const box = el("div", "exed-msgs");
1155
+ for (const i of issues) {
1156
+ box.append(el("div", `exed-msg exed-msg-${i.severity}`, [i.severity === "error" ? "\u26A0 " : "\u25B3 ", i.message]));
1157
+ }
1158
+ return box;
1159
+ }
1160
+ render();
1161
+ return {
1162
+ setValue: (v) => {
1163
+ src = v ?? "";
1164
+ render();
1165
+ },
1166
+ // text mode is host-driven; don't reset it here
1167
+ setText: (on) => {
1168
+ raw = on;
1169
+ render();
1170
+ },
1171
+ destroy: () => {
1172
+ closePopover();
1173
+ host.replaceChildren();
1174
+ host.classList.remove("exed-root");
1175
+ }
1176
+ };
1177
+ }
1178
+
1179
+ // src/valuewizard.ts
1180
+ var isIdent = (v) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v);
1181
+ var enumSrc = (v) => isIdent(v) ? v : JSON.stringify(v);
1182
+ function valueWizard(opts) {
1183
+ const host = el("div", "exed-vwiz");
1184
+ const pickCtx = { catalogue: opts.catalogue, defaultScope: opts.defaultScope, scopeOrder: opts.scopeOrder };
1185
+ const optBtn = (label, onClick) => button("exed-opt", label, onClick);
1186
+ const head = (title) => {
1187
+ const h = el("div", "exed-vwiz-head");
1188
+ if (opts.onCancel) h.append(button("exed-vwiz-back", "\u2715", opts.onCancel, "Cancel"));
1189
+ h.append(el("span", "exed-vwiz-title", [title]));
1190
+ return h;
1191
+ };
1192
+ host.append(head("Pick a value"));
1193
+ const body = el("div", "exed-vwiz-body");
1194
+ host.append(body);
1195
+ let kind = opts.expectedType === "number" ? "number" : opts.expectedType === "string" ? "text" : opts.expectedType === "boolean" ? "bool" : opts.expectedType === "enum" && opts.expectedEnumValues?.length ? "enum" : "menu";
1196
+ const draw = () => {
1197
+ body.replaceChildren();
1198
+ const other = () => button("exed-vwiz-other", "\u21A9 a different kind", () => {
1199
+ kind = "menu";
1200
+ draw();
1201
+ });
1202
+ switch (kind) {
1203
+ case "menu":
1204
+ body.append(optBtn("A property\u2026", () => {
1205
+ kind = "property";
1206
+ draw();
1207
+ }));
1208
+ body.append(optBtn("A number", () => {
1209
+ kind = "number";
1210
+ draw();
1211
+ }));
1212
+ body.append(optBtn("Text", () => {
1213
+ kind = "text";
1214
+ draw();
1215
+ }));
1216
+ body.append(optBtn("True / False", () => {
1217
+ kind = "bool";
1218
+ draw();
1219
+ }));
1220
+ if (opts.expectedEnumValues?.length) body.append(optBtn("A listed value", () => {
1221
+ kind = "enum";
1222
+ draw();
1223
+ }));
1224
+ break;
1225
+ case "property":
1226
+ body.append(propertyPicker(pickCtx, { onPick: (e) => opts.onCommit(refOf(e, opts.defaultScope)) }));
1227
+ body.append(other());
1228
+ break;
1229
+ case "number":
1230
+ body.append(textField({ caption: "Number", placeholder: "e.g. 5 or 0.5", validate: (v) => v.trim() !== "" && Number.isFinite(Number(v)), onCommit: (v) => opts.onCommit(String(Number(v))) }));
1231
+ body.append(other());
1232
+ break;
1233
+ case "text":
1234
+ body.append(textField({ caption: "Text", placeholder: "e.g. autumn", onCommit: (v) => opts.onCommit(JSON.stringify(v)) }));
1235
+ body.append(other());
1236
+ break;
1237
+ case "bool": {
1238
+ const row = el("div", "exed-field-row");
1239
+ row.append(optBtn("true", () => opts.onCommit("true")), optBtn("false", () => opts.onCommit("false")));
1240
+ body.append(row, other());
1241
+ break;
1242
+ }
1243
+ case "enum":
1244
+ for (const v of opts.expectedEnumValues ?? []) body.append(optBtn(v, () => opts.onCommit(enumSrc(v))));
1245
+ body.append(other());
1246
+ break;
1247
+ }
1248
+ };
1249
+ draw();
1250
+ return host;
1251
+ }
1252
+
1253
+ // src/effects.ts
1254
+ var clone = (list) => list.map((e) => e.kind === "set" ? { ...e } : { ...e, args: [...e.args] });
1255
+ var addSet = (list, target, value) => [...clone(list), { kind: "set", target, value }];
1256
+ var addEmit = (list, event) => [...clone(list), { kind: "emit", event, args: [] }];
1257
+ var removeAt = (list, i) => clone(list).filter((_, idx) => idx !== i);
1258
+ function moveAt(list, i, dir) {
1259
+ const next = clone(list);
1260
+ const j = i + dir;
1261
+ if (j < 0 || j >= next.length) return next;
1262
+ [next[i], next[j]] = [next[j], next[i]];
1263
+ return next;
1264
+ }
1265
+ function updateAt(list, i, patch) {
1266
+ const next = clone(list);
1267
+ const cur = next[i];
1268
+ if (!cur) return next;
1269
+ next[i] = { ...cur, ...patch };
1270
+ return next;
1271
+ }
1272
+ function setArgAt(list, i, argIdx, value) {
1273
+ const next = clone(list);
1274
+ const cur = next[i];
1275
+ if (!cur || cur.kind !== "emit") return next;
1276
+ cur.args[argIdx] = value;
1277
+ return next;
1278
+ }
1279
+ function addArg(list, i, value = "0") {
1280
+ const next = clone(list);
1281
+ const cur = next[i];
1282
+ if (!cur || cur.kind !== "emit") return next;
1283
+ cur.args.push(value);
1284
+ return next;
1285
+ }
1286
+ function removeArgAt(list, i, argIdx) {
1287
+ const next = clone(list);
1288
+ const cur = next[i];
1289
+ if (!cur || cur.kind !== "emit") return next;
1290
+ cur.args.splice(argIdx, 1);
1291
+ return next;
1292
+ }
1293
+ function seedValueSrc(type, enumValues) {
1294
+ switch (type) {
1295
+ case "boolean":
1296
+ return "true";
1297
+ case "number":
1298
+ return "0";
1299
+ case "enum":
1300
+ return JSON.stringify(enumValues?.[0] ?? "");
1301
+ case "string":
1302
+ return '""';
1303
+ default:
1304
+ return "0";
1305
+ }
1306
+ }
1307
+ function mountEffectsEditor(host, opts) {
1308
+ const defaultScope = opts.dialect.defaultScope;
1309
+ let effects = clone(opts.effects ?? []);
1310
+ let textMode = opts.text ?? false;
1311
+ let popover = null;
1312
+ const inner = [];
1313
+ host.classList.add("exed-root", "exed-effects-root");
1314
+ const closePopover = () => {
1315
+ popover?.close();
1316
+ popover = null;
1317
+ };
1318
+ const commit = (next) => {
1319
+ effects = clone(next);
1320
+ opts.onChange(effects);
1321
+ render();
1322
+ };
1323
+ const openValueWizard = (anchor, expected, onCommit) => {
1324
+ closePopover();
1325
+ popover = openPopover(anchor, (close) => valueWizard({
1326
+ catalogue: opts.catalogue,
1327
+ scopeOrder: opts.scopeOrder ?? [],
1328
+ defaultScope,
1329
+ ...expected?.type ? { expectedType: expected.type } : {},
1330
+ ...expected?.enumValues ? { expectedEnumValues: expected.enumValues } : {},
1331
+ onCommit: (src) => {
1332
+ onCommit(src);
1333
+ close();
1334
+ },
1335
+ onCancel: close
1336
+ }));
1337
+ };
1338
+ const targetType = (target) => opts.catalogue.find((e) => refOf(e, defaultScope) === target)?.type;
1339
+ const valueDisplay = (value, onChange, type) => {
1340
+ const sub = el("div", "exed-effect-value");
1341
+ const addTerm = type === "number" ? "arithmetic" : type === "boolean" ? "boolean" : void 0;
1342
+ inner.push(mountExpressionEditor(sub, {
1343
+ value,
1344
+ schema: opts.schema,
1345
+ dialect: opts.dialect,
1346
+ catalogue: opts.catalogue,
1347
+ scopeOrder: opts.scopeOrder,
1348
+ functions: opts.functions,
1349
+ mode: "flat",
1350
+ ...addTerm ? { addTerm } : {},
1351
+ text: textMode,
1352
+ onChange
1353
+ }));
1354
+ return sub;
1355
+ };
1356
+ const pickTarget = (anchor, accept, onPick) => {
1357
+ closePopover();
1358
+ popover = openPopover(anchor, (close) => {
1359
+ const wrap = el("div", "exed-picker");
1360
+ const search = el("input", "exed-input");
1361
+ search.type = "text";
1362
+ search.placeholder = "Search properties\u2026";
1363
+ const list = el("div", "exed-picker-list");
1364
+ const pool = filterCatalogue(opts.catalogue, { acceptTypes: accept });
1365
+ const draw = () => {
1366
+ list.replaceChildren();
1367
+ const groups = groupByScope(searchCatalogue(pool, search.value, defaultScope), opts.scopeOrder ?? []);
1368
+ if (!groups.length) {
1369
+ list.append(el("div", "exed-hint", ["No matching properties."]));
1370
+ return;
1371
+ }
1372
+ for (const g of groups) {
1373
+ list.append(el("div", "exed-picker-scope", [g.scope]));
1374
+ for (const e of g.entries) {
1375
+ const row = button("exed-opt", "", () => {
1376
+ onPick(e);
1377
+ close();
1378
+ });
1379
+ row.append(el("span", "exed-opt-name", [displayName(e, defaultScope)]), el("span", "exed-opt-type", [e.type]));
1380
+ if (e.purpose) row.append(el("span", "exed-opt-purpose", [e.purpose]));
1381
+ list.append(row);
1382
+ }
1383
+ }
1384
+ };
1385
+ search.addEventListener("input", draw);
1386
+ draw();
1387
+ wrap.append(search, list);
1388
+ setTimeout(() => search.focus(), 0);
1389
+ return wrap;
1390
+ });
1391
+ };
1392
+ const iconBtn = (glyph, title, onClick, danger = false) => button(`exed-eff-icon${danger ? " danger" : ""}`, glyph, onClick, title);
1393
+ function setRow(eff, i) {
1394
+ const row = el("div", "exed-effect exed-effect-set");
1395
+ const targetBtn = button("exed-pill exed-pill-prop", eff.target || "(pick property)", (e) => {
1396
+ pickTarget(e.currentTarget, void 0, (entry) => {
1397
+ commit(updateAt(effects, i, { target: refOf(entry, defaultScope), value: seedValueSrc(entry.type, entry.enumValues) }));
1398
+ });
1399
+ }, "change the target property");
1400
+ row.append(targetBtn, el("span", "exed-effect-eq", ["="]));
1401
+ row.append(valueDisplay(eff.value, (src) => {
1402
+ effects = updateAt(effects, i, { value: src });
1403
+ opts.onChange(effects);
1404
+ }, targetType(eff.target)));
1405
+ row.append(rowActions2(i));
1406
+ return row;
1407
+ }
1408
+ function emitRow(eff, i) {
1409
+ const row = el("div", "exed-effect exed-effect-emit");
1410
+ const head = el("div", "exed-effect-head");
1411
+ head.append(el("span", "exed-effect-kw", ["emit"]));
1412
+ const eventBtn = button("exed-pill exed-pill-event", eff.event ? `"${eff.event}"` : "(name)", (e) => {
1413
+ editEvent(e.currentTarget, eff.event, (name) => commit(updateAt(effects, i, { event: name })));
1414
+ }, "name the host event");
1415
+ head.append(eventBtn);
1416
+ head.append(rowActions2(i));
1417
+ row.append(head);
1418
+ const args = el("div", "exed-effect-args");
1419
+ eff.args.forEach((a, ai) => {
1420
+ const slot = el("div", "exed-effect-arg");
1421
+ slot.append(valueDisplay(a, (src) => {
1422
+ effects = setArgAt(effects, i, ai, src);
1423
+ opts.onChange(effects);
1424
+ }));
1425
+ slot.append(iconBtn("\u2715", "remove argument", () => commit(removeArgAt(effects, i, ai)), true));
1426
+ args.append(slot);
1427
+ });
1428
+ args.append(button("exed-eff-add", "+ argument", (e) => openValueWizard(e.currentTarget, void 0, (src) => commit(addArg(effects, i, src)))));
1429
+ row.append(args);
1430
+ return row;
1431
+ }
1432
+ const editEvent = (anchor, current, onName) => {
1433
+ closePopover();
1434
+ popover = openPopover(anchor, (close) => {
1435
+ const wrap = el("div", "exed-picker");
1436
+ const input = el("input", "exed-input");
1437
+ input.type = "text";
1438
+ input.value = current;
1439
+ input.placeholder = "event name";
1440
+ const apply = () => {
1441
+ const v = input.value.trim();
1442
+ if (v) onName(v);
1443
+ close();
1444
+ };
1445
+ input.addEventListener("keydown", (e) => {
1446
+ if (e.key === "Enter") {
1447
+ e.preventDefault();
1448
+ e.stopPropagation();
1449
+ apply();
1450
+ }
1451
+ });
1452
+ wrap.append(input);
1453
+ for (const name of opts.events ?? []) {
1454
+ if (name === current) continue;
1455
+ wrap.append(button("exed-opt", name, () => {
1456
+ onName(name);
1457
+ close();
1458
+ }));
1459
+ }
1460
+ wrap.append(button("exed-btn primary", "Apply", apply));
1461
+ setTimeout(() => input.focus(), 0);
1462
+ return wrap;
1463
+ });
1464
+ };
1465
+ const rowActions2 = (i) => {
1466
+ const acts = el("div", "exed-effect-acts");
1467
+ if (i > 0) acts.append(iconBtn("\u2191", "move up", () => commit(moveAt(effects, i, -1))));
1468
+ if (i < effects.length - 1) acts.append(iconBtn("\u2193", "move down", () => commit(moveAt(effects, i, 1))));
1469
+ acts.append(iconBtn("\u2715", "remove effect", () => commit(removeAt(effects, i)), true));
1470
+ return acts;
1471
+ };
1472
+ function render() {
1473
+ closePopover();
1474
+ for (const h of inner.splice(0)) h.destroy();
1475
+ host.replaceChildren();
1476
+ const listEl = el("div", "exed-effect-list");
1477
+ if (!effects.length) listEl.append(el("div", "exed-effect-empty", ["No effects yet."]));
1478
+ effects.forEach((eff, i) => listEl.append(eff.kind === "set" ? setRow(eff, i) : emitRow(eff, i)));
1479
+ host.append(listEl);
1480
+ const bar = el("div", "exed-effect-addbar");
1481
+ bar.append(button("exed-eff-add", "+ set property", (e) => {
1482
+ const anchor = e.currentTarget;
1483
+ pickTarget(anchor, void 0, (entry) => openValueWizard(anchor, entry, (src) => commit(addSet(effects, refOf(entry, defaultScope), src))));
1484
+ }));
1485
+ if (opts.allowEmit !== false) {
1486
+ bar.append(button("exed-eff-add", "+ emit event", (e) => editEvent(e.currentTarget, "", (name) => commit(addEmit(effects, name)))));
1487
+ }
1488
+ host.append(bar);
1489
+ }
1490
+ render();
1491
+ return {
1492
+ setValue: (next) => {
1493
+ effects = clone(next ?? []);
1494
+ render();
1495
+ },
1496
+ // Flip every live inline value editor in place — keeps any open target/wizard popover alive.
1497
+ setText: (on) => {
1498
+ textMode = on;
1499
+ for (const h of inner) h.setText(on);
1500
+ },
1501
+ destroy: () => {
1502
+ closePopover();
1503
+ for (const h of inner.splice(0)) h.destroy();
1504
+ host.replaceChildren();
1505
+ host.classList.remove("exed-root", "exed-effects-root");
1506
+ }
1507
+ };
1508
+ }
1509
+
1510
+ // src/preview.ts
1511
+ function frozenCtx(src, o) {
1512
+ const v = validateSource(src, o.schema, o.dialect);
1513
+ const ctx = {
1514
+ schema: o.schema,
1515
+ dialect: o.dialect,
1516
+ defaultScope: o.dialect.defaultScope,
1517
+ catalogue: o.catalogue,
1518
+ scopeOrder: o.scopeOrder ?? [],
1519
+ functions: [],
1520
+ byPath: v.byPath,
1521
+ getAst: () => v.ast,
1522
+ apply: () => {
1523
+ },
1524
+ openPopover: () => {
1525
+ },
1526
+ pickNode: () => {
1527
+ },
1528
+ // enables labelled node pills; never fires (preview is read-only)
1529
+ ...o.nodeLabel ? { nodeLabel: o.nodeLabel } : {}
1530
+ };
1531
+ return { ctx, ast: v.ast };
1532
+ }
1533
+ function exprPills(src, o) {
1534
+ const { ctx, ast } = frozenCtx(src, o);
1535
+ return ast ? renderNode(ast, [], ctx) : el("span", "exed-preview-raw", [src]);
1536
+ }
1537
+ function renderConditionPreview(src, o) {
1538
+ return el("div", "exed-preview", [exprPills(src, o)]);
1539
+ }
1540
+ function renderEffectsPreview(effects, o) {
1541
+ const wrap = el("div", "exed-preview exed-preview-effects");
1542
+ for (const eff of effects) {
1543
+ const row = el("div", "exed-preview-eff");
1544
+ if (eff.kind === "set") {
1545
+ row.append(el("span", "exed-pill exed-pill-prop", [eff.target || "(property)"]));
1546
+ row.append(el("span", "exed-effect-eq", ["="]));
1547
+ row.append(exprPills(eff.value, o));
1548
+ } else {
1549
+ row.append(el("span", "exed-effect-kw", ["emit"]));
1550
+ row.append(el("span", "exed-pill exed-pill-event", [eff.event ? `"${eff.event}"` : "(name)"]));
1551
+ row.append(el("span", "exed-paren", ["("]));
1552
+ eff.args.forEach((a, i) => {
1553
+ if (i > 0) row.append(el("span", "exed-comma", [", "]));
1554
+ row.append(exprPills(a, o));
1555
+ });
1556
+ row.append(el("span", "exed-paren", [")"]));
1557
+ }
1558
+ wrap.append(row);
1559
+ }
1560
+ return wrap;
1561
+ }
1562
+
1563
+ exports.ARITHMETIC_OPS = ARITHMETIC_OPS;
1564
+ exports.BINARY_LABEL = BINARY_LABEL;
1565
+ exports.COMPARISON_OPS = COMPARISON_OPS;
1566
+ exports.UNARY_LABEL = UNARY_LABEL;
1567
+ exports.addArg = addArg;
1568
+ exports.addChildToContainer = addChildToContainer;
1569
+ exports.addEmit = addEmit;
1570
+ exports.addSet = addSet;
1571
+ exports.astToTree = astToTree;
1572
+ exports.binary = binary;
1573
+ exports.boolLit = boolLit;
1574
+ exports.buildSubGroupClause = buildSubGroupClause;
1575
+ exports.callNode = callNode;
1576
+ exports.deleteAt = deleteAt;
1577
+ exports.displayName = displayName;
1578
+ exports.filterCatalogue = filterCatalogue;
1579
+ exports.findEnumPeer = findEnumPeer;
1580
+ exports.firstEmptyLeafPath = firstEmptyLeafPath;
1581
+ exports.flagDelta = flagDelta;
1582
+ exports.flipContainerOp = flipContainerOp;
1583
+ exports.formatNumber = formatNumber;
1584
+ exports.getNodeAt = getNodeAt;
1585
+ exports.groupByScope = groupByScope;
1586
+ exports.insertSiblingClauseAt = insertSiblingClauseAt;
1587
+ exports.isComparisonOp = isComparisonOp;
1588
+ exports.isPlaceholderForOp = isPlaceholderForOp;
1589
+ exports.isWrappedInNot = isWrappedInNot;
1590
+ exports.issuesAt = issuesAt;
1591
+ exports.lookup = lookup;
1592
+ exports.mountEffectsEditor = mountEffectsEditor;
1593
+ exports.mountExpressionEditor = mountExpressionEditor;
1594
+ exports.moveAt = moveAt;
1595
+ exports.moveChildInContainer = moveChildInContainer;
1596
+ exports.needsParens = needsParens;
1597
+ exports.notNode = notNode;
1598
+ exports.numLit = numLit;
1599
+ exports.opSwapGroup = opSwapGroup;
1600
+ exports.pathKey = pathKey;
1601
+ exports.placeholderForOp = placeholderForOp;
1602
+ exports.redirectDeleteForPlaceholderSibling = redirectDeleteForPlaceholderSibling;
1603
+ exports.refOf = refOf;
1604
+ exports.removeArgAt = removeArgAt;
1605
+ exports.removeAt = removeAt;
1606
+ exports.renderConditionPreview = renderConditionPreview;
1607
+ exports.renderEffectsPreview = renderEffectsPreview;
1608
+ exports.scopedVar = scopedVar;
1609
+ exports.searchCatalogue = searchCatalogue;
1610
+ exports.seedValueSrc = seedValueSrc;
1611
+ exports.setArgAt = setArgAt;
1612
+ exports.setNodeAt = setNodeAt;
1613
+ exports.strLit = strLit;
1614
+ exports.toggleContainerNot = toggleContainerNot;
1615
+ exports.toggleNotAt = toggleNotAt;
1616
+ exports.updateAt = updateAt;
1617
+ exports.validateSource = validateSource;
1618
+ exports.valueWizard = valueWizard;
1619
+ exports.wrapInNotAt = wrapInNotAt;
1620
+ //# sourceMappingURL=index.cjs.map
1621
+ //# sourceMappingURL=index.cjs.map