@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
@@ -1,5 +1,5 @@
1
1
  export declare const LITE_PARSER_TEMPLATE = "\n// Lite Parser - Regex-based for minimal bundle size\n\nfunction parseLite(code) {\n const trimmed = code.trim();\n\n // Handle event handlers: \"on click toggle .active\"\n const onMatch = trimmed.match(/^on\\s+(\\w+)(?:\\s+from\\s+([^\\s]+))?\\s+(.+)$/i);\n if (onMatch) {\n return {\n type: 'event',\n event: onMatch[1],\n filter: onMatch[2] ? { type: 'selector', value: onMatch[2] } : undefined,\n modifiers: {},\n body: parseCommands(onMatch[3]),\n };\n }\n\n // Handle \"every Nms\" event pattern\n const everyMatch = trimmed.match(/^every\\s+(\\d+)(ms|s)?\\s+(.+)$/i);\n if (everyMatch) {\n const ms = everyMatch[2] === 's' ? parseInt(everyMatch[1]) * 1000 : parseInt(everyMatch[1]);\n return {\n type: 'event',\n event: 'interval:' + ms,\n modifiers: {},\n body: parseCommands(everyMatch[3]),\n };\n }\n\n // Handle \"init\" pattern\n const initMatch = trimmed.match(/^init\\s+(.+)$/i);\n if (initMatch) {\n return {\n type: 'event',\n event: 'init',\n modifiers: {},\n body: parseCommands(initMatch[1]),\n };\n }\n\n return { type: 'sequence', commands: parseCommands(trimmed) };\n}\n\nfunction parseCommands(code) {\n const parts = code.split(/\\s+(?:then|and)\\s+/i);\n return parts.map(parseCommand).filter(Boolean);\n}\n\nfunction parseCommand(code) {\n const trimmed = code.trim();\n if (!trimmed) return null;\n\n let match;\n\n // toggle .class [on target]\n match = trimmed.match(/^toggle\\s+(\\.\\w+|\\w+)(?:\\s+on\\s+(.+))?$/i);\n if (match) {\n return {\n type: 'command',\n name: 'toggle',\n args: [{ type: 'selector', value: match[1] }],\n target: match[2] ? parseTarget(match[2]) : undefined,\n };\n }\n\n // add .class [to target]\n match = trimmed.match(/^add\\s+(\\.\\w+|\\w+)(?:\\s+to\\s+(.+))?$/i);\n if (match) {\n return {\n type: 'command',\n name: 'add',\n args: [{ type: 'selector', value: match[1] }],\n target: match[2] ? parseTarget(match[2]) : undefined,\n };\n }\n\n // remove .class [from target] | remove [target]\n match = trimmed.match(/^remove\\s+(\\.\\w+)(?:\\s+from\\s+(.+))?$/i);\n if (match) {\n return {\n type: 'command',\n name: 'removeClass',\n args: [{ type: 'selector', value: match[1] }],\n target: match[2] ? parseTarget(match[2]) : undefined,\n };\n }\n match = trimmed.match(/^remove\\s+(.+)$/i);\n if (match) {\n return {\n type: 'command',\n name: 'remove',\n args: [],\n target: parseTarget(match[1]),\n };\n }\n\n // put \"content\" into target\n match = trimmed.match(/^put\\s+(?:\"([^\"]+)\"|'([^']+)'|(\\S+))\\s+(into|before|after)\\s+(.+)$/i);\n if (match) {\n const content = match[1] || match[2] || match[3];\n return {\n type: 'command',\n name: 'put',\n args: [{ type: 'literal', value: content }],\n modifier: match[4],\n target: parseTarget(match[5]),\n };\n }\n\n // set target to value | set :var to value\n match = trimmed.match(/^set\\s+(.+?)\\s+to\\s+(.+)$/i);\n if (match) {\n return {\n type: 'command',\n name: 'set',\n args: [parseTarget(match[1]), parseLiteValue(match[2])],\n };\n }\n\n // log message\n match = trimmed.match(/^log\\s+(.+)$/i);\n if (match) {\n return {\n type: 'command',\n name: 'log',\n args: [parseLiteValue(match[1])],\n };\n }\n\n // send event [to target]\n match = trimmed.match(/^send\\s+(\\w+)(?:\\s+to\\s+(.+))?$/i);\n if (match) {\n return {\n type: 'command',\n name: 'send',\n args: [{ type: 'literal', value: match[1] }],\n target: match[2] ? parseTarget(match[2]) : undefined,\n };\n }\n\n // wait Nms | wait Ns\n match = trimmed.match(/^wait\\s+(\\d+)(ms|s)?$/i);\n if (match) {\n const ms = match[2] === 's' ? parseInt(match[1]) * 1000 : parseInt(match[1]);\n return {\n type: 'command',\n name: 'wait',\n args: [{ type: 'literal', value: ms }],\n };\n }\n\n // show/hide shortcuts\n match = trimmed.match(/^(show|hide)(?:\\s+(.+))?$/i);\n if (match) {\n return {\n type: 'command',\n name: match[1].toLowerCase(),\n args: [],\n target: match[2] ? parseTarget(match[2]) : undefined,\n };\n }\n\n // Unknown command - try generic parsing\n const parts = trimmed.split(/\\s+/);\n if (parts.length > 0) {\n return {\n type: 'command',\n name: parts[0],\n args: parts.slice(1).map(p => ({ type: 'literal', value: p })),\n };\n }\n\n return null;\n}\n\nfunction parseTarget(str) {\n const s = str.trim();\n if (s === 'me') return { type: 'identifier', value: 'me' };\n if (s === 'body') return { type: 'identifier', value: 'body' };\n if (s.startsWith('#') || s.startsWith('.') || s.startsWith('[')) {\n return { type: 'selector', value: s };\n }\n if (s.startsWith(':')) {\n return { type: 'variable', name: s, scope: 'local' };\n }\n return { type: 'identifier', value: s };\n}\n\nfunction parseLiteValue(str) {\n const s = str.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n return { type: 'literal', value: s.slice(1, -1) };\n }\n if (/^-?\\d+(\\.\\d+)?$/.test(s)) {\n return { type: 'literal', value: parseFloat(s) };\n }\n if (s === 'true') return { type: 'literal', value: true };\n if (s === 'false') return { type: 'literal', value: false };\n if (s === 'null') return { type: 'literal', value: null };\n if (s.startsWith(':')) return { type: 'variable', name: s, scope: 'local' };\n if (s === 'me') return { type: 'identifier', value: 'me' };\n return { type: 'identifier', value: s };\n}\n";
2
- export declare const HYBRID_PARSER_TEMPLATE = "\n// #region generated:hybrid-parser \u2014 DO NOT EDIT (npm run generate:bundles)\n// Hybrid Parser \u2014 generated from parser/hybrid/{aliases,tokenizer,parser-core}.ts\n\nconst COMMAND_ALIASES = {\n flip: \"toggle\",\n switch: \"toggle\",\n display: \"show\",\n reveal: \"show\",\n conceal: \"hide\",\n increase: \"increment\",\n decrease: \"decrement\",\n fire: \"trigger\",\n dispatch: \"send\",\n navigate: \"go\",\n goto: \"go\"\n};\nconst EVENT_ALIASES = {\n clicked: \"click\",\n pressed: \"keydown\",\n changed: \"change\",\n submitted: \"submit\",\n loaded: \"load\"\n};\nfunction normalizeCommand(name) {\n const lower = name.toLowerCase();\n return COMMAND_ALIASES[lower] || lower;\n}\nfunction normalizeEvent(name) {\n const lower = name.toLowerCase();\n return EVENT_ALIASES[lower] || lower;\n}\nfunction addCommandAliases(aliases) {\n Object.assign(COMMAND_ALIASES, aliases);\n}\nfunction addEventAliases(aliases) {\n Object.assign(EVENT_ALIASES, aliases);\n}\n\nconst KEYWORDS = /* @__PURE__ */ new Set([\n \"on\",\n \"from\",\n \"to\",\n \"into\",\n \"before\",\n \"after\",\n \"in\",\n \"of\",\n \"at\",\n \"with\",\n \"if\",\n \"else\",\n \"unless\",\n \"end\",\n \"then\",\n \"and\",\n \"or\",\n \"not\",\n \"repeat\",\n \"times\",\n \"for\",\n \"each\",\n \"while\",\n \"until\",\n \"toggle\",\n \"add\",\n \"remove\",\n \"put\",\n \"set\",\n \"get\",\n \"call\",\n \"return\",\n \"append\",\n \"prepend\",\n \"log\",\n \"send\",\n \"trigger\",\n \"wait\",\n \"settle\",\n \"fetch\",\n \"as\",\n \"show\",\n \"hide\",\n \"take\",\n \"increment\",\n \"decrement\",\n \"focus\",\n \"blur\",\n \"go\",\n \"transition\",\n \"over\",\n \"the\",\n \"a\",\n \"an\",\n \"my\",\n \"its\",\n \"me\",\n \"it\",\n \"you\",\n \"first\",\n \"last\",\n \"next\",\n \"previous\",\n \"closest\",\n \"parent\",\n \"true\",\n \"false\",\n \"null\",\n \"undefined\",\n \"is\",\n \"matches\",\n \"contains\",\n \"includes\",\n \"exists\",\n \"has\",\n \"init\",\n \"every\",\n \"by\",\n \"halt\",\n \"via\",\n \"values\",\n \"default\",\n \"event\"\n]);\nfunction tokenize(code) {\n const tokens = [];\n let pos = 0;\n while (pos < code.length) {\n if (/\\s/.test(code[pos])) {\n pos++;\n continue;\n }\n if (code.slice(pos, pos + 2) === \"--\") {\n while (pos < code.length && code[pos] !== \"\\n\") pos++;\n continue;\n }\n const start = pos;\n if (code[pos] === \"<\" && /[a-zA-Z]/.test(code[pos + 1] || \"\")) {\n pos++;\n while (pos < code.length && code[pos] !== \">\") pos++;\n if (code[pos] === \">\") pos++;\n const val = code.slice(start, pos);\n if (val.endsWith(\"/>\") || val.endsWith(\">\")) {\n const normalized = val.slice(1).replace(/\\/?>$/, \"\");\n tokens.push({ type: \"selector\", value: normalized, pos: start });\n continue;\n }\n }\n if (code.slice(pos, pos + 2) === \"'s\" && !/[a-zA-Z]/.test(code[pos + 2] || \"\")) {\n tokens.push({ type: \"operator\", value: \"'s\", pos: start });\n pos += 2;\n continue;\n }\n if (code[pos] === '\"' || code[pos] === \"'\") {\n const quote = code[pos++];\n while (pos < code.length && code[pos] !== quote) {\n if (code[pos] === \"\\\\\") pos++;\n pos++;\n }\n pos++;\n tokens.push({ type: \"string\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (/\\d/.test(code[pos]) || code[pos] === \"-\" && /\\d/.test(code[pos + 1] || \"\")) {\n if (code[pos] === \"-\") pos++;\n while (pos < code.length && /[\\d.]/.test(code[pos])) pos++;\n if (code.slice(pos, pos + 2) === \"ms\") pos += 2;\n else if (code[pos] === \"s\" && !/[a-zA-Z]/.test(code[pos + 1] || \"\")) pos++;\n else if (code.slice(pos, pos + 2) === \"px\") pos += 2;\n tokens.push({ type: \"number\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \":\") {\n pos++;\n while (pos < code.length && /[\\w]/.test(code[pos])) pos++;\n tokens.push({ type: \"localVar\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"$\") {\n pos++;\n while (pos < code.length && /[\\w]/.test(code[pos])) pos++;\n tokens.push({ type: \"globalVar\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"@\") {\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"#\" || code[pos] === \".\") {\n if (code[pos] === \".\") {\n const afterDot = code.slice(pos + 1).match(/^(once|prevent|stop|debounce|throttle)\\b/i);\n if (afterDot) {\n tokens.push({ type: \"symbol\", value: \".\", pos: start });\n pos++;\n continue;\n }\n }\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"[\") {\n let lookahead = pos + 1;\n while (lookahead < code.length && /\\s/.test(code[lookahead])) lookahead++;\n const nextChar = code[lookahead] || \"\";\n const isArrayLiteral = /['\"\\d\\[\\]:\\$\\-]/.test(nextChar) || nextChar === \"\";\n if (isArrayLiteral) {\n tokens.push({ type: \"symbol\", value: \"[\", pos: start });\n pos++;\n continue;\n } else {\n pos++;\n let depth = 1;\n while (pos < code.length && depth > 0) {\n if (code[pos] === \"[\") depth++;\n if (code[pos] === \"]\") depth--;\n pos++;\n }\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n }\n if (code[pos] === \"]\") {\n tokens.push({ type: \"symbol\", value: \"]\", pos: start });\n pos++;\n continue;\n }\n if (code.slice(pos, pos + 2) === \"'s\") {\n tokens.push({ type: \"operator\", value: \"'s\", pos: start });\n pos += 2;\n continue;\n }\n const twoChar = code.slice(pos, pos + 2);\n if ([\"==\", \"!=\", \"<=\", \">=\", \"&&\", \"||\"].includes(twoChar)) {\n tokens.push({ type: \"operator\", value: twoChar, pos: start });\n pos += 2;\n continue;\n }\n if (code[pos] === \"*\" && /[a-zA-Z]/.test(code[pos + 1] || \"\")) {\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"styleProperty\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (\"+-*/%<>!\".includes(code[pos])) {\n tokens.push({ type: \"operator\", value: code[pos], pos: start });\n pos++;\n continue;\n }\n if (\"()[]{},.\".includes(code[pos])) {\n tokens.push({ type: \"symbol\", value: code[pos], pos: start });\n pos++;\n continue;\n }\n if (/[a-zA-Z_]/.test(code[pos])) {\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n const value = code.slice(start, pos);\n const type = KEYWORDS.has(value.toLowerCase()) ? \"keyword\" : \"identifier\";\n tokens.push({ type, value, pos: start });\n continue;\n }\n pos++;\n }\n tokens.push({ type: \"eof\", value: \"\", pos: code.length });\n return tokens;\n}\n\nvar __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== \"symbol\" ? key + \"\" : key, value);\nclass HybridParser {\n constructor(code) {\n __publicField(this, \"tokens\");\n __publicField(this, \"pos\", 0);\n /**\n * The raw source. Kept because `js \u2026 end` must be sliced from it verbatim \u2014\n * the tokenizer reads a JS body as hyperscript and mangles regexes, template\n * literals and single quotes. Same approach as the full parser's\n * `parseJsCommand`, which slices between token offsets for the same reason.\n */\n __publicField(this, \"source\");\n /**\n * Command keyword \u2192 parse rule. Built once per parse rather than once per\n * command, and the single place the parser's command set is written down:\n * `isCommandKeyword()` reads these keys instead of carrying its own copy.\n *\n * MUST stay in sync with `bundle-generator/parser-templates.ts`'s\n * `HYBRID_PARSER_TEMPLATE` \u2014 core's `generateBundle()` imports this file\n * while the vite-plugin's generator embeds that template, so a name in only\n * one of them makes the reachable command set depend on which generator the\n * user went through. Gated by `capability-emission.test.ts` \u00A73.\n */\n __publicField(this, \"cmdMap\", {\n toggle: () => this.parseToggle(),\n add: () => this.parseAdd(),\n remove: () => this.parseRemove(),\n put: () => this.parsePut(),\n append: () => this.parseInsertion(\"append\"),\n prepend: () => this.parseInsertion(\"prepend\"),\n set: () => this.parseSet(),\n get: () => this.parseGet(),\n call: () => this.parseCall(),\n log: () => this.parseLog(),\n send: () => this.parseSend(\"to\"),\n // `trigger <event> on <target>` \u2014 same node as `send`, per the runtime,\n // where `trigger` is a consolidation alias sharing `send`'s implementation.\n // The target marker differs, which is why it is a parameter and not a\n // second entry pointing at the same rule (that dropped the target).\n trigger: () => this.parseSend(\"on\"),\n wait: () => this.parseWait(),\n show: () => this.parseShow(),\n hide: () => this.parseHide(),\n take: () => this.parseTake(),\n empty: () => this.parseEmpty(),\n increment: () => this.parseIncDec(\"increment\"),\n decrement: () => this.parseIncDec(\"decrement\"),\n focus: () => this.parseFocusBlur(\"focus\"),\n blur: () => this.parseFocusBlur(\"blur\"),\n go: () => this.parseGo(),\n return: () => this.parseReturn(),\n transition: () => this.parseTransition(),\n halt: () => this.parseHalt(),\n copy: () => this.parseCopy(),\n beep: () => this.parseBeep(),\n push: () => this.parseUrlCommand(\"push\"),\n replace: () => this.parseUrlCommand(\"replace\"),\n morph: () => this.parseMorph(),\n js: () => this.parseJs(),\n throw: () => this.parseThrow(),\n break: () => this.parseBare(\"break\"),\n continue: () => this.parseBare(\"continue\"),\n exit: () => this.parseBare(\"exit\")\n });\n this.source = code;\n this.tokens = tokenize(code);\n }\n peek(offset = 0) {\n return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];\n }\n advance() {\n return this.tokens[this.pos++];\n }\n match(...values) {\n const token = this.peek();\n return values.some((v) => token.value.toLowerCase() === v.toLowerCase());\n }\n matchType(...types) {\n return types.includes(this.peek().type);\n }\n expect(value) {\n if (!this.match(value) && normalizeCommand(this.peek().value) !== value) {\n throw new Error(`Expected '${value}', got '${this.peek().value}'`);\n }\n return this.advance();\n }\n isAtEnd() {\n return this.peek().type === \"eof\";\n }\n parse() {\n if (this.match(\"on\")) return this.parseEventHandler();\n if (this.match(\"init\")) {\n this.advance();\n return { type: \"event\", event: \"init\", modifiers: {}, body: this.parseCommandSequence() };\n }\n if (this.match(\"every\")) return this.parseEveryHandler();\n return { type: \"sequence\", commands: this.parseCommandSequence() };\n }\n parseEventHandler() {\n this.expect(\"on\");\n const eventName = this.advance().value;\n const modifiers = {};\n let filter;\n while (this.peek().value === \".\") {\n this.advance();\n const mod = this.advance().value.toLowerCase();\n if (mod === \"once\") modifiers.once = true;\n else if (mod === \"prevent\") modifiers.prevent = true;\n else if (mod === \"stop\") modifiers.stop = true;\n else if (mod === \"debounce\" || mod === \"throttle\") {\n if (this.peek().value === \"(\") {\n this.advance();\n const num = this.advance().value;\n this.expect(\")\");\n if (mod === \"debounce\") modifiers.debounce = parseInt(num) || 100;\n else modifiers.throttle = parseInt(num) || 100;\n }\n }\n }\n if (this.match(\"from\")) {\n this.advance();\n filter = this.parseExpression();\n }\n return {\n type: \"event\",\n event: normalizeEvent(eventName),\n filter,\n modifiers,\n body: this.parseCommandSequence()\n };\n }\n parseEveryHandler() {\n this.expect(\"every\");\n const interval = this.advance().value;\n return {\n type: \"event\",\n event: `interval:${interval}`,\n modifiers: {},\n body: this.parseCommandSequence()\n };\n }\n parseCommandSequence() {\n const commands = [];\n while (!this.isAtEnd() && !this.match(\"end\", \"else\")) {\n const cmd = this.parseCommand();\n if (cmd) commands.push(cmd);\n if (this.match(\"then\", \"and\")) this.advance();\n }\n return commands;\n }\n parseCommand() {\n if (this.match(\"if\", \"unless\")) return this.parseIf();\n if (this.match(\"repeat\")) return this.parseRepeat();\n if (this.match(\"for\")) return this.parseFor();\n if (this.match(\"while\")) return this.parseWhile();\n if (this.match(\"fetch\")) return this.parseFetchBlock();\n const normalized = normalizeCommand(this.peek().value);\n if (this.cmdMap[normalized]) {\n return this.cmdMap[normalized]();\n }\n if (this.match(\"catch\", \"finally\")) {\n throw new Error(`'${this.peek().value}' needs the full parser (use hyperfixi.js)`);\n }\n const stray = this.peek();\n if (!this.isAtEnd() && (stray.type === \"identifier\" || stray.type === \"keyword\") && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n throw new Error(`'${stray.value}' needs the full parser (use hyperfixi.js)`);\n }\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n this.advance();\n }\n return null;\n }\n // Control flow parsing\n parseIf() {\n const isUnless = this.match(\"unless\");\n this.advance();\n const condition = this.parseExpression();\n const body = this.parseCommandSequence();\n let elseBody;\n if (this.match(\"else\")) {\n this.advance();\n elseBody = this.parseCommandSequence();\n }\n if (this.match(\"end\")) this.advance();\n return {\n type: \"if\",\n condition: isUnless ? { type: \"unary\", operator: \"not\", operand: condition } : condition,\n body,\n elseBody\n };\n }\n parseRepeat() {\n this.expect(\"repeat\");\n let count;\n if (!this.match(\"until\", \"while\", \"forever\")) {\n count = this.parseExpression();\n if (this.match(\"times\")) this.advance();\n }\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"repeat\", condition: count, body };\n }\n parseFor() {\n this.expect(\"for\");\n if (this.match(\"each\")) this.advance();\n const variable = this.advance().value;\n this.expect(\"in\");\n const iterable = this.parseExpression();\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"for\", condition: { type: \"forCondition\", variable, iterable }, body };\n }\n parseWhile() {\n this.expect(\"while\");\n const condition = this.parseExpression();\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"while\", condition, body };\n }\n /**\n * A naked URL \u2014 `fetch /api/data \u2026`, `fetch https://x/y \u2026` \u2014 the way upstream\n * reads one: everything up to the next whitespace, verbatim. The tokenizer\n * splits `/api/data` into `/`, `api`, `/`, `data`, and `parseExpression` used\n * to return the first `/` as the URL; the skip fallback then dropped the rest\n * silently, so every unquoted `fetch /path` in a hybrid bundle fetched `/`.\n * Measured 2026-09-04 when the fallback stopped being silent.\n */\n parseNakedUrl() {\n const tok = this.peek();\n const isSlash = tok.type === \"operator\" && tok.value === \"/\";\n const isScheme = (tok.value === \"http\" || tok.value === \"https\") && this.source.slice(tok.pos + tok.value.length, tok.pos + tok.value.length + 3) === \"://\";\n if (!isSlash && !isScheme) return null;\n let end = tok.pos;\n while (end < this.source.length && !/\\s/.test(this.source[end])) end++;\n while (!this.isAtEnd() && this.peek().pos < end) this.advance();\n return { type: \"literal\", value: this.source.slice(tok.pos, end) };\n }\n parseFetchBlock() {\n this.expect(\"fetch\");\n const url = this.parseNakedUrl() ?? this.parseExpression();\n let responseType = { type: \"literal\", value: \"text\" };\n let options;\n let method;\n if (this.match(\"{\")) {\n this.pos--;\n options = this.parseExpression();\n }\n for (let i = 0; i < 3; i++) {\n if (this.match(\"via\") && !method) {\n this.advance();\n method = this.parseExpression();\n continue;\n }\n if (this.match(\"as\")) {\n this.advance();\n if (this.match(\"a\") || this.match(\"an\")) this.advance();\n responseType = this.parseExpression();\n continue;\n }\n if (this.match(\"with\") && !options) {\n this.advance();\n options = this.parseExpression();\n continue;\n }\n break;\n }\n if (this.match(\"then\")) this.advance();\n const body = this.parseCommandSequence();\n return {\n type: \"fetch\",\n condition: { type: \"fetchConfig\", url, responseType, options, method },\n body\n };\n }\n // Command parsing\n parseToggle() {\n this.expect(\"toggle\");\n const what = this.parseExpression();\n let target;\n if (this.match(\"on\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"toggle\", args: [what], target };\n }\n parseAdd() {\n this.expect(\"add\");\n const what = this.parseExpression();\n let target;\n if (this.match(\"to\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"add\", args: [what], target };\n }\n parseRemove() {\n this.expect(\"remove\");\n if (this.matchType(\"selector\") && /^[.@]/.test(this.peek().value)) {\n const what = this.parseExpression();\n let target2;\n if (this.match(\"from\")) {\n this.advance();\n target2 = this.parseExpression();\n }\n return { type: \"command\", name: \"removeClass\", args: [what], target: target2 };\n }\n const target = this.parseExpression();\n return { type: \"command\", name: \"remove\", args: [], target };\n }\n parsePut() {\n this.expect(\"put\");\n const content = this.parseExpression();\n let modifier = \"into\";\n if (this.match(\"into\", \"before\", \"after\", \"at\")) {\n modifier = this.advance().value;\n if (modifier === \"at\") {\n const pos = this.advance().value;\n this.expect(\"of\");\n modifier = `at ${pos} of`;\n }\n }\n const target = this.parseExpression();\n return { type: \"command\", name: \"put\", args: [content], target, modifier };\n }\n /** `append`/`prepend <content> [to <target>]` \u2014 identical shape, both ends. */\n parseInsertion(name) {\n this.expect(name);\n const content = this.parseExpression();\n let target;\n if (this.match(\"to\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name, args: [content], target };\n }\n parseSet() {\n this.expect(\"set\");\n const target = this.parseExpression();\n if (this.match(\"to\")) {\n this.advance();\n const value = this.parseExpression();\n return { type: \"command\", name: \"set\", args: [target, value] };\n }\n return { type: \"command\", name: \"set\", args: [target] };\n }\n parseGet() {\n this.expect(\"get\");\n return { type: \"command\", name: \"get\", args: [this.parseExpression()] };\n }\n parseCall() {\n this.expect(\"call\");\n return { type: \"command\", name: \"call\", args: [this.parseExpression()] };\n }\n parseLog() {\n this.expect(\"log\");\n const args = [];\n while (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n else break;\n }\n return { type: \"command\", name: \"log\", args };\n }\n /**\n * `send <event> to <target>` / `trigger <event> on <target>`.\n *\n * Both emit a `send` node \u2014 the runtime registers `trigger` as a\n * consolidation alias sharing `send`'s implementation, so the bundle\n * generator carries one template for both. The marker is a parameter\n * because it is the ONLY difference: `trigger` used to reuse this rule with\n * `to` hardcoded, which parsed the event name and then silently abandoned\n * `on #target`, leaving the dispatch to land on `me`.\n */\n parseSend(marker) {\n this.advance();\n const first = this.advance();\n let event = first.value;\n let end = first.pos + first.value.length;\n while (!this.isAtEnd() && this.peek().type === \"localVar\" && this.peek().pos === end) {\n const piece = this.advance();\n event += piece.value;\n end = piece.pos + piece.value.length;\n }\n let target;\n if (this.match(marker)) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"send\", args: [{ type: \"literal\", value: event }], target };\n }\n parseWait() {\n this.expect(\"wait\");\n if (this.match(\"for\")) {\n this.advance();\n const event = this.advance().value;\n let target;\n if (this.match(\"from\")) {\n this.advance();\n target = this.parseExpression();\n }\n return {\n type: \"command\",\n name: \"waitFor\",\n args: [{ type: \"literal\", value: event }],\n target\n };\n }\n return { type: \"command\", name: \"wait\", args: [this.parseExpression()] };\n }\n parseShow() {\n this.expect(\"show\");\n let target;\n const modifiers = {};\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\", \"when\", \"where\")) {\n target = this.parseExpression();\n }\n if (!this.isAtEnd() && this.match(\"when\", \"where\")) {\n const keyword = this.advance().value;\n modifiers[keyword] = this.parseExpression();\n }\n return { type: \"command\", name: \"show\", args: [], target, modifiers };\n }\n parseHide() {\n this.expect(\"hide\");\n let target;\n const modifiers = {};\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\", \"when\", \"where\")) {\n target = this.parseExpression();\n }\n if (!this.isAtEnd() && this.match(\"when\", \"where\")) {\n const keyword = this.advance().value;\n modifiers[keyword] = this.parseExpression();\n }\n return { type: \"command\", name: \"hide\", args: [], target, modifiers };\n }\n parseTake() {\n this.expect(\"take\");\n const what = this.parseExpression();\n let from;\n if (this.match(\"from\")) {\n this.advance();\n from = this.parseExpression();\n }\n return { type: \"command\", name: \"take\", args: [what], target: from };\n }\n parseIncDec(name) {\n this.advance();\n const target = this.parseExpression();\n let amount = { type: \"literal\", value: 1 };\n if (this.match(\"by\")) {\n this.advance();\n amount = this.parseExpression();\n }\n return { type: \"command\", name, args: [target, amount] };\n }\n parseFocusBlur(name) {\n this.advance();\n let target;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n target = this.parseExpression();\n }\n return { type: \"command\", name, args: [], target };\n }\n parseGo() {\n this.expect(\"go\");\n if (this.match(\"to\")) this.advance();\n if (this.match(\"url\")) this.advance();\n const dest = this.parseExpression();\n return { type: \"command\", name: \"go\", args: [dest] };\n }\n parseReturn() {\n this.expect(\"return\");\n let value;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n value = this.parseExpression();\n }\n return { type: \"command\", name: \"return\", args: value ? [value] : [] };\n }\n // ---------------------------------------------------------------------------\n // Rules restoring the commands the capability list advertised but this parser\n // could not reach (Finding 13). Every one already had a working template in\n // `bundle-generator/templates.ts`; only the parse rule was missing, so the\n // emitted `case` label was dead and the user's source silently no-opped.\n // ---------------------------------------------------------------------------\n /** `empty [<target>]` \u2014 clears children. Defaults to `me`. */\n parseEmpty() {\n this.expect(\"empty\");\n let target;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"empty\", args: [], target };\n }\n /** `copy <value>` \u2014 clipboard write. */\n parseCopy() {\n this.expect(\"copy\");\n return { type: \"command\", name: \"copy\", args: [this.parseExpression()] };\n }\n /**\n * `beep[!] [<value>, \u2026]` \u2014 debug echo.\n *\n * Upstream spells it `beep!`; hyperfixi accepts both (see the tier list's\n * note on the same nuance). The `!` is consumed here because the tokenizer\n * emits it as a separate operator, and `parseUnary` would otherwise read it\n * as logical negation of the first argument.\n */\n parseBeep() {\n this.advance();\n if (this.match(\"!\")) this.advance();\n const args = [];\n while (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n else break;\n }\n return { type: \"command\", name: \"beep\", args };\n }\n /**\n * `push|replace url <url> [with title <title>]` \u2014 History API navigation.\n *\n * The `url` keyword is consumed here rather than left for the executor, the\n * way `parseGo` already does it. Note `push-url` / `replace-url` are NOT\n * source spellings \u2014 the full parser rejects both \u2014 they exist only as\n * bundle-config aliases resolving to these templates.\n */\n parseUrlCommand(name) {\n this.advance();\n if (this.match(\"url\")) this.advance();\n const url = this.parseExpression();\n const modifiers = {};\n if (this.match(\"with\")) {\n this.advance();\n if (this.match(\"title\")) {\n this.advance();\n modifiers.title = this.parseExpression();\n }\n }\n return { type: \"command\", name, args: [url], modifiers };\n }\n /** `morph [over] <target> with|to|into <content>` \u2014 all four connectives the full parser takes. */\n parseMorph() {\n this.expect(\"morph\");\n let modifier;\n if (this.match(\"over\")) {\n this.advance();\n modifier = \"over\";\n }\n const target = this.parseExpression();\n if (this.match(\"with\", \"to\", \"into\")) this.advance();\n const content = this.parseExpression();\n return { type: \"command\", name: \"morph\", args: [content], target, modifier };\n }\n /**\n * `js[(<params>)] <raw JavaScript> end`.\n *\n * The body is sliced from the ORIGINAL SOURCE between token offsets, never\n * reassembled from tokens \u2014 the tokenizer reads it as hyperscript, which\n * mangles regexes, template literals and possessive-looking quotes. This\n * mirrors the full parser's `parseJsCommand`.\n *\n * Known limit: the terminator scan matches the `end` TOKEN, so `end` inside a\n * JS *string* is safe (the tokenizer keeps a string as one token, quotes\n * included) but a bare JS identifier named `end` would cut the body early.\n * The full parser's `findJsEndBoundary` handles that; replicating it is not\n * worth the bytes in a bundle this size.\n */\n parseJs() {\n this.advance();\n if (this.match(\"(\")) {\n while (!this.isAtEnd() && !this.match(\")\")) this.advance();\n if (this.match(\")\")) this.advance();\n }\n const start = this.peek().pos;\n while (!this.isAtEnd() && !this.match(\"end\")) this.advance();\n const stop = this.peek().pos;\n if (this.match(\"end\")) this.advance();\n const code = this.source.slice(start, stop).trim();\n return { type: \"command\", name: \"js\", args: [{ type: \"literal\", value: code }] };\n }\n /** `throw <value>` */\n parseThrow() {\n this.expect(\"throw\");\n let value;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n value = this.parseExpression();\n }\n return { type: \"command\", name: \"throw\", args: value ? [value] : [] };\n }\n /** `break` / `continue` / `exit` \u2014 bare control-flow signals, no arguments. */\n parseBare(name) {\n this.advance();\n return { type: \"command\", name, args: [] };\n }\n parseHalt() {\n this.expect(\"halt\");\n if (this.match(\"the\")) this.advance();\n if (this.match(\"event\", \"default\")) this.advance();\n return { type: \"command\", name: \"halt\", args: [] };\n }\n // transition <property> to <value> [over <duration>]\n parseTransition() {\n this.expect(\"transition\");\n let target;\n if (this.match(\"my\", \"its\")) {\n const ref = this.advance().value;\n target = { type: \"identifier\", value: ref === \"my\" ? \"me\" : \"it\" };\n } else if (this.matchType(\"selector\")) {\n const expr = this.parseExpression();\n if (expr.type === \"possessive\") {\n return this.parseTransitionRest(expr.object, expr.property);\n }\n target = expr;\n }\n const propToken = this.peek();\n let property;\n if (propToken.type === \"styleProperty\") {\n property = this.advance().value;\n } else if (propToken.type === \"identifier\" || propToken.type === \"keyword\") {\n property = this.advance().value;\n } else {\n property = \"opacity\";\n }\n return this.parseTransitionRest(target, property);\n }\n parseTransitionRest(target, property) {\n let toValue = { type: \"literal\", value: 1 };\n if (this.match(\"to\")) {\n this.advance();\n toValue = this.parseExpression();\n }\n let duration = { type: \"literal\", value: 300 };\n if (this.match(\"over\")) {\n this.advance();\n duration = this.parseExpression();\n }\n return {\n type: \"command\",\n name: \"transition\",\n args: [{ type: \"literal\", value: property }, toValue, duration],\n target\n };\n }\n // Expression parsing with operator precedence\n parseExpression() {\n return this.parseOr();\n }\n parseOr() {\n let left = this.parseAnd();\n while (this.match(\"or\", \"||\")) {\n this.advance();\n left = { type: \"binary\", operator: \"or\", left, right: this.parseAnd() };\n }\n return left;\n }\n parseAnd() {\n let left = this.parseEquality();\n while (this.match(\"and\", \"&&\") && !this.isCommandKeyword(this.peek(1))) {\n this.advance();\n left = { type: \"binary\", operator: \"and\", left, right: this.parseEquality() };\n }\n return left;\n }\n /**\n * Does this token start a command rather than continue an expression?\n *\n * Used in two places with different stakes: terminating an `and` chain\n * (`toggle .x and add .y`), and refusing to read a command name as a\n * possessive property (`my show`).\n *\n * This is a deliberate SUBSET of `cmdMap`'s keys, not a second copy of it \u2014\n * asserted as a subset in `capability-emission.test.ts` \u00A74 so it cannot\n * name something the parser does not dispatch. It is not the full set\n * because several of the newer keys are ordinary English or JS words\n * (`copy`, `empty`, `push`, `replace`, `break`) that appear as property\n * names and operands; promoting them here would change expression parsing,\n * which is a separate decision from command reachability. The cost is that\n * `\u2026 and break` reads `break` as an operand; `then` is the canonical\n * separator and is handled by `parseCommandSequence` directly.\n */\n isCommandKeyword(token) {\n const cmds = [\n \"toggle\",\n \"add\",\n \"remove\",\n \"set\",\n \"put\",\n \"log\",\n \"send\",\n \"wait\",\n \"show\",\n \"hide\",\n \"increment\",\n \"decrement\",\n \"focus\",\n \"blur\",\n \"go\"\n ];\n return cmds.includes(normalizeCommand(token.value));\n }\n parseEquality() {\n let left = this.parseComparison();\n while (this.match(\"==\", \"!=\", \"is\", \"matches\", \"contains\", \"includes\", \"has\")) {\n const op = this.advance().value;\n if (op.toLowerCase() === \"is\" && this.match(\"not\")) {\n this.advance();\n left = { type: \"binary\", operator: \"is not\", left, right: this.parseComparison() };\n } else {\n left = { type: \"binary\", operator: op, left, right: this.parseComparison() };\n }\n }\n return left;\n }\n parseComparison() {\n let left = this.parseAdditive();\n while (this.match(\"<\", \">\", \"<=\", \">=\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseAdditive() };\n }\n return left;\n }\n parseAdditive() {\n let left = this.parseMultiplicative();\n while (this.match(\"+\", \"-\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseMultiplicative() };\n }\n return left;\n }\n parseMultiplicative() {\n let left = this.parseUnary();\n while (this.match(\"*\", \"/\", \"%\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseUnary() };\n }\n return left;\n }\n parseUnary() {\n if (this.match(\"not\", \"!\")) {\n this.advance();\n return { type: \"unary\", operator: \"not\", operand: this.parseUnary() };\n }\n if (this.match(\"-\") && this.peek(1).type === \"number\") {\n this.advance();\n const num = this.advance();\n return { type: \"literal\", value: -parseFloat(num.value) };\n }\n return this.parsePostfix();\n }\n parsePostfix() {\n let left = this.parsePrimary();\n while (true) {\n if (this.match(\"'s\")) {\n this.advance();\n const next = this.peek();\n const prop = next.type === \"styleProperty\" ? this.advance().value : this.advance().value;\n left = { type: \"possessive\", object: left, property: prop };\n } else if (this.peek().type === \"styleProperty\") {\n const prop = this.advance().value;\n left = { type: \"possessive\", object: left, property: prop };\n } else if (this.peek().value === \".\") {\n this.advance();\n const prop = this.advance().value;\n left = { type: \"member\", object: left, property: prop };\n } else if (this.peek().type === \"selector\" && this.peek().value.startsWith(\".\")) {\n const prop = this.advance().value.slice(1);\n left = { type: \"member\", object: left, property: prop };\n } else if (this.peek().value === \"(\") {\n this.advance();\n const args = [];\n while (!this.match(\")\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n }\n this.expect(\")\");\n left = { type: \"call\", callee: left, args };\n } else if (this.peek().value === \"[\" && left.type !== \"selector\") {\n this.advance();\n const index = this.parseExpression();\n this.expect(\"]\");\n left = { type: \"member\", object: left, property: index, computed: true };\n } else {\n break;\n }\n }\n return left;\n }\n parsePrimary() {\n const token = this.peek();\n if (token.value === \"(\") {\n this.advance();\n const expr = this.parseExpression();\n this.expect(\")\");\n return expr;\n }\n if (token.value === \"{\") return this.parseObjectLiteral();\n if (token.value === \"[\") return this.parseArrayLiteral();\n if (token.type === \"number\") {\n this.advance();\n const val = token.value;\n if (val.endsWith(\"ms\")) return { type: \"literal\", value: parseInt(val), unit: \"ms\" };\n if (val.endsWith(\"s\")) return { type: \"literal\", value: parseFloat(val) * 1e3, unit: \"ms\" };\n return { type: \"literal\", value: parseFloat(val) };\n }\n if (token.type === \"string\") {\n this.advance();\n return { type: \"literal\", value: token.value.slice(1, -1) };\n }\n if (this.match(\"true\")) {\n this.advance();\n return { type: \"literal\", value: true };\n }\n if (this.match(\"false\")) {\n this.advance();\n return { type: \"literal\", value: false };\n }\n if (this.match(\"null\")) {\n this.advance();\n return { type: \"literal\", value: null };\n }\n if (this.match(\"undefined\")) {\n this.advance();\n return { type: \"literal\", value: void 0 };\n }\n if (token.type === \"localVar\") {\n this.advance();\n return { type: \"variable\", name: token.value, scope: \"local\" };\n }\n if (token.type === \"globalVar\") {\n this.advance();\n return { type: \"variable\", name: token.value, scope: \"global\" };\n }\n if (token.type === \"selector\") {\n this.advance();\n return { type: \"selector\", value: token.value };\n }\n if (this.match(\"my\")) {\n this.advance();\n const next = this.peek();\n if ((next.type === \"identifier\" || next.type === \"keyword\") && !this.isCommandKeyword(next)) {\n const prop = this.advance().value;\n return { type: \"possessive\", object: { type: \"identifier\", value: \"me\" }, property: prop };\n }\n return { type: \"identifier\", value: \"me\" };\n }\n if (this.match(\"its\")) {\n this.advance();\n const next = this.peek();\n if ((next.type === \"identifier\" || next.type === \"keyword\") && !this.isCommandKeyword(next)) {\n const prop = this.advance().value;\n return { type: \"possessive\", object: { type: \"identifier\", value: \"it\" }, property: prop };\n }\n return { type: \"identifier\", value: \"it\" };\n }\n if (this.match(\"me\")) {\n this.advance();\n return { type: \"identifier\", value: \"me\" };\n }\n if (this.match(\"it\")) {\n this.advance();\n return { type: \"identifier\", value: \"it\" };\n }\n if (this.match(\"you\")) {\n this.advance();\n return { type: \"identifier\", value: \"you\" };\n }\n if (this.match(\"the\", \"a\", \"an\")) {\n this.advance();\n if (this.match(\"first\", \"last\", \"next\", \"previous\", \"closest\", \"parent\")) {\n const position = this.advance().value;\n const target = this.parsePositionalTarget();\n return { type: \"positional\", position, target };\n }\n return this.parsePrimary();\n }\n if (this.match(\"first\", \"last\", \"next\", \"previous\", \"closest\", \"parent\")) {\n const position = this.advance().value;\n const target = this.parsePositionalTarget();\n return { type: \"positional\", position, target };\n }\n if (this.match(\"values\")) {\n this.advance();\n if (this.match(\"of\")) {\n this.advance();\n const target = this.parseExpression();\n return { type: \"valuesOf\", target };\n }\n return { type: \"identifier\", value: token.value };\n }\n if (token.type === \"identifier\" || token.type === \"keyword\") {\n this.advance();\n return { type: \"identifier\", value: token.value };\n }\n this.advance();\n return { type: \"identifier\", value: token.value };\n }\n parseObjectLiteral() {\n this.expect(\"{\");\n const properties = [];\n while (!this.match(\"}\")) {\n const key = this.advance().value;\n this.expect(\":\");\n const value = this.parseExpression();\n properties.push({ key, value });\n if (this.match(\",\")) this.advance();\n }\n this.expect(\"}\");\n return { type: \"object\", properties };\n }\n parseArrayLiteral() {\n this.expect(\"[\");\n const elements = [];\n while (!this.match(\"]\")) {\n elements.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n }\n this.expect(\"]\");\n return { type: \"array\", elements };\n }\n parsePositionalTarget() {\n const token = this.peek();\n if (token.type === \"selector\") {\n return { type: \"selector\", value: this.advance().value };\n }\n if (token.type === \"identifier\" || token.type === \"keyword\") {\n return { type: \"identifier\", value: this.advance().value };\n }\n return this.parseExpression();\n }\n}\n// #endregion generated:hybrid-parser\n";
2
+ export declare const HYBRID_PARSER_TEMPLATE = "\n// #region generated:hybrid-parser \u2014 DO NOT EDIT (npm run generate:bundles)\n// Hybrid Parser \u2014 generated from parser/hybrid/{aliases,tokenizer,parser-core}.ts\n\nconst COMMAND_ALIASES = {\n flip: \"toggle\",\n switch: \"toggle\",\n display: \"show\",\n reveal: \"show\",\n conceal: \"hide\",\n increase: \"increment\",\n decrease: \"decrement\",\n fire: \"trigger\",\n dispatch: \"send\",\n navigate: \"go\",\n goto: \"go\"\n};\nconst EVENT_ALIASES = {\n clicked: \"click\",\n pressed: \"keydown\",\n changed: \"change\",\n submitted: \"submit\",\n loaded: \"load\"\n};\nfunction normalizeCommand(name) {\n const lower = name.toLowerCase();\n return COMMAND_ALIASES[lower] || lower;\n}\nfunction normalizeEvent(name) {\n const lower = name.toLowerCase();\n return EVENT_ALIASES[lower] || lower;\n}\nfunction addCommandAliases(aliases) {\n Object.assign(COMMAND_ALIASES, aliases);\n}\nfunction addEventAliases(aliases) {\n Object.assign(EVENT_ALIASES, aliases);\n}\n\nconst KEYWORDS = /* @__PURE__ */ new Set([\n \"on\",\n \"from\",\n \"to\",\n \"into\",\n \"before\",\n \"after\",\n \"in\",\n \"of\",\n \"at\",\n \"with\",\n \"if\",\n \"else\",\n \"unless\",\n \"end\",\n \"then\",\n \"and\",\n \"or\",\n \"not\",\n \"repeat\",\n \"times\",\n \"for\",\n \"each\",\n \"while\",\n \"until\",\n \"toggle\",\n \"add\",\n \"remove\",\n \"put\",\n \"set\",\n \"get\",\n \"call\",\n \"return\",\n \"append\",\n \"prepend\",\n \"log\",\n \"send\",\n \"trigger\",\n \"wait\",\n \"settle\",\n \"fetch\",\n \"as\",\n \"show\",\n \"hide\",\n \"take\",\n \"increment\",\n \"decrement\",\n \"focus\",\n \"blur\",\n \"go\",\n \"transition\",\n \"over\",\n \"the\",\n \"a\",\n \"an\",\n \"my\",\n \"its\",\n \"me\",\n \"it\",\n \"you\",\n \"first\",\n \"last\",\n \"next\",\n \"previous\",\n \"closest\",\n \"parent\",\n \"true\",\n \"false\",\n \"null\",\n \"undefined\",\n \"is\",\n \"matches\",\n \"contains\",\n \"includes\",\n \"exists\",\n \"has\",\n \"init\",\n \"every\",\n \"by\",\n \"halt\",\n \"via\",\n \"values\",\n \"default\",\n \"event\"\n]);\nfunction tokenize(code) {\n const tokens = [];\n let pos = 0;\n while (pos < code.length) {\n if (/\\s/.test(code[pos])) {\n pos++;\n continue;\n }\n if (code.slice(pos, pos + 2) === \"--\") {\n while (pos < code.length && code[pos] !== \"\\n\") pos++;\n continue;\n }\n const start = pos;\n if (code[pos] === \"<\" && /[a-zA-Z]/.test(code[pos + 1] || \"\")) {\n pos++;\n while (pos < code.length && code[pos] !== \">\") pos++;\n if (code[pos] === \">\") pos++;\n const val = code.slice(start, pos);\n if (val.endsWith(\"/>\") || val.endsWith(\">\")) {\n const normalized = val.slice(1).replace(/\\/?>$/, \"\");\n tokens.push({ type: \"selector\", value: normalized, pos: start });\n continue;\n }\n }\n if (code.slice(pos, pos + 2) === \"'s\" && !/[a-zA-Z]/.test(code[pos + 2] || \"\")) {\n tokens.push({ type: \"operator\", value: \"'s\", pos: start });\n pos += 2;\n continue;\n }\n if (code[pos] === '\"' || code[pos] === \"'\") {\n const quote = code[pos++];\n while (pos < code.length && code[pos] !== quote) {\n if (code[pos] === \"\\\\\") pos++;\n pos++;\n }\n pos++;\n tokens.push({ type: \"string\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (/\\d/.test(code[pos]) || code[pos] === \"-\" && /\\d/.test(code[pos + 1] || \"\")) {\n if (code[pos] === \"-\") pos++;\n while (pos < code.length && /[\\d.]/.test(code[pos])) pos++;\n if (code.slice(pos, pos + 2) === \"ms\") pos += 2;\n else if (code[pos] === \"s\" && !/[a-zA-Z]/.test(code[pos + 1] || \"\")) pos++;\n else if (code.slice(pos, pos + 2) === \"px\") pos += 2;\n tokens.push({ type: \"number\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \":\") {\n pos++;\n while (pos < code.length && /[\\w]/.test(code[pos])) pos++;\n tokens.push({ type: \"localVar\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"$\") {\n pos++;\n while (pos < code.length && /[\\w]/.test(code[pos])) pos++;\n tokens.push({ type: \"globalVar\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"@\") {\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"#\" || code[pos] === \".\") {\n if (code[pos] === \".\") {\n const afterDot = code.slice(pos + 1).match(/^(once|prevent|stop|debounce|throttle)\\b/i);\n if (afterDot) {\n tokens.push({ type: \"symbol\", value: \".\", pos: start });\n pos++;\n continue;\n }\n }\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (code[pos] === \"[\") {\n let lookahead = pos + 1;\n while (lookahead < code.length && /\\s/.test(code[lookahead])) lookahead++;\n const nextChar = code[lookahead] || \"\";\n const isArrayLiteral = /['\"\\d\\[\\]:\\$\\-]/.test(nextChar) || nextChar === \"\";\n if (isArrayLiteral) {\n tokens.push({ type: \"symbol\", value: \"[\", pos: start });\n pos++;\n continue;\n } else {\n pos++;\n let depth = 1;\n while (pos < code.length && depth > 0) {\n if (code[pos] === \"[\") depth++;\n if (code[pos] === \"]\") depth--;\n pos++;\n }\n tokens.push({ type: \"selector\", value: code.slice(start, pos), pos: start });\n continue;\n }\n }\n if (code[pos] === \"]\") {\n tokens.push({ type: \"symbol\", value: \"]\", pos: start });\n pos++;\n continue;\n }\n if (code.slice(pos, pos + 2) === \"'s\") {\n tokens.push({ type: \"operator\", value: \"'s\", pos: start });\n pos += 2;\n continue;\n }\n const twoChar = code.slice(pos, pos + 2);\n if ([\"==\", \"!=\", \"<=\", \">=\", \"&&\", \"||\"].includes(twoChar)) {\n tokens.push({ type: \"operator\", value: twoChar, pos: start });\n pos += 2;\n continue;\n }\n if (code[pos] === \"*\" && /[a-zA-Z]/.test(code[pos + 1] || \"\")) {\n pos++;\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n tokens.push({ type: \"styleProperty\", value: code.slice(start, pos), pos: start });\n continue;\n }\n if (\"+-*/%<>!\".includes(code[pos])) {\n tokens.push({ type: \"operator\", value: code[pos], pos: start });\n pos++;\n continue;\n }\n if (\"()[]{},.\".includes(code[pos])) {\n tokens.push({ type: \"symbol\", value: code[pos], pos: start });\n pos++;\n continue;\n }\n if (/[a-zA-Z_]/.test(code[pos])) {\n while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;\n const value = code.slice(start, pos);\n const type = KEYWORDS.has(value.toLowerCase()) ? \"keyword\" : \"identifier\";\n tokens.push({ type, value, pos: start });\n continue;\n }\n pos++;\n }\n tokens.push({ type: \"eof\", value: \"\", pos: code.length });\n return tokens;\n}\n\nvar __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== \"symbol\" ? key + \"\" : key, value);\nclass HybridParser {\n constructor(code) {\n __publicField(this, \"tokens\");\n __publicField(this, \"pos\", 0);\n /**\n * The raw source. Kept because `js \u2026 end` must be sliced from it verbatim \u2014\n * the tokenizer reads a JS body as hyperscript and mangles regexes, template\n * literals and single quotes. Same approach as the full parser's\n * `parseJsCommand`, which slices between token offsets for the same reason.\n */\n __publicField(this, \"source\");\n /**\n * Command keyword \u2192 parse rule. Built once per parse rather than once per\n * command, and the single place the parser's command set is written down:\n * `isCommandKeyword()` reads these keys instead of carrying its own copy.\n *\n * MUST stay in sync with `bundle-generator/parser-templates.ts`'s\n * `HYBRID_PARSER_TEMPLATE` \u2014 core's `generateBundle()` imports this file\n * while the vite-plugin's generator embeds that template, so a name in only\n * one of them makes the reachable command set depend on which generator the\n * user went through. Gated by `capability-emission.test.ts` \u00A73.\n */\n __publicField(this, \"cmdMap\", {\n toggle: () => this.parseToggle(),\n add: () => this.parseAdd(),\n remove: () => this.parseRemove(),\n put: () => this.parsePut(),\n append: () => this.parseInsertion(\"append\"),\n prepend: () => this.parseInsertion(\"prepend\"),\n set: () => this.parseSet(),\n get: () => this.parseGet(),\n call: () => this.parseCall(),\n log: () => this.parseLog(),\n send: () => this.parseSend(\"to\"),\n // `trigger <event> on <target>` \u2014 same node as `send`, per the runtime,\n // where `trigger` is a consolidation alias sharing `send`'s implementation.\n // The target marker differs, which is why it is a parameter and not a\n // second entry pointing at the same rule (that dropped the target).\n trigger: () => this.parseSend(\"on\"),\n wait: () => this.parseWait(),\n show: () => this.parseShow(),\n hide: () => this.parseHide(),\n take: () => this.parseTake(),\n empty: () => this.parseEmpty(),\n increment: () => this.parseIncDec(\"increment\"),\n decrement: () => this.parseIncDec(\"decrement\"),\n focus: () => this.parseFocusBlur(\"focus\"),\n blur: () => this.parseFocusBlur(\"blur\"),\n go: () => this.parseGo(),\n return: () => this.parseReturn(),\n transition: () => this.parseTransition(),\n halt: () => this.parseHalt(),\n copy: () => this.parseCopy(),\n beep: () => this.parseBeep(),\n push: () => this.parseUrlCommand(\"push\"),\n replace: () => this.parseUrlCommand(\"replace\"),\n morph: () => this.parseMorph(),\n js: () => this.parseJs(),\n throw: () => this.parseThrow(),\n break: () => this.parseBare(\"break\"),\n continue: () => this.parseBare(\"continue\"),\n exit: () => this.parseBare(\"exit\")\n });\n this.source = code;\n this.tokens = tokenize(code);\n }\n peek(offset = 0) {\n return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];\n }\n advance() {\n return this.tokens[this.pos++];\n }\n match(...values) {\n const token = this.peek();\n return values.some((v) => token.value.toLowerCase() === v.toLowerCase());\n }\n matchType(...types) {\n return types.includes(this.peek().type);\n }\n expect(value) {\n if (!this.match(value) && normalizeCommand(this.peek().value) !== value) {\n throw new Error(`Expected '${value}', got '${this.peek().value}'`);\n }\n return this.advance();\n }\n isAtEnd() {\n return this.peek().type === \"eof\";\n }\n parse() {\n if (this.match(\"on\")) return this.parseEventHandler();\n if (this.match(\"init\")) {\n this.advance();\n return { type: \"event\", event: \"init\", modifiers: {}, body: this.parseCommandSequence() };\n }\n if (this.match(\"every\")) return this.parseEveryHandler();\n return { type: \"sequence\", commands: this.parseCommandSequence() };\n }\n parseEventHandler() {\n this.expect(\"on\");\n const eventName = this.advance().value;\n const modifiers = {};\n let filter;\n while (this.peek().value === \".\") {\n this.advance();\n const mod = this.advance().value.toLowerCase();\n if (mod === \"once\") modifiers.once = true;\n else if (mod === \"prevent\") modifiers.prevent = true;\n else if (mod === \"stop\") modifiers.stop = true;\n else if (mod === \"debounce\" || mod === \"throttle\") {\n if (this.peek().value === \"(\") {\n this.advance();\n const num = this.advance().value;\n this.expect(\")\");\n if (mod === \"debounce\") modifiers.debounce = parseInt(num) || 100;\n else modifiers.throttle = parseInt(num) || 100;\n }\n }\n }\n if (this.match(\"from\")) {\n this.advance();\n filter = this.parseExpression();\n }\n return {\n type: \"event\",\n event: normalizeEvent(eventName),\n filter,\n modifiers,\n body: this.parseCommandSequence()\n };\n }\n parseEveryHandler() {\n this.expect(\"every\");\n const interval = this.advance().value;\n return {\n type: \"event\",\n event: `interval:${interval}`,\n modifiers: {},\n body: this.parseCommandSequence()\n };\n }\n parseCommandSequence() {\n const commands = [];\n while (!this.isAtEnd() && !this.match(\"end\", \"else\")) {\n const cmd = this.parseCommand();\n if (cmd) commands.push(cmd);\n if (this.match(\"then\", \"and\")) this.advance();\n }\n return commands;\n }\n parseCommand() {\n if (this.match(\"if\", \"unless\")) return this.parseIf();\n if (this.match(\"repeat\")) return this.parseRepeat();\n if (this.match(\"for\")) return this.parseFor();\n if (this.match(\"while\")) return this.parseWhile();\n if (this.match(\"fetch\")) return this.parseFetchBlock();\n const normalized = normalizeCommand(this.peek().value);\n if (this.cmdMap[normalized]) {\n return this.cmdMap[normalized]();\n }\n if (this.match(\"catch\", \"finally\")) {\n throw new Error(`'${this.peek().value}' needs the full parser (use hyperfixi.js)`);\n }\n const stray = this.peek();\n if (!this.isAtEnd() && (stray.type === \"identifier\" || stray.type === \"keyword\") && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n throw new Error(`'${stray.value}' needs the full parser (use hyperfixi.js)`);\n }\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n this.advance();\n }\n return null;\n }\n // Control flow parsing\n parseIf() {\n const isUnless = this.match(\"unless\");\n this.advance();\n const condition = this.parseExpression();\n const body = this.parseCommandSequence();\n let elseBody;\n if (this.match(\"else\")) {\n this.advance();\n elseBody = this.parseCommandSequence();\n }\n if (this.match(\"end\")) this.advance();\n return {\n type: \"if\",\n condition: isUnless ? { type: \"unary\", operator: \"not\", operand: condition } : condition,\n body,\n elseBody\n };\n }\n parseRepeat() {\n this.expect(\"repeat\");\n let count;\n if (!this.match(\"until\", \"while\", \"forever\")) {\n count = this.parseExpression();\n if (this.match(\"times\")) this.advance();\n }\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"repeat\", condition: count, body };\n }\n parseFor() {\n this.expect(\"for\");\n if (this.match(\"each\")) this.advance();\n const variable = this.advance().value;\n this.expect(\"in\");\n const iterable = this.parseExpression();\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"for\", condition: { type: \"forCondition\", variable, iterable }, body };\n }\n parseWhile() {\n this.expect(\"while\");\n const condition = this.parseExpression();\n const body = this.parseCommandSequence();\n if (this.match(\"end\")) this.advance();\n return { type: \"while\", condition, body };\n }\n /**\n * A naked URL \u2014 `fetch /api/data \u2026`, `fetch https://x/y \u2026` \u2014 the way upstream\n * reads one: everything up to the next whitespace, verbatim. The tokenizer\n * splits `/api/data` into `/`, `api`, `/`, `data`, and `parseExpression` used\n * to return the first `/` as the URL; the skip fallback then dropped the rest\n * silently, so every unquoted `fetch /path` in a hybrid bundle fetched `/`.\n * Measured 2026-09-04 when the fallback stopped being silent.\n */\n parseNakedUrl() {\n const tok = this.peek();\n const isSlash = tok.type === \"operator\" && tok.value === \"/\";\n const isScheme = (tok.value === \"http\" || tok.value === \"https\") && this.source.slice(tok.pos + tok.value.length, tok.pos + tok.value.length + 3) === \"://\";\n if (!isSlash && !isScheme) return null;\n let end = tok.pos;\n while (end < this.source.length && !/\\s/.test(this.source[end])) end++;\n while (!this.isAtEnd() && this.peek().pos < end) this.advance();\n return { type: \"literal\", value: this.source.slice(tok.pos, end) };\n }\n parseFetchBlock() {\n this.expect(\"fetch\");\n const url = this.parseNakedUrl() ?? this.parseExpression();\n let responseType = { type: \"literal\", value: \"text\" };\n let options;\n let method;\n if (this.match(\"{\")) {\n this.pos--;\n options = this.parseExpression();\n }\n for (let i = 0; i < 3; i++) {\n if (this.match(\"via\") && !method) {\n this.advance();\n method = this.parseExpression();\n continue;\n }\n if (this.match(\"as\")) {\n this.advance();\n if (this.match(\"a\") || this.match(\"an\")) this.advance();\n responseType = this.parseExpression();\n continue;\n }\n if (this.match(\"with\") && !options) {\n this.advance();\n options = this.parseExpression();\n continue;\n }\n break;\n }\n if (this.match(\"then\")) this.advance();\n const body = this.parseCommandSequence();\n return {\n type: \"fetch\",\n condition: { type: \"fetchConfig\", url, responseType, options, method },\n body\n };\n }\n // Command parsing\n parseToggle() {\n this.expect(\"toggle\");\n const what = this.parseExpression();\n let target;\n if (this.match(\"on\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"toggle\", args: [what], target };\n }\n parseAdd() {\n this.expect(\"add\");\n const what = this.parseExpression();\n let target;\n if (this.match(\"to\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"add\", args: [what], target };\n }\n parseRemove() {\n this.expect(\"remove\");\n if (this.matchType(\"selector\") && /^[.@]/.test(this.peek().value)) {\n const what = this.parseExpression();\n let target2;\n if (this.match(\"from\")) {\n this.advance();\n target2 = this.parseExpression();\n }\n return { type: \"command\", name: \"removeClass\", args: [what], target: target2 };\n }\n const target = this.parseExpression();\n return { type: \"command\", name: \"remove\", args: [], target };\n }\n parsePut() {\n this.expect(\"put\");\n const content = this.parseExpression();\n let modifier = \"into\";\n if (this.match(\"into\", \"before\", \"after\", \"at\")) {\n modifier = this.advance().value;\n if (modifier === \"at\") {\n const pos = this.advance().value;\n this.expect(\"of\");\n modifier = `at ${pos} of`;\n }\n }\n const target = this.parseExpression();\n return { type: \"command\", name: \"put\", args: [content], target, modifier };\n }\n /** `append`/`prepend <content> [to <target>]` \u2014 identical shape, both ends. */\n parseInsertion(name) {\n this.expect(name);\n const content = this.parseExpression();\n let target;\n if (this.match(\"to\")) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name, args: [content], target };\n }\n parseSet() {\n this.expect(\"set\");\n const target = this.parseExpression();\n if (this.match(\"to\")) {\n this.advance();\n const value = this.parseExpression();\n return { type: \"command\", name: \"set\", args: [target, value] };\n }\n return { type: \"command\", name: \"set\", args: [target] };\n }\n parseGet() {\n this.expect(\"get\");\n return { type: \"command\", name: \"get\", args: [this.parseExpression()] };\n }\n parseCall() {\n this.expect(\"call\");\n return { type: \"command\", name: \"call\", args: [this.parseExpression()] };\n }\n parseLog() {\n this.expect(\"log\");\n const args = [];\n while (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n else break;\n }\n return { type: \"command\", name: \"log\", args };\n }\n /**\n * `send <event> to <target>` / `trigger <event> on <target>`.\n *\n * Both emit a `send` node \u2014 the runtime registers `trigger` as a\n * consolidation alias sharing `send`'s implementation, so the bundle\n * generator carries one template for both. The marker is a parameter\n * because it is the ONLY difference: `trigger` used to reuse this rule with\n * `to` hardcoded, which parsed the event name and then silently abandoned\n * `on #target`, leaving the dispatch to land on `me`.\n */\n parseSend(marker) {\n this.advance();\n const first = this.advance();\n let event = first.value;\n let end = first.pos + first.value.length;\n while (!this.isAtEnd() && this.peek().type === \"localVar\" && this.peek().pos === end) {\n const piece = this.advance();\n event += piece.value;\n end = piece.pos + piece.value.length;\n }\n let target;\n if (this.match(marker)) {\n this.advance();\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"send\", args: [{ type: \"literal\", value: event }], target };\n }\n parseWait() {\n this.expect(\"wait\");\n if (this.match(\"for\")) {\n this.advance();\n const event = this.advance().value;\n let target;\n if (this.match(\"from\")) {\n this.advance();\n target = this.parseExpression();\n }\n return {\n type: \"command\",\n name: \"waitFor\",\n args: [{ type: \"literal\", value: event }],\n target\n };\n }\n return { type: \"command\", name: \"wait\", args: [this.parseExpression()] };\n }\n parseShow() {\n this.expect(\"show\");\n let target;\n const modifiers = {};\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\", \"when\", \"where\")) {\n target = this.parseExpression();\n }\n if (!this.isAtEnd() && this.match(\"when\", \"where\")) {\n const keyword = this.advance().value;\n modifiers[keyword] = this.parseExpression();\n }\n return { type: \"command\", name: \"show\", args: [], target, modifiers };\n }\n parseHide() {\n this.expect(\"hide\");\n let target;\n const modifiers = {};\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\", \"when\", \"where\")) {\n target = this.parseExpression();\n }\n if (!this.isAtEnd() && this.match(\"when\", \"where\")) {\n const keyword = this.advance().value;\n modifiers[keyword] = this.parseExpression();\n }\n return { type: \"command\", name: \"hide\", args: [], target, modifiers };\n }\n parseTake() {\n this.expect(\"take\");\n const what = this.parseExpression();\n let from;\n if (this.match(\"from\")) {\n this.advance();\n from = this.parseExpression();\n }\n return { type: \"command\", name: \"take\", args: [what], target: from };\n }\n parseIncDec(name) {\n this.advance();\n const target = this.parseExpression();\n let amount = { type: \"literal\", value: 1 };\n if (this.match(\"by\")) {\n this.advance();\n amount = this.parseExpression();\n }\n return { type: \"command\", name, args: [target, amount] };\n }\n parseFocusBlur(name) {\n this.advance();\n let target;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n target = this.parseExpression();\n }\n return { type: \"command\", name, args: [], target };\n }\n parseGo() {\n this.expect(\"go\");\n if (this.match(\"to\")) this.advance();\n if (this.match(\"url\")) this.advance();\n const dest = this.parseExpression();\n return { type: \"command\", name: \"go\", args: [dest] };\n }\n parseReturn() {\n this.expect(\"return\");\n let value;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n value = this.parseExpression();\n }\n return { type: \"command\", name: \"return\", args: value ? [value] : [] };\n }\n // ---------------------------------------------------------------------------\n // Rules restoring the commands the capability list advertised but this parser\n // could not reach (Finding 13). Every one already had a working template in\n // `bundle-generator/templates.ts`; only the parse rule was missing, so the\n // emitted `case` label was dead and the user's source silently no-opped.\n // ---------------------------------------------------------------------------\n /** `empty [<target>]` \u2014 clears children. Defaults to `me`. */\n parseEmpty() {\n this.expect(\"empty\");\n let target;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n target = this.parseExpression();\n }\n return { type: \"command\", name: \"empty\", args: [], target };\n }\n /** `copy <value>` \u2014 clipboard write. */\n parseCopy() {\n this.expect(\"copy\");\n return { type: \"command\", name: \"copy\", args: [this.parseExpression()] };\n }\n /**\n * `beep[!] [<value>, \u2026]` \u2014 debug echo.\n *\n * Upstream spells it `beep!`; hyperfixi accepts both (see the tier list's\n * note on the same nuance). The `!` is consumed here because the tokenizer\n * emits it as a separate operator, and `parseUnary` would otherwise read it\n * as logical negation of the first argument.\n */\n parseBeep() {\n this.advance();\n if (this.match(\"!\")) this.advance();\n const args = [];\n while (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n else break;\n }\n return { type: \"command\", name: \"beep\", args };\n }\n /**\n * `push|replace url <url> [with title <title>]` \u2014 History API navigation.\n *\n * The `url` keyword is consumed here rather than left for the executor, the\n * way `parseGo` already does it. Note `push-url` / `replace-url` are NOT\n * source spellings \u2014 the full parser rejects both \u2014 they exist only as\n * bundle-config aliases resolving to these templates.\n */\n parseUrlCommand(name) {\n this.advance();\n if (this.match(\"url\")) this.advance();\n const url = this.parseExpression();\n const modifiers = {};\n if (this.match(\"with\")) {\n this.advance();\n if (this.match(\"title\")) {\n this.advance();\n modifiers.title = this.parseExpression();\n }\n }\n return { type: \"command\", name, args: [url], modifiers };\n }\n /** `morph [over] <target> to|with|into <content>` \u2014 upstream spells it `to`; the full parser also takes `with`; `into` is this parser's own. */\n parseMorph() {\n this.expect(\"morph\");\n let modifier;\n if (this.match(\"over\")) {\n this.advance();\n modifier = \"over\";\n }\n const target = this.parseExpression();\n if (this.match(\"with\", \"to\", \"into\")) this.advance();\n const content = this.parseExpression();\n return { type: \"command\", name: \"morph\", args: [content], target, modifier };\n }\n /**\n * `js[(<params>)] <raw JavaScript> end`.\n *\n * The body is sliced from the ORIGINAL SOURCE between token offsets, never\n * reassembled from tokens \u2014 the tokenizer reads it as hyperscript, which\n * mangles regexes, template literals and possessive-looking quotes. This\n * mirrors the full parser's `parseJsCommand`.\n *\n * Known limit: the terminator scan matches the `end` TOKEN, so `end` inside a\n * JS *string* is safe (the tokenizer keeps a string as one token, quotes\n * included) but a bare JS identifier named `end` would cut the body early.\n * The full parser's `findJsEndBoundary` handles that; replicating it is not\n * worth the bytes in a bundle this size.\n */\n parseJs() {\n this.advance();\n if (this.match(\"(\")) {\n while (!this.isAtEnd() && !this.match(\")\")) this.advance();\n if (this.match(\")\")) this.advance();\n }\n const start = this.peek().pos;\n while (!this.isAtEnd() && !this.match(\"end\")) this.advance();\n const stop = this.peek().pos;\n if (this.match(\"end\")) this.advance();\n const code = this.source.slice(start, stop).trim();\n return { type: \"command\", name: \"js\", args: [{ type: \"literal\", value: code }] };\n }\n /** `throw <value>` */\n parseThrow() {\n this.expect(\"throw\");\n let value;\n if (!this.isAtEnd() && !this.match(\"then\", \"and\", \"end\", \"else\")) {\n value = this.parseExpression();\n }\n return { type: \"command\", name: \"throw\", args: value ? [value] : [] };\n }\n /** `break` / `continue` / `exit` \u2014 bare control-flow signals, no arguments. */\n parseBare(name) {\n this.advance();\n return { type: \"command\", name, args: [] };\n }\n parseHalt() {\n this.expect(\"halt\");\n if (this.match(\"the\")) this.advance();\n if (this.match(\"event\", \"default\")) this.advance();\n return { type: \"command\", name: \"halt\", args: [] };\n }\n // transition <property> to <value> [over <duration>]\n parseTransition() {\n this.expect(\"transition\");\n let target;\n if (this.match(\"my\", \"its\")) {\n const ref = this.advance().value;\n target = { type: \"identifier\", value: ref === \"my\" ? \"me\" : \"it\" };\n } else if (this.matchType(\"selector\")) {\n const expr = this.parseExpression();\n if (expr.type === \"possessive\") {\n return this.parseTransitionRest(expr.object, expr.property);\n }\n target = expr;\n }\n const propToken = this.peek();\n let property;\n if (propToken.type === \"styleProperty\") {\n property = this.advance().value;\n } else if (propToken.type === \"identifier\" || propToken.type === \"keyword\") {\n property = this.advance().value;\n } else {\n property = \"opacity\";\n }\n return this.parseTransitionRest(target, property);\n }\n parseTransitionRest(target, property) {\n let toValue = { type: \"literal\", value: 1 };\n if (this.match(\"to\")) {\n this.advance();\n toValue = this.parseExpression();\n }\n let duration = { type: \"literal\", value: 300 };\n if (this.match(\"over\")) {\n this.advance();\n duration = this.parseExpression();\n }\n return {\n type: \"command\",\n name: \"transition\",\n args: [{ type: \"literal\", value: property }, toValue, duration],\n target\n };\n }\n // Expression parsing with operator precedence\n parseExpression() {\n return this.parseOr();\n }\n parseOr() {\n let left = this.parseAnd();\n while (this.match(\"or\", \"||\")) {\n this.advance();\n left = { type: \"binary\", operator: \"or\", left, right: this.parseAnd() };\n }\n return left;\n }\n parseAnd() {\n let left = this.parseEquality();\n while (this.match(\"and\", \"&&\") && !this.isCommandKeyword(this.peek(1))) {\n this.advance();\n left = { type: \"binary\", operator: \"and\", left, right: this.parseEquality() };\n }\n return left;\n }\n /**\n * Does this token start a command rather than continue an expression?\n *\n * Used in two places with different stakes: terminating an `and` chain\n * (`toggle .x and add .y`), and refusing to read a command name as a\n * possessive property (`my show`).\n *\n * This is a deliberate SUBSET of `cmdMap`'s keys, not a second copy of it \u2014\n * asserted as a subset in `capability-emission.test.ts` \u00A74 so it cannot\n * name something the parser does not dispatch. It is not the full set\n * because several of the newer keys are ordinary English or JS words\n * (`copy`, `empty`, `push`, `replace`, `break`) that appear as property\n * names and operands; promoting them here would change expression parsing,\n * which is a separate decision from command reachability. The cost is that\n * `\u2026 and break` reads `break` as an operand; `then` is the canonical\n * separator and is handled by `parseCommandSequence` directly.\n */\n isCommandKeyword(token) {\n const cmds = [\n \"toggle\",\n \"add\",\n \"remove\",\n \"set\",\n \"put\",\n \"log\",\n \"send\",\n \"wait\",\n \"show\",\n \"hide\",\n \"increment\",\n \"decrement\",\n \"focus\",\n \"blur\",\n \"go\"\n ];\n return cmds.includes(normalizeCommand(token.value));\n }\n parseEquality() {\n let left = this.parseComparison();\n while (this.match(\"==\", \"!=\", \"is\", \"matches\", \"contains\", \"includes\", \"has\")) {\n const op = this.advance().value;\n if (op.toLowerCase() === \"is\" && this.match(\"not\")) {\n this.advance();\n left = { type: \"binary\", operator: \"is not\", left, right: this.parseComparison() };\n } else {\n left = { type: \"binary\", operator: op, left, right: this.parseComparison() };\n }\n }\n return left;\n }\n parseComparison() {\n let left = this.parseAdditive();\n while (this.match(\"<\", \">\", \"<=\", \">=\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseAdditive() };\n }\n return left;\n }\n parseAdditive() {\n let left = this.parseMultiplicative();\n while (this.match(\"+\", \"-\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseMultiplicative() };\n }\n return left;\n }\n parseMultiplicative() {\n let left = this.parseUnary();\n while (this.match(\"*\", \"/\", \"%\")) {\n const op = this.advance().value;\n left = { type: \"binary\", operator: op, left, right: this.parseUnary() };\n }\n return left;\n }\n parseUnary() {\n if (this.match(\"beep\") && this.peek(1).value === \"!\") {\n throw new Error(`'beep!' needs the full parser (use hyperfixi.js)`);\n }\n if (this.match(\"not\", \"!\")) {\n this.advance();\n return { type: \"unary\", operator: \"not\", operand: this.parseUnary() };\n }\n if (this.match(\"-\") && this.peek(1).type === \"number\") {\n this.advance();\n const num = this.advance();\n return { type: \"literal\", value: -parseFloat(num.value) };\n }\n return this.parsePostfix();\n }\n parsePostfix() {\n let left = this.parsePrimary();\n while (true) {\n if (this.match(\"'s\")) {\n this.advance();\n const next = this.peek();\n const prop = next.type === \"styleProperty\" ? this.advance().value : this.advance().value;\n left = { type: \"possessive\", object: left, property: prop };\n } else if (this.peek().type === \"styleProperty\") {\n const prop = this.advance().value;\n left = { type: \"possessive\", object: left, property: prop };\n } else if (this.peek().value === \".\") {\n this.advance();\n const prop = this.advance().value;\n left = { type: \"member\", object: left, property: prop };\n } else if (this.peek().type === \"selector\" && this.peek().value.startsWith(\".\")) {\n const prop = this.advance().value.slice(1);\n left = { type: \"member\", object: left, property: prop };\n } else if (this.peek().value === \"(\") {\n this.advance();\n const args = [];\n while (!this.match(\")\")) {\n args.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n }\n this.expect(\")\");\n left = { type: \"call\", callee: left, args };\n } else if (this.peek().value === \"[\" && left.type !== \"selector\") {\n this.advance();\n const index = this.parseExpression();\n this.expect(\"]\");\n left = { type: \"member\", object: left, property: index, computed: true };\n } else {\n break;\n }\n }\n return left;\n }\n parsePrimary() {\n const token = this.peek();\n if (token.value === \"(\") {\n this.advance();\n const expr = this.parseExpression();\n this.expect(\")\");\n return expr;\n }\n if (token.value === \"{\") return this.parseObjectLiteral();\n if (token.value === \"[\") return this.parseArrayLiteral();\n if (token.type === \"number\") {\n this.advance();\n const val = token.value;\n if (val.endsWith(\"ms\")) return { type: \"literal\", value: parseInt(val), unit: \"ms\" };\n if (val.endsWith(\"s\")) return { type: \"literal\", value: parseFloat(val) * 1e3, unit: \"ms\" };\n return { type: \"literal\", value: parseFloat(val) };\n }\n if (token.type === \"string\") {\n this.advance();\n return { type: \"literal\", value: token.value.slice(1, -1) };\n }\n if (this.match(\"true\")) {\n this.advance();\n return { type: \"literal\", value: true };\n }\n if (this.match(\"false\")) {\n this.advance();\n return { type: \"literal\", value: false };\n }\n if (this.match(\"null\")) {\n this.advance();\n return { type: \"literal\", value: null };\n }\n if (this.match(\"undefined\")) {\n this.advance();\n return { type: \"literal\", value: void 0 };\n }\n if (token.type === \"localVar\") {\n this.advance();\n return { type: \"variable\", name: token.value, scope: \"local\" };\n }\n if (token.type === \"globalVar\") {\n this.advance();\n return { type: \"variable\", name: token.value, scope: \"global\" };\n }\n if (token.type === \"selector\") {\n this.advance();\n return { type: \"selector\", value: token.value };\n }\n if (this.match(\"my\")) {\n this.advance();\n const next = this.peek();\n if ((next.type === \"identifier\" || next.type === \"keyword\") && !this.isCommandKeyword(next)) {\n const prop = this.advance().value;\n return { type: \"possessive\", object: { type: \"identifier\", value: \"me\" }, property: prop };\n }\n return { type: \"identifier\", value: \"me\" };\n }\n if (this.match(\"its\")) {\n this.advance();\n const next = this.peek();\n if ((next.type === \"identifier\" || next.type === \"keyword\") && !this.isCommandKeyword(next)) {\n const prop = this.advance().value;\n return { type: \"possessive\", object: { type: \"identifier\", value: \"it\" }, property: prop };\n }\n return { type: \"identifier\", value: \"it\" };\n }\n if (this.match(\"me\")) {\n this.advance();\n return { type: \"identifier\", value: \"me\" };\n }\n if (this.match(\"it\")) {\n this.advance();\n return { type: \"identifier\", value: \"it\" };\n }\n if (this.match(\"you\")) {\n this.advance();\n return { type: \"identifier\", value: \"you\" };\n }\n if (this.match(\"the\", \"a\", \"an\")) {\n this.advance();\n if (this.match(\"first\", \"last\", \"next\", \"previous\", \"closest\", \"parent\")) {\n const position = this.advance().value;\n const target = this.parsePositionalTarget();\n return { type: \"positional\", position, target };\n }\n return this.parsePrimary();\n }\n if (this.match(\"first\", \"last\", \"next\", \"previous\", \"closest\", \"parent\")) {\n const position = this.advance().value;\n const target = this.parsePositionalTarget();\n return { type: \"positional\", position, target };\n }\n if (this.match(\"values\")) {\n this.advance();\n if (this.match(\"of\")) {\n this.advance();\n const target = this.parseExpression();\n return { type: \"valuesOf\", target };\n }\n return { type: \"identifier\", value: token.value };\n }\n if (token.type === \"identifier\" || token.type === \"keyword\") {\n this.advance();\n return { type: \"identifier\", value: token.value };\n }\n this.advance();\n return { type: \"identifier\", value: token.value };\n }\n parseObjectLiteral() {\n this.expect(\"{\");\n const properties = [];\n while (!this.match(\"}\")) {\n const key = this.advance().value;\n this.expect(\":\");\n const value = this.parseExpression();\n properties.push({ key, value });\n if (this.match(\",\")) this.advance();\n }\n this.expect(\"}\");\n return { type: \"object\", properties };\n }\n parseArrayLiteral() {\n this.expect(\"[\");\n const elements = [];\n while (!this.match(\"]\")) {\n elements.push(this.parseExpression());\n if (this.match(\",\")) this.advance();\n }\n this.expect(\"]\");\n return { type: \"array\", elements };\n }\n parsePositionalTarget() {\n const token = this.peek();\n if (token.type === \"selector\") {\n return { type: \"selector\", value: this.advance().value };\n }\n if (token.type === \"identifier\" || token.type === \"keyword\") {\n return { type: \"identifier\", value: this.advance().value };\n }\n return this.parseExpression();\n }\n}\n// #endregion generated:hybrid-parser\n";
3
3
  export declare function getParserTemplate(type: 'lite' | 'hybrid'): string;
4
4
  export declare const LITE_PARSER_COMMANDS: readonly ["toggle", "add", "remove", "put", "set", "log", "send", "wait", "show", "hide"];
5
5
  export declare function canUseLiteParser(commands: string[], blocks: string[], positional: boolean): boolean;
@@ -0,0 +1,2 @@
1
+ import{D as n,d as e}from"./browser-modular-7CpI5VF2.js";function i(n){const e=n.diagnostics;return e?.length?e.filter(n=>"unconsumed-input"===n.code).map(n=>n.message):[]}let t=null;async function a(){return t||(t=await(import("./index-BwXDEK0m.js"))),t}class s{constructor(e={}){this._initialized=!1,this.config={confidenceThreshold:e.confidenceThreshold??n,fallbackOnLowConfidence:e.fallbackOnLowConfidence??!0}}async initialize(){await a(),this._initialized=!0}isInitialized(){return this._initialized}async transform(n,e,i){if(this.isInitialized()||await this.initialize(),e===i)return{output:n,usedSemantic:!1,confidence:1,sourceLang:e,targetLang:i};const t=await a();try{const a=t.translate(n,e,i);if(a!==n)return{output:a,usedSemantic:!0,confidence:.9,sourceLang:e,targetLang:i}}catch{}return{output:n,usedSemantic:!1,confidence:0,sourceLang:e,targetLang:i}}async parse(n,e){this.isInitialized()||await this.initialize();return(await a()).parseSemantic(n,e).node??null}async render(n,e){return(await a()).render(n,e)}async parseToAST(n,e){this.isInitialized()||await this.initialize();const i=await a(),t=i.parseSemantic(n,e);if(t.confidence>=this.config.confidenceThreshold&&t.node)try{return i.buildAST(t.node).ast}catch(n){}return null}async parseToASTWithDetails(n,e){this.isInitialized()||await this.initialize();const t=await a(),s=t.parseSemantic(n,e);if(s.confidence>=this.config.confidenceThreshold&&s.node)try{const n=t.buildAST(s.node);return{ast:n.ast,usedDirectPath:!0,confidence:s.confidence,lang:e,fallbackText:null,warnings:[...n.warnings??[],...i(s.node)]}}catch{}if(s.node&&this.config.fallbackOnLowConfidence){const n=t.render(s.node,"en");return{ast:null,usedDirectPath:!1,confidence:s.confidence,lang:e,fallbackText:n}}return{ast:null,usedDirectPath:!1,confidence:s.confidence,lang:e,fallbackText:null}}async getAllTranslations(n,e){const i=(await a()).getSupportedLanguages(),t={};for(const a of i)t[a]=await this.transform(n,e,a);return t}}function c(n){return{name:"semantic-bridge",parseToAST:(e,i)=>n.parseToASTWithDetails(e,i),parse:(e,i)=>n.parse(e,i),render:(e,i)=>n.render(e,i)}}export{s as SemanticGrammarBridge,c as createBridgeFrontEnd};
2
+ //# sourceMappingURL=bridge-CI9SOl8u.js.map