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