@hyperfixi/core 3.1.0 → 3.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.
Files changed (97) hide show
  1. package/README.md +28 -36
  2. package/dist/ast/command-slots.d.ts +1 -1
  3. package/dist/ast/nodes.d.ts +2 -0
  4. package/dist/ast-utils/index.cjs +193 -48
  5. package/dist/ast-utils/index.mjs +193 -48
  6. package/dist/ast-utils/interchange/types.d.ts +19 -1
  7. package/dist/behaviors/index.cjs +19 -0
  8. package/dist/behaviors/index.mjs +19 -0
  9. package/dist/bundle-generator/index.cjs +24 -9
  10. package/dist/bundle-generator/index.mjs +24 -9
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/chunks/bridge-Cnj8dQFV.js +2 -0
  13. package/dist/chunks/browser-modular-BNDYF6Dq.js +2 -0
  14. package/dist/chunks/index-DQi2o8Lk.js +2 -0
  15. package/dist/chunks/index-LLuwEQp0.js +2 -0
  16. package/dist/chunks/index-uCOQ8Qn0.js +2 -0
  17. package/dist/commands/animation/transition.d.ts +12 -6
  18. package/dist/commands/helpers/loop-executor.d.ts +2 -2
  19. package/dist/commands/helpers/numeric-target-parser.d.ts +1 -1
  20. package/dist/commands/helpers/variable-access.d.ts +5 -2
  21. package/dist/commands/helpers/write-target.d.ts +5 -0
  22. package/dist/commands/index.cjs +929 -425
  23. package/dist/commands/index.mjs +929 -425
  24. package/dist/commands/templates/render.d.ts +4 -4
  25. package/dist/commands/utility/beep.d.ts +0 -3
  26. package/dist/expressions/index.cjs +7 -13
  27. package/dist/expressions/index.mjs +7 -13
  28. package/dist/htmx/i18n-hooks.d.ts +4 -1
  29. package/dist/hyperfixi-browser-classic-i18n.js +1 -1
  30. package/dist/hyperfixi-browser.js +1 -1
  31. package/dist/hyperfixi-classic-i18n.js +1 -1
  32. package/dist/hyperfixi-hx-v4.js +1 -1
  33. package/dist/hyperfixi-hx.js +1 -1
  34. package/dist/hyperfixi-hybrid-complete.js +1 -1
  35. package/dist/hyperfixi-hybrid-hx.js +1 -1
  36. package/dist/hyperfixi-multilingual.js +1 -1
  37. package/dist/hyperfixi.js +1 -1
  38. package/dist/hyperfixi.mjs +1 -1
  39. package/dist/index.cjs +1140 -485
  40. package/dist/index.min.js +1 -1
  41. package/dist/index.mjs +1140 -485
  42. package/dist/lib/swap-executor.d.ts +1 -1
  43. package/dist/lokascript-browser-classic-i18n.js +1 -1
  44. package/dist/lokascript-browser.js +1 -1
  45. package/dist/lokascript-hybrid-complete.js +1 -1
  46. package/dist/lokascript-hybrid-hx.js +1 -1
  47. package/dist/lokascript-multilingual.js +1 -1
  48. package/dist/metadata.cjs +9 -9
  49. package/dist/metadata.d.ts +1 -1
  50. package/dist/metadata.mjs +9 -9
  51. package/dist/parser/command-grammar.d.ts +1 -0
  52. package/dist/parser/full-parser.cjs +387 -153
  53. package/dist/parser/full-parser.mjs +387 -153
  54. package/dist/parser/helpers/parsing-helpers.d.ts +3 -0
  55. package/dist/parser/hybrid/index.cjs +3 -0
  56. package/dist/parser/hybrid/index.mjs +3 -0
  57. package/dist/parser/hybrid/parser-core.cjs +3 -0
  58. package/dist/parser/hybrid/parser-core.mjs +3 -0
  59. package/dist/parser/hybrid-parser.cjs +3 -0
  60. package/dist/parser/hybrid-parser.mjs +3 -0
  61. package/dist/parser/parser-types.d.ts +2 -0
  62. package/dist/parser/parser.d.ts +9 -0
  63. package/dist/parser/pratt-parser.d.ts +2 -0
  64. package/dist/registry/index.cjs +526 -210
  65. package/dist/registry/index.mjs +526 -210
  66. package/dist/utils/beep.d.ts +8 -0
  67. package/dist/version.d.ts +1 -1
  68. package/package.json +21 -18
  69. package/vocab/htmx/ar.js +22 -20
  70. package/vocab/htmx/bn.js +21 -5
  71. package/vocab/htmx/de.js +33 -10
  72. package/vocab/htmx/en.js +4 -1
  73. package/vocab/htmx/es.js +28 -9
  74. package/vocab/htmx/fr.js +28 -8
  75. package/vocab/htmx/he.js +22 -1
  76. package/vocab/htmx/hi.js +28 -7
  77. package/vocab/htmx/id.js +29 -10
  78. package/vocab/htmx/it.js +26 -5
  79. package/vocab/htmx/ja.js +36 -11
  80. package/vocab/htmx/ko.js +27 -6
  81. package/vocab/htmx/ms.js +24 -1
  82. package/vocab/htmx/pl.js +27 -7
  83. package/vocab/htmx/pt.js +30 -9
  84. package/vocab/htmx/qu.js +32 -9
  85. package/vocab/htmx/ru.js +28 -6
  86. package/vocab/htmx/sw.js +28 -8
  87. package/vocab/htmx/th.js +19 -1
  88. package/vocab/htmx/tl.js +24 -1
  89. package/vocab/htmx/tr.js +34 -8
  90. package/vocab/htmx/uk.js +28 -6
  91. package/vocab/htmx/vi.js +27 -19
  92. package/vocab/htmx/zh.js +28 -9
  93. package/dist/chunks/bridge-DylVo1oZ.js +0 -2
  94. package/dist/chunks/browser-modular-sLPMdxR7.js +0 -2
  95. package/dist/chunks/index-C7lqD4mo.js +0 -2
  96. package/dist/chunks/index-CAPhqA6k.js +0 -2
  97. package/dist/chunks/index-CLdRyZaP.js +0 -2
@@ -537,6 +537,75 @@ function parseAttributeWithValue(expression) {
537
537
  return { name: result.name, value: result.value ?? '' };
538
538
  }
539
539
 
540
+ function _parseDurationComponents(value) {
541
+ const trimmed = value.trim();
542
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*(s|ms|sec|seconds?|milliseconds?)?$/i);
543
+ if (!match) {
544
+ return null;
545
+ }
546
+ const number = parseFloat(match[1]);
547
+ const unit = (match[2] || 'ms').toLowerCase();
548
+ if (unit === 'ms' || unit === 'millisecond' || unit === 'milliseconds') {
549
+ return { number, milliseconds: Math.floor(number) };
550
+ }
551
+ else if (unit === 's' || unit === 'sec' || unit === 'second' || unit === 'seconds') {
552
+ return { number, milliseconds: Math.floor(number * 1000) };
553
+ }
554
+ return null;
555
+ }
556
+ function parseDuration(value, defaultMs = 300) {
557
+ if (typeof value === 'number') {
558
+ return Math.max(0, Math.floor(value));
559
+ }
560
+ if (typeof value === 'string') {
561
+ const result = _parseDurationComponents(value);
562
+ return result ? result.milliseconds : defaultMs;
563
+ }
564
+ return defaultMs;
565
+ }
566
+ function parseDurationStrict(value) {
567
+ if (typeof value === 'number') {
568
+ if (value < 0)
569
+ throw new Error('Duration must be >= 0');
570
+ return Math.floor(value);
571
+ }
572
+ if (typeof value === 'string') {
573
+ const result = _parseDurationComponents(value);
574
+ if (!result) {
575
+ throw new Error(`Invalid duration format: "${value}"`);
576
+ }
577
+ return result.milliseconds;
578
+ }
579
+ throw new Error(`Invalid duration type: ${typeof value}`);
580
+ }
581
+ function parseCSSDurations(durationString) {
582
+ if (!durationString || durationString === 'none') {
583
+ return [0];
584
+ }
585
+ return durationString.split(',').map(duration => {
586
+ const value = parseFloat(duration.trim());
587
+ if (isNaN(value))
588
+ return 0;
589
+ if (duration.includes('s') && !duration.includes('ms')) {
590
+ return value * 1000;
591
+ }
592
+ return value;
593
+ });
594
+ }
595
+ function calculateMaxAnimationTime(durations, delays) {
596
+ let maxTime = 0;
597
+ for (let i = 0; i < durations.length; i++) {
598
+ const duration = durations[i] || 0;
599
+ const delay = delays[i] || 0;
600
+ const totalTime = duration + delay;
601
+ maxTime = Math.max(maxTime, totalTime);
602
+ }
603
+ return maxTime;
604
+ }
605
+ function camelToKebab(str) {
606
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
607
+ }
608
+
540
609
  function batchApply(targets, operation) {
541
610
  for (const element of targets) {
542
611
  operation(element);
@@ -602,15 +671,18 @@ function toggleAttribute(element, name, value) {
602
671
  }
603
672
  }
604
673
  }
674
+ function toCssPropertyName(property) {
675
+ return property.startsWith('--') ? property : camelToKebab(property);
676
+ }
605
677
  function batchSetStyles(targets, styles) {
606
678
  const entries = Object.entries(styles);
607
679
  return batchApplyItems(targets, entries, (element, [property, value]) => {
608
- element.style.setProperty(property, value);
680
+ element.style.setProperty(toCssPropertyName(property), value);
609
681
  });
610
682
  }
611
683
  function batchRemoveStyles(targets, properties) {
612
684
  return batchApplyItems(targets, properties, (element, property) => {
613
- element.style.removeProperty(property);
685
+ element.style.removeProperty(toCssPropertyName(property));
614
686
  });
615
687
  }
616
688
 
@@ -1345,75 +1417,6 @@ let RemoveCommand = (() => {
1345
1417
  })();
1346
1418
  const createRemoveCommand = createFactory(RemoveCommand);
1347
1419
 
1348
- function _parseDurationComponents(value) {
1349
- const trimmed = value.trim();
1350
- const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*(s|ms|sec|seconds?|milliseconds?)?$/i);
1351
- if (!match) {
1352
- return null;
1353
- }
1354
- const number = parseFloat(match[1]);
1355
- const unit = (match[2] || 'ms').toLowerCase();
1356
- if (unit === 'ms' || unit === 'millisecond' || unit === 'milliseconds') {
1357
- return { number, milliseconds: Math.floor(number) };
1358
- }
1359
- else if (unit === 's' || unit === 'sec' || unit === 'second' || unit === 'seconds') {
1360
- return { number, milliseconds: Math.floor(number * 1000) };
1361
- }
1362
- return null;
1363
- }
1364
- function parseDuration(value, defaultMs = 300) {
1365
- if (typeof value === 'number') {
1366
- return Math.max(0, Math.floor(value));
1367
- }
1368
- if (typeof value === 'string') {
1369
- const result = _parseDurationComponents(value);
1370
- return result ? result.milliseconds : defaultMs;
1371
- }
1372
- return defaultMs;
1373
- }
1374
- function parseDurationStrict(value) {
1375
- if (typeof value === 'number') {
1376
- if (value < 0)
1377
- throw new Error('Duration must be >= 0');
1378
- return Math.floor(value);
1379
- }
1380
- if (typeof value === 'string') {
1381
- const result = _parseDurationComponents(value);
1382
- if (!result) {
1383
- throw new Error(`Invalid duration format: "${value}"`);
1384
- }
1385
- return result.milliseconds;
1386
- }
1387
- throw new Error(`Invalid duration type: ${typeof value}`);
1388
- }
1389
- function parseCSSDurations(durationString) {
1390
- if (!durationString || durationString === 'none') {
1391
- return [0];
1392
- }
1393
- return durationString.split(',').map(duration => {
1394
- const value = parseFloat(duration.trim());
1395
- if (isNaN(value))
1396
- return 0;
1397
- if (duration.includes('s') && !duration.includes('ms')) {
1398
- return value * 1000;
1399
- }
1400
- return value;
1401
- });
1402
- }
1403
- function calculateMaxAnimationTime(durations, delays) {
1404
- let maxTime = 0;
1405
- for (let i = 0; i < durations.length; i++) {
1406
- const duration = durations[i] || 0;
1407
- const delay = delays[i] || 0;
1408
- const totalTime = duration + delay;
1409
- maxTime = Math.max(maxTime, totalTime);
1410
- }
1411
- return maxTime;
1412
- }
1413
- function camelToKebab(str) {
1414
- return str.replace(/([A-Z])/g, '-$1').toLowerCase();
1415
- }
1416
-
1417
1420
  function detectSmartElementType(elements) {
1418
1421
  if (elements.length === 0)
1419
1422
  return null;
@@ -1879,13 +1882,6 @@ function isFunction(value) {
1879
1882
  function isInputElement$1(el) {
1880
1883
  return (el !== null && typeof el === 'object' && 'tagName' in el && el.tagName === 'INPUT');
1881
1884
  }
1882
- function isFormElement$1(el) {
1883
- if (el === null || typeof el !== 'object' || !('tagName' in el)) {
1884
- return false;
1885
- }
1886
- const tag = el.tagName;
1887
- return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
1888
- }
1889
1885
  function isHTMLElement$1(el) {
1890
1886
  return (el !== null &&
1891
1887
  typeof el === 'object' &&
@@ -1945,7 +1941,7 @@ const SPECIAL_DOM_PROPERTIES = {
1945
1941
  innertext: el => el.textContent?.trim(),
1946
1942
  innerHTML: el => el.innerHTML,
1947
1943
  outerhtml: el => el.outerHTML,
1948
- value: el => (isFormElement$1(el) ? el.value : undefined),
1944
+ value: el => ('value' in el ? el.value : undefined),
1949
1945
  checked: el => (isInputElement$1(el) ? el.checked : undefined),
1950
1946
  disabled: el => ('disabled' in el ? el.disabled : undefined),
1951
1947
  selected: el => (isOptionElement$1(el) ? el.selected : undefined),
@@ -2815,7 +2811,11 @@ let PutCommand = (() => {
2815
2811
  }
2816
2812
  if (isInsertableNode(v))
2817
2813
  return v;
2818
- return v == null ? '' : String(v);
2814
+ if (v == null)
2815
+ return 'null';
2816
+ if (Array.isArray(v))
2817
+ return arrayFragment(v);
2818
+ return String(v);
2819
2819
  }
2820
2820
  looksLikeCss(s) {
2821
2821
  if (!s)
@@ -2900,6 +2900,19 @@ let PutCommand = (() => {
2900
2900
  })();
2901
2901
  return PutCommand = _classThis;
2902
2902
  })();
2903
+ function arrayFragment(items) {
2904
+ const fragment = document.createDocumentFragment();
2905
+ for (const item of items) {
2906
+ if (isInsertableNode(item)) {
2907
+ fragment.append(item);
2908
+ continue;
2909
+ }
2910
+ const template = document.createElement('template');
2911
+ template.innerHTML = item === null ? '' : String(item);
2912
+ fragment.append(template.content);
2913
+ }
2914
+ return fragment;
2915
+ }
2903
2916
  const createPutCommand = createFactory(PutCommand);
2904
2917
 
2905
2918
  const GLOBAL_WRITE_HOOKS = new Set();
@@ -3022,6 +3035,16 @@ function convertToNumber(value) {
3022
3035
  }
3023
3036
  return 0;
3024
3037
  }
3038
+ function counterAmount(value) {
3039
+ return typeof value === 'number' ? value : parseFloat(String(value));
3040
+ }
3041
+ function counterValue(value) {
3042
+ if (isHTMLElement$2(value)) {
3043
+ const element = value;
3044
+ return counterValue(element.value || element.textContent);
3045
+ }
3046
+ return value ? counterAmount(value) : 0;
3047
+ }
3025
3048
  function getVariableValue(name, context, preferredScope) {
3026
3049
  if (preferredScope === 'element') {
3027
3050
  return getElementVar(context, name);
@@ -3126,35 +3149,39 @@ function setElementPropertyValue(propertyPath, value, context) {
3126
3149
  element[property] = value;
3127
3150
  }
3128
3151
  }
3152
+ function isObjectOwner(value) {
3153
+ return (typeof value === 'object' && value !== null && !Array.isArray(value) && !isHTMLElement$2(value));
3154
+ }
3129
3155
  function getCurrentNumericValue(target, property, scope, context) {
3130
3156
  if (typeof target === 'number') {
3131
3157
  return target;
3132
3158
  }
3133
3159
  if (isHTMLElement$2(target)) {
3134
3160
  const element = target;
3161
+ if (property?.startsWith('@'))
3162
+ return counterValue(element.getAttribute(property.slice(1)));
3135
3163
  if (property) {
3136
3164
  if (property.startsWith('data-') ||
3137
3165
  ['id', 'class', 'title', 'alt', 'src', 'href'].includes(property)) {
3138
- const value = element.getAttribute(property);
3139
- return convertToNumber(value);
3166
+ return counterValue(element.getAttribute(property));
3140
3167
  }
3141
3168
  else {
3142
- const value = element[property];
3143
- return convertToNumber(value);
3169
+ return counterValue(element[property]);
3144
3170
  }
3145
3171
  }
3146
3172
  else {
3147
- const value = element.value || element.textContent;
3148
- return convertToNumber(value);
3173
+ return counterValue(element);
3149
3174
  }
3150
3175
  }
3176
+ if (property && isObjectOwner(target)) {
3177
+ return counterValue(target[property]);
3178
+ }
3151
3179
  if (typeof target === 'string') {
3152
3180
  if (scope === 'element') {
3153
- return convertToNumber(getVariableValue(target, context, 'element'));
3181
+ return counterValue(getVariableValue(target, context, 'element'));
3154
3182
  }
3155
3183
  if (scope === 'global') {
3156
- const value = getVariableValue(target, context, 'global');
3157
- return convertToNumber(value);
3184
+ return counterValue(getVariableValue(target, context, 'global'));
3158
3185
  }
3159
3186
  if (target.includes('.')) {
3160
3187
  return getElementPropertyValue(target, context);
@@ -3168,14 +3195,17 @@ function getCurrentNumericValue(target, property, scope, context) {
3168
3195
  else if (target === 'you' && context.you) {
3169
3196
  return convertToNumber(context.you.value || 0);
3170
3197
  }
3171
- const value = getVariableValue(target, context);
3172
- return convertToNumber(value);
3198
+ return counterValue(getVariableValue(target, context));
3173
3199
  }
3174
- return convertToNumber(target);
3200
+ return counterValue(target);
3175
3201
  }
3176
3202
  function setTargetValue(target, property, scope, newValue, context) {
3177
3203
  if (isHTMLElement$2(target)) {
3178
3204
  const element = target;
3205
+ if (property?.startsWith('@')) {
3206
+ element.setAttribute(property.slice(1), String(newValue));
3207
+ return;
3208
+ }
3179
3209
  if (property) {
3180
3210
  if (property.startsWith('data-') ||
3181
3211
  ['id', 'class', 'title', 'alt', 'src', 'href'].includes(property)) {
@@ -3195,6 +3225,10 @@ function setTargetValue(target, property, scope, newValue, context) {
3195
3225
  }
3196
3226
  return;
3197
3227
  }
3228
+ if (property && isObjectOwner(target)) {
3229
+ target[property] = newValue;
3230
+ return;
3231
+ }
3198
3232
  if (typeof target === 'string') {
3199
3233
  if (scope === 'element') {
3200
3234
  setVariableValue(target, newValue, context, 'element');
@@ -3969,6 +4003,39 @@ function makeTypeCheckHandler(negated) {
3969
4003
  };
3970
4004
  };
3971
4005
  }
4006
+ const OF_OPERAND_BP = 70;
4007
+ function parseOfOperand(ctx, minBp = OF_OPERAND_BP) {
4008
+ let right = ctx.parseExpr(minBp);
4009
+ if (ctx.peek()?.value === 'in') {
4010
+ const inToken = ctx.advance();
4011
+ const scope = ctx.parseExpr(minBp);
4012
+ right = {
4013
+ type: 'binaryExpression',
4014
+ operator: 'in',
4015
+ left: right,
4016
+ right: scope,
4017
+ start: right.start,
4018
+ end: scope.end ?? inToken.end,
4019
+ line: right.line ?? inToken.line,
4020
+ column: right.column ?? inToken.column,
4021
+ };
4022
+ }
4023
+ return right;
4024
+ }
4025
+ const THE_OF_TARGET_BP = OF_OPERAND_BP + 1;
4026
+ function ofHandler(left, token, ctx) {
4027
+ const right = parseOfOperand(ctx);
4028
+ return {
4029
+ type: 'binaryExpression',
4030
+ operator: token.value,
4031
+ left,
4032
+ right,
4033
+ start: left.start,
4034
+ end: right.end ?? token.end,
4035
+ line: left.line ?? token.line,
4036
+ column: left.column ?? token.column,
4037
+ };
4038
+ }
3972
4039
  const PARSER_COMPARISON_FRAGMENT = new Map([
3973
4040
  ['===', leftAssoc(30)],
3974
4041
  ['!==', leftAssoc(30)],
@@ -4013,7 +4080,7 @@ const PARSER_COMPARISON_FRAGMENT = new Map([
4013
4080
  ['is greater than or equal to', leftAssoc(30)],
4014
4081
  ['is less than or equal to', leftAssoc(30)],
4015
4082
  ['in', leftAssoc(30)],
4016
- ['of', rightAssoc(30)],
4083
+ ['of', { infix: { bp: [90, OF_OPERAND_BP], handler: ofHandler } }],
4017
4084
  ['really', leftAssoc(30)],
4018
4085
  [
4019
4086
  'exists',
@@ -4104,6 +4171,89 @@ function getRegisteredNodeWriter(type) {
4104
4171
  return NODE_WRITERS.get(type);
4105
4172
  }
4106
4173
 
4174
+ function beepValues(me, values) {
4175
+ const outputs = values.map(describeBeepValue);
4176
+ const shown = outputs.filter(output => announce(me, output.value));
4177
+ if (shown.length === 0)
4178
+ return outputs;
4179
+ console.group('\ud83d\udd14 beep! Debug Output');
4180
+ for (const output of shown) {
4181
+ console.log(`Value:`, output.value);
4182
+ console.log(`Type:`, output.type);
4183
+ console.log(`Representation:`, output.representation);
4184
+ console.log('---');
4185
+ }
4186
+ console.groupEnd();
4187
+ return outputs;
4188
+ }
4189
+ function announce(me, value) {
4190
+ const target = me;
4191
+ if (typeof CustomEvent !== 'function' || typeof target?.dispatchEvent !== 'function')
4192
+ return true;
4193
+ return target.dispatchEvent(new CustomEvent('hyperscript:beep', {
4194
+ bubbles: true,
4195
+ cancelable: true,
4196
+ detail: { element: me, value },
4197
+ }));
4198
+ }
4199
+ function describeBeepValue(value) {
4200
+ return { value, type: typeOf(value), representation: representationOf(value) };
4201
+ }
4202
+ function typeOf(value) {
4203
+ if (value === null)
4204
+ return 'null';
4205
+ if (value === undefined)
4206
+ return 'undefined';
4207
+ if (Array.isArray(value))
4208
+ return 'array';
4209
+ if (isHTMLElement$2(value))
4210
+ return 'HTMLElement';
4211
+ if (typeof Element !== 'undefined' && value instanceof Element)
4212
+ return 'Element';
4213
+ if (typeof Node !== 'undefined' && value instanceof Node)
4214
+ return 'Node';
4215
+ if (value instanceof Error)
4216
+ return 'Error';
4217
+ if (value instanceof Date)
4218
+ return 'Date';
4219
+ if (value instanceof RegExp)
4220
+ return 'RegExp';
4221
+ return typeof value;
4222
+ }
4223
+ function representationOf(value) {
4224
+ if (value === null)
4225
+ return 'null';
4226
+ if (value === undefined)
4227
+ return 'undefined';
4228
+ if (Array.isArray(value)) {
4229
+ return `Array(${value.length}) [${value
4230
+ .slice(0, 3)
4231
+ .map(v => representationOf(v))
4232
+ .join(', ')}${value.length > 3 ? '...' : ''}]`;
4233
+ }
4234
+ if (isHTMLElement$2(value)) {
4235
+ const el = value;
4236
+ const tag = el.tagName.toLowerCase();
4237
+ const id = el.id ? `#${el.id}` : '';
4238
+ const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
4239
+ return `<${tag}${id}${classes}/>`;
4240
+ }
4241
+ if (value instanceof Error)
4242
+ return `Error: ${value.message}`;
4243
+ if (typeof value === 'string')
4244
+ return value.length > 50 ? `"${value.substring(0, 47)}..."` : `"${value}"`;
4245
+ if (typeof value === 'object') {
4246
+ try {
4247
+ const keys = Object.keys(value);
4248
+ return `Object {${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`;
4249
+ }
4250
+ catch {
4251
+ return '[Object]';
4252
+ }
4253
+ }
4254
+ return String(value);
4255
+ }
4256
+
4107
4257
  const NUMBER_WORDS = {
4108
4258
  1: 'one',
4109
4259
  2: 'two',
@@ -4705,14 +4855,15 @@ const closestExpression = {
4705
4855
  if (typeof selector !== 'string') {
4706
4856
  throw new Error('closest requires a string selector');
4707
4857
  }
4708
- if (!context.me) {
4858
+ const start = args.length > 1 ? args[1] : context.me;
4859
+ if (!(start instanceof Element)) {
4709
4860
  return null;
4710
4861
  }
4711
- return context.me.closest(selector);
4862
+ return start.closest(selector);
4712
4863
  },
4713
4864
  validate(args) {
4714
- if (args.length !== 1) {
4715
- return 'closest requires exactly 1 argument (selector)';
4865
+ if (args.length !== 1 && args.length !== 2) {
4866
+ return 'closest requires a selector and, optionally, the element to start from';
4716
4867
  }
4717
4868
  const arg = args[0];
4718
4869
  if (typeof arg === 'string')
@@ -7098,7 +7249,7 @@ const isEmptyExpression = {
7098
7249
  else if (value instanceof Node || value instanceof Element)
7099
7250
  result = false;
7100
7251
  else if (isObject(value))
7101
- result = Object.keys(value).length === 0;
7252
+ result = value.length === 0;
7102
7253
  else
7103
7254
  result = false;
7104
7255
  return result;
@@ -8175,6 +8326,48 @@ function parseMaybeNamedArgument(ctx) {
8175
8326
  const value = ctx.parseExpression();
8176
8327
  return name !== undefined ? { name, value } : { value };
8177
8328
  }
8329
+ function isNakedNamedArgStart(ctx) {
8330
+ if (ctx.check('{'))
8331
+ return false;
8332
+ if (!ctx.checkIdentifierLike())
8333
+ return false;
8334
+ const next = ctx.peekAt(1);
8335
+ return next !== null && next.value === ':';
8336
+ }
8337
+ function parseNakedNamedArgs(ctx, parseValue, command) {
8338
+ const properties = [];
8339
+ const startPos = ctx.getPosition();
8340
+ do {
8341
+ if (!ctx.checkIdentifierLike())
8342
+ break;
8343
+ const keyToken = ctx.advance();
8344
+ const key = {
8345
+ type: 'identifier',
8346
+ name: keyToken.value,
8347
+ start: keyToken.start,
8348
+ end: keyToken.end,
8349
+ line: keyToken.line,
8350
+ column: keyToken.column,
8351
+ };
8352
+ ctx.consume(':', `Expected ':' after property name in ${command} named arguments`);
8353
+ const value = parseValue();
8354
+ if (value) {
8355
+ properties.push({ key, value });
8356
+ }
8357
+ } while (ctx.match(',') && !ctx.isAtEnd());
8358
+ const endPos = ctx.getPosition();
8359
+ return {
8360
+ type: 'objectLiteral',
8361
+ properties,
8362
+ start: startPos.start,
8363
+ end: endPos.end,
8364
+ line: startPos.line,
8365
+ column: startPos.column,
8366
+ };
8367
+ }
8368
+ function isGluedBeepBang(beep, bang) {
8369
+ return beep?.value === 'beep' && bang?.value === '!' && bang.start === beep.end;
8370
+ }
8178
8371
 
8179
8372
  class CommandNodeBuilder {
8180
8373
  constructor(name) {
@@ -8376,6 +8569,8 @@ function parseTriggerCommand(ctx, identifierNode) {
8376
8569
  const modifiers = {};
8377
8570
  while (!isCommandBoundary(ctx)) {
8378
8571
  if (ctx.check(KEYWORDS.ON) || ctx.check(KEYWORDS.TO)) {
8572
+ if (modifiers['on'])
8573
+ break;
8379
8574
  ctx.advance();
8380
8575
  const target = parseOneArgument(ctx, [KEYWORDS.WITH]);
8381
8576
  if (target)
@@ -8516,6 +8711,9 @@ function parseRepeatCommand(ctx, commandToken) {
8516
8711
  ctx.advance();
8517
8712
  loopType = KEYWORDS.FOREVER;
8518
8713
  }
8714
+ else if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
8715
+ loopType = KEYWORDS.FOREVER;
8716
+ }
8519
8717
  else {
8520
8718
  times = ctx.parseExpression();
8521
8719
  if (ctx.check(KEYWORDS.TIMES)) {
@@ -8558,10 +8756,12 @@ function parseRepeatCommand(ctx, commandToken) {
8558
8756
  loopType = result.terminator;
8559
8757
  ctx.advance();
8560
8758
  condition = ctx.parseExpression();
8561
- if (!ctx.check('end')) {
8759
+ if (ctx.check('end')) {
8760
+ ctx.advance();
8761
+ }
8762
+ else if (!ctx.atEndOfInput()) {
8562
8763
  throw new Error('Expected "end" to close repeat block');
8563
8764
  }
8564
- ctx.advance();
8565
8765
  }
8566
8766
  }
8567
8767
  else {
@@ -8807,7 +9007,7 @@ function parseIfCommand(ctx, commandToken) {
8807
9007
  }));
8808
9008
  }
8809
9009
  }
8810
- if (!consumedElseIf && !ctx.isAtEnd()) {
9010
+ if (!consumedElseIf && !ctx.atEndOfInput()) {
8811
9011
  ctx.consume(KEYWORDS.END, "Expected 'end' after if block");
8812
9012
  }
8813
9013
  }
@@ -8832,7 +9032,6 @@ function parseIfCommand(ctx, commandToken) {
8832
9032
  .build();
8833
9033
  }
8834
9034
  function parseForCommand(ctx, commandToken) {
8835
- const args = [];
8836
9035
  let variable = null;
8837
9036
  let collection = null;
8838
9037
  if (ctx.check(KEYWORDS.EACH)) {
@@ -8875,31 +9074,22 @@ function parseForCommand(ctx, commandToken) {
8875
9074
  }
8876
9075
  }
8877
9076
  const commands = ctx.parseCommandListUntilEnd('for');
8878
- args.push({
8879
- type: 'identifier',
8880
- name: 'for',
8881
- start: commandToken.start,
8882
- end: commandToken.end,
8883
- line: commandToken.line,
8884
- column: commandToken.column,
8885
- });
8886
- const forPos = {
9077
+ const pos = {
8887
9078
  start: commandToken.start,
8888
9079
  end: commandToken.end,
8889
9080
  line: commandToken.line,
8890
9081
  column: commandToken.column,
8891
9082
  };
8892
- args.push(createStringLiteral(variable, forPos));
8893
- args.push(collection);
8894
- if (indexVariable) {
8895
- args.push(createStringLiteral(indexVariable, forPos));
8896
- }
8897
- args.push(createBlock(commands, { ...forPos, end: forPos.end || 0 }));
8898
- return CommandNodeBuilder.from({
8899
- ...commandToken,
8900
- value: 'repeat',
8901
- })
8902
- .withArgs(...args)
9083
+ const modifiers = {
9084
+ loopType: toLegacyExpression(createStringLiteral(KEYWORDS.FOR, pos)),
9085
+ for: toLegacyExpression(createStringLiteral(variable, pos)),
9086
+ in: collection,
9087
+ };
9088
+ if (indexVariable)
9089
+ modifiers['index'] = toLegacyExpression(createStringLiteral(indexVariable, pos));
9090
+ return CommandNodeBuilder.from({ ...commandToken, value: 'repeat' })
9091
+ .withArgs(createBlock(commands, { ...pos, end: pos.end || 0 }))
9092
+ .withModifiers(modifiers)
8903
9093
  .endingAt(ctx.getPosition())
8904
9094
  .build();
8905
9095
  }
@@ -8955,86 +9145,159 @@ function parseMeasureCommand(ctx, identifierNode) {
8955
9145
  }
8956
9146
  return builder.build();
8957
9147
  }
8958
- const CONTEXT_POSSESSIVES = new Set(['my', 'its', 'your']);
9148
+ const CONTEXT_REFS = new Set(['me', 'it', 'you', 'result']);
8959
9149
  function parseTransitionCommand(ctx, commandToken) {
8960
- const args = [];
8961
- const modifiers = {};
8962
- let property = null;
8963
- let target = null;
8964
- const leadToken = ctx.peek();
8965
- const startsTarget = ctx.checkAnySelector() ||
8966
- ctx.checkContextVar() ||
8967
- ctx.check('<') ||
8968
- (isIdentifierLike(leadToken) && CONTEXT_POSSESSIVES.has(leadToken.value));
8969
- if (startsTarget) {
8970
- const parsed = ctx.parsePrimary();
8971
- if ((parsed.type === 'possessiveExpression' ||
8972
- (parsed.type === 'memberExpression' && !parsed.computed)) &&
8973
- parsed.object &&
8974
- parsed.property?.name) {
8975
- target = parsed.object;
8976
- property = {
8977
- type: 'string',
8978
- value: parsed.property.name,
8979
- start: leadToken.start || 0,
8980
- end: ctx.getPosition().end,
8981
- line: leadToken.line,
8982
- column: leadToken.column,
8983
- };
9150
+ const pairs = [];
9151
+ do {
9152
+ pairs.push(parseTransitionPair(ctx));
9153
+ } while (startsAnotherPair(ctx));
9154
+ const [first, ...rest] = pairs;
9155
+ const args = first.owner ? [first.owner, first.property] : [first.property];
9156
+ const modifiers = { to: first.to };
9157
+ if (first.from)
9158
+ modifiers['from'] = first.from;
9159
+ if (rest.length > 0)
9160
+ modifiers['pairs'] = pairsNode(rest);
9161
+ for (let i = 0; i < 3 && !ctx.isAtEnd(); i++) {
9162
+ if (ctx.check('over') && !modifiers['over']) {
9163
+ ctx.advance();
9164
+ modifiers['over'] = ctx.parseExpression();
8984
9165
  }
8985
- else {
8986
- target = parsed;
8987
- if (ctx.check("'s"))
8988
- ctx.advance();
9166
+ else if (ctx.check('using') && !modifiers['using']) {
9167
+ ctx.advance();
9168
+ modifiers['using'] = ctx.parseExpression();
8989
9169
  }
8990
- }
8991
- const firstToken = ctx.peek();
8992
- if (!property && (isIdentifierLike(firstToken) || firstToken.value === '*')) {
8993
- let propertyValue = '';
8994
- if (ctx.check('*')) {
8995
- propertyValue = '*';
9170
+ else if (ctx.check(KEYWORDS.WITH) && !modifiers['with']) {
8996
9171
  ctx.advance();
9172
+ modifiers['with'] = ctx.parsePrimary();
8997
9173
  }
8998
- const hyphenatedName = parseHyphenatedName(ctx);
8999
- if (hyphenatedName) {
9000
- propertyValue += hyphenatedName;
9001
- property = {
9002
- type: 'string',
9003
- value: propertyValue,
9004
- start: firstToken.start || 0,
9005
- end: ctx.getPosition().end,
9006
- line: firstToken.line,
9007
- column: firstToken.column,
9008
- };
9174
+ else {
9175
+ break;
9009
9176
  }
9010
9177
  }
9011
- if (!property) {
9012
- throw new Error('Transition command requires a CSS property');
9178
+ return CommandNodeBuilder.from(commandToken)
9179
+ .withArgs(...args)
9180
+ .withModifiers(modifiers)
9181
+ .endingAt(ctx.getPosition())
9182
+ .build();
9183
+ }
9184
+ function parseTransitionPair(ctx) {
9185
+ const { owner, property } = parseTransitionProperty(ctx);
9186
+ let from = null;
9187
+ if (ctx.check('from')) {
9188
+ ctx.advance();
9189
+ from = ctx.parseExpression();
9013
9190
  }
9014
- if (target)
9015
- args.push(target);
9016
- args.push(property);
9017
9191
  if (!ctx.check(KEYWORDS.TO)) {
9018
9192
  throw new Error('Expected "to" keyword after property in transition command');
9019
9193
  }
9020
9194
  ctx.advance();
9021
- const value = ctx.parseExpression();
9022
- modifiers['to'] = value;
9023
- if (ctx.check('over')) {
9024
- ctx.advance();
9025
- const duration = ctx.parseExpression();
9026
- modifiers['over'] = duration;
9195
+ const to = ctx.parseExpression();
9196
+ return { owner, property, from, to };
9197
+ }
9198
+ function parseTransitionProperty(ctx) {
9199
+ const lead = ctx.peek();
9200
+ if (!lead || isKeyword(lead, [KEYWORDS.TO, 'from'])) {
9201
+ throw new Error('Transition command requires a CSS property');
9027
9202
  }
9028
- if (ctx.check(KEYWORDS.WITH)) {
9203
+ const expr = ctx.parseExpression();
9204
+ const split = splitStyleExpression(expr);
9205
+ if (split)
9206
+ return { owner: split.owner, property: propertyNode(split.name, lead, ctx) };
9207
+ let name = '';
9208
+ if (ctx.check('*')) {
9209
+ name = '*';
9029
9210
  ctx.advance();
9030
- const timingFunction = ctx.parsePrimary();
9031
- modifiers['with'] = timingFunction;
9032
9211
  }
9033
- return CommandNodeBuilder.from(commandToken)
9034
- .withArgs(...args)
9035
- .withModifiers(modifiers)
9036
- .endingAt(ctx.getPosition())
9037
- .build();
9212
+ const hyphenated = parseHyphenatedName(ctx);
9213
+ if (!hyphenated)
9214
+ throw new Error('Transition command requires a CSS property');
9215
+ return { owner: expr, property: propertyNode(name + hyphenated, lead, ctx) };
9216
+ }
9217
+ function splitStyleExpression(node) {
9218
+ const n = node;
9219
+ switch (n.type) {
9220
+ case 'selector':
9221
+ return typeof n.value === 'string' && n.value.startsWith('*')
9222
+ ? { owner: null, name: n.value }
9223
+ : null;
9224
+ case 'identifier':
9225
+ return typeof n.name === 'string' && !CONTEXT_REFS.has(n.name)
9226
+ ? { owner: null, name: n.name }
9227
+ : null;
9228
+ case 'possessiveExpression':
9229
+ case 'memberExpression':
9230
+ return !n.computed && n.object && typeof n.property?.name === 'string'
9231
+ ? { owner: n.object, name: n.property.name }
9232
+ : null;
9233
+ case 'binaryExpression': {
9234
+ if (n.operator !== 'of' || !n.left || !n.right)
9235
+ return null;
9236
+ const inner = splitStyleExpression(n.left);
9237
+ return inner && !inner.owner ? { owner: n.right, name: inner.name } : null;
9238
+ }
9239
+ case 'propertyOfExpression':
9240
+ return n.target && typeof n.property?.name === 'string'
9241
+ ? { owner: n.target, name: n.property.name }
9242
+ : null;
9243
+ default:
9244
+ return null;
9245
+ }
9246
+ }
9247
+ function propertyNode(name, lead, ctx) {
9248
+ return {
9249
+ type: 'string',
9250
+ value: name,
9251
+ start: lead.start || 0,
9252
+ end: ctx.getPosition().end,
9253
+ line: lead.line,
9254
+ column: lead.column,
9255
+ };
9256
+ }
9257
+ function startsAnotherPair(ctx) {
9258
+ if (ctx.isAtEnd() || isComment(ctx.peek()))
9259
+ return false;
9260
+ if (isKeyword(ctx.peek(), ['over', 'using', KEYWORDS.WITH]))
9261
+ return false;
9262
+ return !isCommandBoundary(ctx, ['catch', 'finally', 'on']);
9263
+ }
9264
+ function pairsNode(pairs) {
9265
+ const field = (name, value) => ({
9266
+ key: {
9267
+ type: 'identifier',
9268
+ name,
9269
+ start: value.start,
9270
+ end: value.end,
9271
+ line: value.line,
9272
+ column: value.column,
9273
+ },
9274
+ value,
9275
+ });
9276
+ const elements = pairs.map(pair => {
9277
+ const properties = [field('property', pair.property), field('to', pair.to)];
9278
+ if (pair.owner)
9279
+ properties.push(field('owner', pair.owner));
9280
+ if (pair.from)
9281
+ properties.push(field('from', pair.from));
9282
+ return {
9283
+ type: 'objectLiteral',
9284
+ properties,
9285
+ start: (pair.owner ?? pair.property).start,
9286
+ end: pair.to.end,
9287
+ line: (pair.owner ?? pair.property).line,
9288
+ column: (pair.owner ?? pair.property).column,
9289
+ };
9290
+ });
9291
+ const firstEl = elements[0];
9292
+ const lastEl = elements[elements.length - 1];
9293
+ return {
9294
+ type: 'arrayLiteral',
9295
+ elements,
9296
+ start: firstEl.start,
9297
+ end: lastEl.end,
9298
+ line: firstEl.line,
9299
+ column: firstEl.column,
9300
+ };
9038
9301
  }
9039
9302
  function parseStartCommand(ctx, identifierNode) {
9040
9303
  if (!ctx.match('view')) {
@@ -9331,11 +9594,17 @@ function parseSwapCommand(ctx, identifierNode) {
9331
9594
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
9332
9595
  if (targetExpr)
9333
9596
  args.push(targetExpr);
9334
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
9597
+ const isMorph = identifierNode.name === 'morph';
9598
+ if (!isDelete &&
9599
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
9600
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
9335
9601
  const contentExpr = parseOneArgument(ctx, ['using']);
9336
9602
  if (contentExpr)
9337
9603
  modifiers['with'] = contentExpr;
9338
9604
  }
9605
+ if (isMorph && modifiers['with'] === undefined) {
9606
+ ctx.addError("Expected 'to' and the content to morph into");
9607
+ }
9339
9608
  return CommandNodeBuilder.fromIdentifier(identifierNode)
9340
9609
  .withArgs(...args)
9341
9610
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -10096,6 +10365,8 @@ function atArgumentBoundary(ctx, grammar) {
10096
10365
  const next = ctx.peek();
10097
10366
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
10098
10367
  return false;
10368
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
10369
+ return false;
10099
10370
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
10100
10371
  return true;
10101
10372
  return isKeyword(next, [...grammar.markers]);
@@ -10127,10 +10398,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
10127
10398
  }
10128
10399
  }
10129
10400
  const commaList = new Set(grammar.commaList ?? []);
10401
+ const namedArgs = new Set(grammar.namedArgs ?? []);
10130
10402
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
10131
10403
  const keyword = ctx.advance().value;
10132
10404
  const key = ctx.resolveKeyword(keyword).toLowerCase();
10133
- let value = ctx.parseExpression();
10405
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
10406
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
10407
+ : ctx.parseExpression();
10134
10408
  if (value && commaList.has(key) && ctx.check(',')) {
10135
10409
  const elements = [value];
10136
10410
  while (ctx.match(',')) {
@@ -10295,8 +10569,8 @@ function parseFetchCommand(ctx, commandToken) {
10295
10569
  }
10296
10570
  if (ctx.check('with') && !modifiers['with']) {
10297
10571
  ctx.advance();
10298
- if (isFetchNakedNamedArgStart(ctx)) {
10299
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
10572
+ if (isNakedNamedArgStart(ctx)) {
10573
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
10300
10574
  }
10301
10575
  else {
10302
10576
  modifiers['with'] = ctx.parsePrimary();
@@ -10331,45 +10605,6 @@ function parseFetchCommand(ctx, commandToken) {
10331
10605
  }
10332
10606
  return builder.build();
10333
10607
  }
10334
- function isFetchNakedNamedArgStart(ctx) {
10335
- if (ctx.check('{'))
10336
- return false;
10337
- if (!ctx.checkIdentifierLike())
10338
- return false;
10339
- const next = ctx.peekAt(1);
10340
- return next !== null && next.value === ':';
10341
- }
10342
- function parseFetchNakedNamedArgs(ctx) {
10343
- const properties = [];
10344
- const startPos = ctx.getPosition();
10345
- do {
10346
- if (!ctx.checkIdentifierLike())
10347
- break;
10348
- const keyToken = ctx.advance();
10349
- const key = {
10350
- type: 'identifier',
10351
- name: keyToken.value,
10352
- start: keyToken.start,
10353
- end: keyToken.end,
10354
- line: keyToken.line,
10355
- column: keyToken.column,
10356
- };
10357
- ctx.consume(':', "Expected ':' after property name in fetch named arguments");
10358
- const value = ctx.parsePrimary();
10359
- if (value) {
10360
- properties.push({ key, value });
10361
- }
10362
- } while (ctx.match(',') && !ctx.isAtEnd());
10363
- const endPos = ctx.getPosition();
10364
- return {
10365
- type: 'objectLiteral',
10366
- properties,
10367
- start: startPos.start,
10368
- end: endPos.end,
10369
- line: startPos.line,
10370
- column: startPos.column,
10371
- };
10372
- }
10373
10608
  function findJsEndBoundary(ctx, startPos) {
10374
10609
  const input = ctx.getInputSlice(startPos);
10375
10610
  if (!input) {
@@ -10489,6 +10724,7 @@ function parseTellCommand(ctx, identifierNode) {
10489
10724
  const commands = [];
10490
10725
  while (!ctx.isAtEnd()) {
10491
10726
  if (ctx.checkIsCommand()) {
10727
+ const word = ctx.peek().value;
10492
10728
  try {
10493
10729
  ctx.advance();
10494
10730
  const cmd = ctx.parseCommand();
@@ -10499,7 +10735,8 @@ function parseTellCommand(ctx, identifierNode) {
10499
10735
  break;
10500
10736
  }
10501
10737
  }
10502
- catch {
10738
+ catch (error) {
10739
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
10503
10740
  break;
10504
10741
  }
10505
10742
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -10648,7 +10885,6 @@ const ONE_EXPR = (syntax) => ({
10648
10885
  markers: [],
10649
10886
  syntax,
10650
10887
  });
10651
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
10652
10888
  const COMMAND_GRAMMAR = {
10653
10889
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
10654
10890
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -10659,8 +10895,7 @@ const COMMAND_GRAMMAR = {
10659
10895
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
10660
10896
  },
10661
10897
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
10662
- 'beep!': BEEP,
10663
- beep: BEEP,
10898
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
10664
10899
  blur: ONE_EXPR('blur [<target>]'),
10665
10900
  break: NONE,
10666
10901
  breakpoint: NONE,
@@ -10684,7 +10919,8 @@ const COMMAND_GRAMMAR = {
10684
10919
  render: {
10685
10920
  positional: 'expression',
10686
10921
  markers: ['with'],
10687
- syntax: 'render <template> [with <variables>]',
10922
+ namedArgs: ['with'],
10923
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
10688
10924
  },
10689
10925
  reset: ONE_EXPR('reset [<form>]'),
10690
10926
  return: ONE_EXPR('return [<value>]'),
@@ -11039,7 +11275,10 @@ class Parser {
11039
11275
  const firstToken = this.peek();
11040
11276
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
11041
11277
  let left;
11042
- if (prefixEntry?.prefix) {
11278
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
11279
+ left = this.parseBeepExpression();
11280
+ }
11281
+ else if (prefixEntry?.prefix) {
11043
11282
  const token = this.advance();
11044
11283
  if (this.isAtEnd()) {
11045
11284
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -11059,6 +11298,9 @@ class Parser {
11059
11298
  continue;
11060
11299
  }
11061
11300
  }
11301
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
11302
+ break;
11303
+ }
11062
11304
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
11063
11305
  if (!infixEntry?.infix) {
11064
11306
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -11120,6 +11362,35 @@ class Parser {
11120
11362
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
11121
11363
  return left;
11122
11364
  }
11365
+ parseBeepExpression() {
11366
+ const beepToken = this.advance();
11367
+ this.advance();
11368
+ if (this.isAtEnd()) {
11369
+ this.addError("Expected expression after 'beep!'");
11370
+ return this.createErrorNode();
11371
+ }
11372
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
11373
+ return {
11374
+ type: 'unaryExpression',
11375
+ operator: 'beep!',
11376
+ operand,
11377
+ argument: operand,
11378
+ prefix: true,
11379
+ start: beepToken.start,
11380
+ end: this.previous().end,
11381
+ line: beepToken.line,
11382
+ column: beepToken.column,
11383
+ };
11384
+ }
11385
+ isStyleRefStar(i) {
11386
+ const star = this.tokens[i];
11387
+ const name = this.tokens[i + 1];
11388
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
11389
+ }
11390
+ isSpacedStyleRef(i) {
11391
+ const prev = this.tokens[i - 1];
11392
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
11393
+ }
11123
11394
  tryParseStringPostfix(left, nextToken) {
11124
11395
  const v = nextToken.value;
11125
11396
  let unit = null;
@@ -11322,6 +11593,10 @@ class Parser {
11322
11593
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
11323
11594
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
11324
11595
  while (!this.isAtEnd() && !isStop()) {
11596
+ if (this.checkComment()) {
11597
+ this.advance();
11598
+ continue;
11599
+ }
11325
11600
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
11326
11601
  let parsedCommand = false;
11327
11602
  const isCommandToken = this.checkIsCommand();
@@ -11405,7 +11680,7 @@ class Parser {
11405
11680
  debug.parse('\u2705 Found "end", consuming it');
11406
11681
  this.advance();
11407
11682
  }
11408
- else {
11683
+ else if (!this.atEndOfInput()) {
11409
11684
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
11410
11685
  throw new Error(`Expected "end" to close ${construct} block`);
11411
11686
  }
@@ -11418,7 +11693,7 @@ class Parser {
11418
11693
  if (this.check('end')) {
11419
11694
  this.advance();
11420
11695
  }
11421
- else {
11696
+ else if (!this.atEndOfInput()) {
11422
11697
  throw new Error('Expected "end" to close repeat block');
11423
11698
  }
11424
11699
  }
@@ -11434,7 +11709,10 @@ class Parser {
11434
11709
  this.advance();
11435
11710
  }
11436
11711
  else if (terminator === '') {
11437
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
11712
+ if (!this.atEndOfInput()) {
11713
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
11714
+ }
11715
+ return { commands, terminator: 'end' };
11438
11716
  }
11439
11717
  return { commands, terminator };
11440
11718
  }
@@ -12018,7 +12296,9 @@ class Parser {
12018
12296
  parseTopLevelInitBlock() {
12019
12297
  const pos = this.getPosition();
12020
12298
  const initCommands = this.parseCommandBlock(['end']);
12021
- this.consume('end', "Expected 'end' after init block");
12299
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
12300
+ this.consume('end', "Expected 'end' after init block");
12301
+ }
12022
12302
  return {
12023
12303
  type: 'initBlock',
12024
12304
  commands: initCommands,
@@ -12194,10 +12474,29 @@ class Parser {
12194
12474
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
12195
12475
  };
12196
12476
  }
12477
+ isArgumentListOnly(open) {
12478
+ let depth = 0;
12479
+ for (let i = open; i < this.tokens.length; i++) {
12480
+ const token = this.tokens[i];
12481
+ if (token.kind !== 'operator')
12482
+ continue;
12483
+ if (token.value === '(' || token.value === '[' || token.value === '{')
12484
+ depth++;
12485
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
12486
+ if (--depth === 0)
12487
+ return i === open + 1;
12488
+ }
12489
+ else if (token.value === ',' && depth === 1)
12490
+ return true;
12491
+ }
12492
+ return false;
12493
+ }
12197
12494
  tryParsePseudoCommand(callExpr, sourceExpr) {
12198
12495
  const methodName = callExpr.callee.name;
12199
12496
  const nextToken = this.peek();
12200
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
12497
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
12498
+ const hasPseudoCommandPattern = nextIsArticle ||
12499
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
12201
12500
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
12202
12501
  isContextVar(nextToken);
12203
12502
  if (!hasPseudoCommandPattern) {
@@ -12223,7 +12522,10 @@ class Parser {
12223
12522
  });
12224
12523
  }
12225
12524
  let preposition;
12226
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
12525
+ if (nextIsArticle) {
12526
+ this.advance();
12527
+ }
12528
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
12227
12529
  preposition = this.advance().value.toLowerCase();
12228
12530
  }
12229
12531
  let targetExpr;
@@ -12387,10 +12689,22 @@ class Parser {
12387
12689
  this.consume(']', "Expected ']' after event condition");
12388
12690
  }
12389
12691
  let target;
12692
+ let targetExpression;
12390
12693
  if (this.match('from')) {
12391
- const targetToken = this.advance();
12392
- target = targetToken.value;
12393
- debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
12694
+ if (this.check('the'))
12695
+ this.advance();
12696
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
12697
+ const startIndex = this.current;
12698
+ targetExpression = this.parseExpression();
12699
+ target = this.tokens
12700
+ .slice(startIndex, this.current)
12701
+ .map(t => t.value)
12702
+ .join(' ');
12703
+ }
12704
+ else {
12705
+ target = this.advance().value;
12706
+ }
12707
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
12394
12708
  this.parseTemporalKeywordModifiers(modifiers);
12395
12709
  }
12396
12710
  let attributeName;
@@ -12436,7 +12750,7 @@ class Parser {
12436
12750
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
12437
12751
  const commandName = this.peek().value.toLowerCase();
12438
12752
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
12439
- if (nextIsOpenParen && !isSpecialBodyCommand) {
12753
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
12440
12754
  let expr;
12441
12755
  const savedError = this.error;
12442
12756
  try {
@@ -12468,7 +12782,7 @@ class Parser {
12468
12782
  }
12469
12783
  }
12470
12784
  }
12471
- else if (this.checkIdentifier()) {
12785
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
12472
12786
  const token = this.peek();
12473
12787
  if (this.isCommand(token.value)) {
12474
12788
  const cmd = this.parseCommandWithErrorRecovery();
@@ -12577,6 +12891,7 @@ class Parser {
12577
12891
  ...(eventParams.length > 0 && { args: eventParams }),
12578
12892
  ...(condition && { condition: fromLegacyExpression(condition) }),
12579
12893
  ...(target && { target }),
12894
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
12580
12895
  ...(attributeName && { attributeName }),
12581
12896
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
12582
12897
  ...(customEventSource && { customEventSource }),
@@ -12832,10 +13147,9 @@ class Parser {
12832
13147
  }
12833
13148
  parseCommandCore() {
12834
13149
  const commandToken = this.previous();
12835
- let commandName = commandToken.value;
13150
+ const commandName = commandToken.value;
12836
13151
  if (commandName === 'beep' && this.check('!')) {
12837
13152
  this.advance();
12838
- commandName = 'beep!';
12839
13153
  }
12840
13154
  if (commandName === 'fetch') {
12841
13155
  return parseFetchCommand(this.getContext(), commandToken);
@@ -12934,6 +13248,9 @@ class Parser {
12934
13248
  args.push(this.parsePrimary());
12935
13249
  }
12936
13250
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
13251
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
13252
+ callNode.closestTo = this.parseExpression();
13253
+ }
12937
13254
  if (funcName === 'next' || funcName === 'previous') {
12938
13255
  const relativePositional = this.parseRelativePositionalModifiers();
12939
13256
  if (relativePositional) {
@@ -12973,6 +13290,9 @@ class Parser {
12973
13290
  }
12974
13291
  parseTheXofY() {
12975
13292
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
13293
+ if (this.isStyleRefStar(this.current)) {
13294
+ return this.parsePrimary();
13295
+ }
12976
13296
  if (!this.checkIdentifier()) {
12977
13297
  return this.createIdentifier('the');
12978
13298
  }
@@ -12988,7 +13308,7 @@ class Parser {
12988
13308
  return this.createIdentifier(propertyName);
12989
13309
  }
12990
13310
  this.advance();
12991
- const target = this.parseCall();
13311
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
12992
13312
  return {
12993
13313
  type: 'propertyOfExpression',
12994
13314
  property: { type: 'identifier', name: propertyName },
@@ -13186,6 +13506,13 @@ class Parser {
13186
13506
  checkLiteral() {
13187
13507
  return this.checkPredicate(isLiteral);
13188
13508
  }
13509
+ checkEventNamedCall() {
13510
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
13511
+ return false;
13512
+ const token = this.peek();
13513
+ const next = this.tokens[this.current + 1];
13514
+ return next?.value === '(' && next.start === token.end;
13515
+ }
13189
13516
  checkIdentifierLike() {
13190
13517
  return this.checkPredicate(isIdentifierLike);
13191
13518
  }
@@ -13294,6 +13621,22 @@ class Parser {
13294
13621
  isAtEnd() {
13295
13622
  return this.current >= this.tokens.length;
13296
13623
  }
13624
+ atEndOfInput() {
13625
+ for (let i = this.current; i < this.tokens.length; i++) {
13626
+ if (!isComment(this.tokens[i]))
13627
+ return false;
13628
+ }
13629
+ return true;
13630
+ }
13631
+ checkFeatureStart() {
13632
+ if (this.isAtEnd())
13633
+ return false;
13634
+ const value = this.peek().value;
13635
+ return (value === 'init' ||
13636
+ value === 'on' ||
13637
+ value === 'def' ||
13638
+ getRegisteredFeature(value) !== undefined);
13639
+ }
13297
13640
  peek() {
13298
13641
  if (this.isAtEnd()) {
13299
13642
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -13505,6 +13848,7 @@ class Parser {
13505
13848
  match: this.match.bind(this),
13506
13849
  matchOperator: this.matchOperator.bind(this),
13507
13850
  isAtEnd: this.isAtEnd.bind(this),
13851
+ atEndOfInput: this.atEndOfInput.bind(this),
13508
13852
  createIdentifier: this.createIdentifier.bind(this),
13509
13853
  parseExpression: this.parseExpression.bind(this),
13510
13854
  parsePrimary: this.parsePrimary.bind(this),
@@ -13555,7 +13899,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
13555
13899
  'ignoring case',
13556
13900
  ]);
13557
13901
  Parser.PRATT_TABLE = PARSER_TABLE;
13902
+ Parser.BEEP_OPERAND_BP = 80;
13558
13903
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
13904
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
13905
+ 'closest',
13906
+ 'next',
13907
+ 'previous',
13908
+ 'first',
13909
+ 'last',
13910
+ ]);
13559
13911
  function parse(input, options) {
13560
13912
  const tokens = tokenize(input);
13561
13913
  const parser = new Parser(tokens, options, input);
@@ -13997,11 +14349,16 @@ async function evaluateBinaryExpression(node, context) {
13997
14349
  }
13998
14350
  }
13999
14351
  }
14000
- if (leftNode?.type === 'selector' && leftNode.fromQuery && typeof leftNode.value === 'string') {
14352
+ if (leftNode?.type === 'selector' &&
14353
+ typeof leftNode.value === 'string' &&
14354
+ (leftNode.fromQuery || (operator === 'in' && leftNode.value.startsWith('.')))) {
14001
14355
  const root = await evaluateAST(node.right, context);
14002
14356
  const scope = root && typeof root.querySelectorAll === 'function' ? root : null;
14003
14357
  if (scope) {
14004
- return Array.from(scope.querySelectorAll(leftNode.value));
14358
+ const query = leftNode.fromQuery
14359
+ ? leftNode.value
14360
+ : escapeSelectorForQuery(leftNode, leftNode.value);
14361
+ return Array.from(scope.querySelectorAll(query));
14005
14362
  }
14006
14363
  }
14007
14364
  }
@@ -14013,6 +14370,12 @@ async function evaluateBinaryExpression(node, context) {
14013
14370
  return getExpr(context, 'styleRef').evaluate(context, leftNode.value.slice(1), el);
14014
14371
  }
14015
14372
  if (operator === 'of') {
14373
+ if (leftNode?.type === 'attributeAccess') {
14374
+ const target = await evaluateAST(node.right, context);
14375
+ if (target == null)
14376
+ return undefined;
14377
+ return getExpr(context, 'possessive').evaluate(context, target, `@${leftNode.attributeName}`);
14378
+ }
14016
14379
  const path = propertyPathOf(node.left);
14017
14380
  if (path) {
14018
14381
  const target = await evaluateAST(node.right, context);
@@ -14020,6 +14383,10 @@ async function evaluateBinaryExpression(node, context) {
14020
14383
  for (const seg of path) {
14021
14384
  if (cur == null)
14022
14385
  return undefined;
14386
+ if (isMappableCollection(cur) && !(seg in cur)) {
14387
+ cur = await getExpr(context, 'possessive').evaluate(context, cur, seg);
14388
+ continue;
14389
+ }
14023
14390
  cur = isElement(cur) ? getElementProperty$1(cur, seg) : cur[seg];
14024
14391
  }
14025
14392
  return typeof cur === 'function' ? cur.bind(target) : cur;
@@ -14044,26 +14411,21 @@ async function evaluateBinaryExpression(node, context) {
14044
14411
  const R = node.ignoringCase ? applyCI(right) : right;
14045
14412
  switch (operator) {
14046
14413
  case '+': {
14047
- let addLeft = left;
14048
- let addRight = right;
14049
14414
  if (node.coerceNumeric) {
14050
- if (typeof addLeft === 'string')
14051
- addLeft = convertToNumber(addLeft);
14052
- if (typeof addRight === 'string')
14053
- addRight = convertToNumber(addRight);
14415
+ return counterValue(left) + counterAmount(right);
14054
14416
  }
14055
- if (typeof addLeft === 'string' || typeof addRight === 'string') {
14056
- return String(addLeft ?? '') + String(addRight ?? '');
14417
+ if (typeof left === 'string' || typeof right === 'string') {
14418
+ return String(left ?? '') + String(right ?? '');
14057
14419
  }
14058
- if (Array.isArray(addLeft)) {
14059
- return Array.isArray(addRight) ? [...addLeft, ...addRight] : [...addLeft, addRight];
14420
+ if (Array.isArray(left)) {
14421
+ return Array.isArray(right) ? [...left, ...right] : [...left, right];
14060
14422
  }
14061
- return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, {
14062
- left: addLeft,
14063
- right: addRight,
14064
- }));
14423
+ return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, { left, right }));
14065
14424
  }
14066
14425
  case '-':
14426
+ if (node.coerceNumeric) {
14427
+ return counterValue(left) - counterAmount(right);
14428
+ }
14067
14429
  return unwrapTypedResult(await getExpr(context, 'subtraction').evaluate(context, { left, right }));
14068
14430
  case '*':
14069
14431
  return unwrapTypedResult(await getExpr(context, 'multiplication').evaluate(context, { left, right }));
@@ -14321,9 +14683,8 @@ async function evaluatePropertyOfExpressionNode(node, context) {
14321
14683
  }
14322
14684
  const propertyName = propertyNode.name;
14323
14685
  const target = await evaluateAST(node.target, context);
14324
- if (target == null) {
14325
- throw new Error(`Cannot access property "${propertyName}" of ${target}`);
14326
- }
14686
+ if (target == null)
14687
+ return undefined;
14327
14688
  return getExpr(context, 'possessive').evaluate(context, target, propertyName);
14328
14689
  }
14329
14690
  async function evaluateTemplateLiteralNode(node, context) {
@@ -14441,6 +14802,9 @@ async function evaluateUnaryExpression(node, context) {
14441
14802
  case 'not':
14442
14803
  case '!':
14443
14804
  return logicalExpressions.not.evaluate(context, value);
14805
+ case 'beep!':
14806
+ beepValues(context.me, [value]);
14807
+ return value;
14444
14808
  case 'no':
14445
14809
  return logicalExpressions.no.evaluate(context, value);
14446
14810
  case '-':
@@ -14530,6 +14894,10 @@ async function evaluateCallExpression(node, context) {
14530
14894
  const args = await resolveCallArgs(node.arguments, funcName, context);
14531
14895
  switch (funcName) {
14532
14896
  case 'closest':
14897
+ if (node.closestTo) {
14898
+ const start = unwrapElement(await evaluateAST(node.closestTo, context)) ?? null;
14899
+ return referencesExpressions.closest.evaluate(context, args[0], start);
14900
+ }
14533
14901
  return referencesExpressions.closest.evaluate(context, ...args);
14534
14902
  case 'previous':
14535
14903
  case 'next': {
@@ -15954,10 +16322,29 @@ const STRATEGY_KEYWORDS = {
15954
16322
  innermorph: 'morph',
15955
16323
  outermorph: 'morphOuter',
15956
16324
  };
16325
+ function loneRootWithTag(content, tagName) {
16326
+ let root;
16327
+ if (typeof content === 'string') {
16328
+ const template = document.createElement('template');
16329
+ template.innerHTML = content;
16330
+ root = template.content.firstElementChild;
16331
+ if (!root || root.nextElementSibling)
16332
+ return null;
16333
+ }
16334
+ else {
16335
+ root = content.cloneNode(true);
16336
+ }
16337
+ return root.tagName === tagName && isHTMLElement$2(root) ? root : null;
16338
+ }
15957
16339
  function executeSwap(target, content, strategy, morphOptions) {
15958
16340
  const contentStr = content !== null && !isHTMLElement$2(content) ? content : '';
15959
16341
  const contentEl = isHTMLElement$2(content) ? content : null;
15960
16342
  switch (strategy) {
16343
+ case 'morphMerge': {
16344
+ const root = content !== null ? loneRootWithTag(content, target.tagName) : null;
16345
+ executeSwap(target, root ?? content, root ? 'morphOuter' : 'morph', morphOptions);
16346
+ break;
16347
+ }
15961
16348
  case 'morph':
15962
16349
  if (content !== null) {
15963
16350
  try {
@@ -16247,7 +16634,7 @@ let MorphCommand = (() => {
16247
16634
  if (args.length === 0 && modifiers.with === undefined) {
16248
16635
  throw new Error('[HyperFixi] morph: command requires arguments');
16249
16636
  }
16250
- const strategy = literalText(modifiers.strategy)?.toLowerCase() === 'over' ? 'morphOuter' : 'morph';
16637
+ const strategy = literalText(modifiers.strategy)?.toLowerCase() === 'over' ? 'morphOuter' : 'morphMerge';
16251
16638
  const useViewTransition = modifiers.viewTransition !== undefined;
16252
16639
  const targetNode = modifiers.on !== undefined ? modifiers.on : args[0];
16253
16640
  const contentNode = modifiers.with !== undefined
@@ -17537,6 +17924,19 @@ async function resolveWriteTarget(node, evaluator, context, options) {
17537
17924
  }
17538
17925
  return { kind: 'property', target: property };
17539
17926
  }
17927
+ if (options.ofAndObjectProperty) {
17928
+ const access = ofOrPossessiveAccess(n);
17929
+ if (access) {
17930
+ const owner = await evaluator.evaluate(access.owner, context);
17931
+ const element = Array.isArray(owner) ? owner[0] : owner;
17932
+ if (access.isOf && isHTMLElement$2(element)) {
17933
+ return { kind: 'property', target: { element, property: access.name } };
17934
+ }
17935
+ if (isObjectOwner(owner)) {
17936
+ return { kind: 'member', container: owner, property: access.name };
17937
+ }
17938
+ }
17939
+ }
17540
17940
  if (options.bareReference &&
17541
17941
  (nodeType === 'identifier' || nodeType === 'variable' || nodeType === 'symbol') &&
17542
17942
  typeof n.name === 'string') {
@@ -17546,6 +17946,19 @@ async function resolveWriteTarget(node, evaluator, context, options) {
17546
17946
  }
17547
17947
  return null;
17548
17948
  }
17949
+ function ofOrPossessiveAccess(n) {
17950
+ const [owner, property, isOf] = n.type === 'possessiveExpression'
17951
+ ? [n.object, n.property, false]
17952
+ : n.type === 'propertyOfExpression'
17953
+ ? [n.target, n.property, false]
17954
+ : n.type === 'binaryExpression' && n.operator === 'of'
17955
+ ? [n.right, n.left, true]
17956
+ : [undefined, undefined, false];
17957
+ const name = property?.name;
17958
+ if (!owner || typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name))
17959
+ return null;
17960
+ return { owner, name, isOf };
17961
+ }
17549
17962
 
17550
17963
  function toSetInput(target, value) {
17551
17964
  switch (target.kind) {
@@ -17567,10 +17980,18 @@ function toSetInput(target, value) {
17567
17980
  property: target.target.property,
17568
17981
  value,
17569
17982
  };
17983
+ case 'member':
17984
+ return {
17985
+ type: 'member-assignment',
17986
+ container: target.container,
17987
+ property: target.property,
17988
+ value,
17989
+ };
17570
17990
  default:
17571
17991
  throw new Error(`set: unrequested write-target rung '${target.kind}'`);
17572
17992
  }
17573
17993
  }
17994
+ const SET_RUNGS = { nodeWriters: true, styleSplit: true, ofAndObjectProperty: true };
17574
17995
  let SetCommand = (() => {
17575
17996
  let _classDecorators = [command({ name: 'set' })];
17576
17997
  let _classDescriptor;
@@ -17589,8 +18010,7 @@ let SetCommand = (() => {
17589
18010
  const argScope = firstArg?.scope;
17590
18011
  const writeTarget = await resolveWriteTarget(firstArg, evaluator, context, {
17591
18012
  scopeElements: () => this.resolveTargets(raw.modifiers.on, evaluator, context),
17592
- nodeWriters: true,
17593
- styleSplit: true,
18013
+ ...SET_RUNGS,
17594
18014
  });
17595
18015
  if (writeTarget) {
17596
18016
  return toSetInput(writeTarget, await this.extractValue(raw, evaluator, context));
@@ -17916,12 +18336,30 @@ function getNodeType(node) {
17916
18336
  return 'unknown';
17917
18337
  return node.type || 'unknown';
17918
18338
  }
18339
+ function propertyTarget(node) {
18340
+ const n = node;
18341
+ const owner = n.type === 'propertyOfExpression'
18342
+ ? n.target
18343
+ : n.type === 'possessiveExpression' ||
18344
+ n.type === 'memberExpression' ||
18345
+ n.type === 'propertyAccess'
18346
+ ? n.object
18347
+ : undefined;
18348
+ if (!owner || n.computed)
18349
+ return null;
18350
+ const p = n.property;
18351
+ const name = typeof p === 'string' ? p : typeof p?.name === 'string' ? p.name : undefined;
18352
+ if (!name || !/^[A-Za-z_$][\w$-]*$/.test(name))
18353
+ return null;
18354
+ return { owner, name };
18355
+ }
17919
18356
  async function parseNumericTargetInput(raw, evaluator, context, commandName) {
17920
18357
  if (!raw.args || raw.args.length === 0) {
17921
18358
  throw new Error(`${commandName} command requires a target`);
17922
18359
  }
17923
18360
  const targetArg = raw.args[0];
17924
18361
  let target;
18362
+ let property;
17925
18363
  let extractedScope;
17926
18364
  let customWrite;
17927
18365
  const nodeType = getNodeType(targetArg);
@@ -17936,7 +18374,25 @@ async function parseNumericTargetInput(raw, evaluator, context, commandName) {
17936
18374
  }
17937
18375
  else {
17938
18376
  const writer = getRegisteredNodeWriter(nodeType);
17939
- if (writer) {
18377
+ const attribute = writer
18378
+ ? null
18379
+ : await resolveAttributeWriteTarget(targetArg, evaluator, context, async () => context.me ? [context.me] : []);
18380
+ const prop = writer || attribute ? null : propertyTarget(targetArg);
18381
+ const owner = prop ? await evaluator.evaluate(prop.owner, context) : undefined;
18382
+ const ownerElement = Array.isArray(owner) ? owner[0] : owner;
18383
+ if (attribute?.elements[0]) {
18384
+ target = attribute.elements[0];
18385
+ property = `@${attribute.name}`;
18386
+ }
18387
+ else if (prop && isHTMLElement$2(ownerElement)) {
18388
+ target = ownerElement;
18389
+ property = prop.name;
18390
+ }
18391
+ else if (prop && isObjectOwner(owner)) {
18392
+ target = owner;
18393
+ property = prop.name;
18394
+ }
18395
+ else if (writer) {
17940
18396
  const currentRaw = await evaluator.evaluate(targetArg, context);
17941
18397
  const currentValue = typeof currentRaw === 'number'
17942
18398
  ? currentRaw
@@ -17974,24 +18430,22 @@ async function parseNumericTargetInput(raw, evaluator, context, commandName) {
17974
18430
  if (typeof evaluated === 'number') {
17975
18431
  amount = evaluated;
17976
18432
  }
18433
+ else if (evaluated == null) {
18434
+ amount = NaN;
18435
+ }
17977
18436
  }
17978
18437
  }
17979
18438
  const byModifier = raw.modifiers?.by;
17980
18439
  if (byModifier) {
17981
- const byValue = byModifier.value;
17982
- if (getNodeType(byModifier) === 'literal' && typeof byValue === 'number') {
17983
- amount = byValue;
17984
- }
17985
- else if (getNodeType(byModifier) !== 'literal') {
17986
- const evaluated = await evaluator.evaluate(byModifier, context);
17987
- if (typeof evaluated === 'number') {
17988
- amount = evaluated;
17989
- }
17990
- }
18440
+ const value = getNodeType(byModifier) === 'literal'
18441
+ ? byModifier.value
18442
+ : await evaluator.evaluate(byModifier, context);
18443
+ amount = counterAmount(value);
17991
18444
  }
17992
18445
  return {
17993
18446
  target,
17994
18447
  amount,
18448
+ ...(property && { property }),
17995
18449
  ...(scope && { scope }),
17996
18450
  ...(customWrite && { customWrite }),
17997
18451
  };
@@ -18014,7 +18468,7 @@ let NumericModifyCommand = (() => {
18014
18468
  async execute(input, context) {
18015
18469
  const { target, property, scope, amount = 1, operation = 'increment', customWrite } = input;
18016
18470
  if (customWrite) {
18017
- const delta = isFinite(amount) ? amount : 1;
18471
+ const delta = amount;
18018
18472
  const newValue = operation === 'increment'
18019
18473
  ? customWrite.currentValue + delta
18020
18474
  : customWrite.currentValue - delta;
@@ -18028,8 +18482,7 @@ let NumericModifyCommand = (() => {
18028
18482
  newValue = NaN;
18029
18483
  }
18030
18484
  else {
18031
- const delta = isFinite(amount) ? amount : 1;
18032
- newValue = operation === 'increment' ? currentValue + delta : currentValue - delta;
18485
+ newValue = operation === 'increment' ? currentValue + amount : currentValue - amount;
18033
18486
  }
18034
18487
  setTargetValue(target, property, scope, newValue, context);
18035
18488
  Object.assign(context, { it: newValue });
@@ -18845,17 +19298,7 @@ let BeepCommand = (() => {
18845
19298
  this.debugContext(context);
18846
19299
  return { expressionCount: 0, debugged: true, outputs: [] };
18847
19300
  }
18848
- const outputs = [];
18849
- console.group('\ud83d\udd14 beep! Debug Output');
18850
- for (const expression of expressions) {
18851
- const output = this.debugExpression(expression);
18852
- outputs.push(output);
18853
- console.log(`Value:`, expression);
18854
- console.log(`Type:`, output.type);
18855
- console.log(`Representation:`, output.representation);
18856
- console.log('---');
18857
- }
18858
- console.groupEnd();
19301
+ const outputs = beepValues(context.me, expressions);
18859
19302
  return { expressionCount: expressions.length, debugged: true, outputs };
18860
19303
  }
18861
19304
  debugContext(context) {
@@ -18868,67 +19311,6 @@ let BeepCommand = (() => {
18868
19311
  console.log('variables:', context.variables);
18869
19312
  console.groupEnd();
18870
19313
  }
18871
- debugExpression(expression) {
18872
- return {
18873
- value: expression,
18874
- type: this.getType(expression),
18875
- representation: this.getRepresentation(expression),
18876
- };
18877
- }
18878
- getType(value) {
18879
- if (value === null)
18880
- return 'null';
18881
- if (value === undefined)
18882
- return 'undefined';
18883
- if (Array.isArray(value))
18884
- return 'array';
18885
- if (isHTMLElement$2(value))
18886
- return 'HTMLElement';
18887
- if (value instanceof Element)
18888
- return 'Element';
18889
- if (value instanceof Node)
18890
- return 'Node';
18891
- if (value instanceof Error)
18892
- return 'Error';
18893
- if (value instanceof Date)
18894
- return 'Date';
18895
- if (value instanceof RegExp)
18896
- return 'RegExp';
18897
- return typeof value;
18898
- }
18899
- getRepresentation(value) {
18900
- if (value === null)
18901
- return 'null';
18902
- if (value === undefined)
18903
- return 'undefined';
18904
- if (Array.isArray(value)) {
18905
- return `Array(${value.length}) [${value
18906
- .slice(0, 3)
18907
- .map(v => this.getRepresentation(v))
18908
- .join(', ')}${value.length > 3 ? '...' : ''}]`;
18909
- }
18910
- if (isHTMLElement$2(value)) {
18911
- const el = value;
18912
- const tag = el.tagName.toLowerCase();
18913
- const id = el.id ? `#${el.id}` : '';
18914
- const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
18915
- return `<${tag}${id}${classes}/>`;
18916
- }
18917
- if (value instanceof Error)
18918
- return `Error: ${value.message}`;
18919
- if (typeof value === 'string')
18920
- return value.length > 50 ? `"${value.substring(0, 47)}..."` : `"${value}"`;
18921
- if (typeof value === 'object') {
18922
- try {
18923
- const keys = Object.keys(value);
18924
- return `Object {${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`;
18925
- }
18926
- catch {
18927
- return '[Object]';
18928
- }
18929
- }
18930
- return String(value);
18931
- }
18932
19314
  };
18933
19315
  __setFunctionName(_classThis, "BeepCommand");
18934
19316
  (() => {
@@ -19890,7 +20272,10 @@ function createUntilLoopConfig(conditionExpr, evaluateCondition, context, indexV
19890
20272
  };
19891
20273
  const config = {
19892
20274
  type: 'until',
19893
- shouldContinue: () => !evaluateCondition(conditionExpr, context),
20275
+ shouldContinue: () => {
20276
+ const met = evaluateCondition(conditionExpr, context);
20277
+ return met instanceof Promise ? met.then(value => !value) : !met;
20278
+ },
19894
20279
  };
19895
20280
  return { config, iterCtx };
19896
20281
  }
@@ -19939,6 +20324,18 @@ async function slotText(node, evaluator, context) {
19939
20324
  const v = await evaluator.evaluate(node, context);
19940
20325
  return typeof v === 'string' ? v : undefined;
19941
20326
  }
20327
+ function isEventTarget(value) {
20328
+ return typeof value?.addEventListener === 'function';
20329
+ }
20330
+ async function evaluateLoopCondition(condition, context) {
20331
+ if (typeof condition === 'function')
20332
+ return Boolean(await condition());
20333
+ return evaluateCondition(condition, context);
20334
+ }
20335
+ function loopCount(value) {
20336
+ const count = Number(value);
20337
+ return Number.isNaN(count) ? 0 : count;
20338
+ }
19942
20339
  let RepeatCommand = (() => {
19943
20340
  let _classDecorators = [command({ name: 'repeat' })];
19944
20341
  let _classDescriptor;
@@ -19996,17 +20393,24 @@ let RepeatCommand = (() => {
19996
20393
  };
19997
20394
  }
19998
20395
  if (loopType === 'times' || m.times) {
19999
- const countValue = m.times && (await evaluator.evaluate(m.times, context));
20000
- const count = typeof countValue === 'number' ? countValue : parseInt(String(countValue), 10);
20001
- if (isNaN(count))
20396
+ if (!m.times)
20002
20397
  throw new Error('times loops require a count number');
20398
+ const count = loopCount(await evaluator.evaluate(m.times, context));
20003
20399
  return { type: 'times', count, indexVariable, commands, elseCommands };
20004
20400
  }
20005
20401
  if (loopType === 'while' || m.while) {
20006
20402
  const condition = m.while;
20007
20403
  if (!condition)
20008
20404
  throw new Error('while loops require a condition');
20009
- return { type: 'while', condition, indexVariable, commands, elseCommands, bottomTested };
20405
+ const check = () => evaluator.evaluate(condition, context);
20406
+ return {
20407
+ type: 'while',
20408
+ condition: check,
20409
+ indexVariable,
20410
+ commands,
20411
+ elseCommands,
20412
+ bottomTested,
20413
+ };
20010
20414
  }
20011
20415
  if (loopType === 'until-event' || m.event) {
20012
20416
  const eventName = await text(m.event);
@@ -20024,7 +20428,7 @@ let RepeatCommand = (() => {
20024
20428
  }
20025
20429
  else {
20026
20430
  const target = await evaluator.evaluate(m.from, context);
20027
- if (target instanceof EventTarget)
20431
+ if (isEventTarget(target))
20028
20432
  eventTarget = target;
20029
20433
  else if (target === 'document')
20030
20434
  eventTarget = document;
@@ -20045,7 +20449,15 @@ let RepeatCommand = (() => {
20045
20449
  const condition = m.until;
20046
20450
  if (!condition)
20047
20451
  throw new Error('until loops require a condition');
20048
- return { type: 'until', condition, indexVariable, commands, elseCommands, bottomTested };
20452
+ const check = () => evaluator.evaluate(condition, context);
20453
+ return {
20454
+ type: 'until',
20455
+ condition: check,
20456
+ indexVariable,
20457
+ commands,
20458
+ elseCommands,
20459
+ bottomTested,
20460
+ };
20049
20461
  }
20050
20462
  if (loopType === 'forever') {
20051
20463
  return { type: 'forever', indexVariable, commands, elseCommands };
@@ -20063,10 +20475,10 @@ let RepeatCommand = (() => {
20063
20475
  ({ config, iterCtx } = createTimesLoopConfig(count, indexVariable));
20064
20476
  break;
20065
20477
  case 'while':
20066
- ({ config, iterCtx } = createWhileLoopConfig(condition, evaluateCondition, context, indexVariable));
20478
+ ({ config, iterCtx } = createWhileLoopConfig(condition, evaluateLoopCondition, context, indexVariable));
20067
20479
  break;
20068
20480
  case 'until':
20069
- ({ config, iterCtx } = createUntilLoopConfig(condition, evaluateCondition, context, indexVariable));
20481
+ ({ config, iterCtx } = createUntilLoopConfig(condition, evaluateLoopCondition, context, indexVariable));
20070
20482
  break;
20071
20483
  case 'until-event':
20072
20484
  ({ config, iterCtx } = createUntilEventLoopConfig(eventName, eventTarget, indexVariable));
@@ -21097,6 +21509,29 @@ function namedPropertyOf(node) {
21097
21509
  }
21098
21510
  return null;
21099
21511
  }
21512
+ const RESTORE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert']);
21513
+ function elementsOf(value) {
21514
+ if (Array.isArray(value) || isNodeList(value)) {
21515
+ return Array.from(value).filter(isHTMLElement$2);
21516
+ }
21517
+ return isHTMLElement$2(value) ? [value] : [];
21518
+ }
21519
+ async function valueOf(node, evaluator, context) {
21520
+ const value = await evaluator.evaluate(node, context);
21521
+ const name = node.name;
21522
+ if (value === undefined && node.type === 'identifier' && typeof name === 'string')
21523
+ return name;
21524
+ return value;
21525
+ }
21526
+ function longestTransitionMs(css) {
21527
+ let longest;
21528
+ for (const item of css.split(',')) {
21529
+ const times = [...item.matchAll(/(\d*\.?\d+)(ms|s)\b/g)].map(m => parseFloat(m[1]) * (m[2] === 's' ? 1000 : 1));
21530
+ if (times.length > 0)
21531
+ longest = Math.max(longest ?? 0, times[0] + (times[1] ?? 0));
21532
+ }
21533
+ return longest;
21534
+ }
21100
21535
  let TransitionCommand = (() => {
21101
21536
  let _classDecorators = [command({ name: 'transition' })];
21102
21537
  let _classDescriptor;
@@ -21112,11 +21547,10 @@ let TransitionCommand = (() => {
21112
21547
  let property;
21113
21548
  let target;
21114
21549
  const firstArg = await evaluator.evaluate(raw.args[0], context);
21115
- const asElement = (value) => Array.isArray(value) || isNodeList(value) ? value[0] : value;
21116
- const firstAsElement = asElement(firstArg);
21117
- if (isHTMLElement$2(firstAsElement) ||
21550
+ const owners = elementsOf(firstArg);
21551
+ if (owners.length > 0 ||
21118
21552
  (typeof firstArg === 'string' && /^[#.]|^(?:me|it|you)$/.test(firstArg))) {
21119
- target = firstAsElement;
21553
+ target = owners.length === 1 ? owners[0] : owners.length > 1 ? owners : firstArg;
21120
21554
  property = String(await evaluator.evaluate(raw.args[1], context));
21121
21555
  }
21122
21556
  else if (raw.args.length >= 2) {
@@ -21128,9 +21562,9 @@ let TransitionCommand = (() => {
21128
21562
  throw new Error('transition requires a CSS property');
21129
21563
  property = named.property;
21130
21564
  if (named.objectNode !== undefined) {
21131
- const owner = asElement(await evaluator.evaluate(named.objectNode, context));
21132
- if (isHTMLElement$2(owner))
21133
- target = owner;
21565
+ const owner = elementsOf(await evaluator.evaluate(named.objectNode, context));
21566
+ if (owner.length > 0)
21567
+ target = owner.length === 1 ? owner[0] : owner;
21134
21568
  }
21135
21569
  }
21136
21570
  else {
@@ -21141,60 +21575,131 @@ let TransitionCommand = (() => {
21141
21575
  }
21142
21576
  if (!raw.modifiers?.to)
21143
21577
  throw new Error('transition requires "to <value>"');
21144
- let value = await evaluator.evaluate(raw.modifiers.to, context);
21145
- if (value === undefined && raw.modifiers.to.type === 'identifier') {
21146
- value = raw.modifiers.to.name;
21147
- }
21148
- const result = { property, value: value };
21578
+ const result = {
21579
+ property,
21580
+ value: await valueOf(raw.modifiers.to, evaluator, context),
21581
+ };
21149
21582
  if (target !== undefined)
21150
21583
  result.target = target;
21584
+ if (raw.modifiers?.from)
21585
+ result.from = await valueOf(raw.modifiers.from, evaluator, context);
21586
+ if (raw.modifiers?.pairs) {
21587
+ result.pairs = await this.laterPairs(raw.modifiers.pairs, evaluator, context);
21588
+ }
21151
21589
  if (raw.modifiers?.over)
21152
21590
  result.duration = await evaluator.evaluate(raw.modifiers.over, context);
21153
21591
  if (raw.modifiers?.with)
21154
21592
  result.timingFunction = String(await evaluator.evaluate(raw.modifiers.with, context));
21593
+ if (raw.modifiers?.using)
21594
+ result.using = String(await evaluator.evaluate(raw.modifiers.using, context));
21155
21595
  return result;
21156
21596
  }
21597
+ async laterPairs(node, evaluator, context) {
21598
+ const pairs = [];
21599
+ for (const pairNode of node.elements ?? []) {
21600
+ const fields = new Map();
21601
+ const properties = pairNode
21602
+ .properties ?? [];
21603
+ for (const { key, value } of properties) {
21604
+ if (typeof key.name === 'string')
21605
+ fields.set(key.name, value);
21606
+ }
21607
+ const propertyNode = fields.get('property');
21608
+ const toNode = fields.get('to');
21609
+ if (!propertyNode || !toNode)
21610
+ throw new Error('transition requires property and value');
21611
+ const pair = {
21612
+ property: String(await evaluator.evaluate(propertyNode, context)),
21613
+ value: await valueOf(toNode, evaluator, context),
21614
+ };
21615
+ const ownerNode = fields.get('owner');
21616
+ if (ownerNode) {
21617
+ const owners = elementsOf(await evaluator.evaluate(ownerNode, context));
21618
+ if (owners.length === 0)
21619
+ throw new Error('transition: target element not found');
21620
+ pair.target = owners.length === 1 ? owners[0] : owners;
21621
+ }
21622
+ const fromNode = fields.get('from');
21623
+ if (fromNode)
21624
+ pair.from = await valueOf(fromNode, evaluator, context);
21625
+ pairs.push(pair);
21626
+ }
21627
+ return pairs;
21628
+ }
21157
21629
  async execute(input, context) {
21158
- let { property } = input;
21159
- const { target, value, duration: durationInput, timingFunction } = input;
21160
- if (property.startsWith('*'))
21161
- property = property.substring(1);
21162
- property = camelToKebab(property);
21163
- const targetElement = resolveElement(target, context, 'transition');
21164
- const duration = parseDuration(durationInput, 300);
21165
- const fromValue = getComputedStyle(targetElement).getPropertyValue(property);
21166
- const originalTransition = targetElement.style.transition;
21167
- const transitionProp = `${property} ${duration}ms ${timingFunction || 'ease'}`;
21168
- targetElement.style.transition = originalTransition
21169
- ? `${originalTransition}, ${transitionProp}`
21170
- : transitionProp;
21171
- let toValue = String(value);
21172
- let removeInlineAfter = false;
21173
- if (toValue === 'initial' ||
21174
- toValue === 'inherit' ||
21175
- toValue === 'unset' ||
21176
- toValue === 'revert') {
21177
- const currentInline = targetElement.style.getPropertyValue(property);
21178
- targetElement.style.removeProperty(property);
21179
- toValue = getComputedStyle(targetElement).getPropertyValue(property);
21180
- if (currentInline) {
21181
- targetElement.style.setProperty(property, currentInline);
21182
- }
21183
- removeInlineAfter = true;
21184
- }
21185
- targetElement.style.setProperty(property, toValue);
21186
- const result = await waitForTransitionEnd(targetElement, property, duration);
21187
- targetElement.style.transition = originalTransition;
21188
- if (removeInlineAfter) {
21189
- targetElement.style.removeProperty(property);
21630
+ const duration = parseDuration(input.duration, (input.using !== undefined && longestTransitionMs(input.using)) || 300);
21631
+ const timing = input.timingFunction || 'ease';
21632
+ const jobs = [];
21633
+ for (const pair of [input, ...(input.pairs ?? [])]) {
21634
+ let property = pair.property;
21635
+ if (property.startsWith('*'))
21636
+ property = property.substring(1);
21637
+ property = camelToKebab(property);
21638
+ const elements = Array.isArray(pair.target)
21639
+ ? pair.target.filter(isHTMLElement$2)
21640
+ : [resolveElement(pair.target, context, 'transition')];
21641
+ for (const element of elements) {
21642
+ jobs.push({
21643
+ element,
21644
+ property,
21645
+ pair,
21646
+ fromValue: getComputedStyle(element).getPropertyValue(property),
21647
+ toValue: '',
21648
+ removeInlineAfter: false,
21649
+ });
21650
+ }
21190
21651
  }
21652
+ if (jobs.length === 0)
21653
+ throw new Error('transition: target element not found');
21654
+ for (const job of jobs) {
21655
+ if (job.pair.from !== undefined) {
21656
+ job.element.style.setProperty(job.property, String(job.pair.from));
21657
+ }
21658
+ }
21659
+ for (const job of jobs)
21660
+ if (job.pair.from !== undefined)
21661
+ void job.element.offsetWidth;
21662
+ const originals = new Map();
21663
+ for (const job of jobs) {
21664
+ if (!originals.has(job.element))
21665
+ originals.set(job.element, job.element.style.transition);
21666
+ }
21667
+ for (const [element, original] of originals) {
21668
+ const own = jobs
21669
+ .filter(job => job.element === element)
21670
+ .map(job => `${job.property} ${duration}ms ${timing}`);
21671
+ element.style.transition = input.using ?? [original, ...own].filter(Boolean).join(', ');
21672
+ }
21673
+ for (const job of jobs) {
21674
+ const { element, property } = job;
21675
+ let toValue = String(job.pair.value);
21676
+ if (RESTORE_KEYWORDS.has(toValue)) {
21677
+ const currentInline = element.style.getPropertyValue(property);
21678
+ element.style.removeProperty(property);
21679
+ toValue = getComputedStyle(element).getPropertyValue(property);
21680
+ if (currentInline) {
21681
+ element.style.setProperty(property, currentInline);
21682
+ }
21683
+ job.removeInlineAfter = true;
21684
+ }
21685
+ job.toValue = toValue;
21686
+ element.style.setProperty(property, toValue);
21687
+ }
21688
+ const results = await Promise.all(jobs.map(job => waitForTransitionEnd(job.element, job.property, duration)));
21689
+ for (const [element, original] of originals)
21690
+ element.style.transition = original;
21691
+ for (const job of jobs) {
21692
+ if (job.removeInlineAfter)
21693
+ job.element.style.removeProperty(job.property);
21694
+ }
21695
+ const first = jobs[0];
21191
21696
  return {
21192
- element: targetElement,
21193
- property,
21194
- fromValue,
21195
- toValue,
21697
+ element: first.element,
21698
+ property: first.property,
21699
+ fromValue: first.fromValue,
21700
+ toValue: first.toValue,
21196
21701
  duration,
21197
- completed: result.completed,
21702
+ completed: results.every(result => result.completed),
21198
21703
  };
21199
21704
  }
21200
21705
  };
@@ -21207,13 +21712,16 @@ let TransitionCommand = (() => {
21207
21712
  })();
21208
21713
  _classThis.metadata = commandMeta({
21209
21714
  description: 'Animate CSS properties using CSS transitions',
21210
- syntax: 'transition [<target>] <property> to <value> [over <duration>] [with <timing>]',
21715
+ syntax: 'transition <property> [from <value>] to <value> [<property> \u2026 to <value>]\u2026 [over <duration>] [using <css> | with <timing>]',
21211
21716
  examples: [
21212
21717
  'transition opacity to 0.5',
21213
21718
  'transition my *opacity to 0 over 200ms',
21214
21719
  "transition #box's *opacity to 0 over 200ms",
21215
21720
  'transition left to 100px over 500ms',
21216
21721
  'transition background-color to red over 1s with ease-in-out',
21722
+ 'transition *width to 100px *height to 50px over 300ms',
21723
+ 'transition *opacity of #panel from 0 to 1 over 200ms',
21724
+ 'transition my *opacity to 0 using "opacity 1s ease-in"',
21217
21725
  ],
21218
21726
  sideEffects: ['style-change', 'timing'],
21219
21727
  category: 'animation',
@@ -22339,16 +22847,12 @@ class RenderCommand {
22339
22847
  }
22340
22848
  RenderCommand.metadata = commandMeta({
22341
22849
  description: 'Render templates with @if, @else, and @repeat directives',
22342
- syntax: [
22343
- 'render <template>',
22344
- 'render <template> with <variables>',
22345
- 'render <template> with (key: value, ...)',
22346
- ],
22850
+ syntax: ['render <template>', 'render <template> with <name>: <value>, ...'],
22347
22851
  examples: [
22348
22852
  'render myTemplate',
22349
- 'render myTemplate with (name: "Alice")',
22350
- 'render "<template>Hello ${name}!</template>" with (name: "World")',
22351
- 'render template with (items: data)',
22853
+ 'render myTemplate with name: "Alice"',
22854
+ 'render "<template>Hello ${name}!</template>" with name: "World"',
22855
+ 'render template with items: data',
22352
22856
  ],
22353
22857
  category: 'templates',
22354
22858
  compatibility: 'standard',