@lokascript/hyperscript-adapter 2.9.4 → 2.11.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +16 -11
  3. package/dist/hyperscript-i18n-ar.global.js +5 -6
  4. package/dist/hyperscript-i18n-bn.global.js +5 -6
  5. package/dist/hyperscript-i18n-de.global.js +5 -6
  6. package/dist/hyperscript-i18n-east-asian.global.js +5 -6
  7. package/dist/hyperscript-i18n-en.global.js +7 -8
  8. package/dist/hyperscript-i18n-es.global.js +5 -6
  9. package/dist/hyperscript-i18n-fr.global.js +5 -6
  10. package/dist/hyperscript-i18n-he.global.js +5 -6
  11. package/dist/hyperscript-i18n-hi.global.js +5 -6
  12. package/dist/hyperscript-i18n-id.global.js +5 -6
  13. package/dist/hyperscript-i18n-it.global.js +5 -6
  14. package/dist/hyperscript-i18n-ja.global.js +5 -6
  15. package/dist/hyperscript-i18n-ko.global.js +5 -6
  16. package/dist/hyperscript-i18n-lite.global.js +2 -2
  17. package/dist/hyperscript-i18n-ms.global.js +5 -6
  18. package/dist/hyperscript-i18n-pl.global.js +5 -6
  19. package/dist/hyperscript-i18n-pt.global.js +5 -6
  20. package/dist/hyperscript-i18n-qu.global.js +5 -6
  21. package/dist/hyperscript-i18n-ru.global.js +5 -6
  22. package/dist/hyperscript-i18n-slavic.global.js +5 -6
  23. package/dist/hyperscript-i18n-south-asian.global.js +5 -6
  24. package/dist/hyperscript-i18n-southeast-asian.global.js +5 -6
  25. package/dist/hyperscript-i18n-sw.global.js +5 -6
  26. package/dist/hyperscript-i18n-th.global.js +5 -6
  27. package/dist/hyperscript-i18n-tl.global.js +5 -6
  28. package/dist/hyperscript-i18n-tr.global.js +5 -6
  29. package/dist/hyperscript-i18n-uk.global.js +5 -6
  30. package/dist/hyperscript-i18n-vi.global.js +5 -6
  31. package/dist/hyperscript-i18n-western.global.js +5 -6
  32. package/dist/hyperscript-i18n-zh.global.js +5 -6
  33. package/dist/hyperscript-i18n.global.js +27 -28
  34. package/dist/hyperscript-i18n.global.js.map +1 -1
  35. package/dist/index.cjs +107 -86
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +75 -12
  38. package/dist/index.d.ts +75 -12
  39. package/dist/index.js +107 -93
  40. package/dist/index.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/attribute-translator.ts +12 -7
  43. package/src/browser-lite.ts +18 -3
  44. package/src/generated/syntax-table.ts +4 -4
  45. package/src/host-validate.ts +63 -0
  46. package/src/hyperscript-renderer.ts +64 -5
  47. package/src/index.ts +6 -1
  48. package/src/language-resolver.ts +15 -3
  49. package/src/plugin.ts +57 -7
  50. package/src/preprocessor-core.ts +217 -0
  51. package/src/preprocessor.ts +14 -215
  52. package/src/slim-plugin.ts +18 -3
  53. package/src/slim-preprocessor.ts +16 -154
@@ -1,157 +1,30 @@
1
1
  /**
2
- * Preprocessor
2
+ * Preprocessor (full path)
3
3
  *
4
4
  * Translates non-English hyperscript to English using the semantic parser
5
5
  * (with optional i18n fallback), so the original _hyperscript can parse it.
6
+ *
7
+ * The strategy/split/strip skeleton lives in preprocessor-core.ts, shared
8
+ * with the slim path — only HOW one statement is parsed and rendered
9
+ * differs here: semantic's parseSemantic + render('en'), plus a
10
+ * translate() rescue when the parse is confident but yields no node.
6
11
  */
7
12
 
8
- import {
9
- translate,
10
- render,
11
- parseSemantic,
12
- isLanguageRegistered,
13
- tryGetProfile,
14
- } from '@lokascript/semantic';
15
-
16
- export interface PreprocessorConfig {
17
- /**
18
- * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.
19
- * Can be a single number (applies to all languages) or a per-language map.
20
- * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce
21
- * inherently lower confidence scores than SVO languages (es, fr, de).
22
- *
23
- * @example
24
- * // Single threshold
25
- * { confidenceThreshold: 0.5 }
26
- *
27
- * @example
28
- * // Per-language thresholds
29
- * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }
30
- */
31
- confidenceThreshold: number | Record<string, number>;
32
- /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */
33
- strategy: 'semantic' | 'i18n' | 'auto';
34
- /** Whether to return original text on failure. Default: true */
35
- fallbackToOriginal: boolean;
36
- /** Optional i18n toEnglish function (loaded dynamically if available) */
37
- i18nToEnglish?: (input: string, locale: string) => string;
38
- }
13
+ import { translate, render, parseSemantic, isLanguageRegistered } from '@lokascript/semantic';
39
14
 
40
- const DEFAULT_THRESHOLD = 0.5;
15
+ import { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';
41
16
 
42
- const DEFAULT_CONFIG: PreprocessorConfig = {
43
- confidenceThreshold: DEFAULT_THRESHOLD,
44
- strategy: 'semantic',
45
- fallbackToOriginal: true,
46
- };
47
-
48
- /**
49
- * Resolve the confidence threshold for a specific language.
50
- * Supports both a single number and a per-language map with '*' as default.
51
- */
52
- function resolveThreshold(threshold: number | Record<string, number>, lang: string): number {
53
- if (typeof threshold === 'number') return threshold;
54
- return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;
55
- }
17
+ export type { PreprocessorConfig };
56
18
 
57
19
  /**
58
20
  * Preprocess non-English hyperscript into English.
59
21
  *
60
22
  * Uses the semantic parser to parse the input in the source language,
61
23
  * then renders back to English. Falls through to i18n if configured.
62
- *
63
- * Handles _hyperscript feature prefixes (e.g. "on click", "on every keyup")
64
- * by stripping them, translating only the command portion, then reassembling.
65
- */
66
- export function preprocessToEnglish(
67
- src: string,
68
- lang: string,
69
- config: Partial<PreprocessorConfig> = {}
70
- ): string {
71
- // English→English is identity; skip semantic parsing which may mangle the input
72
- if (lang === 'en') return src;
73
-
74
- const cfg = { ...DEFAULT_CONFIG, ...config };
75
-
76
- // Try translating the full string first
77
- const fullResult = tryTranslateWithStrategies(src, lang, cfg);
78
- if (fullResult !== null) return fullResult;
79
-
80
- // If full translation failed, try stripping event/feature prefix.
81
- // _hyperscript attributes often contain "on <event> <commands>" — the semantic
82
- // parser only understands command syntax, not event declarations.
83
- const stripped = stripEventPrefix(src);
84
- if (stripped) {
85
- const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);
86
- if (translated !== null) return stripped.prefix + translated;
87
- }
88
-
89
- // Fallback: return original
90
- return cfg.fallbackToOriginal ? src : src;
91
- }
92
-
93
- /**
94
- * Try all configured translation strategies on the input.
95
- * Returns null if none succeed.
96
24
  */
97
- function tryTranslateWithStrategies(
98
- src: string,
99
- lang: string,
100
- cfg: PreprocessorConfig
101
- ): string | null {
102
- // Strategy: semantic-only
103
- if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {
104
- const threshold = resolveThreshold(cfg.confidenceThreshold, lang);
105
- const result = trySemanticTranslation(src, lang, threshold);
106
- if (result !== null) return result;
107
- }
108
-
109
- // Strategy: i18n fallback (auto mode or i18n-only)
110
- if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {
111
- const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);
112
- if (result !== null) return result;
113
- }
114
-
115
- return null;
116
- }
117
-
118
- /**
119
- * Match _hyperscript event handler prefix: "on [every] <event>[filter][.modifiers] "
120
- *
121
- * Examples:
122
- * "on click toggle .active" → prefix: "on click ", commands: "toggle .active"
123
- * "on every click toggle .active" → prefix: "on every click ", commands: "toggle .active"
124
- * "on click.debounce(300) toggle .x" → prefix: "on click.debounce(300) ", commands: "toggle .x"
125
- * "on keyup[key=='Enter'] set x to 1" → prefix: "on keyup[key=='Enter'] ", commands: "set x to 1"
126
- * "on click from body toggle .active" → prefix: "on click from body ", commands: "toggle .active"
127
- */
128
- const EVENT_PREFIX_RE =
129
- /^(on\s+(?:every\s+)?[\w-]+(?:\[.*?\])?(?:\.[\w-]+(?:\([^)]*\))?)*(?:\s+from\s+\S+)?(?:\s+queue\s+\w+)?\s+)/;
130
-
131
- function stripEventPrefix(src: string): { prefix: string; commands: string } | null {
132
- const match = src.match(EVENT_PREFIX_RE);
133
- if (!match) return null;
134
- const prefix = match[1];
135
- const commands = src.slice(prefix.length);
136
- if (!commands) return null;
137
- return { prefix, commands };
138
- }
139
-
140
- /**
141
- * Try semantic translation: parse in source language, render to English.
142
- * Returns null if confidence is below threshold.
143
- */
144
- function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {
145
- try {
146
- // Handle compound statements (split on "then" boundaries and newlines)
147
- const statements = splitStatements(src, lang);
148
- if (statements.length > 1) {
149
- return translateCompound(statements, lang, threshold);
150
- }
151
-
152
- // Check if the semantic parser can handle this
153
- if (!isLanguageRegistered(lang)) return null;
154
-
25
+ export const preprocessToEnglish = createPreprocessToEnglish({
26
+ isLanguageRegistered,
27
+ translateSingle(src, lang, threshold) {
155
28
  const result = parseSemantic(src, lang);
156
29
  if (result.confidence < threshold || !result.node) {
157
30
  // If we got SOME confidence but no node, fall back to translate()
@@ -163,79 +36,5 @@ function trySemanticTranslation(src: string, lang: string, threshold: number): s
163
36
 
164
37
  // Render the semantic node to English
165
38
  return render(result.node, 'en');
166
- } catch {
167
- return null;
168
- }
169
- }
170
-
171
- /**
172
- * Try i18n grammar transformation to English.
173
- * Returns null if the result is identical to input (no translation happened).
174
- */
175
- function tryI18nTranslation(
176
- src: string,
177
- lang: string,
178
- toEnglish: (input: string, locale: string) => string
179
- ): string | null {
180
- try {
181
- const result = toEnglish(src, lang);
182
- return result !== src ? result : null;
183
- } catch {
184
- return null;
185
- }
186
- }
187
-
188
- /**
189
- * Get all "then" keyword forms for a language (primary + alternatives).
190
- * Falls back to English "then" if profile is unavailable.
191
- */
192
- function getThenKeywords(lang: string): string[] {
193
- const keywords = ['then']; // Always include English
194
- const profile = tryGetProfile(lang);
195
- if (profile?.keywords?.then) {
196
- const kw = profile.keywords.then;
197
- if (kw.primary && kw.primary !== 'then') keywords.push(kw.primary);
198
- if (kw.alternatives) {
199
- for (const alt of kw.alternatives) {
200
- if (!keywords.includes(alt)) keywords.push(alt);
201
- }
202
- }
203
- }
204
- return keywords;
205
- }
206
-
207
- /**
208
- * Split a hyperscript source string into individual statements.
209
- * Splits on language-specific "then" keywords and newlines.
210
- */
211
- function splitStatements(src: string, lang: string): string[] {
212
- const thenWords = getThenKeywords(lang);
213
- // Escape regex special chars and build pattern
214
- const escaped = thenWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
215
- const pattern = new RegExp(`\\s+(?:${escaped.join('|')})\\s+`, 'i');
216
-
217
- // Split on newlines first
218
- const lines = src
219
- .split('\n')
220
- .map(l => l.trim())
221
- .filter(l => l.length > 0);
222
- const result: string[] = [];
223
- for (const line of lines) {
224
- const parts = line.split(pattern);
225
- result.push(...parts);
226
- }
227
- return result;
228
- }
229
-
230
- /**
231
- * Translate compound statements individually and rejoin with " then ".
232
- */
233
- function translateCompound(statements: string[], lang: string, threshold: number): string | null {
234
- const translated: string[] = [];
235
- for (const stmt of statements) {
236
- const result = trySemanticTranslation(stmt.trim(), lang, threshold);
237
- if (result === null) return null; // If any part fails, fail the whole compound
238
- translated.push(result);
239
- }
240
- return translated.join(' then ');
241
- }
39
+ },
40
+ });
@@ -10,19 +10,34 @@ import { preprocessToEnglish } from './slim-preprocessor';
10
10
  import type { PreprocessorConfig } from './preprocessor';
11
11
  import type { PluginOptions } from './plugin';
12
12
  import { installAttributeTranslator, type HyperscriptHost } from './attribute-translator';
13
+ import { acceptedByHost, warnRejectedOnce, type HyperscriptParseHost } from './host-validate';
13
14
 
14
15
  export type { PluginOptions };
15
16
 
16
17
  export function hyperscriptI18n(options: PluginOptions = {}) {
17
18
  return function plugin(hs: unknown): void {
18
- installAttributeTranslator(hs as HyperscriptHost, (src, elt) => {
19
+ const host = hs as HyperscriptHost & HyperscriptParseHost;
20
+ installAttributeTranslator(host, (src, elt) => {
19
21
  const lang = resolveLanguageWithOptions(elt, options);
20
22
  if (!lang || lang === 'en') return src;
21
23
 
22
24
  const english = preprocessToEnglish(src, lang, options);
23
25
 
24
- if (options.debug && english !== src) {
25
- console.log(`[hyperscript-i18n] ${lang}: "${src}" → "${english}"`);
26
+ if (english !== src) {
27
+ // Host-parser validity gate — see plugin.ts / host-validate.ts.
28
+ if (options.validateWithHost !== false && !acceptedByHost(host, english)) {
29
+ if (options.debug) {
30
+ console.log(
31
+ `[hyperscript-i18n] ${lang}: host rejected "${english}" — keeping "${src}"`
32
+ );
33
+ } else {
34
+ warnRejectedOnce(lang, src, english);
35
+ }
36
+ return src;
37
+ }
38
+ if (options.debug) {
39
+ console.log(`[hyperscript-i18n] ${lang}: "${src}" → "${english}"`);
40
+ }
26
41
  }
27
42
 
28
43
  return english;
@@ -1,172 +1,34 @@
1
1
  /**
2
2
  * Slim Preprocessor
3
3
  *
4
- * Same logic as preprocessor.ts but imports from @lokascript/semantic/core
5
- * and uses a custom hyperscript renderer instead of the semantic package's
6
- * render(). This avoids importing English language data (tokenizer, patterns,
7
- * profile), saving ~35 KB per per-language bundle.
4
+ * Same skeleton as preprocessor.ts (shared via preprocessor-core.ts) but
5
+ * imports from @lokascript/semantic/core and renders through the custom
6
+ * hyperscript renderer instead of the semantic package's render(). This
7
+ * avoids importing English language data (tokenizer, patterns, profile),
8
+ * saving ~35 KB per per-language bundle.
8
9
  *
9
- * Used by per-language browser bundles for tree-shaking.
10
+ * Used by per-language browser bundles for tree-shaking. Must never
11
+ * import from '@lokascript/semantic' (the full package) at runtime.
10
12
  */
11
13
 
12
14
  // Import from /core — does NOT trigger all-language registration.
13
15
  // Languages are registered separately via side-effect imports in bundle entries.
14
- import {
15
- parseWithConfidence,
16
- isLanguageRegistered,
17
- tryGetProfile,
18
- } from '@lokascript/semantic/core';
16
+ import { parseWithConfidence, isLanguageRegistered } from '@lokascript/semantic/core';
19
17
 
20
18
  import { renderToHyperscript } from './hyperscript-renderer';
21
- import type { PreprocessorConfig } from './preprocessor';
22
-
23
- const DEFAULT_THRESHOLD = 0.5;
24
-
25
- const DEFAULT_CONFIG: PreprocessorConfig = {
26
- confidenceThreshold: DEFAULT_THRESHOLD,
27
- strategy: 'semantic',
28
- fallbackToOriginal: true,
29
- };
19
+ import { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';
30
20
 
31
- function resolveThreshold(threshold: number | Record<string, number>, lang: string): number {
32
- if (typeof threshold === 'number') return threshold;
33
- return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;
34
- }
21
+ export type { PreprocessorConfig };
35
22
 
36
23
  /**
37
- * Preprocess non-English hyperscript into English.
38
- * Identical to preprocessToEnglish in preprocessor.ts but with slim imports.
39
- *
40
- * Handles _hyperscript feature prefixes (e.g. "on click", "on every keyup")
41
- * by stripping them, translating only the command portion, then reassembling.
24
+ * Preprocess non-English hyperscript into English (slim imports).
42
25
  */
43
- export function preprocessToEnglish(
44
- src: string,
45
- lang: string,
46
- config: Partial<PreprocessorConfig> = {}
47
- ): string {
48
- const cfg = { ...DEFAULT_CONFIG, ...config };
49
-
50
- // Try translating the full string first
51
- const fullResult = tryTranslateWithStrategies(src, lang, cfg);
52
- if (fullResult !== null) return fullResult;
53
-
54
- // If full translation failed, try stripping event/feature prefix
55
- const stripped = stripEventPrefix(src);
56
- if (stripped) {
57
- const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);
58
- if (translated !== null) return stripped.prefix + translated;
59
- }
60
-
61
- return src;
62
- }
63
-
64
- function tryTranslateWithStrategies(
65
- src: string,
66
- lang: string,
67
- cfg: PreprocessorConfig
68
- ): string | null {
69
- if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {
70
- const threshold = resolveThreshold(cfg.confidenceThreshold, lang);
71
- const result = trySemanticTranslation(src, lang, threshold);
72
- if (result !== null) return result;
73
- }
74
-
75
- if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {
76
- const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);
77
- if (result !== null) return result;
78
- }
79
-
80
- return null;
81
- }
82
-
83
- /** Match _hyperscript event handler prefix. */
84
- const EVENT_PREFIX_RE =
85
- /^(on\s+(?:every\s+)?[\w-]+(?:\[.*?\])?(?:\.[\w-]+(?:\([^)]*\))?)*(?:\s+from\s+\S+)?(?:\s+queue\s+\w+)?\s+)/;
86
-
87
- function stripEventPrefix(src: string): { prefix: string; commands: string } | null {
88
- const match = src.match(EVENT_PREFIX_RE);
89
- if (!match) return null;
90
- const prefix = match[1];
91
- const commands = src.slice(prefix.length);
92
- if (!commands) return null;
93
- return { prefix, commands };
94
- }
95
-
96
- function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {
97
- try {
98
- const statements = splitStatements(src, lang);
99
- if (statements.length > 1) {
100
- return translateCompound(statements, lang, threshold);
101
- }
102
-
103
- if (!isLanguageRegistered(lang)) return null;
104
-
26
+ export const preprocessToEnglish = createPreprocessToEnglish({
27
+ isLanguageRegistered,
28
+ translateSingle(src, lang, threshold) {
105
29
  const result = parseWithConfidence(src, lang);
106
30
  if (result.confidence < threshold || !result.node) return null;
107
31
 
108
32
  return renderToHyperscript(result.node);
109
- } catch {
110
- return null;
111
- }
112
- }
113
-
114
- function tryI18nTranslation(
115
- src: string,
116
- lang: string,
117
- toEnglish: (input: string, locale: string) => string
118
- ): string | null {
119
- try {
120
- const result = toEnglish(src, lang);
121
- return result !== src ? result : null;
122
- } catch {
123
- return null;
124
- }
125
- }
126
-
127
- /**
128
- * Get all "then" keyword forms for a language (primary + alternatives).
129
- * Falls back to English "then" if profile is unavailable.
130
- */
131
- function getThenKeywords(lang: string): string[] {
132
- const keywords = ['then']; // Always include English
133
- const profile = tryGetProfile(lang);
134
- if (profile?.keywords?.then) {
135
- const kw = profile.keywords.then;
136
- if (kw.primary && kw.primary !== 'then') keywords.push(kw.primary);
137
- if (kw.alternatives) {
138
- for (const alt of kw.alternatives) {
139
- if (!keywords.includes(alt)) keywords.push(alt);
140
- }
141
- }
142
- }
143
- return keywords;
144
- }
145
-
146
- function splitStatements(src: string, lang: string): string[] {
147
- const thenWords = getThenKeywords(lang);
148
- // Escape regex special chars and build pattern
149
- const escaped = thenWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
150
- const pattern = new RegExp(`\\s+(?:${escaped.join('|')})\\s+`, 'i');
151
-
152
- const lines = src
153
- .split('\n')
154
- .map(l => l.trim())
155
- .filter(l => l.length > 0);
156
- const result: string[] = [];
157
- for (const line of lines) {
158
- const parts = line.split(pattern);
159
- result.push(...parts);
160
- }
161
- return result;
162
- }
163
-
164
- function translateCompound(statements: string[], lang: string, threshold: number): string | null {
165
- const translated: string[] = [];
166
- for (const stmt of statements) {
167
- const result = trySemanticTranslation(stmt.trim(), lang, threshold);
168
- if (result === null) return null;
169
- translated.push(result);
170
- }
171
- return translated.join(' then ');
172
- }
33
+ },
34
+ });