@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,112 @@
1
+ /**
2
+ * Vocab registry for the upstream-htmx adapter.
3
+ *
4
+ * Accepts the SAME payload shape as hyperfixi core's htmx-compat
5
+ * orchestrator (`packages/core/src/htmx/i18n-orchestrator.ts`), so the
6
+ * generated vocab modules under `packages/core/vocab/htmx/{lang}.js`
7
+ * (which call `window.__hyperfixi_i18n.register(lang, payload)`) work
8
+ * verbatim against this adapter — one generated artifact, two consumers.
9
+ *
10
+ * register('es', {
11
+ * hyperfixi: {
12
+ * attrs: { 'hx-obtener': 'hx-get', 'sse-conectar': 'sse-connect' },
13
+ * events: { clic: 'click', cambiar: 'change' },
14
+ * },
15
+ * });
16
+ *
17
+ * Unlike core's orchestrator this registry needs no inverted index: the
18
+ * adapter canonicalizes (localized → canonical), which is exactly the
19
+ * direction the parse maps are published in. There is deliberately no
20
+ * KEYS copy here either — the vocab data is self-describing (full
21
+ * attribute names on both sides), so the canonical key set lives only in
22
+ * core's generator (`packages/core/scripts/gen-htmx-vocab.mjs`).
23
+ */
24
+
25
+ import { normLang } from './lang-resolver.js';
26
+
27
+ export interface HtmxVocab {
28
+ /** Map of localized attribute name → canonical English form (fully qualified). */
29
+ attrs?: Record<string, string>;
30
+ /** Map of localized event name → canonical English form (`'clic'` → `'click'`). */
31
+ events?: Record<string, string>;
32
+ }
33
+
34
+ export interface VocabPayload {
35
+ hyperfixi?: HtmxVocab;
36
+ }
37
+
38
+ /** Per-language vocab, keyed by normalized language code. */
39
+ const REG = new Map<string, HtmxVocab>();
40
+
41
+ /** Languages we've already warned about being missing. */
42
+ const warnedMissingLang = new Set<string>();
43
+
44
+ /** Listeners notified after a vocab registration completes. */
45
+ const vocabUpdateListeners = new Set<() => void>();
46
+
47
+ /**
48
+ * Register a vocab module for a language. Idempotent — re-registering a
49
+ * language replaces its vocab entirely.
50
+ */
51
+ export function register(code: string, data: VocabPayload): void {
52
+ const lang = normLang(code);
53
+ REG.set(lang, data?.hyperfixi ?? {});
54
+ warnedMissingLang.delete(lang); // we know about it now
55
+ for (const listener of vocabUpdateListeners) listener();
56
+ }
57
+
58
+ /** Look up the vocab registered for a (normalized) language code. */
59
+ export function vocabFor(lang: string): HtmxVocab | undefined {
60
+ return REG.get(lang);
61
+ }
62
+
63
+ /** Inspect whether any vocab is registered for a language. Mainly for tests. */
64
+ export function isLangRegistered(code: string): boolean {
65
+ return REG.has(normLang(code));
66
+ }
67
+
68
+ /** True if at least one language has registered vocab. */
69
+ export function hasAnyVocab(): boolean {
70
+ return REG.size > 0;
71
+ }
72
+
73
+ /** All language codes with registered vocab. */
74
+ export function vocabLangs(): string[] {
75
+ return [...REG.keys()];
76
+ }
77
+
78
+ /** Subscribe to vocab-registration notifications. Returns an unsubscribe fn. */
79
+ export function onVocabUpdate(listener: () => void): () => void {
80
+ vocabUpdateListeners.add(listener);
81
+ return () => {
82
+ vocabUpdateListeners.delete(listener);
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Warn once per language that an element sits in a lang scope with no
88
+ * registered vocab, so authors notice unloaded vocab modules instead of
89
+ * silently getting English-only behavior.
90
+ */
91
+ export function warnMissingLangOnce(lang: string): void {
92
+ if (warnedMissingLang.has(lang)) return;
93
+ warnedMissingLang.add(lang);
94
+ if (typeof console !== 'undefined') {
95
+ console.warn(
96
+ `[htmx-i18n] No vocab registered for lang="${lang}". ` +
97
+ `Elements in this language scope keep their localized attribute names ` +
98
+ `and htmx will not see them. Load the ${lang} vocab module ` +
99
+ `(packages/core/vocab/htmx/${lang}.js) before htmx processes the page.`
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Reset registry state — drop all registrations and listeners. Mainly for
106
+ * tests; production code should leave registrations in place.
107
+ */
108
+ export function resetRegistry(): void {
109
+ REG.clear();
110
+ warnedMissingLang.clear();
111
+ vocabUpdateListeners.clear();
112
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Resolver mode — the adapter's mechanism-(c) form, usable today against
3
+ * the reference-patched htmx build (docs/reference-patches/) and ready
4
+ * for upstream if the attribute-resolver seam lands.
5
+ *
6
+ * Where the default shim CANONICALIZES (copies localized attrs onto the
7
+ * element), resolver mode answers htmx's attribute lookups directly:
8
+ * core asks "what should I read for `hx-get` on this element?" and the
9
+ * resolver returns the localized name actually present (`hx-obtener`)
10
+ * for the element's language scope. Zero DOM mutation, fully
11
+ * devtools-faithful — the loka-js ideal.
12
+ *
13
+ * Two seams must both be fed (the loka-js lesson): the per-element
14
+ * resolver answers reads, and `additionalAttributeSelectors` feeds the
15
+ * document-level discovery scan, which a per-element function cannot
16
+ * drive. `installResolverMode()` wires both and keeps the selector list
17
+ * fresh as vocab modules register.
18
+ *
19
+ * Scope: value-bearing attributes only. The hx-on colon family stays
20
+ * with the shim/executor mode — its event name is part of the attribute
21
+ * NAME, which is a different question than "what name holds this
22
+ * value". Trigger VALUES (`hx-trigger="clic"`) are also name-level
23
+ * out-of-scope; keep authoring canonical event values, or pair with
24
+ * executor mode.
25
+ */
26
+
27
+ import { langOf } from './lang-resolver.js';
28
+ import { onVocabUpdate, vocabFor, vocabLangs } from './registry.js';
29
+
30
+ let resolverModeActive = false;
31
+
32
+ /** True while resolver mode owns localization (the shim stands down). */
33
+ export function isResolverMode(): boolean {
34
+ return resolverModeActive;
35
+ }
36
+
37
+ /** Mainly for tests; installResolverMode() manages this in production. */
38
+ export function setResolverMode(active: boolean): void {
39
+ resolverModeActive = active;
40
+ }
41
+
42
+ /**
43
+ * The per-element resolver htmx consults on attribute reads:
44
+ * `(elt, canonicalName) → localized name present on elt, or null`.
45
+ */
46
+ export function attributeResolver(elt: Element, name: string): string | null {
47
+ const lang = langOf(elt);
48
+ if (lang === 'en') return null;
49
+ const attrs = vocabFor(lang)?.attrs;
50
+ if (!attrs) return null;
51
+ for (const [localized, canonical] of Object.entries(attrs)) {
52
+ if (canonical === name && elt.hasAttribute?.(localized)) return localized;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * CSS attribute selectors for every registered localized name, for
59
+ * htmx's discovery scan (`config.additionalAttributeSelectors`). The
60
+ * hx-on family is excluded — see the module header.
61
+ */
62
+ export function additionalAttributeSelectors(): string[] {
63
+ const names = new Set<string>();
64
+ for (const lang of vocabLangs()) {
65
+ const attrs = vocabFor(lang)?.attrs ?? {};
66
+ for (const [localized, canonical] of Object.entries(attrs)) {
67
+ if (canonical === 'hx-on') continue;
68
+ names.add(`[${localized}]`);
69
+ }
70
+ }
71
+ return [...names];
72
+ }
73
+
74
+ interface ResolverConfigurableHtmx {
75
+ config?: {
76
+ attributeResolver?: ((elt: Element, name: string) => string | null) | null;
77
+ additionalAttributeSelectors?: string[];
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Wire resolver mode into a (reference-patched) htmx instance: install
83
+ * the resolver, feed the discovery selectors, keep them fresh on vocab
84
+ * registration, and stand the canonicalization shim down. Returns an
85
+ * uninstall function that restores shim behavior.
86
+ */
87
+ export function installResolverMode(htmx: ResolverConfigurableHtmx): () => void {
88
+ if (!htmx?.config) {
89
+ throw new Error('[htmx-i18n] installResolverMode: htmx.config not found');
90
+ }
91
+ const cfg = htmx.config;
92
+ cfg.attributeResolver = attributeResolver;
93
+ cfg.additionalAttributeSelectors = additionalAttributeSelectors();
94
+ const unsubscribe = onVocabUpdate(() => {
95
+ cfg.additionalAttributeSelectors = additionalAttributeSelectors();
96
+ });
97
+ resolverModeActive = true;
98
+ return () => {
99
+ unsubscribe();
100
+ cfg.attributeResolver = null;
101
+ cfg.additionalAttributeSelectors = [];
102
+ resolverModeActive = false;
103
+ };
104
+ }