@hyperfixi/core 3.1.1 → 3.3.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 +36 -17
  10. package/dist/bundle-generator/index.mjs +36 -17
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/chunks/bridge-CI9SOl8u.js +2 -0
  13. package/dist/chunks/browser-modular-7CpI5VF2.js +2 -0
  14. package/dist/chunks/index-BwXDEK0m.js +2 -0
  15. package/dist/chunks/index-DQi2o8Lk.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 +938 -425
  23. package/dist/commands/index.mjs +938 -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 +1149 -485
  40. package/dist/index.min.js +1 -1
  41. package/dist/index.mjs +1149 -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 +396 -153
  53. package/dist/parser/full-parser.mjs +396 -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 +535 -210
  65. package/dist/registry/index.mjs +535 -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-D7qrHhV0.js +0 -2
  94. package/dist/chunks/browser-modular-C4LFxfoS.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
@@ -2252,6 +2252,14 @@ function convertNode(node, infer) {
2252
2252
  ...pos(node),
2253
2253
  };
2254
2254
  case 'identifier':
2255
+ if (node.scope === 'element' || node.scope === 'local' || node.scope === 'global') {
2256
+ return {
2257
+ type: 'variable',
2258
+ name: (node.name ?? ''),
2259
+ scope: node.scope,
2260
+ ...pos(node),
2261
+ };
2262
+ }
2255
2263
  return {
2256
2264
  type: 'identifier',
2257
2265
  value: (node.name ?? node.value ?? ''),
@@ -2274,6 +2282,15 @@ function convertNode(node, infer) {
2274
2282
  operand: convertNode(node.argument ?? node.operand, infer),
2275
2283
  ...pos(node),
2276
2284
  };
2285
+ case 'typeCheckExpression':
2286
+ return {
2287
+ type: 'typeCheck',
2288
+ value: convertNode(node.value, infer),
2289
+ typeName: String(node.typeName),
2290
+ nullOk: node.nullOk !== false,
2291
+ negated: node.negated === true,
2292
+ ...pos(node),
2293
+ };
2277
2294
  case 'timeExpression':
2278
2295
  return { type: 'literal', value: node.value, ...pos(node) };
2279
2296
  case 'templateLiteral':
@@ -2285,6 +2302,28 @@ function convertNode(node, infer) {
2285
2302
  scope: (node.scope ?? 'local'),
2286
2303
  ...pos(node),
2287
2304
  };
2305
+ case 'arrayLiteral':
2306
+ return {
2307
+ type: 'array',
2308
+ elements: (node.elements ?? []).map(element => convertNode(element, infer)),
2309
+ ...pos(node),
2310
+ };
2311
+ case 'attributeAccess':
2312
+ return {
2313
+ type: 'possessive',
2314
+ object: { type: 'identifier', value: 'me' },
2315
+ property: `@${node.attributeName}`,
2316
+ ...pos(node),
2317
+ };
2318
+ case 'propertyOfExpression': {
2319
+ const property = node.property;
2320
+ return {
2321
+ type: 'possessive',
2322
+ object: convertNode(node.target, infer),
2323
+ property: (property?.name ?? property?.value ?? ''),
2324
+ ...pos(node),
2325
+ };
2326
+ }
2288
2327
  case 'htmlSelector':
2289
2328
  return {
2290
2329
  type: 'selector',
@@ -2336,6 +2375,21 @@ function convertCommand$1(node, infer) {
2336
2375
  if (name === 'repeat') {
2337
2376
  return convertRepeatCommand(node, infer);
2338
2377
  }
2378
+ const original = node.originalCommand;
2379
+ const sum = node.modifiers?.to;
2380
+ const counted = node.args?.[0];
2381
+ if (name === 'set' &&
2382
+ (original === 'increment' || original === 'decrement') &&
2383
+ sum?.type === 'binaryExpression' &&
2384
+ counted) {
2385
+ return convertCommand$1({
2386
+ ...node,
2387
+ name: original,
2388
+ args: [counted],
2389
+ modifiers: { by: sum.right },
2390
+ originalCommand: undefined,
2391
+ }, infer);
2392
+ }
2339
2393
  const args = (node.args ?? []).map(arg => convertNode(arg, infer));
2340
2394
  const target = node.target ? convertNode(node.target, infer) : undefined;
2341
2395
  const modifiers = node.modifiers
@@ -2384,6 +2438,64 @@ function convertIfCommand(node, infer) {
2384
2438
  };
2385
2439
  }
2386
2440
  function convertRepeatCommand(node, infer) {
2441
+ const slots = (node.modifiers ?? {});
2442
+ const form = slotText(slots.loopType);
2443
+ if (form === undefined)
2444
+ return convertPositionalRepeat(node, infer);
2445
+ const [bodyBlock, elseBlock] = (node.args ?? []);
2446
+ const body = extractBlockCommands(bodyBlock, infer);
2447
+ const indexName = slotText(slots.index);
2448
+ const shared = {
2449
+ ...(indexName ? { indexName } : {}),
2450
+ ...(elseBlock ? { elseBody: extractBlockCommands(elseBlock, infer) } : {}),
2451
+ ...pos(node),
2452
+ };
2453
+ const operand = (slot) => (slot ? convertNode(slot, infer) : undefined);
2454
+ switch (form) {
2455
+ case 'for':
2456
+ return {
2457
+ type: 'foreach',
2458
+ itemName: slotText(slots.for) ?? 'it',
2459
+ collection: operand(slots.in) ?? { type: 'literal', value: null },
2460
+ body,
2461
+ ...shared,
2462
+ };
2463
+ case 'times':
2464
+ return { type: 'repeat', count: operand(slots.times), body, ...shared };
2465
+ case 'while':
2466
+ case 'until': {
2467
+ const test = operand(slots[form]) ?? { type: 'literal', value: form === 'until' };
2468
+ return {
2469
+ type: 'while',
2470
+ condition: form === 'until' ? { type: 'unary', operator: 'not', operand: test } : test,
2471
+ ...(slots.bottomTested?.value === true ? { bottomTested: true } : {}),
2472
+ body,
2473
+ ...shared,
2474
+ };
2475
+ }
2476
+ case 'until-event': {
2477
+ const event = slotText(slots.event);
2478
+ if (!event) {
2479
+ return { type: 'error', message: "'repeat until event' has no event name", ...pos(node) };
2480
+ }
2481
+ const target = operand(slots.from);
2482
+ return {
2483
+ type: 'repeat',
2484
+ untilEvent: event,
2485
+ ...(target ? { untilEventTarget: target } : {}),
2486
+ body,
2487
+ ...shared,
2488
+ };
2489
+ }
2490
+ default:
2491
+ return { type: 'repeat', body, ...shared };
2492
+ }
2493
+ }
2494
+ function slotText(slot) {
2495
+ const text = slot?.value ?? slot?.name;
2496
+ return typeof text === 'string' ? text : undefined;
2497
+ }
2498
+ function convertPositionalRepeat(node, infer) {
2387
2499
  const args = (node.args ?? []);
2388
2500
  if (args.length === 0) {
2389
2501
  return { type: 'repeat', body: [], ...pos(node) };
@@ -2700,6 +2812,15 @@ function toCoreAST(node) {
2700
2812
  prefix: true,
2701
2813
  ...nodePos(node),
2702
2814
  };
2815
+ case 'typeCheck':
2816
+ return {
2817
+ type: 'typeCheckExpression',
2818
+ value: toCoreAST(node.value),
2819
+ typeName: node.typeName,
2820
+ nullOk: node.nullOk,
2821
+ negated: node.negated,
2822
+ ...nodePos(node),
2823
+ };
2703
2824
  case 'member':
2704
2825
  return {
2705
2826
  type: 'memberExpression',
@@ -2710,6 +2831,12 @@ function toCoreAST(node) {
2710
2831
  computed: node.computed ?? false,
2711
2832
  ...nodePos(node),
2712
2833
  };
2834
+ case 'array':
2835
+ return {
2836
+ type: 'arrayLiteral',
2837
+ elements: node.elements.map(element => toCoreAST(element)),
2838
+ ...nodePos(node),
2839
+ };
2713
2840
  case 'possessive':
2714
2841
  return {
2715
2842
  type: 'possessiveExpression',
@@ -2772,72 +2899,85 @@ function convertCommand(node) {
2772
2899
  }
2773
2900
  return result;
2774
2901
  }
2902
+ function block(nodes) {
2903
+ return { type: 'block', commands: nodes.map(n => toCoreAST(n)), ...POS };
2904
+ }
2775
2905
  function convertIf(node) {
2776
- const result = {
2906
+ const [elseIf, ...laterElseIfs] = node.elseIfBranches ?? [];
2907
+ const elseBranch = elseIf
2908
+ ? [
2909
+ {
2910
+ type: 'if',
2911
+ condition: elseIf.condition,
2912
+ thenBranch: elseIf.body,
2913
+ elseIfBranches: laterElseIfs,
2914
+ ...(node.elseBranch ? { elseBranch: node.elseBranch } : {}),
2915
+ },
2916
+ ]
2917
+ : node.elseBranch;
2918
+ return {
2777
2919
  type: 'command',
2778
2920
  name: 'if',
2779
2921
  isBlocking: true,
2780
- condition: toCoreAST(node.condition),
2781
- thenBranch: node.thenBranch.map(n => toCoreAST(n)),
2782
- args: [],
2922
+ args: [
2923
+ toCoreAST(node.condition),
2924
+ block(node.thenBranch),
2925
+ ...(elseBranch ? [block(elseBranch)] : []),
2926
+ ],
2783
2927
  ...nodePos(node),
2784
2928
  };
2785
- if (node.elseBranch) {
2786
- result.elseBranch = node.elseBranch.map(n => toCoreAST(n));
2787
- }
2788
- return result;
2789
2929
  }
2790
- function convertRepeat(node) {
2791
- const result = {
2930
+ function loopCommand(node, form, operands) {
2931
+ const modifiers = { loopType: slotString(form) };
2932
+ for (const [slot, value] of Object.entries(operands)) {
2933
+ if (value)
2934
+ modifiers[slot] = value;
2935
+ }
2936
+ if (node.indexName)
2937
+ modifiers.index = slotString(node.indexName);
2938
+ return {
2792
2939
  type: 'command',
2793
2940
  name: 'repeat',
2794
2941
  isBlocking: true,
2795
- body: node.body.map(n => toCoreAST(n)),
2796
- args: [],
2942
+ args: [block(node.body), ...(node.elseBody ? [block(node.elseBody)] : [])],
2943
+ modifiers,
2797
2944
  ...nodePos(node),
2798
2945
  };
2946
+ }
2947
+ function slotString(value) {
2948
+ return { type: 'string', value, ...POS };
2949
+ }
2950
+ function convertRepeat(node) {
2799
2951
  if (node.count !== undefined) {
2800
- result.count =
2801
- typeof node.count === 'number'
2802
- ? { type: 'literal', value: node.count, raw: String(node.count), ...POS }
2803
- : toCoreAST(node.count);
2804
- result.loopVariant = 'times';
2805
- }
2806
- else if (node.whileCondition !== undefined) {
2807
- result.condition = toCoreAST(node.whileCondition);
2808
- result.loopVariant = 'while';
2809
- }
2810
- else if (node.untilEvent !== undefined) {
2811
- result.untilEvent = node.untilEvent;
2812
- result.loopVariant = 'until';
2952
+ const times = typeof node.count === 'number'
2953
+ ? { type: 'literal', value: node.count, raw: String(node.count), ...POS }
2954
+ : toCoreAST(node.count);
2955
+ return loopCommand(node, 'times', { times });
2956
+ }
2957
+ if (node.whileCondition !== undefined) {
2958
+ return loopCommand(node, 'while', { while: toCoreAST(node.whileCondition) });
2959
+ }
2960
+ if (node.untilEvent !== undefined) {
2961
+ return loopCommand(node, 'until-event', {
2962
+ event: slotString(node.untilEvent),
2963
+ from: node.untilEventTarget ? toCoreAST(node.untilEventTarget) : undefined,
2964
+ });
2813
2965
  }
2814
- return result;
2966
+ return loopCommand(node, 'forever', {});
2815
2967
  }
2816
2968
  function convertForEach(node) {
2817
- return {
2818
- type: 'command',
2819
- name: 'repeat',
2820
- isBlocking: true,
2821
- loopVariant: 'for',
2822
- itemName: node.itemName,
2823
- ...(node.indexName ? { indexName: node.indexName } : {}),
2824
- collection: toCoreAST(node.collection),
2825
- body: node.body.map(n => toCoreAST(n)),
2826
- args: [],
2827
- ...nodePos(node),
2828
- };
2969
+ return loopCommand(node, 'for', {
2970
+ for: slotString(node.itemName),
2971
+ in: toCoreAST(node.collection),
2972
+ });
2829
2973
  }
2830
2974
  function convertWhile(node) {
2831
- return {
2832
- type: 'command',
2833
- name: 'repeat',
2834
- isBlocking: true,
2835
- loopVariant: 'while',
2836
- condition: toCoreAST(node.condition),
2837
- body: node.body.map(n => toCoreAST(n)),
2838
- args: [],
2839
- ...nodePos(node),
2840
- };
2975
+ return loopCommand(node, 'while', {
2976
+ while: toCoreAST(node.condition),
2977
+ bottomTested: node.bottomTested
2978
+ ? { type: 'literal', value: true, raw: 'true', ...POS }
2979
+ : undefined,
2980
+ });
2841
2981
  }
2842
2982
  function flattenEventModifiers(modifiers) {
2843
2983
  if (!modifiers)
@@ -3241,6 +3381,9 @@ function getChildren(node) {
3241
3381
  case 'unary':
3242
3382
  children.push(node.operand);
3243
3383
  break;
3384
+ case 'typeCheck':
3385
+ children.push(node.value);
3386
+ break;
3244
3387
  case 'member': {
3245
3388
  const m = node;
3246
3389
  children.push(m.object);
@@ -3378,6 +3521,8 @@ function friendlyTypeName(node) {
3378
3521
  return `Binary Expression (${node.operator})`;
3379
3522
  case 'unary':
3380
3523
  return `Unary Expression (${node.operator})`;
3524
+ case 'typeCheck':
3525
+ return `Type Check (${node.typeName})`;
3381
3526
  case 'literal':
3382
3527
  return 'Literal';
3383
3528
  case 'identifier':
@@ -1,4 +1,4 @@
1
- export type InterchangeNode = EventNode | CommandNode | LiteralNode | IdentifierNode | SelectorNode | VariableNode | BinaryNode | UnaryNode | MemberNode | PossessiveNode | CallNode | IfNode | RepeatNode | ForEachNode | WhileNode | PositionalNode | ErrorNode;
1
+ export type InterchangeNode = EventNode | CommandNode | LiteralNode | IdentifierNode | SelectorNode | VariableNode | BinaryNode | UnaryNode | TypeCheckNode | MemberNode | PossessiveNode | ArrayNode | CallNode | IfNode | RepeatNode | ForEachNode | WhileNode | PositionalNode | ErrorNode;
2
2
  export interface BaseNode {
3
3
  readonly type: string;
4
4
  readonly start?: number;
@@ -35,6 +35,13 @@ export interface UnaryNode extends BaseNode {
35
35
  readonly operator: string;
36
36
  readonly operand: InterchangeNode;
37
37
  }
38
+ export interface TypeCheckNode extends BaseNode {
39
+ readonly type: 'typeCheck';
40
+ readonly value: InterchangeNode;
41
+ readonly typeName: string;
42
+ readonly nullOk: boolean;
43
+ readonly negated: boolean;
44
+ }
38
45
  export interface MemberNode extends BaseNode {
39
46
  readonly type: 'member';
40
47
  readonly object: InterchangeNode;
@@ -46,6 +53,10 @@ export interface PossessiveNode extends BaseNode {
46
53
  readonly object: InterchangeNode;
47
54
  readonly property: string;
48
55
  }
56
+ export interface ArrayNode extends BaseNode {
57
+ readonly type: 'array';
58
+ readonly elements: readonly InterchangeNode[];
59
+ }
49
60
  export interface CallNode extends BaseNode {
50
61
  readonly type: 'call';
51
62
  readonly callee: InterchangeNode;
@@ -92,7 +103,10 @@ export interface RepeatNode extends BaseNode {
92
103
  readonly count?: number | InterchangeNode;
93
104
  readonly whileCondition?: InterchangeNode;
94
105
  readonly untilEvent?: string;
106
+ readonly untilEventTarget?: InterchangeNode;
107
+ readonly indexName?: string;
95
108
  readonly body: InterchangeNode[];
109
+ readonly elseBody?: InterchangeNode[];
96
110
  }
97
111
  export interface ForEachNode extends BaseNode {
98
112
  readonly type: 'foreach';
@@ -100,11 +114,15 @@ export interface ForEachNode extends BaseNode {
100
114
  readonly indexName?: string;
101
115
  readonly collection: InterchangeNode;
102
116
  readonly body: InterchangeNode[];
117
+ readonly elseBody?: InterchangeNode[];
103
118
  }
104
119
  export interface WhileNode extends BaseNode {
105
120
  readonly type: 'while';
106
121
  readonly condition: InterchangeNode;
122
+ readonly bottomTested?: boolean;
123
+ readonly indexName?: string;
107
124
  readonly body: InterchangeNode[];
125
+ readonly elseBody?: InterchangeNode[];
108
126
  }
109
127
  export interface EventModifiers {
110
128
  readonly prevent?: boolean;
@@ -655,10 +655,29 @@ function isHTMLElement(value) {
655
655
  'classList' in value);
656
656
  }
657
657
 
658
+ function loneRootWithTag(content, tagName) {
659
+ let root;
660
+ if (typeof content === 'string') {
661
+ const template = document.createElement('template');
662
+ template.innerHTML = content;
663
+ root = template.content.firstElementChild;
664
+ if (!root || root.nextElementSibling)
665
+ return null;
666
+ }
667
+ else {
668
+ root = content.cloneNode(true);
669
+ }
670
+ return root.tagName === tagName && isHTMLElement(root) ? root : null;
671
+ }
658
672
  function executeSwap(target, content, strategy, morphOptions) {
659
673
  const contentStr = content !== null && !isHTMLElement(content) ? content : '';
660
674
  const contentEl = isHTMLElement(content) ? content : null;
661
675
  switch (strategy) {
676
+ case 'morphMerge': {
677
+ const root = content !== null ? loneRootWithTag(content, target.tagName) : null;
678
+ executeSwap(target, root ?? content, root ? 'morphOuter' : 'morph', morphOptions);
679
+ break;
680
+ }
662
681
  case 'morph':
663
682
  if (content !== null) {
664
683
  try {
@@ -653,10 +653,29 @@ function isHTMLElement(value) {
653
653
  'classList' in value);
654
654
  }
655
655
 
656
+ function loneRootWithTag(content, tagName) {
657
+ let root;
658
+ if (typeof content === 'string') {
659
+ const template = document.createElement('template');
660
+ template.innerHTML = content;
661
+ root = template.content.firstElementChild;
662
+ if (!root || root.nextElementSibling)
663
+ return null;
664
+ }
665
+ else {
666
+ root = content.cloneNode(true);
667
+ }
668
+ return root.tagName === tagName && isHTMLElement(root) ? root : null;
669
+ }
656
670
  function executeSwap(target, content, strategy, morphOptions) {
657
671
  const contentStr = content !== null && !isHTMLElement(content) ? content : '';
658
672
  const contentEl = isHTMLElement(content) ? content : null;
659
673
  switch (strategy) {
674
+ case 'morphMerge': {
675
+ const root = content !== null ? loneRootWithTag(content, target.tagName) : null;
676
+ executeSwap(target, root ?? content, root ? 'morphOuter' : 'morph', morphOptions);
677
+ break;
678
+ }
660
679
  case 'morph':
661
680
  if (content !== null) {
662
681
  try {
@@ -262,13 +262,23 @@ const COMMAND_IMPLEMENTATIONS_TS = {
262
262
  if (isOuter) {
263
263
  morphlexMorph(target, contentStr);
264
264
  } else {
265
- // morphlex's morphInner takes an ELEMENT, not a string \u2014 the same
266
- // wrapping src/lib/morph-adapter.ts does. Passing the string threw
267
- // straight into the fallback below, so inner morph never actually
268
- // morphed (it silently degraded to innerHTML on every call).
269
- const wrapper = document.createElement(target.tagName);
270
- wrapper.innerHTML = contentStr;
271
- morphlexMorphInner(target, wrapper);
265
+ // Upstream's morph MERGES a lone root element that has the
266
+ // target's tag (attributes and children); anything else morphs the
267
+ // children \u2014 the full runtime's 'morphMerge' (lib/swap-executor.ts).
268
+ const template = document.createElement('template');
269
+ template.innerHTML = contentStr;
270
+ const root = template.content.firstElementChild;
271
+ if (root && !root.nextElementSibling && root.tagName === target.tagName) {
272
+ morphlexMorph(target, root);
273
+ } else {
274
+ // morphlex's morphInner takes an ELEMENT, not a string \u2014 the same
275
+ // wrapping src/lib/morph-adapter.ts does. Passing the string threw
276
+ // straight into the fallback below, so inner morph never actually
277
+ // morphed (it silently degraded to innerHTML on every call).
278
+ const wrapper = document.createElement(target.tagName);
279
+ wrapper.innerHTML = contentStr;
280
+ morphlexMorphInner(target, wrapper);
281
+ }
272
282
  }
273
283
  } catch (error) {
274
284
  // Fallback to innerHTML/outerHTML if morph fails
@@ -313,13 +323,15 @@ const COMMAND_IMPLEMENTATIONS_TS = {
313
323
  return newVal;
314
324
  }
315
325
 
316
- if (target.type === 'possessive' && isStyleProp(target.property)) {
326
+ if (target.type === 'possessive') {
327
+ const style = isStyleProp(target.property);
317
328
  const obj = await evaluate(target.object, ctx);
318
329
  const elements = toElementArray(obj);
319
330
  for (const el of elements) {
320
- const current = parseFloat(getStyleProp(el, target.property) || '0') || 0;
321
- const newVal = current + amount;
322
- setStyleProp(el, target.property, newVal);
331
+ const was = style ? getStyleProp(el, target.property) : Reflect.get(el, target.property);
332
+ const newVal = (parseFloat(was || '0') || 0) + amount;
333
+ if (style) setStyleProp(el, target.property, newVal);
334
+ else Reflect.set(el, target.property, newVal);
323
335
  ctx.it = newVal;
324
336
  }
325
337
  return ctx.it;
@@ -349,13 +361,15 @@ const COMMAND_IMPLEMENTATIONS_TS = {
349
361
  return newVal;
350
362
  }
351
363
 
352
- if (target.type === 'possessive' && isStyleProp(target.property)) {
364
+ if (target.type === 'possessive') {
365
+ const style = isStyleProp(target.property);
353
366
  const obj = await evaluate(target.object, ctx);
354
367
  const elements = toElementArray(obj);
355
368
  for (const el of elements) {
356
- const current = parseFloat(getStyleProp(el, target.property) || '0') || 0;
357
- const newVal = current - amount;
358
- setStyleProp(el, target.property, newVal);
369
+ const was = style ? getStyleProp(el, target.property) : Reflect.get(el, target.property);
370
+ const newVal = (parseFloat(was || '0') || 0) - amount;
371
+ if (style) setStyleProp(el, target.property, newVal);
372
+ else Reflect.set(el, target.property, newVal);
359
373
  ctx.it = newVal;
360
374
  }
361
375
  return ctx.it;
@@ -592,7 +606,9 @@ const BLOCK_IMPLEMENTATIONS_TS = {
592
606
  repeat: `
593
607
  case 'repeat': {
594
608
  const count = await evaluate(block.condition!, ctx);
595
- const n = typeof count === 'number' ? count : parseInt(String(count));
609
+ // A count reads as upstream's \`index < times\` does, and core's loopCount:
610
+ // "6.5" loops 7 times, "6abc" none (parseInt read 6 for both), true once.
611
+ const n = Number(count) || 0;
596
612
  for (let i = 0; i < n && i < MAX_LOOP_ITERATIONS; i++) {
597
613
  ctx.locals.set('__loop_index__', i);
598
614
  ctx.locals.set('__loop_count__', i + 1);
@@ -2342,7 +2358,7 @@ class HybridParser {
2342
2358
  }
2343
2359
  return { type: "command", name, args: [url], modifiers };
2344
2360
  }
2345
- /** \`morph [over] <target> with|to|into <content>\` \u2014 all four connectives the full parser takes. */
2361
+ /** \`morph [over] <target> to|with|into <content>\` \u2014 upstream spells it \`to\`; the full parser also takes \`with\`; \`into\` is this parser's own. */
2346
2362
  parseMorph() {
2347
2363
  this.expect("morph");
2348
2364
  let modifier;
@@ -2540,6 +2556,9 @@ class HybridParser {
2540
2556
  return left;
2541
2557
  }
2542
2558
  parseUnary() {
2559
+ if (this.match("beep") && this.peek(1).value === "!") {
2560
+ throw new Error(\`'beep!' needs the full parser (use hyperfixi.js)\`);
2561
+ }
2543
2562
  if (this.match("not", "!")) {
2544
2563
  this.advance();
2545
2564
  return { type: "unary", operator: "not", operand: this.parseUnary() };
@@ -260,13 +260,23 @@ const COMMAND_IMPLEMENTATIONS_TS = {
260
260
  if (isOuter) {
261
261
  morphlexMorph(target, contentStr);
262
262
  } else {
263
- // morphlex's morphInner takes an ELEMENT, not a string \u2014 the same
264
- // wrapping src/lib/morph-adapter.ts does. Passing the string threw
265
- // straight into the fallback below, so inner morph never actually
266
- // morphed (it silently degraded to innerHTML on every call).
267
- const wrapper = document.createElement(target.tagName);
268
- wrapper.innerHTML = contentStr;
269
- morphlexMorphInner(target, wrapper);
263
+ // Upstream's morph MERGES a lone root element that has the
264
+ // target's tag (attributes and children); anything else morphs the
265
+ // children \u2014 the full runtime's 'morphMerge' (lib/swap-executor.ts).
266
+ const template = document.createElement('template');
267
+ template.innerHTML = contentStr;
268
+ const root = template.content.firstElementChild;
269
+ if (root && !root.nextElementSibling && root.tagName === target.tagName) {
270
+ morphlexMorph(target, root);
271
+ } else {
272
+ // morphlex's morphInner takes an ELEMENT, not a string \u2014 the same
273
+ // wrapping src/lib/morph-adapter.ts does. Passing the string threw
274
+ // straight into the fallback below, so inner morph never actually
275
+ // morphed (it silently degraded to innerHTML on every call).
276
+ const wrapper = document.createElement(target.tagName);
277
+ wrapper.innerHTML = contentStr;
278
+ morphlexMorphInner(target, wrapper);
279
+ }
270
280
  }
271
281
  } catch (error) {
272
282
  // Fallback to innerHTML/outerHTML if morph fails
@@ -311,13 +321,15 @@ const COMMAND_IMPLEMENTATIONS_TS = {
311
321
  return newVal;
312
322
  }
313
323
 
314
- if (target.type === 'possessive' && isStyleProp(target.property)) {
324
+ if (target.type === 'possessive') {
325
+ const style = isStyleProp(target.property);
315
326
  const obj = await evaluate(target.object, ctx);
316
327
  const elements = toElementArray(obj);
317
328
  for (const el of elements) {
318
- const current = parseFloat(getStyleProp(el, target.property) || '0') || 0;
319
- const newVal = current + amount;
320
- setStyleProp(el, target.property, newVal);
329
+ const was = style ? getStyleProp(el, target.property) : Reflect.get(el, target.property);
330
+ const newVal = (parseFloat(was || '0') || 0) + amount;
331
+ if (style) setStyleProp(el, target.property, newVal);
332
+ else Reflect.set(el, target.property, newVal);
321
333
  ctx.it = newVal;
322
334
  }
323
335
  return ctx.it;
@@ -347,13 +359,15 @@ const COMMAND_IMPLEMENTATIONS_TS = {
347
359
  return newVal;
348
360
  }
349
361
 
350
- if (target.type === 'possessive' && isStyleProp(target.property)) {
362
+ if (target.type === 'possessive') {
363
+ const style = isStyleProp(target.property);
351
364
  const obj = await evaluate(target.object, ctx);
352
365
  const elements = toElementArray(obj);
353
366
  for (const el of elements) {
354
- const current = parseFloat(getStyleProp(el, target.property) || '0') || 0;
355
- const newVal = current - amount;
356
- setStyleProp(el, target.property, newVal);
367
+ const was = style ? getStyleProp(el, target.property) : Reflect.get(el, target.property);
368
+ const newVal = (parseFloat(was || '0') || 0) - amount;
369
+ if (style) setStyleProp(el, target.property, newVal);
370
+ else Reflect.set(el, target.property, newVal);
357
371
  ctx.it = newVal;
358
372
  }
359
373
  return ctx.it;
@@ -590,7 +604,9 @@ const BLOCK_IMPLEMENTATIONS_TS = {
590
604
  repeat: `
591
605
  case 'repeat': {
592
606
  const count = await evaluate(block.condition!, ctx);
593
- const n = typeof count === 'number' ? count : parseInt(String(count));
607
+ // A count reads as upstream's \`index < times\` does, and core's loopCount:
608
+ // "6.5" loops 7 times, "6abc" none (parseInt read 6 for both), true once.
609
+ const n = Number(count) || 0;
594
610
  for (let i = 0; i < n && i < MAX_LOOP_ITERATIONS; i++) {
595
611
  ctx.locals.set('__loop_index__', i);
596
612
  ctx.locals.set('__loop_count__', i + 1);
@@ -2340,7 +2356,7 @@ class HybridParser {
2340
2356
  }
2341
2357
  return { type: "command", name, args: [url], modifiers };
2342
2358
  }
2343
- /** \`morph [over] <target> with|to|into <content>\` \u2014 all four connectives the full parser takes. */
2359
+ /** \`morph [over] <target> to|with|into <content>\` \u2014 upstream spells it \`to\`; the full parser also takes \`with\`; \`into\` is this parser's own. */
2344
2360
  parseMorph() {
2345
2361
  this.expect("morph");
2346
2362
  let modifier;
@@ -2538,6 +2554,9 @@ class HybridParser {
2538
2554
  return left;
2539
2555
  }
2540
2556
  parseUnary() {
2557
+ if (this.match("beep") && this.peek(1).value === "!") {
2558
+ throw new Error(\`'beep!' needs the full parser (use hyperfixi.js)\`);
2559
+ }
2541
2560
  if (this.match("not", "!")) {
2542
2561
  this.advance();
2543
2562
  return { type: "unary", operator: "not", operand: this.parseUnary() };