@hyperfixi/core 3.1.1 → 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-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
@@ -592,7 +602,9 @@ const BLOCK_IMPLEMENTATIONS_TS = {
592
602
  repeat: `
593
603
  case 'repeat': {
594
604
  const count = await evaluate(block.condition!, ctx);
595
- const n = typeof count === 'number' ? count : parseInt(String(count));
605
+ // A count reads as upstream's \`index < times\` does, and core's loopCount:
606
+ // "6.5" loops 7 times, "6abc" none (parseInt read 6 for both), true once.
607
+ const n = Number(count) || 0;
596
608
  for (let i = 0; i < n && i < MAX_LOOP_ITERATIONS; i++) {
597
609
  ctx.locals.set('__loop_index__', i);
598
610
  ctx.locals.set('__loop_count__', i + 1);
@@ -2342,7 +2354,7 @@ class HybridParser {
2342
2354
  }
2343
2355
  return { type: "command", name, args: [url], modifiers };
2344
2356
  }
2345
- /** \`morph [over] <target> with|to|into <content>\` \u2014 all four connectives the full parser takes. */
2357
+ /** \`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
2358
  parseMorph() {
2347
2359
  this.expect("morph");
2348
2360
  let modifier;
@@ -2540,6 +2552,9 @@ class HybridParser {
2540
2552
  return left;
2541
2553
  }
2542
2554
  parseUnary() {
2555
+ if (this.match("beep") && this.peek(1).value === "!") {
2556
+ throw new Error(\`'beep!' needs the full parser (use hyperfixi.js)\`);
2557
+ }
2543
2558
  if (this.match("not", "!")) {
2544
2559
  this.advance();
2545
2560
  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
@@ -590,7 +600,9 @@ const BLOCK_IMPLEMENTATIONS_TS = {
590
600
  repeat: `
591
601
  case 'repeat': {
592
602
  const count = await evaluate(block.condition!, ctx);
593
- const n = typeof count === 'number' ? count : parseInt(String(count));
603
+ // A count reads as upstream's \`index < times\` does, and core's loopCount:
604
+ // "6.5" loops 7 times, "6abc" none (parseInt read 6 for both), true once.
605
+ const n = Number(count) || 0;
594
606
  for (let i = 0; i < n && i < MAX_LOOP_ITERATIONS; i++) {
595
607
  ctx.locals.set('__loop_index__', i);
596
608
  ctx.locals.set('__loop_count__', i + 1);
@@ -2340,7 +2352,7 @@ class HybridParser {
2340
2352
  }
2341
2353
  return { type: "command", name, args: [url], modifiers };
2342
2354
  }
2343
- /** \`morph [over] <target> with|to|into <content>\` \u2014 all four connectives the full parser takes. */
2355
+ /** \`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
2356
  parseMorph() {
2345
2357
  this.expect("morph");
2346
2358
  let modifier;
@@ -2538,6 +2550,9 @@ class HybridParser {
2538
2550
  return left;
2539
2551
  }
2540
2552
  parseUnary() {
2553
+ if (this.match("beep") && this.peek(1).value === "!") {
2554
+ throw new Error(\`'beep!' needs the full parser (use hyperfixi.js)\`);
2555
+ }
2541
2556
  if (this.match("not", "!")) {
2542
2557
  this.advance();
2543
2558
  return { type: "unary", operator: "not", operand: this.parseUnary() };