@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.
- package/README.md +187 -0
- package/dist/htmx-i18n.global.js +2 -0
- package/dist/htmx-i18n.global.js.map +1 -0
- package/dist/index.cjs +424 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +305 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/browser.ts +108 -0
- package/src/canonicalize.ts +186 -0
- package/src/extension.ts +122 -0
- package/src/hx-on.ts +197 -0
- package/src/index.ts +57 -0
- package/src/lang-resolver.ts +34 -0
- package/src/registry.ts +112 -0
- package/src/resolver.ts +104 -0
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|