@wildwinter/expr-editor 0.12.2 → 0.13.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BinaryOp, ExprNode, AstPath, PropertyType, UnaryOp, ExpressionValidationResult, ExpressionValidationIssue, ExpressionSchema, Dialect } from '@wildwinter/expr';
1
+ import { ExprNode, BinaryOp, AstPath, PropertyType, UnaryOp, ExpressionValidationResult, ExpressionValidationIssue, ExpressionSchema, Dialect } from '@wildwinter/expr';
2
2
  export { ExpressionValidationIssue, ExpressionValidationResult, PropertyType } from '@wildwinter/expr';
3
3
 
4
4
  declare const boolLit: (value: boolean) => ExprNode;
@@ -53,6 +53,16 @@ declare function findChoicePeer(ast: ExprNode, path: AstPath): {
53
53
  } | null;
54
54
  /** @deprecated Renamed to {@link findChoicePeer} - it serves qualities as well as enums now. */
55
55
  declare const findEnumPeer: typeof findChoicePeer;
56
+ /** A name-form ref in ONE form, so two spellings of the same property compare equal: the default
57
+ * scope may be written or left off (`@patter.debt` and `@debt`), and refs are case-insensitive. */
58
+ declare const normaliseRef: (ref: string, defaultScope: string) => string;
59
+ /** The name-form ref a call ADVANCES, when the call is the plain `advance(@x)` shape: one scopedvar
60
+ * argument and nothing else. Null for any other call, and for an argument that is not a property
61
+ * (`advance(f(@x))` keeps its explicit form, since collapsing it would hide the middle).
62
+ *
63
+ * `advance` is the language's own built-in rather than a dialect function, so it is matched by name
64
+ * here the way the evaluator and validator match it. */
65
+ declare function advancedRef(node: ExprNode, defaultScope: string): string | null;
56
66
  /**
57
67
  * DFS for the first unfilled slot in a template-inserted clause: an empty string
58
68
  * literal (a tag / value the author still has to set) or an unnamed flag delta.
@@ -108,6 +118,12 @@ declare function moveChildInContainer(ast: ExprNode, chainPath: AstPath, from: n
108
118
  /** Human label for a binary operator (words for logical, glyphs for relational). */
109
119
  declare const BINARY_LABEL: Record<BinaryOp, string>;
110
120
  declare const UNARY_LABEL: Record<UnaryOp, string>;
121
+ /** Plain-language name for an operator. ONE table, because it is wanted in three places that must
122
+ * agree: a pill's accessible label, the clause wizard's operator step, and the swap menu. The swap
123
+ * menu once printed the label and the raw source instead ("> >"), which is what a second table
124
+ * buys you. Never the glyph twice: this reads NEXT to `BINARY_LABEL`, so it must say something the
125
+ * glyph does not. */
126
+ declare const OP_WORD: Record<BinaryOp | UnaryOp, string>;
111
127
  declare const COMPARISON_OPS: BinaryOp[];
112
128
  declare const ARITHMETIC_OPS: BinaryOp[];
113
129
  /** Property types a comparison clause can be built on - what the clause wizard offers as a left-hand
@@ -274,6 +290,11 @@ interface EditCtx {
274
290
  * (an enum-typed outcome target). Lets the string editor show an enum picker
275
291
  * even without a comparison peer. No effect on non-root literals. */
276
292
  valueEnumValues?: string[];
293
+ /** The name-form ref of the SET this value belongs to, when there is one. Its only use is the
294
+ * self-advance idiom: `@debt = advance(@debt)` names the property three times to say one thing,
295
+ * so when the value is exactly `advance(<this ref>)` the call collapses to a single "advances"
296
+ * word and the row reads `debt advances`. Any other target, or any other shape, renders in full. */
297
+ selfAdvanceRef?: string;
277
298
  /** Host actions for a property pill (e.g. "Go to definition"). When set,
278
299
  * right-clicking a property pill opens a menu of these actions. The host
279
300
  * resolves what each does (navigation etc.); empty array = no menu. */
@@ -329,6 +350,10 @@ interface ExpressionEditorOptions {
329
350
  /** Enum values for the single root literal of a value field (an enum-typed
330
351
  * target): the literal editor then offers them as a picker. Flat mode only. */
331
352
  valueEnumValues?: string[];
353
+ /** The ref of the SET this value belongs to (flat value fields only). Lets `advance(<that ref>)`
354
+ * render as a single "advances" word: the outcome row already names the property in its target
355
+ * column, so the call would say it twice more. */
356
+ selfAdvanceRef?: string;
332
357
  /** Flat single-value field: an empty value renders one editable "set a value…"
333
358
  * placeholder pill (a root string literal) rather than the condition
334
359
  * clause-menu empty state. Pairs with mode:"flat". */
@@ -492,4 +517,4 @@ interface ValueWizardOptions {
492
517
  /** Build the wizard UI into a fresh element (drive it via the host's popover). */
493
518
  declare function valueWizard(opts: ValueWizardOptions): HTMLElement;
494
519
 
495
- export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, type CatalogueEntry, EQUALITY_OPS, type EditCtx, type EditorEffect, type EffectsEditorHandle, type EffectsEditorOptions, type ExpressionEditorHandle, type ExpressionEditorOptions, type Filter, type FunctionTemplateSpec, type PreviewOptions, type TreeRow, UNARY_LABEL, type Validation, type ValueWizardOptions, type WizardSpec, type WizardStepSpec, type WizardValue, addArg, addChildToContainer, addEmit, addSet, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
520
+ export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, type CatalogueEntry, EQUALITY_OPS, type EditCtx, type EditorEffect, type EffectsEditorHandle, type EffectsEditorOptions, type ExpressionEditorHandle, type ExpressionEditorOptions, type Filter, type FunctionTemplateSpec, OP_WORD, type PreviewOptions, type TreeRow, UNARY_LABEL, type Validation, type ValueWizardOptions, type WizardSpec, type WizardStepSpec, type WizardValue, addArg, addChildToContainer, addEmit, addSet, advancedRef, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, normaliseRef, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BinaryOp, ExprNode, AstPath, PropertyType, UnaryOp, ExpressionValidationResult, ExpressionValidationIssue, ExpressionSchema, Dialect } from '@wildwinter/expr';
1
+ import { ExprNode, BinaryOp, AstPath, PropertyType, UnaryOp, ExpressionValidationResult, ExpressionValidationIssue, ExpressionSchema, Dialect } from '@wildwinter/expr';
2
2
  export { ExpressionValidationIssue, ExpressionValidationResult, PropertyType } from '@wildwinter/expr';
3
3
 
4
4
  declare const boolLit: (value: boolean) => ExprNode;
@@ -53,6 +53,16 @@ declare function findChoicePeer(ast: ExprNode, path: AstPath): {
53
53
  } | null;
54
54
  /** @deprecated Renamed to {@link findChoicePeer} - it serves qualities as well as enums now. */
55
55
  declare const findEnumPeer: typeof findChoicePeer;
56
+ /** A name-form ref in ONE form, so two spellings of the same property compare equal: the default
57
+ * scope may be written or left off (`@patter.debt` and `@debt`), and refs are case-insensitive. */
58
+ declare const normaliseRef: (ref: string, defaultScope: string) => string;
59
+ /** The name-form ref a call ADVANCES, when the call is the plain `advance(@x)` shape: one scopedvar
60
+ * argument and nothing else. Null for any other call, and for an argument that is not a property
61
+ * (`advance(f(@x))` keeps its explicit form, since collapsing it would hide the middle).
62
+ *
63
+ * `advance` is the language's own built-in rather than a dialect function, so it is matched by name
64
+ * here the way the evaluator and validator match it. */
65
+ declare function advancedRef(node: ExprNode, defaultScope: string): string | null;
56
66
  /**
57
67
  * DFS for the first unfilled slot in a template-inserted clause: an empty string
58
68
  * literal (a tag / value the author still has to set) or an unnamed flag delta.
@@ -108,6 +118,12 @@ declare function moveChildInContainer(ast: ExprNode, chainPath: AstPath, from: n
108
118
  /** Human label for a binary operator (words for logical, glyphs for relational). */
109
119
  declare const BINARY_LABEL: Record<BinaryOp, string>;
110
120
  declare const UNARY_LABEL: Record<UnaryOp, string>;
121
+ /** Plain-language name for an operator. ONE table, because it is wanted in three places that must
122
+ * agree: a pill's accessible label, the clause wizard's operator step, and the swap menu. The swap
123
+ * menu once printed the label and the raw source instead ("> >"), which is what a second table
124
+ * buys you. Never the glyph twice: this reads NEXT to `BINARY_LABEL`, so it must say something the
125
+ * glyph does not. */
126
+ declare const OP_WORD: Record<BinaryOp | UnaryOp, string>;
111
127
  declare const COMPARISON_OPS: BinaryOp[];
112
128
  declare const ARITHMETIC_OPS: BinaryOp[];
113
129
  /** Property types a comparison clause can be built on - what the clause wizard offers as a left-hand
@@ -274,6 +290,11 @@ interface EditCtx {
274
290
  * (an enum-typed outcome target). Lets the string editor show an enum picker
275
291
  * even without a comparison peer. No effect on non-root literals. */
276
292
  valueEnumValues?: string[];
293
+ /** The name-form ref of the SET this value belongs to, when there is one. Its only use is the
294
+ * self-advance idiom: `@debt = advance(@debt)` names the property three times to say one thing,
295
+ * so when the value is exactly `advance(<this ref>)` the call collapses to a single "advances"
296
+ * word and the row reads `debt advances`. Any other target, or any other shape, renders in full. */
297
+ selfAdvanceRef?: string;
277
298
  /** Host actions for a property pill (e.g. "Go to definition"). When set,
278
299
  * right-clicking a property pill opens a menu of these actions. The host
279
300
  * resolves what each does (navigation etc.); empty array = no menu. */
@@ -329,6 +350,10 @@ interface ExpressionEditorOptions {
329
350
  /** Enum values for the single root literal of a value field (an enum-typed
330
351
  * target): the literal editor then offers them as a picker. Flat mode only. */
331
352
  valueEnumValues?: string[];
353
+ /** The ref of the SET this value belongs to (flat value fields only). Lets `advance(<that ref>)`
354
+ * render as a single "advances" word: the outcome row already names the property in its target
355
+ * column, so the call would say it twice more. */
356
+ selfAdvanceRef?: string;
332
357
  /** Flat single-value field: an empty value renders one editable "set a value…"
333
358
  * placeholder pill (a root string literal) rather than the condition
334
359
  * clause-menu empty state. Pairs with mode:"flat". */
@@ -492,4 +517,4 @@ interface ValueWizardOptions {
492
517
  /** Build the wizard UI into a fresh element (drive it via the host's popover). */
493
518
  declare function valueWizard(opts: ValueWizardOptions): HTMLElement;
494
519
 
495
- export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, type CatalogueEntry, EQUALITY_OPS, type EditCtx, type EditorEffect, type EffectsEditorHandle, type EffectsEditorOptions, type ExpressionEditorHandle, type ExpressionEditorOptions, type Filter, type FunctionTemplateSpec, type PreviewOptions, type TreeRow, UNARY_LABEL, type Validation, type ValueWizardOptions, type WizardSpec, type WizardStepSpec, type WizardValue, addArg, addChildToContainer, addEmit, addSet, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
520
+ export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, type CatalogueEntry, EQUALITY_OPS, type EditCtx, type EditorEffect, type EffectsEditorHandle, type EffectsEditorOptions, type ExpressionEditorHandle, type ExpressionEditorOptions, type Filter, type FunctionTemplateSpec, OP_WORD, type PreviewOptions, type TreeRow, UNARY_LABEL, type Validation, type ValueWizardOptions, type WizardSpec, type WizardStepSpec, type WizardValue, addArg, addChildToContainer, addEmit, addSet, advancedRef, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, normaliseRef, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { parseAndValidate, unparse } from '@wildwinter/expr';
1
+ import { parseAndValidate, parse, unparse } from '@wildwinter/expr';
2
2
 
3
3
  // src/ast.ts
4
4
  var boolLit = (value) => ({ kind: "bool", value });
@@ -91,6 +91,16 @@ function findChoicePeer(ast, path) {
91
91
  return other.kind === "scopedvar" ? { scope: other.scope, name: other.name } : null;
92
92
  }
93
93
  var findEnumPeer = findChoicePeer;
94
+ var normaliseRef = (ref, defaultScope) => {
95
+ const low = ref.toLowerCase();
96
+ const prefix = `@${defaultScope.toLowerCase()}.`;
97
+ return low.startsWith(prefix) ? `@${low.slice(prefix.length)}` : low;
98
+ };
99
+ function advancedRef(node, defaultScope) {
100
+ if (node.kind !== "call" || node.name !== "advance" || node.args.length !== 1) return null;
101
+ const arg = node.args[0];
102
+ return arg.kind === "scopedvar" ? normaliseRef(`@${arg.scope}.${arg.name}`, defaultScope) : null;
103
+ }
94
104
  function firstEmptyLeafPath(node, base = []) {
95
105
  switch (node.kind) {
96
106
  case "string":
@@ -219,6 +229,22 @@ var BINARY_LABEL = {
219
229
  "/": "\xF7"
220
230
  };
221
231
  var UNARY_LABEL = { not: "NOT", neg: "\u2212" };
232
+ var OP_WORD = {
233
+ "==": "equals",
234
+ "!=": "not equal to",
235
+ ">": "greater than",
236
+ ">=": "at least",
237
+ "<": "less than",
238
+ "<=": "at most",
239
+ and: "and",
240
+ or: "or",
241
+ "+": "plus",
242
+ "-": "minus",
243
+ "*": "times",
244
+ "/": "divided by",
245
+ not: "not",
246
+ neg: "negative"
247
+ };
222
248
  var COMPARISON_OPS = ["==", "!=", ">", ">=", "<", "<="];
223
249
  var ARITHMETIC_OPS = ["+", "-", "*", "/"];
224
250
  var COMPARABLE_TYPES = ["boolean", "number", "string", "enum", "quality"];
@@ -404,20 +430,6 @@ function pill(kind, label, onClick, opts = {}) {
404
430
  if (opts.path) b.dataset["exedPath"] = opts.path.join("/");
405
431
  return b;
406
432
  }
407
- var OP_ARIA = {
408
- "==": "equals",
409
- "!=": "not equal to",
410
- ">": "greater than",
411
- ">=": "at least",
412
- "<": "less than",
413
- "<=": "at most",
414
- "and": "and",
415
- "or": "or",
416
- "+": "plus",
417
- "-": "minus",
418
- "*": "times",
419
- "/": "divided by"
420
- };
421
433
  var issueText = (ctx, path) => {
422
434
  const list = issuesAt(ctx.byPath, path);
423
435
  return list.length ? list.map((i) => i.message).join("; ") : void 0;
@@ -459,6 +471,16 @@ function renderNode(node, path, ctx, parentOp, side) {
459
471
  for (let i = 1; i < node.args.length; i++) row2.append(renderNode(node.args[i], [...path, "args", i], ctx));
460
472
  return row2;
461
473
  }
474
+ if (ctx.selfAdvanceRef && advancedRef(node, ctx.defaultScope) === normaliseRef(ctx.selfAdvanceRef, ctx.defaultScope)) {
475
+ const row2 = el("span", "exed-call exed-call-advance");
476
+ row2.append(pill(
477
+ "advance",
478
+ "advances",
479
+ (b) => callEditor(ctx, path, node, b),
480
+ { issue, title: "advance one stage", aria: "advances one stage" }
481
+ ));
482
+ return row2;
483
+ }
462
484
  const row = el("span", "exed-call");
463
485
  row.append(pill("func", node.name, (b) => callEditor(ctx, path, node, b), { issue }));
464
486
  row.append(el("span", "exed-paren", ["("]));
@@ -471,7 +493,7 @@ function renderNode(node, path, ctx, parentOp, side) {
471
493
  }
472
494
  case "unary": {
473
495
  const row = el("span", "exed-unary");
474
- row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove", aria: `${OP_ARIA[node.op] ?? node.op} (click to remove)` }));
496
+ row.append(pill("op", UNARY_LABEL[node.op], () => ctx.apply(deleteOrUnwrapNot(ctx, path, node)), { title: "remove", aria: `${OP_WORD[node.op]} (click to remove)` }));
475
497
  row.append(renderNode(node.operand, [...path, "operand"], ctx));
476
498
  return row;
477
499
  }
@@ -483,7 +505,7 @@ function renderNode(node, path, ctx, parentOp, side) {
483
505
  const swap = opSwapGroup(node.op);
484
506
  const opPill = pill("op", BINARY_LABEL[node.op], (b) => {
485
507
  if (swap) operatorEditor(ctx, path, node.op, swap, b);
486
- }, { title: swap ? "swap operator" : void 0, aria: OP_ARIA[node.op] ?? node.op });
508
+ }, { title: swap ? "swap operator" : void 0, aria: OP_WORD[node.op] });
487
509
  if (!swap) opPill.classList.add("exed-op-structural");
488
510
  row.append(opPill);
489
511
  row.append(renderNode(node.right, [...path, "right"], ctx, node.op, "right"));
@@ -594,12 +616,14 @@ function operatorEditor(ctx, path, current, group, anchor) {
594
616
  const wrap = el("div", "exed-menu");
595
617
  wrap.append(el("div", "exed-menu-head", ["Operator"]));
596
618
  for (const op of group) {
597
- wrap.append(button(`exed-opt${op === current ? " sel" : ""}`, `${BINARY_LABEL[op]} ${op}`, () => {
619
+ const row = button(`exed-opt exed-opt-op${op === current ? " sel" : ""}`, "", () => {
598
620
  const node = ctx.getAst();
599
621
  const cur = setNodeAt(node, path, { ...getBinary(ctx, path), op });
600
622
  ctx.apply(cur);
601
623
  close();
602
- }));
624
+ });
625
+ row.append(el("span", "exed-opt-name", [BINARY_LABEL[op]]), el("span", "exed-opt-purpose", [OP_WORD[op]]));
626
+ wrap.append(row);
603
627
  }
604
628
  return wrap;
605
629
  });
@@ -715,17 +739,9 @@ function propertyPicker(ctx, opts) {
715
739
  }
716
740
 
717
741
  // src/clausewizard.ts
718
- var OP_WORD = {
719
- "==": "equals",
720
- "!=": "not equal to",
721
- ">": "greater than",
722
- ">=": "at least",
723
- "<": "less than",
724
- "<=": "at most"
725
- };
726
742
  var opButton = (o, onClick) => {
727
- const b = button("exed-opt", "", onClick);
728
- b.append(el("span", "exed-opt-name", [BINARY_LABEL[o]]), el("span", "exed-opt-purpose", [OP_WORD[o] ?? ""]));
743
+ const b = button("exed-opt exed-opt-op", "", onClick);
744
+ b.append(el("span", "exed-opt-name", [BINARY_LABEL[o]]), el("span", "exed-opt-purpose", [OP_WORD[o]]));
729
745
  return b;
730
746
  };
731
747
  function header(host, title, back, cancel) {
@@ -1148,6 +1164,7 @@ function mountExpressionEditor(host, opts) {
1148
1164
  },
1149
1165
  ...opts.requireNonEmpty ? { requireNonEmpty: true } : {},
1150
1166
  ...opts.valueEnumValues ? { valueEnumValues: opts.valueEnumValues } : {},
1167
+ ...opts.selfAdvanceRef ? { selfAdvanceRef: opts.selfAdvanceRef } : {},
1151
1168
  ...opts.propertyActions ? { propertyActions: opts.propertyActions } : {},
1152
1169
  ...opts.pickNode ? { pickNode: opts.pickNode } : {},
1153
1170
  ...opts.nodeLabel ? { nodeLabel: opts.nodeLabel } : {}
@@ -1411,6 +1428,14 @@ function valueWizard(opts) {
1411
1428
  }
1412
1429
 
1413
1430
  // src/effects.ts
1431
+ function isSelfAdvance(eff, defaultScope, dialect) {
1432
+ if (eff.kind !== "set" || !eff.target) return false;
1433
+ try {
1434
+ return advancedRef(parse(eff.value, dialect), defaultScope) === normaliseRef(eff.target, defaultScope);
1435
+ } catch {
1436
+ return false;
1437
+ }
1438
+ }
1414
1439
  var clone = (list) => list.map((e) => e.kind === "set" ? { ...e } : { ...e, args: [...e.args] });
1415
1440
  var addSet = (list, target, value) => [...clone(list), { kind: "set", target, value }];
1416
1441
  var addEmit = (list, event) => [...clone(list), { kind: "emit", event, args: [] }];
@@ -1507,7 +1532,7 @@ function mountEffectsEditor(host, opts) {
1507
1532
  }), { container: opts.popoverContainer });
1508
1533
  };
1509
1534
  const targetType = (target) => opts.catalogue.find((e) => refOf(e, defaultScope) === target)?.type;
1510
- const valueDisplay = (value, onChange, type) => {
1535
+ const valueDisplay = (value, onChange, type, selfAdvanceRef) => {
1511
1536
  const sub = el("div", "exed-effect-value");
1512
1537
  const addTerm = type === "number" ? "arithmetic" : type === "boolean" ? "boolean" : void 0;
1513
1538
  inner.push(mountExpressionEditor(sub, {
@@ -1519,6 +1544,7 @@ function mountEffectsEditor(host, opts) {
1519
1544
  functions: opts.functions,
1520
1545
  mode: "flat",
1521
1546
  ...addTerm ? { addTerm } : {},
1547
+ ...selfAdvanceRef ? { selfAdvanceRef } : {},
1522
1548
  ...opts.popoverContainer ? { popoverContainer: opts.popoverContainer } : {},
1523
1549
  text: textMode,
1524
1550
  onChange
@@ -1569,11 +1595,18 @@ function mountEffectsEditor(host, opts) {
1569
1595
  commit(updateAt(effects, i, { target: refOf(entry, defaultScope), value: initialValueFor(entry, defaultScope).src }));
1570
1596
  });
1571
1597
  }, "change the target property");
1572
- row.append(targetBtn, el("span", "exed-effect-eq", ["="]));
1598
+ const advances = isSelfAdvance(eff, defaultScope, opts.dialect);
1599
+ row.append(targetBtn);
1600
+ if (!advances) row.append(el("span", "exed-effect-eq", ["="]));
1573
1601
  row.append(valueDisplay(eff.value, (src) => {
1574
- effects = updateAt(effects, i, { value: src });
1602
+ const next = updateAt(effects, i, { value: src });
1603
+ if (isSelfAdvance(next[i], defaultScope, opts.dialect) !== advances) {
1604
+ commit(next);
1605
+ return;
1606
+ }
1607
+ effects = next;
1575
1608
  opts.onChange(effects);
1576
- }, targetType(eff.target)));
1609
+ }, targetType(eff.target), advances ? eff.target : void 0));
1577
1610
  row.append(rowActions2(i));
1578
1611
  return row;
1579
1612
  }
@@ -1687,7 +1720,7 @@ function mountEffectsEditor(host, opts) {
1687
1720
  }
1688
1721
 
1689
1722
  // src/preview.ts
1690
- function frozenCtx(src, o) {
1723
+ function frozenCtx(src, o, selfAdvanceRef) {
1691
1724
  const v = validateSource(src, o.schema, o.dialect);
1692
1725
  const ctx = {
1693
1726
  schema: o.schema,
@@ -1706,6 +1739,7 @@ function frozenCtx(src, o) {
1706
1739
  pickNode: () => {
1707
1740
  },
1708
1741
  // enables labelled node pills; never fires (preview is read-only)
1742
+ ...selfAdvanceRef ? { selfAdvanceRef } : {},
1709
1743
  ...o.nodeLabel ? { nodeLabel: o.nodeLabel } : {},
1710
1744
  ...o.propertyActions ? {
1711
1745
  propertyActions: o.propertyActions,
@@ -1716,8 +1750,8 @@ function frozenCtx(src, o) {
1716
1750
  };
1717
1751
  return { ctx, ast: v.ast };
1718
1752
  }
1719
- function exprPills(src, o) {
1720
- const { ctx, ast } = frozenCtx(src, o);
1753
+ function exprPills(src, o, selfAdvanceRef) {
1754
+ const { ctx, ast } = frozenCtx(src, o, selfAdvanceRef);
1721
1755
  return ast ? renderNode(ast, [], ctx) : el("span", "exed-preview-raw", [src]);
1722
1756
  }
1723
1757
  function renderConditionPreview(src, o) {
@@ -1729,9 +1763,10 @@ function renderEffectsPreview(effects, o) {
1729
1763
  for (const eff of effects) {
1730
1764
  const row = el("div", "exed-preview-eff");
1731
1765
  if (eff.kind === "set") {
1766
+ const advances = isSelfAdvance(eff, o.dialect.defaultScope, o.dialect);
1732
1767
  row.append(el("span", "exed-pill exed-pill-prop", [eff.target || "(property)"]));
1733
- row.append(el("span", "exed-effect-eq", ["="]));
1734
- row.append(exprPills(eff.value, o));
1768
+ if (!advances) row.append(el("span", "exed-effect-eq", ["="]));
1769
+ row.append(exprPills(eff.value, o, advances ? eff.target : void 0));
1735
1770
  } else {
1736
1771
  row.append(el("span", "exed-effect-kw", ["emit"]));
1737
1772
  row.append(el("span", "exed-pill exed-pill-event", [eff.event ? `"${eff.event}"` : "(name)"]));
@@ -1747,6 +1782,6 @@ function renderEffectsPreview(effects, o) {
1747
1782
  return wrap;
1748
1783
  }
1749
1784
 
1750
- export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, EQUALITY_OPS, UNARY_LABEL, addArg, addChildToContainer, addEmit, addSet, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
1785
+ export { ARITHMETIC_OPS, BINARY_LABEL, COMPARABLE_TYPES, COMPARISON_OPS, EQUALITY_OPS, OP_WORD, UNARY_LABEL, addArg, addChildToContainer, addEmit, addSet, advancedRef, astToTree, binary, boolLit, buildSubGroupClause, callNode, choicesOf, comparisonOpsFor, deleteAt, displayName, filterCatalogue, findChoicePeer, findEnumPeer, firstEmptyLeafPath, flagDelta, flipContainerOp, formatNumber, getNodeAt, groupByScope, initialValueFor, insertSiblingClauseAt, isComparisonOp, isPlaceholderForOp, isWrappedInNot, issuesAt, lookup, mountEffectsEditor, mountExpressionEditor, moveAt, moveChildInContainer, needsParens, normaliseRef, notNode, numLit, opSwapGroup, pathKey, placeholderForOp, redirectDeleteForPlaceholderSibling, refOf, removeArgAt, removeAt, renderConditionPreview, renderEffectsPreview, rhsTypesFor, scopedVar, searchCatalogue, seedValueSrc, setArgAt, setNodeAt, strLit, toggleContainerNot, toggleNotAt, updateAt, validateSource, valueWizard, wrapInNotAt };
1751
1786
  //# sourceMappingURL=index.js.map
1752
1787
  //# sourceMappingURL=index.js.map