@lokascript/htmx-adapter 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Per-element language resolution for localized htmx attribute names.
3
+ *
4
+ * Mirrors `packages/core/src/htmx/lang-resolver.ts` (which in turn mirrors
5
+ * loka-js's `lang-resolver.js`) so a page migrating between hyperfixi's
6
+ * embedded htmx-compat layer and this upstream-htmx adapter resolves
7
+ * languages identically. Resolution order:
8
+ *
9
+ * 1. `data-hyperfixi-lang` attribute on the element itself
10
+ * 2. `data-hyperfixi-lang` on any ancestor
11
+ * 3. `lang` attribute on any ancestor (HTML standard)
12
+ * 4. `'en'` fallback
13
+ *
14
+ * Results are normalized to the part before the first `-`/`_` (`es-MX` →
15
+ * `es`) and lowercased so callers can index vocab maps by 2-letter code
16
+ * without caring about regional variants.
17
+ */
18
+ /** Normalize a language tag — `es-MX` / `ES_mx` → `es`. */
19
+ declare function normLang(s: string | null | undefined): string;
20
+ /** Resolve the language code for an element via ancestor walk. */
21
+ declare function langOf(elt: Element): string;
22
+
23
+ /**
24
+ * Vocab registry for the upstream-htmx adapter.
25
+ *
26
+ * Accepts the SAME payload shape as hyperfixi core's htmx-compat
27
+ * orchestrator (`packages/core/src/htmx/i18n-orchestrator.ts`), so the
28
+ * generated vocab modules under `packages/core/vocab/htmx/{lang}.js`
29
+ * (which call `window.__hyperfixi_i18n.register(lang, payload)`) work
30
+ * verbatim against this adapter — one generated artifact, two consumers.
31
+ *
32
+ * register('es', {
33
+ * hyperfixi: {
34
+ * attrs: { 'hx-obtener': 'hx-get', 'sse-conectar': 'sse-connect' },
35
+ * events: { clic: 'click', cambiar: 'change' },
36
+ * },
37
+ * });
38
+ *
39
+ * Unlike core's orchestrator this registry needs no inverted index: the
40
+ * adapter canonicalizes (localized → canonical), which is exactly the
41
+ * direction the parse maps are published in. There is deliberately no
42
+ * KEYS copy here either — the vocab data is self-describing (full
43
+ * attribute names on both sides), so the canonical key set lives only in
44
+ * core's generator (`packages/core/scripts/gen-htmx-vocab.mjs`).
45
+ */
46
+ interface HtmxVocab {
47
+ /** Map of localized attribute name → canonical English form (fully qualified). */
48
+ attrs?: Record<string, string>;
49
+ /** Map of localized event name → canonical English form (`'clic'` → `'click'`). */
50
+ events?: Record<string, string>;
51
+ }
52
+ interface VocabPayload {
53
+ hyperfixi?: HtmxVocab;
54
+ }
55
+ /**
56
+ * Register a vocab module for a language. Idempotent — re-registering a
57
+ * language replaces its vocab entirely.
58
+ */
59
+ declare function register(code: string, data: VocabPayload): void;
60
+ /** Look up the vocab registered for a (normalized) language code. */
61
+ declare function vocabFor(lang: string): HtmxVocab | undefined;
62
+ /** Inspect whether any vocab is registered for a language. Mainly for tests. */
63
+ declare function isLangRegistered(code: string): boolean;
64
+ /** True if at least one language has registered vocab. */
65
+ declare function hasAnyVocab(): boolean;
66
+ /** Subscribe to vocab-registration notifications. Returns an unsubscribe fn. */
67
+ declare function onVocabUpdate(listener: () => void): () => void;
68
+ /**
69
+ * Reset registry state — drop all registrations and listeners. Mainly for
70
+ * tests; production code should leave registrations in place.
71
+ */
72
+ declare function resetRegistry(): void;
73
+
74
+ /**
75
+ * Localized → canonical attribute canonicalization for upstream htmx.
76
+ *
77
+ * htmx v4 has no hook to override how core resolves an attribute name —
78
+ * it reads `hx-get` literally. Until an upstream resolver seam exists
79
+ * (see docs/UPSTREAM_HOOK_PROPOSAL.md), this module makes localized
80
+ * authoring work by copying each localized attribute to its canonical
81
+ * name on the same element *before* htmx processes the node:
82
+ *
83
+ * <button lang="es" hx-obtener="/api"> → + hx-get="/api"
84
+ *
85
+ * Design rules (mirroring the loka-js invariants where the mechanism
86
+ * allows):
87
+ *
88
+ * - **The authored attribute is never removed or rewritten** — devtools
89
+ * keeps showing what the author wrote. The one exception is an
90
+ * author-written canonical `hx-trigger` whose *value* uses localized
91
+ * event names (`hx-trigger="clic"`): there is no separate canonical
92
+ * target to write to, so the value is translated in place (idempotent —
93
+ * the maps are localized → canonical, so a second pass is a no-op).
94
+ * - **An existing canonical attribute always wins.** If the element
95
+ * already has `hx-get`, a localized `hx-obtener` never overwrites it.
96
+ * - **No vocab, no work.** With no languages registered every function
97
+ * here is a cheap no-op, so stock htmx pages pay ~nothing.
98
+ *
99
+ * Only attributes in the `hx-` / `sse-` / `ws-` namespaces are ever
100
+ * considered — the brand prefix is preserved across languages (Phase 8
101
+ * convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the
102
+ * prefix doubles as the discovery anchor.
103
+ */
104
+ /**
105
+ * Translate localized event names inside an `hx-trigger` value.
106
+ *
107
+ * hx-trigger grammar: comma-separated specs, each `eventName[filter]
108
+ * modifier…`. Only the leading event token of each spec is translated
109
+ * (preserving an attached `[...]` filter); modifiers like `delay:500ms`,
110
+ * `from:body`, `once` are language-invariant and left alone. Unknown
111
+ * tokens pass through untouched, which also makes translation idempotent
112
+ * (the maps are localized → canonical only).
113
+ */
114
+ declare function translateTriggerValue(value: string, events: Record<string, string>): string;
115
+ /**
116
+ * Canonicalize one element's localized htmx attributes in place.
117
+ * Returns true if any attribute was added or updated.
118
+ */
119
+ declare function canonicalizeElement(elt: Element): boolean;
120
+ /**
121
+ * Canonicalize an element and all its descendants. Returns the number of
122
+ * elements that changed. This is what the htmx extension hook and the
123
+ * initial document sweep call — htmx processes subtrees, so we mirror
124
+ * that granularity.
125
+ */
126
+ declare function canonicalizeTree(root: Element | Document | DocumentFragment | null): number;
127
+
128
+ /**
129
+ * htmx extension + auto-sweep wiring.
130
+ *
131
+ * Primary target is **htmx v4**, whose extensions register via
132
+ * `htmx.registerExtension(name, ext)` and hook lifecycle events through
133
+ * underscore-named methods (event name with `:` → `_`), each receiving
134
+ * `(elt, detail)`. Verified against htmx 4.0.0-beta5 (see
135
+ * test/browser/vendor/README.md): `process(root)` fires
136
+ * `htmx:before:process` on the processed root — `document.body`
137
+ * initially, each swapped-in subtree afterwards — BEFORE any element
138
+ * init or hx-on binding, so canonicalizing in `htmx_before_process`
139
+ * covers everything htmx will read. `htmx_before_process_node` is kept
140
+ * as a defensive alias for other v4 prereleases that used per-node
141
+ * naming; an unmatched key is inert.
142
+ *
143
+ * A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)
144
+ * is included because the localized attribute names are version-agnostic
145
+ * data — but v2 support is best-effort, not a tested target.
146
+ *
147
+ * The extension hook alone is not enough for the *initial* page: script
148
+ * order decides whether our sweep beats htmx's own DOMContentLoaded scan.
149
+ * `installAutoSweep()` handles that — load this adapter (and vocab
150
+ * modules) BEFORE the htmx <script> tag, mirroring loka-js's
151
+ * "orchestrator before libraries" rule, and the sweep listener registers
152
+ * ahead of htmx's.
153
+ */
154
+ declare const EXTENSION_NAME = "lokascript-i18n";
155
+ /** Minimal shape of the htmx global we interact with. */
156
+ interface HtmxLike {
157
+ /** htmx v4 registration entry point. */
158
+ registerExtension?(name: string, extension: object): void;
159
+ /** htmx v1/v2 registration entry point. */
160
+ defineExtension?(name: string, extension: object): void;
161
+ }
162
+ /**
163
+ * Build the extension object. v4 hooks and the v2 `onEvent` callback are
164
+ * both present — each API only reads the members it knows about.
165
+ */
166
+ declare function createExtension(): object;
167
+ /**
168
+ * Register the extension with an htmx global. Returns which API accepted
169
+ * it (`'v4'` / `'v2'`) or `null` if the object exposes neither.
170
+ */
171
+ declare function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null;
172
+ /**
173
+ * Sweep the whole document now (if parsed) or on DOMContentLoaded, and
174
+ * re-sweep whenever a vocab module registers after the initial sweep
175
+ * (e.g. a vocab <script> below htmx, or dynamic registration).
176
+ *
177
+ * Returns a cleanup function (mainly for tests).
178
+ */
179
+ declare function installAutoSweep(doc?: Document): () => void;
180
+
181
+ /**
182
+ * Resolver mode — the adapter's mechanism-(c) form, usable today against
183
+ * the reference-patched htmx build (docs/reference-patches/) and ready
184
+ * for upstream if the attribute-resolver seam lands.
185
+ *
186
+ * Where the default shim CANONICALIZES (copies localized attrs onto the
187
+ * element), resolver mode answers htmx's attribute lookups directly:
188
+ * core asks "what should I read for `hx-get` on this element?" and the
189
+ * resolver returns the localized name actually present (`hx-obtener`)
190
+ * for the element's language scope. Zero DOM mutation, fully
191
+ * devtools-faithful — the loka-js ideal.
192
+ *
193
+ * Two seams must both be fed (the loka-js lesson): the per-element
194
+ * resolver answers reads, and `additionalAttributeSelectors` feeds the
195
+ * document-level discovery scan, which a per-element function cannot
196
+ * drive. `installResolverMode()` wires both and keeps the selector list
197
+ * fresh as vocab modules register.
198
+ *
199
+ * Scope: value-bearing attributes only. The hx-on colon family stays
200
+ * with the shim/executor mode — its event name is part of the attribute
201
+ * NAME, which is a different question than "what name holds this
202
+ * value". Trigger VALUES (`hx-trigger="clic"`) are also name-level
203
+ * out-of-scope; keep authoring canonical event values, or pair with
204
+ * executor mode.
205
+ */
206
+ /** True while resolver mode owns localization (the shim stands down). */
207
+ declare function isResolverMode(): boolean;
208
+ /** Mainly for tests; installResolverMode() manages this in production. */
209
+ declare function setResolverMode(active: boolean): void;
210
+ /**
211
+ * The per-element resolver htmx consults on attribute reads:
212
+ * `(elt, canonicalName) → localized name present on elt, or null`.
213
+ */
214
+ declare function attributeResolver(elt: Element, name: string): string | null;
215
+ /**
216
+ * CSS attribute selectors for every registered localized name, for
217
+ * htmx's discovery scan (`config.additionalAttributeSelectors`). The
218
+ * hx-on family is excluded — see the module header.
219
+ */
220
+ declare function additionalAttributeSelectors(): string[];
221
+ interface ResolverConfigurableHtmx {
222
+ config?: {
223
+ attributeResolver?: ((elt: Element, name: string) => string | null) | null;
224
+ additionalAttributeSelectors?: string[];
225
+ };
226
+ }
227
+ /**
228
+ * Wire resolver mode into a (reference-patched) htmx instance: install
229
+ * the resolver, feed the discovery selectors, keep them fresh on vocab
230
+ * registration, and stand the canonicalization shim down. Returns an
231
+ * uninstall function that restores shim behavior.
232
+ */
233
+ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void;
234
+
235
+ /**
236
+ * hx-on body support — opt-in "executor mode".
237
+ *
238
+ * Upstream htmx evals `hx-on:*` bodies as JavaScript. In the hyperfixi
239
+ * ecosystem the convention is hyperscript bodies (`hx-on:click="toggle
240
+ * .active on me"`), which htmx's JS eval cannot run — and localized
241
+ * bodies (`hx-en:clic="alternar .active"`) doubly so. This module lets a
242
+ * page opt in to hyperscript-body semantics by configuring an executor:
243
+ *
244
+ * setBodyExecutor((code, elt, evt) => _hyperscript.evaluate(code, { me: elt, event: evt }))
245
+ * setBodyTranslator((body, lang) => HyperscriptI18n.preprocess(body, lang))
246
+ *
247
+ * With an executor set, the adapter CLAIMS every hx-on-family attribute
248
+ * (localized-named or canonical-named — all bodies are treated as
249
+ * hyperscript; mixed JS/hyperscript pages have no reliable detection):
250
+ * it installs a real event listener that runs the (lazily translated)
251
+ * body through the executor, and keeps htmx away from it:
252
+ *
253
+ * - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx
254
+ * never recognized them anyway — and NO canonical `hx-on:*` sibling is
255
+ * created.
256
+ * - Canonical-named attrs (`hx-on:click`) are REMOVED after claiming.
257
+ * This is the one place the adapter deletes an authored attribute: if
258
+ * it stayed, htmx would eval the hyperscript body as JS — a console
259
+ * error plus a double-execution attempt on every fire. Documented as
260
+ * the executor-mode exception in the README.
261
+ *
262
+ * With no executor set (the default), none of this runs and bodies keep
263
+ * upstream JS semantics — the behavior-preservation invariant.
264
+ *
265
+ * Translation is lazy (first event fire, memoized) so a translator that
266
+ * loads after the initial sweep still applies. Both auto-detection
267
+ * (`autoDetectBodyHooks`) and manual configuration are supported; the
268
+ * executor is also re-read at fire time so replacing it takes effect on
269
+ * live listeners.
270
+ */
271
+ type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;
272
+ type BodyTranslator = (body: string, lang: string) => string;
273
+ /** Configure the body executor. Setting/clearing it notifies subscribers. */
274
+ declare function setBodyExecutor(fn: BodyExecutor | null): void;
275
+ /** Configure the body translator (localized body → English hyperscript). */
276
+ declare function setBodyTranslator(fn: BodyTranslator | null): void;
277
+ declare function hasBodyExecutor(): boolean;
278
+ declare function hasBodyTranslator(): boolean;
279
+ /** Subscribe to executor changes. Returns an unsubscribe fn. */
280
+ declare function onBodyHooksChanged(listener: () => void): () => void;
281
+ /**
282
+ * Claim one hx-on-family attribute on an element: install the executor
283
+ * listener and neutralize htmx's view of it. Returns true if the claim
284
+ * happened now (false when no executor, already claimed, or malformed).
285
+ *
286
+ * `lang` is the element's language at claim time — used for lazy body
287
+ * translation. `events` is the language's event-name map (for the
288
+ * listener's event name; the shorthand/unknown cases pass through).
289
+ */
290
+ declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string>): boolean;
291
+ /**
292
+ * Auto-detect body hooks from page globals — called by the browser entry
293
+ * at load and again at DOMContentLoaded. Never overwrites hooks that
294
+ * were set explicitly.
295
+ *
296
+ * - `window._hyperscript` → executor (original _hyperscript; pairs with
297
+ * `@lokascript/hyperscript-adapter` for multilingual `_=` too)
298
+ * - `window.HyperscriptI18n.preprocess` → translator (the
299
+ * hyperscript-adapter browser bundles expose exactly this)
300
+ */
301
+ declare function autoDetectBodyHooks(win: object): void;
302
+ /** Reset all body-hook state. Mainly for tests. */
303
+ declare function resetBodyHooks(): void;
304
+
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 };
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Per-element language resolution for localized htmx attribute names.
3
+ *
4
+ * Mirrors `packages/core/src/htmx/lang-resolver.ts` (which in turn mirrors
5
+ * loka-js's `lang-resolver.js`) so a page migrating between hyperfixi's
6
+ * embedded htmx-compat layer and this upstream-htmx adapter resolves
7
+ * languages identically. Resolution order:
8
+ *
9
+ * 1. `data-hyperfixi-lang` attribute on the element itself
10
+ * 2. `data-hyperfixi-lang` on any ancestor
11
+ * 3. `lang` attribute on any ancestor (HTML standard)
12
+ * 4. `'en'` fallback
13
+ *
14
+ * Results are normalized to the part before the first `-`/`_` (`es-MX` →
15
+ * `es`) and lowercased so callers can index vocab maps by 2-letter code
16
+ * without caring about regional variants.
17
+ */
18
+ /** Normalize a language tag — `es-MX` / `ES_mx` → `es`. */
19
+ declare function normLang(s: string | null | undefined): string;
20
+ /** Resolve the language code for an element via ancestor walk. */
21
+ declare function langOf(elt: Element): string;
22
+
23
+ /**
24
+ * Vocab registry for the upstream-htmx adapter.
25
+ *
26
+ * Accepts the SAME payload shape as hyperfixi core's htmx-compat
27
+ * orchestrator (`packages/core/src/htmx/i18n-orchestrator.ts`), so the
28
+ * generated vocab modules under `packages/core/vocab/htmx/{lang}.js`
29
+ * (which call `window.__hyperfixi_i18n.register(lang, payload)`) work
30
+ * verbatim against this adapter — one generated artifact, two consumers.
31
+ *
32
+ * register('es', {
33
+ * hyperfixi: {
34
+ * attrs: { 'hx-obtener': 'hx-get', 'sse-conectar': 'sse-connect' },
35
+ * events: { clic: 'click', cambiar: 'change' },
36
+ * },
37
+ * });
38
+ *
39
+ * Unlike core's orchestrator this registry needs no inverted index: the
40
+ * adapter canonicalizes (localized → canonical), which is exactly the
41
+ * direction the parse maps are published in. There is deliberately no
42
+ * KEYS copy here either — the vocab data is self-describing (full
43
+ * attribute names on both sides), so the canonical key set lives only in
44
+ * core's generator (`packages/core/scripts/gen-htmx-vocab.mjs`).
45
+ */
46
+ interface HtmxVocab {
47
+ /** Map of localized attribute name → canonical English form (fully qualified). */
48
+ attrs?: Record<string, string>;
49
+ /** Map of localized event name → canonical English form (`'clic'` → `'click'`). */
50
+ events?: Record<string, string>;
51
+ }
52
+ interface VocabPayload {
53
+ hyperfixi?: HtmxVocab;
54
+ }
55
+ /**
56
+ * Register a vocab module for a language. Idempotent — re-registering a
57
+ * language replaces its vocab entirely.
58
+ */
59
+ declare function register(code: string, data: VocabPayload): void;
60
+ /** Look up the vocab registered for a (normalized) language code. */
61
+ declare function vocabFor(lang: string): HtmxVocab | undefined;
62
+ /** Inspect whether any vocab is registered for a language. Mainly for tests. */
63
+ declare function isLangRegistered(code: string): boolean;
64
+ /** True if at least one language has registered vocab. */
65
+ declare function hasAnyVocab(): boolean;
66
+ /** Subscribe to vocab-registration notifications. Returns an unsubscribe fn. */
67
+ declare function onVocabUpdate(listener: () => void): () => void;
68
+ /**
69
+ * Reset registry state — drop all registrations and listeners. Mainly for
70
+ * tests; production code should leave registrations in place.
71
+ */
72
+ declare function resetRegistry(): void;
73
+
74
+ /**
75
+ * Localized → canonical attribute canonicalization for upstream htmx.
76
+ *
77
+ * htmx v4 has no hook to override how core resolves an attribute name —
78
+ * it reads `hx-get` literally. Until an upstream resolver seam exists
79
+ * (see docs/UPSTREAM_HOOK_PROPOSAL.md), this module makes localized
80
+ * authoring work by copying each localized attribute to its canonical
81
+ * name on the same element *before* htmx processes the node:
82
+ *
83
+ * <button lang="es" hx-obtener="/api"> → + hx-get="/api"
84
+ *
85
+ * Design rules (mirroring the loka-js invariants where the mechanism
86
+ * allows):
87
+ *
88
+ * - **The authored attribute is never removed or rewritten** — devtools
89
+ * keeps showing what the author wrote. The one exception is an
90
+ * author-written canonical `hx-trigger` whose *value* uses localized
91
+ * event names (`hx-trigger="clic"`): there is no separate canonical
92
+ * target to write to, so the value is translated in place (idempotent —
93
+ * the maps are localized → canonical, so a second pass is a no-op).
94
+ * - **An existing canonical attribute always wins.** If the element
95
+ * already has `hx-get`, a localized `hx-obtener` never overwrites it.
96
+ * - **No vocab, no work.** With no languages registered every function
97
+ * here is a cheap no-op, so stock htmx pages pay ~nothing.
98
+ *
99
+ * Only attributes in the `hx-` / `sse-` / `ws-` namespaces are ever
100
+ * considered — the brand prefix is preserved across languages (Phase 8
101
+ * convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the
102
+ * prefix doubles as the discovery anchor.
103
+ */
104
+ /**
105
+ * Translate localized event names inside an `hx-trigger` value.
106
+ *
107
+ * hx-trigger grammar: comma-separated specs, each `eventName[filter]
108
+ * modifier…`. Only the leading event token of each spec is translated
109
+ * (preserving an attached `[...]` filter); modifiers like `delay:500ms`,
110
+ * `from:body`, `once` are language-invariant and left alone. Unknown
111
+ * tokens pass through untouched, which also makes translation idempotent
112
+ * (the maps are localized → canonical only).
113
+ */
114
+ declare function translateTriggerValue(value: string, events: Record<string, string>): string;
115
+ /**
116
+ * Canonicalize one element's localized htmx attributes in place.
117
+ * Returns true if any attribute was added or updated.
118
+ */
119
+ declare function canonicalizeElement(elt: Element): boolean;
120
+ /**
121
+ * Canonicalize an element and all its descendants. Returns the number of
122
+ * elements that changed. This is what the htmx extension hook and the
123
+ * initial document sweep call — htmx processes subtrees, so we mirror
124
+ * that granularity.
125
+ */
126
+ declare function canonicalizeTree(root: Element | Document | DocumentFragment | null): number;
127
+
128
+ /**
129
+ * htmx extension + auto-sweep wiring.
130
+ *
131
+ * Primary target is **htmx v4**, whose extensions register via
132
+ * `htmx.registerExtension(name, ext)` and hook lifecycle events through
133
+ * underscore-named methods (event name with `:` → `_`), each receiving
134
+ * `(elt, detail)`. Verified against htmx 4.0.0-beta5 (see
135
+ * test/browser/vendor/README.md): `process(root)` fires
136
+ * `htmx:before:process` on the processed root — `document.body`
137
+ * initially, each swapped-in subtree afterwards — BEFORE any element
138
+ * init or hx-on binding, so canonicalizing in `htmx_before_process`
139
+ * covers everything htmx will read. `htmx_before_process_node` is kept
140
+ * as a defensive alias for other v4 prereleases that used per-node
141
+ * naming; an unmatched key is inert.
142
+ *
143
+ * A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)
144
+ * is included because the localized attribute names are version-agnostic
145
+ * data — but v2 support is best-effort, not a tested target.
146
+ *
147
+ * The extension hook alone is not enough for the *initial* page: script
148
+ * order decides whether our sweep beats htmx's own DOMContentLoaded scan.
149
+ * `installAutoSweep()` handles that — load this adapter (and vocab
150
+ * modules) BEFORE the htmx <script> tag, mirroring loka-js's
151
+ * "orchestrator before libraries" rule, and the sweep listener registers
152
+ * ahead of htmx's.
153
+ */
154
+ declare const EXTENSION_NAME = "lokascript-i18n";
155
+ /** Minimal shape of the htmx global we interact with. */
156
+ interface HtmxLike {
157
+ /** htmx v4 registration entry point. */
158
+ registerExtension?(name: string, extension: object): void;
159
+ /** htmx v1/v2 registration entry point. */
160
+ defineExtension?(name: string, extension: object): void;
161
+ }
162
+ /**
163
+ * Build the extension object. v4 hooks and the v2 `onEvent` callback are
164
+ * both present — each API only reads the members it knows about.
165
+ */
166
+ declare function createExtension(): object;
167
+ /**
168
+ * Register the extension with an htmx global. Returns which API accepted
169
+ * it (`'v4'` / `'v2'`) or `null` if the object exposes neither.
170
+ */
171
+ declare function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null;
172
+ /**
173
+ * Sweep the whole document now (if parsed) or on DOMContentLoaded, and
174
+ * re-sweep whenever a vocab module registers after the initial sweep
175
+ * (e.g. a vocab <script> below htmx, or dynamic registration).
176
+ *
177
+ * Returns a cleanup function (mainly for tests).
178
+ */
179
+ declare function installAutoSweep(doc?: Document): () => void;
180
+
181
+ /**
182
+ * Resolver mode — the adapter's mechanism-(c) form, usable today against
183
+ * the reference-patched htmx build (docs/reference-patches/) and ready
184
+ * for upstream if the attribute-resolver seam lands.
185
+ *
186
+ * Where the default shim CANONICALIZES (copies localized attrs onto the
187
+ * element), resolver mode answers htmx's attribute lookups directly:
188
+ * core asks "what should I read for `hx-get` on this element?" and the
189
+ * resolver returns the localized name actually present (`hx-obtener`)
190
+ * for the element's language scope. Zero DOM mutation, fully
191
+ * devtools-faithful — the loka-js ideal.
192
+ *
193
+ * Two seams must both be fed (the loka-js lesson): the per-element
194
+ * resolver answers reads, and `additionalAttributeSelectors` feeds the
195
+ * document-level discovery scan, which a per-element function cannot
196
+ * drive. `installResolverMode()` wires both and keeps the selector list
197
+ * fresh as vocab modules register.
198
+ *
199
+ * Scope: value-bearing attributes only. The hx-on colon family stays
200
+ * with the shim/executor mode — its event name is part of the attribute
201
+ * NAME, which is a different question than "what name holds this
202
+ * value". Trigger VALUES (`hx-trigger="clic"`) are also name-level
203
+ * out-of-scope; keep authoring canonical event values, or pair with
204
+ * executor mode.
205
+ */
206
+ /** True while resolver mode owns localization (the shim stands down). */
207
+ declare function isResolverMode(): boolean;
208
+ /** Mainly for tests; installResolverMode() manages this in production. */
209
+ declare function setResolverMode(active: boolean): void;
210
+ /**
211
+ * The per-element resolver htmx consults on attribute reads:
212
+ * `(elt, canonicalName) → localized name present on elt, or null`.
213
+ */
214
+ declare function attributeResolver(elt: Element, name: string): string | null;
215
+ /**
216
+ * CSS attribute selectors for every registered localized name, for
217
+ * htmx's discovery scan (`config.additionalAttributeSelectors`). The
218
+ * hx-on family is excluded — see the module header.
219
+ */
220
+ declare function additionalAttributeSelectors(): string[];
221
+ interface ResolverConfigurableHtmx {
222
+ config?: {
223
+ attributeResolver?: ((elt: Element, name: string) => string | null) | null;
224
+ additionalAttributeSelectors?: string[];
225
+ };
226
+ }
227
+ /**
228
+ * Wire resolver mode into a (reference-patched) htmx instance: install
229
+ * the resolver, feed the discovery selectors, keep them fresh on vocab
230
+ * registration, and stand the canonicalization shim down. Returns an
231
+ * uninstall function that restores shim behavior.
232
+ */
233
+ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void;
234
+
235
+ /**
236
+ * hx-on body support — opt-in "executor mode".
237
+ *
238
+ * Upstream htmx evals `hx-on:*` bodies as JavaScript. In the hyperfixi
239
+ * ecosystem the convention is hyperscript bodies (`hx-on:click="toggle
240
+ * .active on me"`), which htmx's JS eval cannot run — and localized
241
+ * bodies (`hx-en:clic="alternar .active"`) doubly so. This module lets a
242
+ * page opt in to hyperscript-body semantics by configuring an executor:
243
+ *
244
+ * setBodyExecutor((code, elt, evt) => _hyperscript.evaluate(code, { me: elt, event: evt }))
245
+ * setBodyTranslator((body, lang) => HyperscriptI18n.preprocess(body, lang))
246
+ *
247
+ * With an executor set, the adapter CLAIMS every hx-on-family attribute
248
+ * (localized-named or canonical-named — all bodies are treated as
249
+ * hyperscript; mixed JS/hyperscript pages have no reliable detection):
250
+ * it installs a real event listener that runs the (lazily translated)
251
+ * body through the executor, and keeps htmx away from it:
252
+ *
253
+ * - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx
254
+ * never recognized them anyway — and NO canonical `hx-on:*` sibling is
255
+ * created.
256
+ * - Canonical-named attrs (`hx-on:click`) are REMOVED after claiming.
257
+ * This is the one place the adapter deletes an authored attribute: if
258
+ * it stayed, htmx would eval the hyperscript body as JS — a console
259
+ * error plus a double-execution attempt on every fire. Documented as
260
+ * the executor-mode exception in the README.
261
+ *
262
+ * With no executor set (the default), none of this runs and bodies keep
263
+ * upstream JS semantics — the behavior-preservation invariant.
264
+ *
265
+ * Translation is lazy (first event fire, memoized) so a translator that
266
+ * loads after the initial sweep still applies. Both auto-detection
267
+ * (`autoDetectBodyHooks`) and manual configuration are supported; the
268
+ * executor is also re-read at fire time so replacing it takes effect on
269
+ * live listeners.
270
+ */
271
+ type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;
272
+ type BodyTranslator = (body: string, lang: string) => string;
273
+ /** Configure the body executor. Setting/clearing it notifies subscribers. */
274
+ declare function setBodyExecutor(fn: BodyExecutor | null): void;
275
+ /** Configure the body translator (localized body → English hyperscript). */
276
+ declare function setBodyTranslator(fn: BodyTranslator | null): void;
277
+ declare function hasBodyExecutor(): boolean;
278
+ declare function hasBodyTranslator(): boolean;
279
+ /** Subscribe to executor changes. Returns an unsubscribe fn. */
280
+ declare function onBodyHooksChanged(listener: () => void): () => void;
281
+ /**
282
+ * Claim one hx-on-family attribute on an element: install the executor
283
+ * listener and neutralize htmx's view of it. Returns true if the claim
284
+ * happened now (false when no executor, already claimed, or malformed).
285
+ *
286
+ * `lang` is the element's language at claim time — used for lazy body
287
+ * translation. `events` is the language's event-name map (for the
288
+ * listener's event name; the shorthand/unknown cases pass through).
289
+ */
290
+ declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string>): boolean;
291
+ /**
292
+ * Auto-detect body hooks from page globals — called by the browser entry
293
+ * at load and again at DOMContentLoaded. Never overwrites hooks that
294
+ * were set explicitly.
295
+ *
296
+ * - `window._hyperscript` → executor (original _hyperscript; pairs with
297
+ * `@lokascript/hyperscript-adapter` for multilingual `_=` too)
298
+ * - `window.HyperscriptI18n.preprocess` → translator (the
299
+ * hyperscript-adapter browser bundles expose exactly this)
300
+ */
301
+ declare function autoDetectBodyHooks(win: object): void;
302
+ /** Reset all body-hook state. Mainly for tests. */
303
+ declare function resetBodyHooks(): void;
304
+
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 };