@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/src/registry.ts
ADDED
|
@@ -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
|
+
}
|
package/src/resolver.ts
ADDED
|
@@ -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
|
+
}
|