@lokascript/htmx-adapter 3.0.0 → 3.1.1
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.
- package/CHANGELOG.md +206 -3
- package/README.md +44 -16
- package/dist/htmx-i18n.global.js +1 -1
- package/dist/htmx-i18n.global.js.map +1 -1
- package/dist/index.cjs +207 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -31
- package/dist/index.d.ts +141 -31
- package/dist/index.js +198 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +20 -5
- package/src/canonicalize.ts +131 -45
- package/src/extension.ts +175 -26
- package/src/hx-on.ts +153 -43
- package/src/index.ts +15 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/lang-resolver.ts","../src/registry.ts","../src/hx-on.ts","../src/resolver.ts","../src/canonicalize.ts","../src/extension.ts"],"sourcesContent":["/**\n * @lokascript/htmx-adapter — multilingual adapter for upstream htmx v4.\n *\n * Lets authors write `hx-*` / `sse-*` / `ws-*` attributes in 24 languages\n * against the stock htmx library. Localized names are canonicalized on\n * the element before htmx processes it (see canonicalize.ts); vocab data\n * is the same generated `packages/core/vocab/htmx/{lang}.js` modules the\n * embedded hyperfixi htmx-compat layer uses.\n *\n * Programmatic use:\n *\n * import { register, registerWith, installAutoSweep } from '@lokascript/htmx-adapter';\n * register('es', { hyperfixi: { attrs: { 'hx-obtener': 'hx-get' }, events: { clic: 'click' } } });\n * registerWith(window.htmx); // v4 extension (v2 fallback)\n * installAutoSweep(); // initial-page sweep\n *\n * Browser IIFE (`./browser` export) does all of the above automatically.\n */\n\nexport { langOf, normLang } from './lang-resolver.js';\nexport {\n register,\n vocabFor,\n isLangRegistered,\n hasAnyVocab,\n onVocabUpdate,\n resetRegistry,\n type HtmxVocab,\n type VocabPayload,\n} from './registry.js';\nexport { canonicalizeElement, canonicalizeTree, translateTriggerValue } from './canonicalize.js';\nexport {\n EXTENSION_NAME,\n createExtension,\n registerWith,\n installAutoSweep,\n type HtmxLike,\n} from './extension.js';\nexport {\n attributeResolver,\n additionalAttributeSelectors,\n installResolverMode,\n isResolverMode,\n setResolverMode,\n} from './resolver.js';\nexport {\n setBodyExecutor,\n setBodyTranslator,\n hasBodyExecutor,\n hasBodyTranslator,\n onBodyHooksChanged,\n claimHxOnAttribute,\n autoDetectBodyHooks,\n resetBodyHooks,\n type BodyExecutor,\n type BodyTranslator,\n} from './hx-on.js';\n","/**\n * Per-element language resolution for localized htmx attribute names.\n *\n * Mirrors `packages/core/src/htmx/lang-resolver.ts` (which in turn mirrors\n * loka-js's `lang-resolver.js`) so a page migrating between hyperfixi's\n * embedded htmx-compat layer and this upstream-htmx adapter resolves\n * languages identically. Resolution order:\n *\n * 1. `data-hyperfixi-lang` attribute on the element itself\n * 2. `data-hyperfixi-lang` on any ancestor\n * 3. `lang` attribute on any ancestor (HTML standard)\n * 4. `'en'` fallback\n *\n * Results are normalized to the part before the first `-`/`_` (`es-MX` →\n * `es`) and lowercased so callers can index vocab maps by 2-letter code\n * without caring about regional variants.\n */\n\n/** Normalize a language tag — `es-MX` / `ES_mx` → `es`. */\nexport function normLang(s: string | null | undefined): string {\n if (!s) return 'en';\n return s.split(/[-_]/)[0].toLowerCase();\n}\n\n/** Resolve the language code for an element via ancestor walk. */\nexport function langOf(elt: Element): string {\n const own = elt.getAttribute?.('data-hyperfixi-lang');\n if (own) return normLang(own);\n const dx = elt.closest?.('[data-hyperfixi-lang]');\n if (dx) return normLang(dx.getAttribute('data-hyperfixi-lang'));\n const la = elt.closest?.('[lang]');\n if (la) return normLang(la.getAttribute('lang'));\n return 'en';\n}\n","/**\n * Vocab registry for the upstream-htmx adapter.\n *\n * Accepts the SAME payload shape as hyperfixi core's htmx-compat\n * orchestrator (`packages/core/src/htmx/i18n-orchestrator.ts`), so the\n * generated vocab modules under `packages/core/vocab/htmx/{lang}.js`\n * (which call `window.__hyperfixi_i18n.register(lang, payload)`) work\n * verbatim against this adapter — one generated artifact, two consumers.\n *\n * register('es', {\n * hyperfixi: {\n * attrs: { 'hx-obtener': 'hx-get', 'sse-conectar': 'sse-connect' },\n * events: { clic: 'click', cambiar: 'change' },\n * },\n * });\n *\n * Unlike core's orchestrator this registry needs no inverted index: the\n * adapter canonicalizes (localized → canonical), which is exactly the\n * direction the parse maps are published in. There is deliberately no\n * KEYS copy here either — the vocab data is self-describing (full\n * attribute names on both sides), so the canonical key set lives only in\n * core's generator (`packages/core/scripts/gen-htmx-vocab.mjs`).\n */\n\nimport { normLang } from './lang-resolver.js';\n\nexport interface HtmxVocab {\n /** Map of localized attribute name → canonical English form (fully qualified). */\n attrs?: Record<string, string>;\n /** Map of localized event name → canonical English form (`'clic'` → `'click'`). */\n events?: Record<string, string>;\n}\n\nexport interface VocabPayload {\n hyperfixi?: HtmxVocab;\n}\n\n/** Per-language vocab, keyed by normalized language code. */\nconst REG = new Map<string, HtmxVocab>();\n\n/** Languages we've already warned about being missing. */\nconst warnedMissingLang = new Set<string>();\n\n/** Listeners notified after a vocab registration completes. */\nconst vocabUpdateListeners = new Set<() => void>();\n\n/**\n * Register a vocab module for a language. Idempotent — re-registering a\n * language replaces its vocab entirely.\n */\nexport function register(code: string, data: VocabPayload): void {\n const lang = normLang(code);\n REG.set(lang, data?.hyperfixi ?? {});\n warnedMissingLang.delete(lang); // we know about it now\n for (const listener of vocabUpdateListeners) listener();\n}\n\n/** Look up the vocab registered for a (normalized) language code. */\nexport function vocabFor(lang: string): HtmxVocab | undefined {\n return REG.get(lang);\n}\n\n/** Inspect whether any vocab is registered for a language. Mainly for tests. */\nexport function isLangRegistered(code: string): boolean {\n return REG.has(normLang(code));\n}\n\n/** True if at least one language has registered vocab. */\nexport function hasAnyVocab(): boolean {\n return REG.size > 0;\n}\n\n/** All language codes with registered vocab. */\nexport function vocabLangs(): string[] {\n return [...REG.keys()];\n}\n\n/** Subscribe to vocab-registration notifications. Returns an unsubscribe fn. */\nexport function onVocabUpdate(listener: () => void): () => void {\n vocabUpdateListeners.add(listener);\n return () => {\n vocabUpdateListeners.delete(listener);\n };\n}\n\n/**\n * Warn once per language that an element sits in a lang scope with no\n * registered vocab, so authors notice unloaded vocab modules instead of\n * silently getting English-only behavior.\n */\nexport function warnMissingLangOnce(lang: string): void {\n if (warnedMissingLang.has(lang)) return;\n warnedMissingLang.add(lang);\n if (typeof console !== 'undefined') {\n console.warn(\n `[htmx-i18n] No vocab registered for lang=\"${lang}\". ` +\n `Elements in this language scope keep their localized attribute names ` +\n `and htmx will not see them. Load the ${lang} vocab module ` +\n `(packages/core/vocab/htmx/${lang}.js) before htmx processes the page.`\n );\n }\n}\n\n/**\n * Reset registry state — drop all registrations and listeners. Mainly for\n * tests; production code should leave registrations in place.\n */\nexport function resetRegistry(): void {\n REG.clear();\n warnedMissingLang.clear();\n vocabUpdateListeners.clear();\n}\n","/**\n * hx-on body support — opt-in \"executor mode\".\n *\n * Upstream htmx evals `hx-on:*` bodies as JavaScript. In the hyperfixi\n * ecosystem the convention is hyperscript bodies (`hx-on:click=\"toggle\n * .active on me\"`), which htmx's JS eval cannot run — and localized\n * bodies (`hx-en:clic=\"alternar .active\"`) doubly so. This module lets a\n * page opt in to hyperscript-body semantics by configuring an executor:\n *\n * setBodyExecutor((code, elt, evt) => _hyperscript.evaluate(code, { me: elt, event: evt }))\n * setBodyTranslator((body, lang) => HyperscriptI18n.preprocess(body, lang))\n *\n * With an executor set, the adapter CLAIMS every hx-on-family attribute\n * (localized-named or canonical-named — all bodies are treated as\n * hyperscript; mixed JS/hyperscript pages have no reliable detection):\n * it installs a real event listener that runs the (lazily translated)\n * body through the executor, and keeps htmx away from it:\n *\n * - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx\n * never recognized them anyway — and NO canonical `hx-on:*` sibling is\n * created.\n * - Canonical-named attrs (`hx-on:click`) are REMOVED after claiming.\n * This is the one place the adapter deletes an authored attribute: if\n * it stayed, htmx would eval the hyperscript body as JS — a console\n * error plus a double-execution attempt on every fire. Documented as\n * the executor-mode exception in the README.\n *\n * With no executor set (the default), none of this runs and bodies keep\n * upstream JS semantics — the behavior-preservation invariant.\n *\n * Translation is lazy (first event fire, memoized) so a translator that\n * loads after the initial sweep still applies. Both auto-detection\n * (`autoDetectBodyHooks`) and manual configuration are supported; the\n * executor is also re-read at fire time so replacing it takes effect on\n * live listeners.\n */\n\nexport type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;\nexport type BodyTranslator = (body: string, lang: string) => string;\n\nlet executor: BodyExecutor | null = null;\nlet translator: BodyTranslator | null = null;\n\n/** Listeners notified when the executor is set/cleared (drives re-sweeps). */\nconst hookChangeListeners = new Set<() => void>();\n\n/**\n * Per-element set of already-claimed EVENT names (sweep idempotency).\n * Keyed by resolved event name rather than attribute name so a localized\n * attr and its canonical form never both install a listener — e.g. after\n * a v1 (no-executor) sweep created an `hx-on:click` sibling of\n * `hx-en:clic` and an executor registered later, the re-sweep claims one\n * of them and neutralizes the other. When both are genuinely authored,\n * DOM attribute order decides which body wins.\n */\nlet claimed = new WeakMap<Element, Set<string>>();\n\n/** Configure the body executor. Setting/clearing it notifies subscribers. */\nexport function setBodyExecutor(fn: BodyExecutor | null): void {\n executor = fn;\n for (const listener of hookChangeListeners) listener();\n}\n\n/** Configure the body translator (localized body → English hyperscript). */\nexport function setBodyTranslator(fn: BodyTranslator | null): void {\n translator = fn;\n}\n\nexport function hasBodyExecutor(): boolean {\n return executor !== null;\n}\n\nexport function hasBodyTranslator(): boolean {\n return translator !== null;\n}\n\n/** Subscribe to executor changes. Returns an unsubscribe fn. */\nexport function onBodyHooksChanged(listener: () => void): () => void {\n hookChangeListeners.add(listener);\n return () => {\n hookChangeListeners.delete(listener);\n };\n}\n\n/**\n * Resolve the DOM event name for an hx-on attribute suffix.\n * `hx-on::after-swap` shorthand (leading `:`) means the `htmx:` namespace;\n * plain suffixes translate through the vocab events map.\n */\nfunction eventNameForSuffix(rawSuffix: string, events: Record<string, string>): string {\n if (rawSuffix.startsWith(':')) return `htmx${rawSuffix}`;\n return events[rawSuffix] ?? rawSuffix;\n}\n\n/**\n * Claim one hx-on-family attribute on an element: install the executor\n * listener and neutralize htmx's view of it. Returns true if the claim\n * happened now (false when no executor, already claimed, or malformed).\n *\n * `lang` is the element's language at claim time — used for lazy body\n * translation. `events` is the language's event-name map (for the\n * listener's event name; the shorthand/unknown cases pass through).\n */\nexport function claimHxOnAttribute(\n elt: Element,\n attrName: string,\n lang: string,\n events: Record<string, string>\n): boolean {\n if (!executor) return false;\n\n const colon = attrName.indexOf(':');\n if (colon <= 0) return false; // colon-form only; legacy composite hx-on=\"…\" unsupported\n\n const eventName = eventNameForSuffix(attrName.slice(colon + 1), events);\n const already = claimed.get(elt);\n if (already?.has(eventName)) {\n // Duplicate claim for an already-claimed event — no second listener,\n // but canonical-named attrs still get neutralized so htmx never\n // JS-evals them.\n if (attrName.startsWith('hx-on:') && elt.hasAttribute(attrName)) {\n elt.removeAttribute(attrName);\n return true;\n }\n return false;\n }\n\n const body = elt.getAttribute(attrName) ?? '';\n\n // Lazy, memoized translation: translator may register after the sweep,\n // and repeated fires shouldn't re-translate.\n let translatedBody: string | null = null;\n elt.addEventListener(eventName, evt => {\n if (!executor) return; // executor cleared after claim — go quiet\n if (translatedBody === null) {\n translatedBody = lang !== 'en' && translator ? translator(body, lang) : body;\n }\n try {\n executor(translatedBody, elt, evt);\n } catch (err) {\n if (typeof console !== 'undefined') {\n console.error(`[htmx-i18n] hx-on body execution failed (${attrName} → ${eventName})`, err);\n }\n }\n });\n\n // Canonical-named claims are removed so htmx never JS-evals the\n // hyperscript body (double-execution guard). Localized names are\n // invisible to htmx and stay verbatim.\n if (attrName.startsWith('hx-on:')) elt.removeAttribute(attrName);\n\n if (already) {\n already.add(eventName);\n } else {\n claimed.set(elt, new Set([eventName]));\n }\n return true;\n}\n\n/**\n * Auto-detect body hooks from page globals — called by the browser entry\n * at load and again at DOMContentLoaded. Never overwrites hooks that\n * were set explicitly.\n *\n * - `window._hyperscript` → executor (original _hyperscript; pairs with\n * `@lokascript/hyperscript-adapter` for multilingual `_=` too)\n * - `window.HyperscriptI18n.preprocess` → translator (the\n * hyperscript-adapter browser bundles expose exactly this)\n */\nexport function autoDetectBodyHooks(win: object): void {\n const w = win as {\n _hyperscript?: ((code: string) => unknown) & {\n evaluate?: (code: string, ctx?: object) => unknown;\n };\n HyperscriptI18n?: { preprocess?: (src: string, lang: string) => string };\n };\n\n if (!executor && typeof w._hyperscript === 'function') {\n const hs = w._hyperscript;\n setBodyExecutor((code, elt, evt) =>\n typeof hs.evaluate === 'function' ? hs.evaluate(code, { me: elt, event: evt }) : hs(code)\n );\n }\n\n if (!translator && typeof w.HyperscriptI18n?.preprocess === 'function') {\n const preprocess = w.HyperscriptI18n.preprocess;\n setBodyTranslator((body, lang) => preprocess(body, lang));\n }\n}\n\n/** Reset all body-hook state. Mainly for tests. */\nexport function resetBodyHooks(): void {\n executor = null;\n translator = null;\n hookChangeListeners.clear();\n claimed = new WeakMap();\n}\n","/**\n * Resolver mode — the adapter's mechanism-(c) form, usable today against\n * the reference-patched htmx build (docs/reference-patches/) and ready\n * for upstream if the attribute-resolver seam lands.\n *\n * Where the default shim CANONICALIZES (copies localized attrs onto the\n * element), resolver mode answers htmx's attribute lookups directly:\n * core asks \"what should I read for `hx-get` on this element?\" and the\n * resolver returns the localized name actually present (`hx-obtener`)\n * for the element's language scope. Zero DOM mutation, fully\n * devtools-faithful — the loka-js ideal.\n *\n * Two seams must both be fed (the loka-js lesson): the per-element\n * resolver answers reads, and `additionalAttributeSelectors` feeds the\n * document-level discovery scan, which a per-element function cannot\n * drive. `installResolverMode()` wires both and keeps the selector list\n * fresh as vocab modules register.\n *\n * Scope: value-bearing attributes only. The hx-on colon family stays\n * with the shim/executor mode — its event name is part of the attribute\n * NAME, which is a different question than \"what name holds this\n * value\". Trigger VALUES (`hx-trigger=\"clic\"`) are also name-level\n * out-of-scope; keep authoring canonical event values, or pair with\n * executor mode.\n */\n\nimport { langOf } from './lang-resolver.js';\nimport { onVocabUpdate, vocabFor, vocabLangs } from './registry.js';\n\nlet resolverModeActive = false;\n\n/** True while resolver mode owns localization (the shim stands down). */\nexport function isResolverMode(): boolean {\n return resolverModeActive;\n}\n\n/** Mainly for tests; installResolverMode() manages this in production. */\nexport function setResolverMode(active: boolean): void {\n resolverModeActive = active;\n}\n\n/**\n * The per-element resolver htmx consults on attribute reads:\n * `(elt, canonicalName) → localized name present on elt, or null`.\n */\nexport function attributeResolver(elt: Element, name: string): string | null {\n const lang = langOf(elt);\n if (lang === 'en') return null;\n const attrs = vocabFor(lang)?.attrs;\n if (!attrs) return null;\n for (const [localized, canonical] of Object.entries(attrs)) {\n if (canonical === name && elt.hasAttribute?.(localized)) return localized;\n }\n return null;\n}\n\n/**\n * CSS attribute selectors for every registered localized name, for\n * htmx's discovery scan (`config.additionalAttributeSelectors`). The\n * hx-on family is excluded — see the module header.\n */\nexport function additionalAttributeSelectors(): string[] {\n const names = new Set<string>();\n for (const lang of vocabLangs()) {\n const attrs = vocabFor(lang)?.attrs ?? {};\n for (const [localized, canonical] of Object.entries(attrs)) {\n if (canonical === 'hx-on') continue;\n names.add(`[${localized}]`);\n }\n }\n return [...names];\n}\n\ninterface ResolverConfigurableHtmx {\n config?: {\n attributeResolver?: ((elt: Element, name: string) => string | null) | null;\n additionalAttributeSelectors?: string[];\n };\n}\n\n/**\n * Wire resolver mode into a (reference-patched) htmx instance: install\n * the resolver, feed the discovery selectors, keep them fresh on vocab\n * registration, and stand the canonicalization shim down. Returns an\n * uninstall function that restores shim behavior.\n */\nexport function installResolverMode(htmx: ResolverConfigurableHtmx): () => void {\n if (!htmx?.config) {\n throw new Error('[htmx-i18n] installResolverMode: htmx.config not found');\n }\n const cfg = htmx.config;\n cfg.attributeResolver = attributeResolver;\n cfg.additionalAttributeSelectors = additionalAttributeSelectors();\n const unsubscribe = onVocabUpdate(() => {\n cfg.additionalAttributeSelectors = additionalAttributeSelectors();\n });\n resolverModeActive = true;\n return () => {\n unsubscribe();\n cfg.attributeResolver = null;\n cfg.additionalAttributeSelectors = [];\n resolverModeActive = false;\n };\n}\n","/**\n * Localized → canonical attribute canonicalization for upstream htmx.\n *\n * htmx v4 has no hook to override how core resolves an attribute name —\n * it reads `hx-get` literally. Until an upstream resolver seam exists\n * (see docs/UPSTREAM_HOOK_PROPOSAL.md), this module makes localized\n * authoring work by copying each localized attribute to its canonical\n * name on the same element *before* htmx processes the node:\n *\n * <button lang=\"es\" hx-obtener=\"/api\"> → + hx-get=\"/api\"\n *\n * Design rules (mirroring the loka-js invariants where the mechanism\n * allows):\n *\n * - **The authored attribute is never removed or rewritten** — devtools\n * keeps showing what the author wrote. The one exception is an\n * author-written canonical `hx-trigger` whose *value* uses localized\n * event names (`hx-trigger=\"clic\"`): there is no separate canonical\n * target to write to, so the value is translated in place (idempotent —\n * the maps are localized → canonical, so a second pass is a no-op).\n * - **An existing canonical attribute always wins.** If the element\n * already has `hx-get`, a localized `hx-obtener` never overwrites it.\n * - **No vocab, no work.** With no languages registered every function\n * here is a cheap no-op, so stock htmx pages pay ~nothing.\n *\n * Only attributes in the `hx-` / `sse-` / `ws-` namespaces are ever\n * considered — the brand prefix is preserved across languages (Phase 8\n * convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the\n * prefix doubles as the discovery anchor.\n */\n\nimport { langOf } from './lang-resolver.js';\nimport { hasAnyVocab, vocabFor, warnMissingLangOnce } from './registry.js';\nimport { claimHxOnAttribute, hasBodyExecutor } from './hx-on.js';\nimport { isResolverMode } from './resolver.js';\n\n/** Attribute namespaces the adapter touches. */\nconst NS_RE = /^(?:hx|sse|ws)-/;\n\n/**\n * Translate localized event names inside an `hx-trigger` value.\n *\n * hx-trigger grammar: comma-separated specs, each `eventName[filter]\n * modifier…`. Only the leading event token of each spec is translated\n * (preserving an attached `[...]` filter); modifiers like `delay:500ms`,\n * `from:body`, `once` are language-invariant and left alone. Unknown\n * tokens pass through untouched, which also makes translation idempotent\n * (the maps are localized → canonical only).\n */\nexport function translateTriggerValue(value: string, events: Record<string, string>): string {\n return value\n .split(',')\n .map(spec => {\n const trimmed = spec.trim();\n if (!trimmed) return trimmed;\n const parts = trimmed.split(/\\s+/);\n const m = parts[0].match(/^([^[\\s]+)(\\[.*)?$/);\n if (m) {\n const translated = events[m[1]];\n if (translated) parts[0] = translated + (m[2] ?? '');\n }\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Canonicalize one element's localized htmx attributes in place.\n * Returns true if any attribute was added or updated.\n */\nexport function canonicalizeElement(elt: Element): boolean {\n if (!elt.attributes || elt.attributes.length === 0) return false;\n\n // Cheap prefilter before any lang resolution: bail unless some\n // attribute is in our namespaces.\n let hasNsAttr = false;\n for (const attr of Array.from(elt.attributes)) {\n if (NS_RE.test(attr.name)) {\n hasNsAttr = true;\n break;\n }\n }\n if (!hasNsAttr) return false;\n\n const executorMode = hasBodyExecutor();\n const lang = langOf(elt);\n if (lang === 'en' && !executorMode) return false;\n\n const vocab = vocabFor(lang);\n if (lang !== 'en' && !vocab) {\n warnMissingLangOnce(lang);\n // Executor mode still claims canonical-named hx-on:* attrs below —\n // body semantics don't depend on vocab being loaded.\n if (!executorMode) return false;\n }\n\n const attrs = vocab?.attrs ?? {};\n const events = vocab?.events ?? {};\n let changed = false;\n\n // Snapshot — we mutate the attribute list while iterating.\n for (const attr of Array.from(elt.attributes)) {\n const name = attr.name;\n if (!NS_RE.test(name)) continue;\n\n // Executor mode owns the hx-on family outright: canonical-named\n // attrs are claimed (listener installed, attr removed so htmx never\n // JS-evals the hyperscript body)…\n if (executorMode && name.startsWith('hx-on:')) {\n if (claimHxOnAttribute(elt, name, lang, events)) changed = true;\n continue;\n }\n\n // Exact match: hx-obtener → hx-get.\n let canonical = attrs[name];\n\n // Colon family (hx-on:*): hx-en:clic → hx-on:click. The base is\n // looked up in attrs, the event suffix in events.\n let colonBase: string | undefined;\n if (!canonical) {\n const colon = name.indexOf(':');\n if (colon > 0) {\n colonBase = attrs[name.slice(0, colon)];\n if (colonBase) {\n const suffix = name.slice(colon + 1);\n canonical = `${colonBase}:${events[suffix] ?? suffix}`;\n }\n }\n }\n\n if (!canonical) continue;\n\n // …and localized-named hx-on attrs are claimed in place: no\n // canonical sibling is created, the authored attr stays verbatim\n // (htmx never recognized it).\n if (executorMode && colonBase === 'hx-on') {\n if (claimHxOnAttribute(elt, name, lang, events)) changed = true;\n continue;\n }\n\n if (canonical === name || elt.hasAttribute(canonical)) continue;\n\n let value = attr.value;\n if (canonical === 'hx-trigger') value = translateTriggerValue(value, events);\n elt.setAttribute(canonical, value);\n changed = true;\n }\n\n // Author-written canonical hx-trigger with localized event values\n // (`hx-trigger=\"clic\"`). Translated in place — the only mutation of an\n // authored attribute, because there is no separate canonical target.\n const trigger = elt.getAttribute('hx-trigger');\n if (trigger !== null && Object.keys(events).length > 0) {\n const translated = translateTriggerValue(trigger, events);\n if (translated !== trigger) {\n elt.setAttribute('hx-trigger', translated);\n changed = true;\n }\n }\n\n return changed;\n}\n\n/**\n * Canonicalize an element and all its descendants. Returns the number of\n * elements that changed. This is what the htmx extension hook and the\n * initial document sweep call — htmx processes subtrees, so we mirror\n * that granularity.\n */\nexport function canonicalizeTree(root: Element | Document | DocumentFragment | null): number {\n if (!root) return 0;\n // Resolver mode (patched htmx answers reads directly) stands the\n // canonicalization shim down entirely — zero DOM mutation.\n if (isResolverMode()) return 0;\n // Executor mode must sweep even with no vocab loaded (English\n // hyperscript bodies need claiming too).\n if (!hasAnyVocab() && !hasBodyExecutor()) return 0;\n let count = 0;\n if (root instanceof Element && canonicalizeElement(root)) count++;\n if (typeof (root as Element).querySelectorAll === 'function') {\n for (const el of Array.from((root as Element).querySelectorAll('*'))) {\n if (canonicalizeElement(el)) count++;\n }\n }\n return count;\n}\n","/**\n * htmx extension + auto-sweep wiring.\n *\n * Primary target is **htmx v4**, whose extensions register via\n * `htmx.registerExtension(name, ext)` and hook lifecycle events through\n * underscore-named methods (event name with `:` → `_`), each receiving\n * `(elt, detail)`. Verified against htmx 4.0.0-beta5 (see\n * test/browser/vendor/README.md): `process(root)` fires\n * `htmx:before:process` on the processed root — `document.body`\n * initially, each swapped-in subtree afterwards — BEFORE any element\n * init or hx-on binding, so canonicalizing in `htmx_before_process`\n * covers everything htmx will read. `htmx_before_process_node` is kept\n * as a defensive alias for other v4 prereleases that used per-node\n * naming; an unmatched key is inert.\n *\n * A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)\n * is included because the localized attribute names are version-agnostic\n * data — but v2 support is best-effort, not a tested target.\n *\n * The extension hook alone is not enough for the *initial* page: script\n * order decides whether our sweep beats htmx's own DOMContentLoaded scan.\n * `installAutoSweep()` handles that — load this adapter (and vocab\n * modules) BEFORE the htmx <script> tag, mirroring loka-js's\n * \"orchestrator before libraries\" rule, and the sweep listener registers\n * ahead of htmx's.\n */\n\nimport { canonicalizeTree } from './canonicalize.js';\nimport { onVocabUpdate } from './registry.js';\nimport { onBodyHooksChanged } from './hx-on.js';\n\nexport const EXTENSION_NAME = 'lokascript-i18n';\n\n/** Minimal shape of the htmx global we interact with. */\nexport interface HtmxLike {\n /** htmx v4 registration entry point. */\n registerExtension?(name: string, extension: object): void;\n /** htmx v1/v2 registration entry point. */\n defineExtension?(name: string, extension: object): void;\n}\n\n/**\n * Build the extension object. v4 hooks and the v2 `onEvent` callback are\n * both present — each API only reads the members it knows about.\n */\nexport function createExtension(): object {\n return {\n // htmx v4 (verified on 4.0.0-beta5): fires on each process() root\n // before element init and hx-on binding.\n htmx_before_process(elt: Element): void {\n canonicalizeTree(elt);\n },\n // Defensive alias for v4 prereleases with per-node hook naming.\n htmx_before_process_node(elt: Element): void {\n canonicalizeTree(elt);\n },\n // htmx v1/v2 fallback: single event dispatcher.\n onEvent(name: string, evt: CustomEvent & { target?: EventTarget | null }): void {\n if (name !== 'htmx:beforeProcessNode') return;\n const detail = evt?.detail as { elt?: Element } | undefined;\n const elt = detail?.elt ?? (evt?.target instanceof Element ? evt.target : null);\n if (elt) canonicalizeTree(elt);\n },\n };\n}\n\n/**\n * Register the extension with an htmx global. Returns which API accepted\n * it (`'v4'` / `'v2'`) or `null` if the object exposes neither.\n */\nexport function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null {\n if (!htmx) return null;\n const ext = createExtension();\n if (typeof htmx.registerExtension === 'function') {\n htmx.registerExtension(EXTENSION_NAME, ext);\n return 'v4';\n }\n if (typeof htmx.defineExtension === 'function') {\n htmx.defineExtension(EXTENSION_NAME, ext);\n return 'v2';\n }\n return null;\n}\n\n/**\n * Sweep the whole document now (if parsed) or on DOMContentLoaded, and\n * re-sweep whenever a vocab module registers after the initial sweep\n * (e.g. a vocab <script> below htmx, or dynamic registration).\n *\n * Returns a cleanup function (mainly for tests).\n */\nexport function installAutoSweep(doc: Document = document): () => void {\n const sweep = (): void => {\n canonicalizeTree(doc.body ?? doc.documentElement);\n };\n\n let removeDomListener: (() => void) | null = null;\n if (doc.readyState === 'loading') {\n const onReady = (): void => sweep();\n doc.addEventListener('DOMContentLoaded', onReady, { once: true });\n removeDomListener = () => doc.removeEventListener('DOMContentLoaded', onReady);\n } else {\n sweep();\n }\n\n const unsubscribeVocab = onVocabUpdate(() => {\n if (doc.readyState !== 'loading') sweep();\n });\n\n // A body executor configured after the initial sweep flips the hx-on\n // family into executor mode — re-sweep so already-canonicalized\n // hx-on:* attrs get claimed (listener installed, attr removed).\n const unsubscribeBodyHooks = onBodyHooksChanged(() => {\n if (doc.readyState !== 'loading') sweep();\n });\n\n return () => {\n removeDomListener?.();\n unsubscribeVocab();\n unsubscribeBodyHooks();\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/lang-resolver.ts","../src/registry.ts","../src/hx-on.ts","../src/resolver.ts","../src/canonicalize.ts","../src/extension.ts"],"sourcesContent":["/**\n * @lokascript/htmx-adapter — multilingual adapter for upstream htmx v4.\n *\n * Lets authors write `hx-*` / `sse-*` / `ws-*` attributes in 24 languages\n * against the stock htmx library. Localized names are canonicalized on\n * the element before htmx processes it (see canonicalize.ts); vocab data\n * is the same generated `packages/core/vocab/htmx/{lang}.js` modules the\n * embedded hyperfixi htmx-compat layer uses.\n *\n * Programmatic use:\n *\n * import { register, registerWith, installAutoSweep } from '@lokascript/htmx-adapter';\n * register('es', { hyperfixi: { attrs: { 'hx-obtener': 'hx-get' }, events: { clic: 'click' } } });\n * registerWith(window.htmx); // v4 extension (v2 fallback)\n * installAutoSweep(); // initial-page sweep\n *\n * Browser IIFE (`./browser` export) does all of the above automatically.\n */\n\nexport { langOf, normLang } from './lang-resolver.js';\nexport {\n register,\n vocabFor,\n isLangRegistered,\n hasAnyVocab,\n onVocabUpdate,\n resetRegistry,\n type HtmxVocab,\n type VocabPayload,\n} from './registry.js';\nexport {\n canonicalizeElement,\n canonicalizeTree,\n translateTriggerValue,\n setTriggerSpecSplitter,\n wasCreatedByAdapter,\n TOP_LEVEL_COMMA_RE,\n type TriggerSpecSplitter,\n} from './canonicalize.js';\nexport {\n EXTENSION_NAME,\n createExtension,\n registerWith,\n installAutoSweep,\n type HtmxLike,\n} from './extension.js';\nexport {\n attributeResolver,\n additionalAttributeSelectors,\n installResolverMode,\n isResolverMode,\n setResolverMode,\n} from './resolver.js';\nexport {\n setBodyExecutor,\n setBodyTranslator,\n hasBodyExecutor,\n hasBodyTranslator,\n onBodyHooksChanged,\n claimHxOnAttribute,\n claimedAttrNames,\n isClaimed,\n removeClaimedCanonicalAttrs,\n setNeutralizeOnClaim,\n neutralizesOnClaim,\n autoDetectBodyHooks,\n resetBodyHooks,\n type BodyExecutor,\n type BodyTranslator,\n type ClaimOptions,\n} from './hx-on.js';\n","/**\n * Per-element language resolution for localized htmx attribute names.\n *\n * Mirrors `packages/core/src/htmx/lang-resolver.ts` (which in turn mirrors\n * loka-js's `lang-resolver.js`) so a page migrating between hyperfixi's\n * embedded htmx-compat layer and this upstream-htmx adapter resolves\n * languages identically. Resolution order:\n *\n * 1. `data-hyperfixi-lang` attribute on the element itself\n * 2. `data-hyperfixi-lang` on any ancestor\n * 3. `lang` attribute on any ancestor (HTML standard)\n * 4. `'en'` fallback\n *\n * Results are normalized to the part before the first `-`/`_` (`es-MX` →\n * `es`) and lowercased so callers can index vocab maps by 2-letter code\n * without caring about regional variants.\n */\n\n/** Normalize a language tag — `es-MX` / `ES_mx` → `es`. */\nexport function normLang(s: string | null | undefined): string {\n if (!s) return 'en';\n return s.split(/[-_]/)[0].toLowerCase();\n}\n\n/** Resolve the language code for an element via ancestor walk. */\nexport function langOf(elt: Element): string {\n const own = elt.getAttribute?.('data-hyperfixi-lang');\n if (own) return normLang(own);\n const dx = elt.closest?.('[data-hyperfixi-lang]');\n if (dx) return normLang(dx.getAttribute('data-hyperfixi-lang'));\n const la = elt.closest?.('[lang]');\n if (la) return normLang(la.getAttribute('lang'));\n return 'en';\n}\n","/**\n * Vocab registry for the upstream-htmx adapter.\n *\n * Accepts the SAME payload shape as hyperfixi core's htmx-compat\n * orchestrator (`packages/core/src/htmx/i18n-orchestrator.ts`), so the\n * generated vocab modules under `packages/core/vocab/htmx/{lang}.js`\n * (which call `window.__hyperfixi_i18n.register(lang, payload)`) work\n * verbatim against this adapter — one generated artifact, two consumers.\n *\n * register('es', {\n * hyperfixi: {\n * attrs: { 'hx-obtener': 'hx-get', 'sse-conectar': 'sse-connect' },\n * events: { clic: 'click', cambiar: 'change' },\n * },\n * });\n *\n * Unlike core's orchestrator this registry needs no inverted index: the\n * adapter canonicalizes (localized → canonical), which is exactly the\n * direction the parse maps are published in. There is deliberately no\n * KEYS copy here either — the vocab data is self-describing (full\n * attribute names on both sides), so the canonical key set lives only in\n * core's generator (`packages/core/scripts/gen-htmx-vocab.mjs`).\n */\n\nimport { normLang } from './lang-resolver.js';\n\nexport interface HtmxVocab {\n /** Map of localized attribute name → canonical English form (fully qualified). */\n attrs?: Record<string, string>;\n /** Map of localized event name → canonical English form (`'clic'` → `'click'`). */\n events?: Record<string, string>;\n}\n\nexport interface VocabPayload {\n hyperfixi?: HtmxVocab;\n}\n\n/** Per-language vocab, keyed by normalized language code. */\nconst REG = new Map<string, HtmxVocab>();\n\n/** Languages we've already warned about being missing. */\nconst warnedMissingLang = new Set<string>();\n\n/** Listeners notified after a vocab registration completes. */\nconst vocabUpdateListeners = new Set<() => void>();\n\n/**\n * Register a vocab module for a language. Idempotent — re-registering a\n * language replaces its vocab entirely.\n */\nexport function register(code: string, data: VocabPayload): void {\n const lang = normLang(code);\n REG.set(lang, data?.hyperfixi ?? {});\n warnedMissingLang.delete(lang); // we know about it now\n for (const listener of vocabUpdateListeners) listener();\n}\n\n/** Look up the vocab registered for a (normalized) language code. */\nexport function vocabFor(lang: string): HtmxVocab | undefined {\n return REG.get(lang);\n}\n\n/** Inspect whether any vocab is registered for a language. Mainly for tests. */\nexport function isLangRegistered(code: string): boolean {\n return REG.has(normLang(code));\n}\n\n/** True if at least one language has registered vocab. */\nexport function hasAnyVocab(): boolean {\n return REG.size > 0;\n}\n\n/** All language codes with registered vocab. */\nexport function vocabLangs(): string[] {\n return [...REG.keys()];\n}\n\n/** Subscribe to vocab-registration notifications. Returns an unsubscribe fn. */\nexport function onVocabUpdate(listener: () => void): () => void {\n vocabUpdateListeners.add(listener);\n return () => {\n vocabUpdateListeners.delete(listener);\n };\n}\n\n/**\n * Warn once per language that an element sits in a lang scope with no\n * registered vocab, so authors notice unloaded vocab modules instead of\n * silently getting English-only behavior.\n */\nexport function warnMissingLangOnce(lang: string): void {\n if (warnedMissingLang.has(lang)) return;\n warnedMissingLang.add(lang);\n if (typeof console !== 'undefined') {\n console.warn(\n `[htmx-i18n] No vocab registered for lang=\"${lang}\". ` +\n `Elements in this language scope keep their localized attribute names ` +\n `and htmx will not see them. Load the ${lang} vocab module ` +\n `(packages/core/vocab/htmx/${lang}.js) before htmx processes the page.`\n );\n }\n}\n\n/**\n * Reset registry state — drop all registrations and listeners. Mainly for\n * tests; production code should leave registrations in place.\n */\nexport function resetRegistry(): void {\n REG.clear();\n warnedMissingLang.clear();\n vocabUpdateListeners.clear();\n}\n","/**\n * hx-on body support — opt-in \"executor mode\".\n *\n * Upstream htmx evals `hx-on:*` bodies as JavaScript. In the hyperfixi\n * ecosystem the convention is hyperscript bodies (`hx-on:click=\"toggle\n * .active on me\"`), which htmx's JS eval cannot run — and localized\n * bodies (`hx-en:clic=\"alternar .active\"`) doubly so. This module lets a\n * page opt in to hyperscript-body semantics by configuring an executor:\n *\n * setBodyExecutor((code, elt, evt) => _hyperscript.evaluate(code, { me: elt, event: evt }))\n * setBodyTranslator((body, lang) => HyperscriptI18n.preprocess(body, lang))\n *\n * With an executor set, the adapter CLAIMS every hx-on-family attribute\n * (localized-named or canonical-named — all bodies are treated as\n * hyperscript; mixed JS/hyperscript pages have no reliable detection):\n * it installs a real event listener that runs the (lazily translated)\n * body through the executor, and keeps htmx away from it:\n *\n * - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx\n * never recognized them anyway — and NO canonical `hx-on:*` sibling is\n * created.\n * - Canonical-named attrs (`hx-on:click`) must be kept away from htmx's\n * own binder: if htmx bound them, it would eval the hyperscript body\n * as JS — a console error plus a double-execution attempt on every\n * fire. A claim RECORDS the attribute (name + body) on the element;\n * what keeps htmx off it is decided by whoever owns the runtime:\n * - htmx v4, extension accepted: the extension's cancelable\n * `htmx:before:on:init` hook consults the record per node and\n * cancels htmx's binding — the authored attribute stays in the DOM\n * (see extension.ts). `registerWith` turns claim-time removal OFF\n * only after the registration actually succeeded.\n * - everywhere else (htmx v2, a rejected v4 registration, no htmx):\n * `neutralizeOnClaim` is ON (the default) and the canonical-named\n * attribute is removed at claim time — the only guard that does not\n * depend on a hook being installed, documented as the executor-mode\n * exception in the README.\n *\n * With no executor set (the default), none of this runs and bodies keep\n * upstream JS semantics — the behavior-preservation invariant.\n *\n * Translation is lazy (first event fire, memoized) so a translator that\n * loads after the initial sweep still applies. Both auto-detection\n * (`autoDetectBodyHooks`) and manual configuration are supported; the\n * executor is also re-read at fire time so replacing it takes effect on\n * live listeners. The body is re-read on every re-claim, so\n * `htmx.process(elt, true)` after editing the attribute runs the new body.\n */\n\nexport type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;\nexport type BodyTranslator = (body: string, lang: string) => string;\n\n/** Own-key lookup: vocab maps are plain objects, and `constructor` is not an event. */\nconst hasOwn = (o: object, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k);\n\nlet executor: BodyExecutor | null = null;\nlet translator: BodyTranslator | null = null;\n\n/** Listeners notified when the executor is set/cleared (drives re-sweeps). */\nconst hookChangeListeners = new Set<() => void>();\n\n/** One claimed hx-on attribute on an element. */\ninterface Claim {\n /** The attribute the listener was installed for (`hx-on:click` / `hx-en:clic`). */\n attrName: string;\n /** Element language at claim time — drives lazy body translation. */\n lang: string;\n /** Authored body, refreshed on re-claim when the attribute changed. */\n body: string;\n /** Memoized translation of `body` (null = not translated yet). */\n translated: string | null;\n}\n\n/**\n * Per-element claims keyed by resolved EVENT name (sweep idempotency).\n * Keyed by event rather than attribute name so a localized attr and its\n * canonical form never both install a listener. When both are genuinely\n * authored, DOM attribute order decides which body wins; an\n * adapter-created canonical sibling (a no-executor sweep copied\n * `hx-en:clic` to `hx-on:click`, then an executor arrived) is removed\n * on the re-claim — it was never authored, so the never-mutate rule does\n * not cover it, and leaving it would hand htmx a foreign-language body.\n */\nlet claimed = new WeakMap<Element, Map<string, Claim>>();\n\n/**\n * Whether a claim on a canonical-named `hx-on:*` attribute deletes it.\n *\n * ON (default) is the hook-independent guard. `registerWith` turns it\n * OFF only once htmx v4 has actually accepted the extension, because\n * only then does the per-node `htmx_before_on_init` cancellation exist\n * to keep htmx off the preserved attribute. Consumers wiring\n * `createExtension()` into `htmx.registerExtension` themselves must\n * call `setNeutralizeOnClaim(false)` after a successful registration\n * to get the same zero-mutation behaviour.\n */\nlet neutralizeOnClaim = true;\n\nexport function setNeutralizeOnClaim(enabled: boolean): void {\n neutralizeOnClaim = enabled;\n}\n\nexport function neutralizesOnClaim(): boolean {\n return neutralizeOnClaim;\n}\n\n/** Configure the body executor. Setting/clearing it notifies subscribers. */\nexport function setBodyExecutor(fn: BodyExecutor | null): void {\n executor = fn;\n for (const listener of hookChangeListeners) listener();\n}\n\n/** Configure the body translator (localized body → English hyperscript). */\nexport function setBodyTranslator(fn: BodyTranslator | null): void {\n translator = fn;\n}\n\nexport function hasBodyExecutor(): boolean {\n return executor !== null;\n}\n\nexport function hasBodyTranslator(): boolean {\n return translator !== null;\n}\n\n/** Subscribe to executor changes. Returns an unsubscribe fn. */\nexport function onBodyHooksChanged(listener: () => void): () => void {\n hookChangeListeners.add(listener);\n return () => {\n hookChangeListeners.delete(listener);\n };\n}\n\n/** True when the adapter has claimed at least one hx-on attribute on `elt`. */\nexport function isClaimed(elt: Element): boolean {\n return (claimed.get(elt)?.size ?? 0) > 0;\n}\n\n/** Names of the attributes claimed on `elt` (canonical and localized). */\nexport function claimedAttrNames(elt: Element): Set<string> {\n const names = new Set<string>();\n const claims = claimed.get(elt);\n if (claims) for (const c of claims.values()) names.add(c.attrName);\n return names;\n}\n\n/**\n * Remove the claimed CANONICAL-named (`hx-on:*`) attributes from `elt`.\n * The per-node fallback for runtimes that will bind the node anyway\n * (v4 mixed nodes). Localized names are invisible to htmx and stay.\n * Returns how many were removed.\n */\nexport function removeClaimedCanonicalAttrs(elt: Element): number {\n let n = 0;\n for (const name of claimedAttrNames(elt)) {\n if (name.startsWith('hx-on:') && elt.hasAttribute(name)) {\n elt.removeAttribute(name);\n n++;\n }\n }\n return n;\n}\n\n/**\n * Resolve the DOM event name for an hx-on attribute suffix.\n * `hx-on::after-swap` shorthand (leading `:`) means the `htmx:` namespace;\n * plain suffixes translate through the vocab events map (own keys only —\n * `constructor` is not an event).\n */\nfunction eventNameForSuffix(rawSuffix: string, events: Record<string, string>): string {\n if (rawSuffix.startsWith(':')) return `htmx${rawSuffix}`;\n return hasOwn(events, rawSuffix) ? events[rawSuffix] : rawSuffix;\n}\n\nexport interface ClaimOptions {\n /**\n * The attribute was written by the adapter's own canonicalization (a\n * no-executor sweep's `hx-on:*` copy of a localized attr), not by the\n * author. Such a duplicate is always removed on claim.\n */\n adapterCreated?: boolean;\n}\n\n/**\n * Claim one hx-on-family attribute on an element: record it, install the\n * executor listener, and — when claim-time neutralization is on — remove\n * a canonical-named attribute so htmx never JS-evals it. Returns true if\n * the element changed or a listener was installed now (false when no\n * executor, already claimed for that event, or malformed).\n *\n * `lang` is the element's language at claim time — used for lazy body\n * translation. `events` is the language's event-name map (for the\n * listener's event name; the shorthand/unknown cases pass through).\n */\nexport function claimHxOnAttribute(\n elt: Element,\n attrName: string,\n lang: string,\n events: Record<string, string>,\n options: ClaimOptions = {}\n): boolean {\n if (!executor) return false;\n\n const colon = attrName.indexOf(':');\n if (colon <= 0) return false; // colon-form only; legacy composite hx-on=\"…\" unsupported\n\n const eventName = eventNameForSuffix(attrName.slice(colon + 1), events);\n const canonical = attrName.startsWith('hx-on:');\n const body = elt.getAttribute(attrName) ?? '';\n\n let claims = claimed.get(elt);\n const existing = claims?.get(eventName);\n if (existing) {\n if (existing.attrName === attrName) {\n // Re-claim of the same attribute (re-sweep, or a force re-process\n // after the author edited it): refresh the body, no new listener.\n if (existing.body !== body) {\n existing.body = body;\n existing.translated = null;\n }\n if (canonical && neutralizeOnClaim && elt.hasAttribute(attrName)) {\n elt.removeAttribute(attrName);\n return true;\n }\n return false;\n }\n // A second attribute for an already-claimed event (localized +\n // canonical naming the same event). No second listener. A canonical\n // duplicate is removed when it is adapter-created (never authored)\n // or when claim-time neutralization is on; an authored one stays\n // for the v4 hook to cancel.\n if (canonical && (options.adapterCreated || neutralizeOnClaim) && elt.hasAttribute(attrName)) {\n elt.removeAttribute(attrName);\n return true;\n }\n return false;\n }\n\n const claim: Claim = { attrName, lang, body, translated: null };\n elt.addEventListener(eventName, evt => {\n if (!executor) return; // executor cleared after claim — go quiet\n if (claim.translated === null) {\n claim.translated =\n claim.lang !== 'en' && translator ? translator(claim.body, claim.lang) : claim.body;\n }\n try {\n executor(claim.translated, elt, evt);\n } catch (err) {\n if (typeof console !== 'undefined') {\n console.error(`[htmx-i18n] hx-on body execution failed (${attrName} → ${eventName})`, err);\n }\n }\n });\n\n if (!claims) {\n claims = new Map();\n claimed.set(elt, claims);\n }\n claims.set(eventName, claim);\n\n // Hook-independent guard: with neutralization on, a canonical-named\n // claim is deleted so htmx never JS-evals the hyperscript body. With it\n // off (v4 extension accepted) the attribute stays and the extension's\n // before:on:init hook cancels htmx's binding instead. Localized names\n // are invisible to htmx and always stay verbatim.\n if (canonical && neutralizeOnClaim) elt.removeAttribute(attrName);\n return true;\n}\n\n/**\n * Auto-detect body hooks from page globals — called by the browser entry\n * at load and again at DOMContentLoaded. Never overwrites hooks that\n * were set explicitly.\n *\n * - `window._hyperscript` → executor (original _hyperscript; pairs with\n * `@lokascript/hyperscript-adapter` for multilingual `_=` too)\n * - `window.HyperscriptI18n.preprocess` → translator (the\n * hyperscript-adapter browser bundles expose exactly this)\n */\nexport function autoDetectBodyHooks(win: object): void {\n const w = win as {\n _hyperscript?: ((code: string) => unknown) & {\n evaluate?: (code: string, ctx?: object) => unknown;\n };\n HyperscriptI18n?: { preprocess?: (src: string, lang: string) => string };\n };\n\n if (!executor && typeof w._hyperscript === 'function') {\n const hs = w._hyperscript;\n setBodyExecutor((code, elt, evt) =>\n typeof hs.evaluate === 'function' ? hs.evaluate(code, { me: elt, event: evt }) : hs(code)\n );\n }\n\n if (!translator && typeof w.HyperscriptI18n?.preprocess === 'function') {\n const preprocess = w.HyperscriptI18n.preprocess;\n setBodyTranslator((body, lang) => preprocess(body, lang));\n }\n}\n\n/** Reset all body-hook state. Mainly for tests. */\nexport function resetBodyHooks(): void {\n executor = null;\n translator = null;\n hookChangeListeners.clear();\n claimed = new WeakMap();\n neutralizeOnClaim = true;\n}\n","/**\n * Resolver mode — the adapter's mechanism-(c) form, usable today against\n * the reference-patched htmx build (docs/reference-patches/) and ready\n * for upstream if the attribute-resolver seam lands.\n *\n * Where the default shim CANONICALIZES (copies localized attrs onto the\n * element), resolver mode answers htmx's attribute lookups directly:\n * core asks \"what should I read for `hx-get` on this element?\" and the\n * resolver returns the localized name actually present (`hx-obtener`)\n * for the element's language scope. Zero DOM mutation, fully\n * devtools-faithful — the loka-js ideal.\n *\n * Two seams must both be fed (the loka-js lesson): the per-element\n * resolver answers reads, and `additionalAttributeSelectors` feeds the\n * document-level discovery scan, which a per-element function cannot\n * drive. `installResolverMode()` wires both and keeps the selector list\n * fresh as vocab modules register.\n *\n * Scope: value-bearing attributes only. The hx-on colon family stays\n * with the shim/executor mode — its event name is part of the attribute\n * NAME, which is a different question than \"what name holds this\n * value\". Trigger VALUES (`hx-trigger=\"clic\"`) are also name-level\n * out-of-scope; keep authoring canonical event values, or pair with\n * executor mode.\n */\n\nimport { langOf } from './lang-resolver.js';\nimport { onVocabUpdate, vocabFor, vocabLangs } from './registry.js';\n\nlet resolverModeActive = false;\n\n/** True while resolver mode owns localization (the shim stands down). */\nexport function isResolverMode(): boolean {\n return resolverModeActive;\n}\n\n/** Mainly for tests; installResolverMode() manages this in production. */\nexport function setResolverMode(active: boolean): void {\n resolverModeActive = active;\n}\n\n/**\n * The per-element resolver htmx consults on attribute reads:\n * `(elt, canonicalName) → localized name present on elt, or null`.\n */\nexport function attributeResolver(elt: Element, name: string): string | null {\n const lang = langOf(elt);\n if (lang === 'en') return null;\n const attrs = vocabFor(lang)?.attrs;\n if (!attrs) return null;\n for (const [localized, canonical] of Object.entries(attrs)) {\n if (canonical === name && elt.hasAttribute?.(localized)) return localized;\n }\n return null;\n}\n\n/**\n * CSS attribute selectors for every registered localized name, for\n * htmx's discovery scan (`config.additionalAttributeSelectors`). The\n * hx-on family is excluded — see the module header.\n */\nexport function additionalAttributeSelectors(): string[] {\n const names = new Set<string>();\n for (const lang of vocabLangs()) {\n const attrs = vocabFor(lang)?.attrs ?? {};\n for (const [localized, canonical] of Object.entries(attrs)) {\n if (canonical === 'hx-on') continue;\n names.add(`[${localized}]`);\n }\n }\n return [...names];\n}\n\ninterface ResolverConfigurableHtmx {\n config?: {\n attributeResolver?: ((elt: Element, name: string) => string | null) | null;\n additionalAttributeSelectors?: string[];\n };\n}\n\n/**\n * Wire resolver mode into a (reference-patched) htmx instance: install\n * the resolver, feed the discovery selectors, keep them fresh on vocab\n * registration, and stand the canonicalization shim down. Returns an\n * uninstall function that restores shim behavior.\n */\nexport function installResolverMode(htmx: ResolverConfigurableHtmx): () => void {\n if (!htmx?.config) {\n throw new Error('[htmx-i18n] installResolverMode: htmx.config not found');\n }\n const cfg = htmx.config;\n cfg.attributeResolver = attributeResolver;\n cfg.additionalAttributeSelectors = additionalAttributeSelectors();\n const unsubscribe = onVocabUpdate(() => {\n cfg.additionalAttributeSelectors = additionalAttributeSelectors();\n });\n resolverModeActive = true;\n return () => {\n unsubscribe();\n cfg.attributeResolver = null;\n cfg.additionalAttributeSelectors = [];\n resolverModeActive = false;\n };\n}\n","/**\n * Localized → canonical attribute canonicalization for upstream htmx.\n *\n * htmx v4 has no hook to override how core resolves an attribute name —\n * it reads `hx-get` literally. Until an upstream resolver seam exists\n * (see docs/UPSTREAM_HOOK_PROPOSAL.md), this module makes localized\n * authoring work by copying each localized attribute to its canonical\n * name on the same element *before* htmx processes the node:\n *\n * <button lang=\"es\" hx-obtener=\"/api\"> → + hx-get=\"/api\"\n *\n * Design rules (mirroring the loka-js invariants where the mechanism\n * allows):\n *\n * - **The authored attribute is never removed or rewritten** — devtools\n * keeps showing what the author wrote. Two documented exceptions:\n * an author-written canonical `hx-trigger` whose *value* uses localized\n * event names (`hx-trigger=\"clic\"`) has no separate canonical target,\n * so the value is translated in place (idempotent — the maps are\n * localized → canonical, so a second pass is a no-op); and in executor\n * mode a canonical-named `hx-on:*` attribute may be removed by the\n * claim when no htmx hook exists to cancel htmx's binding (hx-on.ts).\n * - **An existing canonical attribute always wins.** If the element\n * already has `hx-get`, a localized `hx-obtener` never overwrites it.\n * - **No vocab, no work.** With no languages registered every function\n * here is a cheap no-op, so stock htmx pages pay ~nothing.\n *\n * Only attributes in the `hx-` / `sse-` / `ws-` namespaces are ever\n * considered — the brand prefix is preserved across languages (Phase 8\n * convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the\n * prefix doubles as the discovery anchor.\n */\n\nimport { langOf } from './lang-resolver.js';\nimport { hasAnyVocab, vocabFor, warnMissingLangOnce } from './registry.js';\nimport { claimHxOnAttribute, hasBodyExecutor } from './hx-on.js';\nimport { isResolverMode } from './resolver.js';\n\n/** Own-key lookup: vocab maps are plain objects, and `constructor` is not an event. */\nconst hasOwn = (o: object, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k);\n\n/** Attribute namespaces the adapter touches. */\nconst NS_RE = /^(?:hx|sse|ws)-/;\n\n/**\n * Byte-mirror of htmx v4's `HCON.split` top-level-comma regex (vendored\n * 4.0.0, `HCON.split`; pinned against the vendored file by\n * test/vendor-mirror.test.ts). Spec boundaries must match core's grammar:\n * commas inside `[filters]`, `(calls)`, and quoted strings — e.g.\n * `from:\".a, .b\"` — are NOT separators.\n *\n * On htmx 4.0.0 the extension's `init(internalAPI)` swaps in htmx's own\n * `HCON.split` (see setTriggerSpecSplitter), so the mirror is the\n * fallback for v2 and for pages where the extension is not registered.\n */\n// The `\\[` spelling is htmx's own and must stay byte-identical (the drift guard compares sources).\n/* oxlint-disable no-useless-escape */\nexport const TOP_LEVEL_COMMA_RE =\n /,(?![^\\[]*\\])(?![^(]*\\))(?![^<]*\\/>)(?=(?:[^\"']|\"[^\"]*\"|'[^']*')*$)/;\n/* oxlint-enable no-useless-escape */\n\nexport type TriggerSpecSplitter = (value: string) => string[];\n\nconst mirrorSplit: TriggerSpecSplitter = value => value.split(TOP_LEVEL_COMMA_RE);\nlet splitSpecs: TriggerSpecSplitter = mirrorSplit;\n\n/**\n * Use htmx's own top-level splitter (`internalAPI.HCON.split` on 4.0.0)\n * instead of the mirror. Pass `null` to restore the mirror.\n */\nexport function setTriggerSpecSplitter(fn: TriggerSpecSplitter | null): void {\n splitSpecs = fn ?? mirrorSplit;\n}\n\n/** `hx-trigger` and its v4 modifier forms (`hx-trigger:inherited`, …). */\nfunction isTriggerAttr(name: string): boolean {\n return name === 'hx-trigger' || name.startsWith('hx-trigger:');\n}\n\n/**\n * Translate localized event names inside an `hx-trigger` value.\n *\n * hx-trigger grammar: comma-separated specs, each `eventName[filter]\n * modifier…`. Only the leading event token of each spec is translated\n * (an attached `[...]` filter, all modifiers like `delay:500ms` /\n * `from:body` / `once`, and the authored spacing stay byte-identical —\n * the split removes exactly the commas the join restores, so an\n * all-canonical value comes back verbatim). Unknown tokens pass\n * through untouched, which also makes translation idempotent (the maps\n * are localized → canonical only). Only the map's OWN keys translate:\n * `constructor` is not an event.\n */\nexport function translateTriggerValue(value: string, events: Record<string, string>): string {\n return splitSpecs(value)\n .map(spec =>\n spec.replace(/^(\\s*)([^[\\s]+)/, (whole, ws: string, evt: string) =>\n hasOwn(events, evt) ? ws + events[evt] : whole\n )\n )\n .join(',');\n}\n\n/**\n * Canonical attributes THIS module wrote (per element), as opposed to\n * authored ones. Lets executor mode tell an adapter-created `hx-on:*`\n * sibling from an authored one when both name the same event.\n */\nconst created = new WeakMap<Element, Set<string>>();\n\nfunction markCreated(elt: Element, name: string): void {\n let names = created.get(elt);\n if (!names) {\n names = new Set();\n created.set(elt, names);\n }\n names.add(name);\n}\n\n/** True when `name` on `elt` was written by canonicalization, not authored. */\nexport function wasCreatedByAdapter(elt: Element, name: string): boolean {\n return created.get(elt)?.has(name) ?? false;\n}\n\n/**\n * Canonicalize one element's localized htmx attributes in place.\n * Returns true if any attribute was added or updated.\n */\nexport function canonicalizeElement(elt: Element): boolean {\n if (!elt.attributes || elt.attributes.length === 0) return false;\n\n // Cheap prefilter before any lang resolution: bail unless some\n // attribute is in our namespaces.\n let hasNsAttr = false;\n for (const attr of Array.from(elt.attributes)) {\n if (NS_RE.test(attr.name)) {\n hasNsAttr = true;\n break;\n }\n }\n if (!hasNsAttr) return false;\n\n const executorMode = hasBodyExecutor();\n const lang = langOf(elt);\n if (lang === 'en' && !executorMode) return false;\n\n const vocab = vocabFor(lang);\n if (lang !== 'en' && !vocab) {\n warnMissingLangOnce(lang);\n // Executor mode still claims canonical-named hx-on:* attrs below —\n // body semantics don't depend on vocab being loaded.\n if (!executorMode) return false;\n }\n\n const attrs = vocab?.attrs ?? {};\n const events = vocab?.events ?? {};\n let hasEvents = false;\n for (const _ in events) {\n hasEvents = true;\n break;\n }\n let changed = false;\n\n // Snapshot — we mutate the attribute list while iterating, and the\n // canonical attributes written below must not be revisited.\n for (const attr of Array.from(elt.attributes)) {\n const name = attr.name;\n if (!NS_RE.test(name)) continue;\n\n // Executor mode owns the hx-on family outright: canonical-named\n // attrs are claimed (listener installed; removal is the claim's\n // call — see hx-on.ts)…\n if (executorMode && name.startsWith('hx-on:')) {\n if (\n claimHxOnAttribute(elt, name, lang, events, {\n adapterCreated: wasCreatedByAdapter(elt, name),\n })\n ) {\n changed = true;\n }\n continue;\n }\n\n // Author-written canonical hx-trigger (including modifier forms like\n // hx-trigger:inherited) with localized event values\n // (`hx-trigger=\"clic\"`). Translated in place — a documented mutation\n // of an authored attribute, because there is no separate canonical\n // target. Idempotent by construction.\n if (isTriggerAttr(name)) {\n if (hasEvents) {\n const translated = translateTriggerValue(attr.value, events);\n if (translated !== attr.value) {\n elt.setAttribute(name, translated);\n changed = true;\n }\n }\n continue;\n }\n\n // Exact match: hx-obtener → hx-get.\n let canonical = hasOwn(attrs, name) ? attrs[name] : undefined;\n\n // Colon family: the base is looked up in attrs; what the suffix\n // means depends on the base. For hx-on the suffix is an EVENT name\n // (hx-en:clic → hx-on:click) and translates through events. For\n // every other attribute a colon suffix is an htmx v4 MODIFIER\n // (`:inherited` / `:append` — #attributeValue reads name+\":inherited\"\n // etc.), never an event name: pass it through verbatim so e.g.\n // hx-objetivo:inherited → hx-target:inherited, and an events-map\n // collision can never corrupt the modifier.\n let colonBase: string | undefined;\n if (!canonical) {\n const colon = name.indexOf(':');\n if (colon > 0) {\n const base = name.slice(0, colon);\n colonBase = hasOwn(attrs, base) ? attrs[base] : undefined;\n if (colonBase) {\n const suffix = name.slice(colon + 1);\n canonical =\n colonBase === 'hx-on'\n ? `${colonBase}:${hasOwn(events, suffix) ? events[suffix] : suffix}`\n : `${colonBase}:${suffix}`;\n }\n }\n }\n\n if (!canonical) continue;\n\n // …and localized-named hx-on attrs are claimed in place: no\n // canonical sibling is created, the authored attr stays verbatim\n // (htmx never recognized it).\n if (executorMode && colonBase === 'hx-on') {\n if (claimHxOnAttribute(elt, name, lang, events)) changed = true;\n continue;\n }\n\n if (canonical === name || elt.hasAttribute(canonical)) continue;\n\n let value = attr.value;\n // hx-trigger and its modifier forms carry event names in the VALUE —\n // translate on the way over.\n if (isTriggerAttr(canonical)) value = translateTriggerValue(value, events);\n elt.setAttribute(canonical, value);\n markCreated(elt, canonical);\n changed = true;\n }\n\n return changed;\n}\n\n/**\n * Canonicalize an element and all its descendants. Returns the number of\n * elements that changed. This is what the htmx extension hook and the\n * initial document sweep call — htmx processes subtrees, so we mirror\n * that granularity.\n */\nexport function canonicalizeTree(root: Element | Document | DocumentFragment | null): number {\n if (!root) return 0;\n // Resolver mode (patched htmx answers reads directly) stands the\n // canonicalization shim down entirely — zero DOM mutation.\n if (isResolverMode()) return 0;\n // Executor mode must sweep even with no vocab loaded (English\n // hyperscript bodies need claiming too).\n if (!hasAnyVocab() && !hasBodyExecutor()) return 0;\n let count = 0;\n if (root instanceof Element && canonicalizeElement(root)) count++;\n if (typeof (root as Element).querySelectorAll === 'function') {\n for (const el of Array.from((root as Element).querySelectorAll('*'))) {\n if (canonicalizeElement(el)) count++;\n }\n }\n return count;\n}\n","/**\n * htmx extension + auto-sweep wiring.\n *\n * Primary target is **htmx v4**, whose extensions register via\n * `htmx.registerExtension(name, ext)` and hook lifecycle events through\n * underscore-named methods (event name with `:` → `_`), each receiving\n * `(elt, detail)`. Verified against htmx 4.0.0 (see\n * test/browser/vendor/README.md): `process(root)` fires\n * `htmx:before:process` on the processed root — `document.body`\n * initially, each swapped-in subtree afterwards — BEFORE any element\n * init or hx-on binding, so canonicalizing in `htmx_before_process`\n * covers everything htmx will read. `htmx_before_process_node` is kept\n * as a defensive alias for other v4 prereleases that used per-node\n * naming; an unmatched key is inert. The cancelable\n * `htmx_before_on_init` hook (fires per hx-on-carrying node, after\n * before:process; documented as cancelable in htmx 4.0.0's `htmx.d.ts`)\n * lets executor mode keep claimed `hx-on:*` attributes in the DOM\n * instead of removing them — see createExtension.\n *\n * A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)\n * is included because the localized attribute names are version-agnostic\n * data — but v2 support is best-effort, not a tested target. Measured on\n * 2.0.10: `processNode` binds `hx-on` (`processHxOnWildcard`) BEFORE it\n * fires `htmx:beforeProcessNode` per node, so v2 has no pre-bind seam and\n * executor-mode claims keep removing canonical `hx-on:*` attrs there.\n *\n * The extension hook alone is not enough for the *initial* page: script\n * order decides whether our sweep beats htmx's own DOMContentLoaded scan.\n * `installAutoSweep()` handles that — load this adapter (and vocab\n * modules) BEFORE the htmx <script> tag, mirroring loka-js's\n * \"orchestrator before libraries\" rule, and the sweep listener registers\n * ahead of htmx's.\n */\n\nimport { canonicalizeTree, setTriggerSpecSplitter } from './canonicalize.js';\nimport { onVocabUpdate } from './registry.js';\nimport {\n claimedAttrNames,\n onBodyHooksChanged,\n removeClaimedCanonicalAttrs,\n setNeutralizeOnClaim,\n} from './hx-on.js';\n\nexport const EXTENSION_NAME = 'lokascript-i18n';\n\n/** Minimal shape of the htmx global we interact with. */\nexport interface HtmxLike {\n /**\n * htmx v4 registration entry point. Returns `false` when the name is\n * rejected — `htmx.config.extensions` is a non-empty allowlist that\n * omits it, or it was already registered. (`htmx.d.ts` types the\n * return as `void`; the runtime, verified on 4.0.0, returns `false`.)\n */\n registerExtension?(name: string, extension: object): boolean | void;\n /** htmx v1/v2 registration entry point. */\n defineExtension?(name: string, extension: object): void;\n /** htmx v4 config — the parts that decide which hx-on spellings it binds. */\n config?: {\n /** Alternate attribute prefix (`data-hx-` by default; `''` disables it). */\n prefix?: string;\n /** Separator between `hx-on` and the event name (`:` by default). */\n metaCharacter?: string;\n };\n}\n\n/** The slice of htmx v4's `internalAPI` the extension reads. */\ninterface InternalApiLike {\n HCON?: { split?(value: string): string[] };\n}\n\n/**\n * The hx-on attribute names htmx v4 will bind on a node, mirroring\n * `#prefixes(\"hx-on\")` + `#handleHxOnAttributes` (vendored 4.0.0):\n * for each prefix `p` in [`hx-on`, `config.prefix + \"on\"`], the bare\n * composite `p` (`hx-on=\"event -> code\"`) and `p + metaCharacter + event`.\n * `hx-online` / `data-hx-once` are NOT bound — the char after the prefix\n * must be the meta character.\n */\nfunction isHtmxBindableHxOn(name: string, prefixes: string[], mc: string): boolean {\n for (const p of prefixes) {\n if (name === p) return true;\n if (name.startsWith(p) && name.charAt(p.length) === mc) return true;\n }\n return false;\n}\n\n/**\n * Build the extension object. v4 hooks and the v2 `onEvent` callback are\n * both present — each API only reads the members it knows about. Pass\n * the htmx instance so the v4 hook can read `config.prefix` /\n * `config.metaCharacter` (defaults are assumed otherwise).\n *\n * `init(internalAPI)` (v4 only) takes exactly one thing from the API:\n * `HCON.split`, htmx's own top-level trigger-spec splitter, which\n * replaces the regex mirror in canonicalize.ts for the lifetime of the\n * page. The rest of the 4.0.0 surface — `attributeValue`,\n * `parseTriggerSpecs`, `getAttributeObject`, `insertContent`, `morph`,\n * `initSecurity`, `onTrigger`, `htmxProp`, `triggerHtmxEvent`,\n * `executeJavaScript` — was evaluated and not used: `parseTriggerSpecs`\n * is parse-only (no serializer, so translating through it would mean a\n * hand-written spec serializer); `attributeValue`'s inheritance-aware\n * reads buy nothing for a sweep that visits every element anyway; and\n * `htmxProp(node).onInitialized = true` WOULD keep htmx's binder off a\n * node, but couples to a private flag where `htmx:before:on:init` is a\n * typed, documented, cancelable event — the hook below uses that.\n */\nexport function createExtension(htmx?: HtmxLike | null): object {\n const cfg = htmx?.config;\n const prefix = cfg && 'prefix' in cfg ? (cfg.prefix ?? '') : 'data-hx-';\n const mc = cfg?.metaCharacter || ':';\n const hxOnPrefixes = prefix ? ['hx-on', prefix + 'on'] : ['hx-on'];\n\n return {\n init(internalAPI?: InternalApiLike): void {\n const split = internalAPI?.HCON?.split;\n if (typeof split === 'function') setTriggerSpecSplitter(split.bind(internalAPI!.HCON));\n },\n // htmx v4 (verified on 4.0.0): fires on each process() root\n // before element init and hx-on binding.\n htmx_before_process(elt: Element): void {\n canonicalizeTree(elt);\n },\n // Defensive alias for v4 prereleases with per-node hook naming.\n htmx_before_process_node(elt: Element): void {\n canonicalizeTree(elt);\n },\n // htmx v4 (verified on 4.0.0): fires per node carrying an\n // hx-on-family attribute, cancelable — returning false makes htmx\n // skip JS-binding that node entirely. This is executor mode's\n // zero-mutation double-execution guard, decided from what the sweep\n // RECORDED on the node (not from whether an executor is set now):\n //\n // - nothing claimed on this node → htmx proceeds as usual;\n // - every hx-on attribute htmx would bind here is claimed → cancel,\n // the authored attributes stay in the DOM;\n // - the node also carries forms htmx must bind and the adapter never\n // claims (the legacy composite `hx-on=\"event -> code\"`, the\n // `config.prefix` spelling, a custom meta character) → per-node\n // cancellation would kill those too, so fall back to removing the\n // claimed canonical attrs and let htmx bind the rest.\n htmx_before_on_init(elt: Element): boolean | undefined {\n const claimedNames = claimedAttrNames(elt);\n if (claimedNames.size === 0) return undefined;\n let unclaimedBindable = false;\n for (const attr of Array.from(elt.attributes)) {\n const name = attr.name;\n if (claimedNames.has(name)) continue;\n if (isHtmxBindableHxOn(name, hxOnPrefixes, mc)) {\n unclaimedBindable = true;\n break;\n }\n }\n if (!unclaimedBindable) return false;\n removeClaimedCanonicalAttrs(elt);\n return undefined;\n },\n // htmx v1/v2 fallback: single event dispatcher.\n onEvent(name: string, evt: CustomEvent & { target?: EventTarget | null }): void {\n if (name !== 'htmx:beforeProcessNode') return;\n const detail = evt?.detail as { elt?: Element } | undefined;\n const elt = detail?.elt ?? (evt?.target instanceof Element ? evt.target : null);\n if (elt) canonicalizeTree(elt);\n },\n };\n}\n\n/**\n * Register the extension with an htmx global. Returns which API accepted\n * it (`'v4'` / `'v2'`) or `null` if the object exposes neither, or if\n * v4 rejected the registration.\n *\n * Only an ACCEPTED v4 registration turns claim-time neutralization off:\n * that is the one runtime whose per-node `htmx:before:on:init` hook can\n * keep htmx off a preserved `hx-on:*` attribute. Everything else keeps\n * the hook-independent removal guard.\n */\nexport function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null {\n if (!htmx) return null;\n const ext = createExtension(htmx);\n if (typeof htmx.registerExtension === 'function') {\n // v4 REJECTS the registration (returns false) when\n // `htmx.config.extensions` is an allowlist that omits us, or on a\n // duplicate name. Nothing we return from a hook can then run, so\n // claim-time neutralization must stay on — turning it off here would\n // leave claimed hx-on:* bodies in the DOM for htmx to JS-eval.\n if (htmx.registerExtension(EXTENSION_NAME, ext) === false) {\n if (typeof console !== 'undefined') {\n console.warn(\n `[htmx-i18n] htmx.registerExtension(\"${EXTENSION_NAME}\") returned false — the ` +\n 'extension is not registered. If htmx.config.extensions (the <meta name=\"htmx-config\"> ' +\n `allowlist) is set, add \"${EXTENSION_NAME}\" to it. Swapped-in content will not be ` +\n 'canonicalized; executor-mode claims fall back to removing canonical hx-on:* attrs.'\n );\n }\n setNeutralizeOnClaim(true);\n return null;\n }\n setNeutralizeOnClaim(false);\n return 'v4';\n }\n if (typeof htmx.defineExtension === 'function') {\n htmx.defineExtension(EXTENSION_NAME, ext);\n // v2 binds hx-on before it fires beforeProcessNode — no pre-bind\n // seam, so removal at claim time stays the guard.\n setNeutralizeOnClaim(true);\n return 'v2';\n }\n return null;\n}\n\n/**\n * Sweep the whole document once the DOM is parsed, and re-sweep whenever\n * a vocab module registers or the body executor changes after that\n * (e.g. a vocab <script> below htmx, a lazily loaded _hyperscript, or\n * dynamic registration).\n *\n * \"Parsed\" means DOMContentLoaded has FIRED (or `readyState` is already\n * `'complete'`) — not merely `readyState !== 'loading'`. During\n * DOMContentLoaded dispatch `readyState` is already `'interactive'`, and\n * a `defer`/module adapter runs at `'interactive'` before htmx exists;\n * sweeping then would claim in the default remove mode before\n * `registerWith` had a chance to turn neutralization off. A `load`\n * listener covers a script injected after DOMContentLoaded but before\n * `'complete'` (DOMContentLoaded will not fire again for it).\n *\n * Returns a cleanup function (mainly for tests).\n */\nexport function installAutoSweep(doc: Document = document): () => void {\n const sweep = (): void => {\n canonicalizeTree(doc.body ?? doc.documentElement);\n };\n\n let ready = doc.readyState === 'complete';\n let removeReadyListeners: (() => void) | null = null;\n if (ready) {\n sweep();\n } else {\n const onReady = (): void => {\n if (ready) return;\n ready = true;\n removeReadyListeners?.();\n sweep();\n };\n const win = doc.defaultView;\n doc.addEventListener('DOMContentLoaded', onReady);\n win?.addEventListener('load', onReady);\n removeReadyListeners = () => {\n doc.removeEventListener('DOMContentLoaded', onReady);\n win?.removeEventListener('load', onReady);\n };\n }\n\n const unsubscribeVocab = onVocabUpdate(() => {\n if (ready) sweep();\n });\n\n // A body executor configured after the initial sweep flips the hx-on\n // family into executor mode — re-sweep so already-canonicalized\n // hx-on:* attrs get claimed (listener installed; an adapter-created\n // canonical sibling is removed, an authored one is left for the v4\n // hook or removed by the claim, per the neutralization setting).\n const unsubscribeBodyHooks = onBodyHooksChanged(() => {\n if (ready) sweep();\n });\n\n return () => {\n removeReadyListeners?.();\n unsubscribeVocab();\n unsubscribeBodyHooks();\n };\n}\n"],"mappings":"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","names":["hasOwn"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -86,11 +86,13 @@ declare function resetRegistry(): void;
|
|
|
86
86
|
* allows):
|
|
87
87
|
*
|
|
88
88
|
* - **The authored attribute is never removed or rewritten** — devtools
|
|
89
|
-
* keeps showing what the author wrote.
|
|
90
|
-
* author-written canonical `hx-trigger` whose *value* uses localized
|
|
91
|
-
* event names (`hx-trigger="clic"`)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
89
|
+
* keeps showing what the author wrote. Two documented exceptions:
|
|
90
|
+
* an author-written canonical `hx-trigger` whose *value* uses localized
|
|
91
|
+
* event names (`hx-trigger="clic"`) has no separate canonical target,
|
|
92
|
+
* so the value is translated in place (idempotent — the maps are
|
|
93
|
+
* localized → canonical, so a second pass is a no-op); and in executor
|
|
94
|
+
* mode a canonical-named `hx-on:*` attribute may be removed by the
|
|
95
|
+
* claim when no htmx hook exists to cancel htmx's binding (hx-on.ts).
|
|
94
96
|
* - **An existing canonical attribute always wins.** If the element
|
|
95
97
|
* already has `hx-get`, a localized `hx-obtener` never overwrites it.
|
|
96
98
|
* - **No vocab, no work.** With no languages registered every function
|
|
@@ -101,17 +103,40 @@ declare function resetRegistry(): void;
|
|
|
101
103
|
* convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the
|
|
102
104
|
* prefix doubles as the discovery anchor.
|
|
103
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Byte-mirror of htmx v4's `HCON.split` top-level-comma regex (vendored
|
|
108
|
+
* 4.0.0, `HCON.split`; pinned against the vendored file by
|
|
109
|
+
* test/vendor-mirror.test.ts). Spec boundaries must match core's grammar:
|
|
110
|
+
* commas inside `[filters]`, `(calls)`, and quoted strings — e.g.
|
|
111
|
+
* `from:".a, .b"` — are NOT separators.
|
|
112
|
+
*
|
|
113
|
+
* On htmx 4.0.0 the extension's `init(internalAPI)` swaps in htmx's own
|
|
114
|
+
* `HCON.split` (see setTriggerSpecSplitter), so the mirror is the
|
|
115
|
+
* fallback for v2 and for pages where the extension is not registered.
|
|
116
|
+
*/
|
|
117
|
+
declare const TOP_LEVEL_COMMA_RE: RegExp;
|
|
118
|
+
type TriggerSpecSplitter = (value: string) => string[];
|
|
119
|
+
/**
|
|
120
|
+
* Use htmx's own top-level splitter (`internalAPI.HCON.split` on 4.0.0)
|
|
121
|
+
* instead of the mirror. Pass `null` to restore the mirror.
|
|
122
|
+
*/
|
|
123
|
+
declare function setTriggerSpecSplitter(fn: TriggerSpecSplitter | null): void;
|
|
104
124
|
/**
|
|
105
125
|
* Translate localized event names inside an `hx-trigger` value.
|
|
106
126
|
*
|
|
107
127
|
* hx-trigger grammar: comma-separated specs, each `eventName[filter]
|
|
108
128
|
* modifier…`. Only the leading event token of each spec is translated
|
|
109
|
-
* (
|
|
110
|
-
* `from:body
|
|
111
|
-
*
|
|
112
|
-
*
|
|
129
|
+
* (an attached `[...]` filter, all modifiers like `delay:500ms` /
|
|
130
|
+
* `from:body` / `once`, and the authored spacing stay byte-identical —
|
|
131
|
+
* the split removes exactly the commas the join restores, so an
|
|
132
|
+
* all-canonical value comes back verbatim). Unknown tokens pass
|
|
133
|
+
* through untouched, which also makes translation idempotent (the maps
|
|
134
|
+
* are localized → canonical only). Only the map's OWN keys translate:
|
|
135
|
+
* `constructor` is not an event.
|
|
113
136
|
*/
|
|
114
137
|
declare function translateTriggerValue(value: string, events: Record<string, string>): string;
|
|
138
|
+
/** True when `name` on `elt` was written by canonicalization, not authored. */
|
|
139
|
+
declare function wasCreatedByAdapter(elt: Element, name: string): boolean;
|
|
115
140
|
/**
|
|
116
141
|
* Canonicalize one element's localized htmx attributes in place.
|
|
117
142
|
* Returns true if any attribute was added or updated.
|
|
@@ -131,18 +156,25 @@ declare function canonicalizeTree(root: Element | Document | DocumentFragment |
|
|
|
131
156
|
* Primary target is **htmx v4**, whose extensions register via
|
|
132
157
|
* `htmx.registerExtension(name, ext)` and hook lifecycle events through
|
|
133
158
|
* underscore-named methods (event name with `:` → `_`), each receiving
|
|
134
|
-
* `(elt, detail)`. Verified against htmx 4.0.0
|
|
159
|
+
* `(elt, detail)`. Verified against htmx 4.0.0 (see
|
|
135
160
|
* test/browser/vendor/README.md): `process(root)` fires
|
|
136
161
|
* `htmx:before:process` on the processed root — `document.body`
|
|
137
162
|
* initially, each swapped-in subtree afterwards — BEFORE any element
|
|
138
163
|
* init or hx-on binding, so canonicalizing in `htmx_before_process`
|
|
139
164
|
* covers everything htmx will read. `htmx_before_process_node` is kept
|
|
140
165
|
* as a defensive alias for other v4 prereleases that used per-node
|
|
141
|
-
* naming; an unmatched key is inert.
|
|
166
|
+
* naming; an unmatched key is inert. The cancelable
|
|
167
|
+
* `htmx_before_on_init` hook (fires per hx-on-carrying node, after
|
|
168
|
+
* before:process; documented as cancelable in htmx 4.0.0's `htmx.d.ts`)
|
|
169
|
+
* lets executor mode keep claimed `hx-on:*` attributes in the DOM
|
|
170
|
+
* instead of removing them — see createExtension.
|
|
142
171
|
*
|
|
143
172
|
* A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)
|
|
144
173
|
* is included because the localized attribute names are version-agnostic
|
|
145
|
-
* data — but v2 support is best-effort, not a tested target.
|
|
174
|
+
* data — but v2 support is best-effort, not a tested target. Measured on
|
|
175
|
+
* 2.0.10: `processNode` binds `hx-on` (`processHxOnWildcard`) BEFORE it
|
|
176
|
+
* fires `htmx:beforeProcessNode` per node, so v2 has no pre-bind seam and
|
|
177
|
+
* executor-mode claims keep removing canonical `hx-on:*` attrs there.
|
|
146
178
|
*
|
|
147
179
|
* The extension hook alone is not enough for the *initial* page: script
|
|
148
180
|
* order decides whether our sweep beats htmx's own DOMContentLoaded scan.
|
|
@@ -154,25 +186,69 @@ declare function canonicalizeTree(root: Element | Document | DocumentFragment |
|
|
|
154
186
|
declare const EXTENSION_NAME = "lokascript-i18n";
|
|
155
187
|
/** Minimal shape of the htmx global we interact with. */
|
|
156
188
|
interface HtmxLike {
|
|
157
|
-
/**
|
|
158
|
-
|
|
189
|
+
/**
|
|
190
|
+
* htmx v4 registration entry point. Returns `false` when the name is
|
|
191
|
+
* rejected — `htmx.config.extensions` is a non-empty allowlist that
|
|
192
|
+
* omits it, or it was already registered. (`htmx.d.ts` types the
|
|
193
|
+
* return as `void`; the runtime, verified on 4.0.0, returns `false`.)
|
|
194
|
+
*/
|
|
195
|
+
registerExtension?(name: string, extension: object): boolean | void;
|
|
159
196
|
/** htmx v1/v2 registration entry point. */
|
|
160
197
|
defineExtension?(name: string, extension: object): void;
|
|
198
|
+
/** htmx v4 config — the parts that decide which hx-on spellings it binds. */
|
|
199
|
+
config?: {
|
|
200
|
+
/** Alternate attribute prefix (`data-hx-` by default; `''` disables it). */
|
|
201
|
+
prefix?: string;
|
|
202
|
+
/** Separator between `hx-on` and the event name (`:` by default). */
|
|
203
|
+
metaCharacter?: string;
|
|
204
|
+
};
|
|
161
205
|
}
|
|
162
206
|
/**
|
|
163
207
|
* Build the extension object. v4 hooks and the v2 `onEvent` callback are
|
|
164
|
-
* both present — each API only reads the members it knows about.
|
|
208
|
+
* both present — each API only reads the members it knows about. Pass
|
|
209
|
+
* the htmx instance so the v4 hook can read `config.prefix` /
|
|
210
|
+
* `config.metaCharacter` (defaults are assumed otherwise).
|
|
211
|
+
*
|
|
212
|
+
* `init(internalAPI)` (v4 only) takes exactly one thing from the API:
|
|
213
|
+
* `HCON.split`, htmx's own top-level trigger-spec splitter, which
|
|
214
|
+
* replaces the regex mirror in canonicalize.ts for the lifetime of the
|
|
215
|
+
* page. The rest of the 4.0.0 surface — `attributeValue`,
|
|
216
|
+
* `parseTriggerSpecs`, `getAttributeObject`, `insertContent`, `morph`,
|
|
217
|
+
* `initSecurity`, `onTrigger`, `htmxProp`, `triggerHtmxEvent`,
|
|
218
|
+
* `executeJavaScript` — was evaluated and not used: `parseTriggerSpecs`
|
|
219
|
+
* is parse-only (no serializer, so translating through it would mean a
|
|
220
|
+
* hand-written spec serializer); `attributeValue`'s inheritance-aware
|
|
221
|
+
* reads buy nothing for a sweep that visits every element anyway; and
|
|
222
|
+
* `htmxProp(node).onInitialized = true` WOULD keep htmx's binder off a
|
|
223
|
+
* node, but couples to a private flag where `htmx:before:on:init` is a
|
|
224
|
+
* typed, documented, cancelable event — the hook below uses that.
|
|
165
225
|
*/
|
|
166
|
-
declare function createExtension(): object;
|
|
226
|
+
declare function createExtension(htmx?: HtmxLike | null): object;
|
|
167
227
|
/**
|
|
168
228
|
* Register the extension with an htmx global. Returns which API accepted
|
|
169
|
-
* it (`'v4'` / `'v2'`) or `null` if the object exposes neither
|
|
229
|
+
* it (`'v4'` / `'v2'`) or `null` if the object exposes neither, or if
|
|
230
|
+
* v4 rejected the registration.
|
|
231
|
+
*
|
|
232
|
+
* Only an ACCEPTED v4 registration turns claim-time neutralization off:
|
|
233
|
+
* that is the one runtime whose per-node `htmx:before:on:init` hook can
|
|
234
|
+
* keep htmx off a preserved `hx-on:*` attribute. Everything else keeps
|
|
235
|
+
* the hook-independent removal guard.
|
|
170
236
|
*/
|
|
171
237
|
declare function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null;
|
|
172
238
|
/**
|
|
173
|
-
* Sweep the whole document
|
|
174
|
-
*
|
|
175
|
-
* (e.g. a vocab <script> below htmx,
|
|
239
|
+
* Sweep the whole document once the DOM is parsed, and re-sweep whenever
|
|
240
|
+
* a vocab module registers or the body executor changes after that
|
|
241
|
+
* (e.g. a vocab <script> below htmx, a lazily loaded _hyperscript, or
|
|
242
|
+
* dynamic registration).
|
|
243
|
+
*
|
|
244
|
+
* "Parsed" means DOMContentLoaded has FIRED (or `readyState` is already
|
|
245
|
+
* `'complete'`) — not merely `readyState !== 'loading'`. During
|
|
246
|
+
* DOMContentLoaded dispatch `readyState` is already `'interactive'`, and
|
|
247
|
+
* a `defer`/module adapter runs at `'interactive'` before htmx exists;
|
|
248
|
+
* sweeping then would claim in the default remove mode before
|
|
249
|
+
* `registerWith` had a chance to turn neutralization off. A `load`
|
|
250
|
+
* listener covers a script injected after DOMContentLoaded but before
|
|
251
|
+
* `'complete'` (DOMContentLoaded will not fire again for it).
|
|
176
252
|
*
|
|
177
253
|
* Returns a cleanup function (mainly for tests).
|
|
178
254
|
*/
|
|
@@ -253,11 +329,21 @@ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void
|
|
|
253
329
|
* - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx
|
|
254
330
|
* never recognized them anyway — and NO canonical `hx-on:*` sibling is
|
|
255
331
|
* created.
|
|
256
|
-
* - Canonical-named attrs (`hx-on:click`)
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
332
|
+
* - Canonical-named attrs (`hx-on:click`) must be kept away from htmx's
|
|
333
|
+
* own binder: if htmx bound them, it would eval the hyperscript body
|
|
334
|
+
* as JS — a console error plus a double-execution attempt on every
|
|
335
|
+
* fire. A claim RECORDS the attribute (name + body) on the element;
|
|
336
|
+
* what keeps htmx off it is decided by whoever owns the runtime:
|
|
337
|
+
* - htmx v4, extension accepted: the extension's cancelable
|
|
338
|
+
* `htmx:before:on:init` hook consults the record per node and
|
|
339
|
+
* cancels htmx's binding — the authored attribute stays in the DOM
|
|
340
|
+
* (see extension.ts). `registerWith` turns claim-time removal OFF
|
|
341
|
+
* only after the registration actually succeeded.
|
|
342
|
+
* - everywhere else (htmx v2, a rejected v4 registration, no htmx):
|
|
343
|
+
* `neutralizeOnClaim` is ON (the default) and the canonical-named
|
|
344
|
+
* attribute is removed at claim time — the only guard that does not
|
|
345
|
+
* depend on a hook being installed, documented as the executor-mode
|
|
346
|
+
* exception in the README.
|
|
261
347
|
*
|
|
262
348
|
* With no executor set (the default), none of this runs and bodies keep
|
|
263
349
|
* upstream JS semantics — the behavior-preservation invariant.
|
|
@@ -266,10 +352,13 @@ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void
|
|
|
266
352
|
* loads after the initial sweep still applies. Both auto-detection
|
|
267
353
|
* (`autoDetectBodyHooks`) and manual configuration are supported; the
|
|
268
354
|
* executor is also re-read at fire time so replacing it takes effect on
|
|
269
|
-
* live listeners.
|
|
355
|
+
* live listeners. The body is re-read on every re-claim, so
|
|
356
|
+
* `htmx.process(elt, true)` after editing the attribute runs the new body.
|
|
270
357
|
*/
|
|
271
358
|
type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;
|
|
272
359
|
type BodyTranslator = (body: string, lang: string) => string;
|
|
360
|
+
declare function setNeutralizeOnClaim(enabled: boolean): void;
|
|
361
|
+
declare function neutralizesOnClaim(): boolean;
|
|
273
362
|
/** Configure the body executor. Setting/clearing it notifies subscribers. */
|
|
274
363
|
declare function setBodyExecutor(fn: BodyExecutor | null): void;
|
|
275
364
|
/** Configure the body translator (localized body → English hyperscript). */
|
|
@@ -278,16 +367,37 @@ declare function hasBodyExecutor(): boolean;
|
|
|
278
367
|
declare function hasBodyTranslator(): boolean;
|
|
279
368
|
/** Subscribe to executor changes. Returns an unsubscribe fn. */
|
|
280
369
|
declare function onBodyHooksChanged(listener: () => void): () => void;
|
|
370
|
+
/** True when the adapter has claimed at least one hx-on attribute on `elt`. */
|
|
371
|
+
declare function isClaimed(elt: Element): boolean;
|
|
372
|
+
/** Names of the attributes claimed on `elt` (canonical and localized). */
|
|
373
|
+
declare function claimedAttrNames(elt: Element): Set<string>;
|
|
374
|
+
/**
|
|
375
|
+
* Remove the claimed CANONICAL-named (`hx-on:*`) attributes from `elt`.
|
|
376
|
+
* The per-node fallback for runtimes that will bind the node anyway
|
|
377
|
+
* (v4 mixed nodes). Localized names are invisible to htmx and stay.
|
|
378
|
+
* Returns how many were removed.
|
|
379
|
+
*/
|
|
380
|
+
declare function removeClaimedCanonicalAttrs(elt: Element): number;
|
|
381
|
+
interface ClaimOptions {
|
|
382
|
+
/**
|
|
383
|
+
* The attribute was written by the adapter's own canonicalization (a
|
|
384
|
+
* no-executor sweep's `hx-on:*` copy of a localized attr), not by the
|
|
385
|
+
* author. Such a duplicate is always removed on claim.
|
|
386
|
+
*/
|
|
387
|
+
adapterCreated?: boolean;
|
|
388
|
+
}
|
|
281
389
|
/**
|
|
282
|
-
* Claim one hx-on-family attribute on an element: install the
|
|
283
|
-
* listener and
|
|
284
|
-
*
|
|
390
|
+
* Claim one hx-on-family attribute on an element: record it, install the
|
|
391
|
+
* executor listener, and — when claim-time neutralization is on — remove
|
|
392
|
+
* a canonical-named attribute so htmx never JS-evals it. Returns true if
|
|
393
|
+
* the element changed or a listener was installed now (false when no
|
|
394
|
+
* executor, already claimed for that event, or malformed).
|
|
285
395
|
*
|
|
286
396
|
* `lang` is the element's language at claim time — used for lazy body
|
|
287
397
|
* translation. `events` is the language's event-name map (for the
|
|
288
398
|
* listener's event name; the shorthand/unknown cases pass through).
|
|
289
399
|
*/
|
|
290
|
-
declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string
|
|
400
|
+
declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string>, options?: ClaimOptions): boolean;
|
|
291
401
|
/**
|
|
292
402
|
* Auto-detect body hooks from page globals — called by the browser entry
|
|
293
403
|
* at load and again at DOMContentLoaded. Never overwrites hooks that
|
|
@@ -302,4 +412,4 @@ declare function autoDetectBodyHooks(win: object): void;
|
|
|
302
412
|
/** Reset all body-hook state. Mainly for tests. */
|
|
303
413
|
declare function resetBodyHooks(): void;
|
|
304
414
|
|
|
305
|
-
export { type BodyExecutor, type BodyTranslator, EXTENSION_NAME, type HtmxLike, type HtmxVocab, type VocabPayload, additionalAttributeSelectors, attributeResolver, autoDetectBodyHooks, canonicalizeElement, canonicalizeTree, claimHxOnAttribute, createExtension, hasAnyVocab, hasBodyExecutor, hasBodyTranslator, installAutoSweep, installResolverMode, isLangRegistered, isResolverMode, langOf, normLang, onBodyHooksChanged, onVocabUpdate, register, registerWith, resetBodyHooks, resetRegistry, setBodyExecutor, setBodyTranslator, setResolverMode, translateTriggerValue, vocabFor };
|
|
415
|
+
export { type BodyExecutor, type BodyTranslator, type ClaimOptions, EXTENSION_NAME, type HtmxLike, type HtmxVocab, TOP_LEVEL_COMMA_RE, type TriggerSpecSplitter, type VocabPayload, additionalAttributeSelectors, attributeResolver, autoDetectBodyHooks, canonicalizeElement, canonicalizeTree, claimHxOnAttribute, claimedAttrNames, createExtension, hasAnyVocab, hasBodyExecutor, hasBodyTranslator, installAutoSweep, installResolverMode, isClaimed, isLangRegistered, isResolverMode, langOf, neutralizesOnClaim, normLang, onBodyHooksChanged, onVocabUpdate, register, registerWith, removeClaimedCanonicalAttrs, resetBodyHooks, resetRegistry, setBodyExecutor, setBodyTranslator, setNeutralizeOnClaim, setResolverMode, setTriggerSpecSplitter, translateTriggerValue, vocabFor, wasCreatedByAdapter };
|