@lokascript/domain-voice 2.1.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,420 @@
1
+ /**
2
+ * Voice Natural Language Renderer
3
+ *
4
+ * Renders a SemanticNode back into natural-language voice command syntax
5
+ * for a target language. Inverse of the parser.
6
+ *
7
+ * Markers are derived from schemas (single source of truth) rather than
8
+ * maintained as a parallel data structure.
9
+ */
10
+
11
+ import type { SemanticNode, CommandSchema } from '@lokascript/framework';
12
+ import { extractRoleValue } from '@lokascript/framework';
13
+ import { allSchemas } from '../schemas/index';
14
+
15
+ // =============================================================================
16
+ // Keyword Tables
17
+ // =============================================================================
18
+
19
+ const COMMAND_KEYWORDS: Record<string, Record<string, string>> = {
20
+ navigate: {
21
+ en: 'navigate',
22
+ es: 'navegar',
23
+ ja: '移動',
24
+ ar: 'انتقل',
25
+ ko: '이동',
26
+ zh: '导航',
27
+ tr: 'git',
28
+ fr: 'naviguer',
29
+ },
30
+ click: {
31
+ en: 'click',
32
+ es: 'clic',
33
+ ja: 'クリック',
34
+ ar: 'انقر',
35
+ ko: '클릭',
36
+ zh: '点击',
37
+ tr: 'tıkla',
38
+ fr: 'cliquer',
39
+ },
40
+ type: {
41
+ en: 'type',
42
+ es: 'escribir',
43
+ ja: '入力',
44
+ ar: 'اكتب',
45
+ ko: '입력',
46
+ zh: '输入',
47
+ tr: 'yaz',
48
+ fr: 'taper',
49
+ },
50
+ scroll: {
51
+ en: 'scroll',
52
+ es: 'desplazar',
53
+ ja: 'スクロール',
54
+ ar: 'تمرير',
55
+ ko: '스크롤',
56
+ zh: '滚动',
57
+ tr: 'kaydır',
58
+ fr: 'défiler',
59
+ },
60
+ read: {
61
+ en: 'read',
62
+ es: 'leer',
63
+ ja: '読む',
64
+ ar: 'اقرأ',
65
+ ko: '읽기',
66
+ zh: '朗读',
67
+ tr: 'oku',
68
+ fr: 'lire',
69
+ },
70
+ zoom: {
71
+ en: 'zoom',
72
+ es: 'zoom',
73
+ ja: 'ズーム',
74
+ ar: 'تكبير',
75
+ ko: '확대',
76
+ zh: '缩放',
77
+ tr: 'yakınlaş',
78
+ fr: 'zoomer',
79
+ },
80
+ select: {
81
+ en: 'select',
82
+ es: 'seleccionar',
83
+ ja: '選択',
84
+ ar: 'اختر',
85
+ ko: '선택',
86
+ zh: '选择',
87
+ tr: 'seç',
88
+ fr: 'sélectionner',
89
+ },
90
+ back: {
91
+ en: 'back',
92
+ es: 'atrás',
93
+ ja: '戻る',
94
+ ar: 'رجوع',
95
+ ko: '뒤로',
96
+ zh: '返回',
97
+ tr: 'geri',
98
+ fr: 'retour',
99
+ },
100
+ forward: {
101
+ en: 'forward',
102
+ es: 'adelante',
103
+ ja: '進む',
104
+ ar: 'تقدم',
105
+ ko: '앞으로',
106
+ zh: '前进',
107
+ tr: 'ileri',
108
+ fr: 'avancer',
109
+ },
110
+ focus: {
111
+ en: 'focus',
112
+ es: 'enfocar',
113
+ ja: 'フォーカス',
114
+ ar: 'ركز',
115
+ ko: '포커스',
116
+ zh: '聚焦',
117
+ tr: 'odakla',
118
+ fr: 'focaliser',
119
+ },
120
+ close: {
121
+ en: 'close',
122
+ es: 'cerrar',
123
+ ja: '閉じる',
124
+ ar: 'أغلق',
125
+ ko: '닫기',
126
+ zh: '关闭',
127
+ tr: 'kapat',
128
+ fr: 'fermer',
129
+ },
130
+ open: {
131
+ en: 'open',
132
+ es: 'abrir',
133
+ ja: '開く',
134
+ ar: 'افتح',
135
+ ko: '열기',
136
+ zh: '打开',
137
+ tr: 'aç',
138
+ fr: 'ouvrir',
139
+ },
140
+ search: {
141
+ en: 'search',
142
+ es: 'buscar',
143
+ ja: '検索',
144
+ ar: 'ابحث',
145
+ ko: '검색',
146
+ zh: '搜索',
147
+ tr: 'ara',
148
+ fr: 'chercher',
149
+ },
150
+ help: {
151
+ en: 'help',
152
+ es: 'ayuda',
153
+ ja: 'ヘルプ',
154
+ ar: 'مساعدة',
155
+ ko: '도움말',
156
+ zh: '帮助',
157
+ tr: 'yardım',
158
+ fr: 'aide',
159
+ },
160
+ };
161
+
162
+ // =============================================================================
163
+ // Schema-Derived Marker Lookup (single source of truth)
164
+ // =============================================================================
165
+
166
+ type MarkerLookup = Record<string, Record<string, Record<string, string>>>;
167
+
168
+ function buildMarkerLookup(schemas: CommandSchema[]): MarkerLookup {
169
+ const lookup: MarkerLookup = {};
170
+ for (const schema of schemas) {
171
+ const actionMarkers: Record<string, Record<string, string>> = {};
172
+ for (const role of schema.roles) {
173
+ if (role.markerOverride) {
174
+ actionMarkers[role.role] = role.markerOverride as Record<string, string>;
175
+ }
176
+ }
177
+ lookup[schema.action] = actionMarkers;
178
+ }
179
+ return lookup;
180
+ }
181
+
182
+ const SCHEMA_MARKERS = buildMarkerLookup(allSchemas);
183
+
184
+ /** Get marker for a specific action + role + language from schemas. */
185
+ function getMarker(action: string, role: string, lang: string): string {
186
+ return SCHEMA_MARKERS[action]?.[role]?.[lang] ?? '';
187
+ }
188
+
189
+ // =============================================================================
190
+ // Word Order Helpers
191
+ // =============================================================================
192
+
193
+ const SOV_LANGUAGES = new Set(['ja', 'ko', 'tr']);
194
+
195
+ function kw(command: string, lang: string): string {
196
+ return COMMAND_KEYWORDS[command]?.[lang] ?? command;
197
+ }
198
+
199
+ // =============================================================================
200
+ // Per-Command Renderers
201
+ // =============================================================================
202
+
203
+ function renderSingleRole(node: SemanticNode, lang: string, roleName: string): string {
204
+ const value = extractRoleValue(node, roleName) || '';
205
+ const keyword = kw(node.action, lang);
206
+
207
+ if (!value) return keyword;
208
+
209
+ if (SOV_LANGUAGES.has(lang)) {
210
+ const parts = [value];
211
+ const marker = getMarker(node.action, roleName, lang);
212
+ if (marker) parts.push(marker);
213
+ parts.push(keyword);
214
+ return parts.join(' ');
215
+ }
216
+
217
+ // SVO / VSO: keyword [marker] value
218
+ const marker = getMarker(node.action, roleName, lang);
219
+ if (marker) return `${keyword} ${marker} ${value}`;
220
+ return `${keyword} ${value}`;
221
+ }
222
+
223
+ function renderNavigate(node: SemanticNode, lang: string): string {
224
+ const dest = extractRoleValue(node, 'destination') || '';
225
+ const keyword = kw('navigate', lang);
226
+
227
+ if (!dest) return keyword;
228
+
229
+ const marker = getMarker('navigate', 'destination', lang);
230
+
231
+ if (SOV_LANGUAGES.has(lang)) {
232
+ return marker ? `${dest} ${marker} ${keyword}` : `${dest} ${keyword}`;
233
+ }
234
+
235
+ return marker ? `${keyword} ${marker} ${dest}` : `${keyword} ${dest}`;
236
+ }
237
+
238
+ function renderClick(node: SemanticNode, lang: string): string {
239
+ return renderSingleRole(node, lang, 'patient');
240
+ }
241
+
242
+ function renderType(node: SemanticNode, lang: string): string {
243
+ const text = extractRoleValue(node, 'patient') || '';
244
+ const dest = extractRoleValue(node, 'destination');
245
+ const keyword = kw('type', lang);
246
+
247
+ if (!text) return keyword;
248
+
249
+ if (SOV_LANGUAGES.has(lang)) {
250
+ const parts: string[] = [];
251
+ if (dest) {
252
+ parts.push(dest);
253
+ const destMarker = getMarker('type', 'destination', lang);
254
+ if (destMarker) parts.push(destMarker);
255
+ }
256
+ parts.push(text);
257
+ const patientMarker = getMarker('type', 'patient', lang);
258
+ if (patientMarker) parts.push(patientMarker);
259
+ parts.push(keyword);
260
+ return parts.join(' ');
261
+ }
262
+
263
+ // SVO: keyword text [into dest]
264
+ const parts = [keyword, text];
265
+ if (dest) {
266
+ const destMarker = getMarker('type', 'destination', lang);
267
+ if (destMarker) parts.push(destMarker);
268
+ parts.push(dest);
269
+ }
270
+ return parts.filter(Boolean).join(' ');
271
+ }
272
+
273
+ function renderScroll(node: SemanticNode, lang: string): string {
274
+ const manner = extractRoleValue(node, 'manner') || '';
275
+ const keyword = kw('scroll', lang);
276
+
277
+ if (!manner) return keyword;
278
+
279
+ if (SOV_LANGUAGES.has(lang)) {
280
+ return `${manner} ${keyword}`;
281
+ }
282
+
283
+ return `${keyword} ${manner}`;
284
+ }
285
+
286
+ function renderRead(node: SemanticNode, lang: string): string {
287
+ return renderSingleRole(node, lang, 'patient');
288
+ }
289
+
290
+ function renderZoom(node: SemanticNode, lang: string): string {
291
+ const manner = extractRoleValue(node, 'manner') || '';
292
+ const keyword = kw('zoom', lang);
293
+
294
+ if (!manner) return keyword;
295
+
296
+ if (SOV_LANGUAGES.has(lang)) {
297
+ return `${manner} ${keyword}`;
298
+ }
299
+
300
+ return `${keyword} ${manner}`;
301
+ }
302
+
303
+ function renderSelect(node: SemanticNode, lang: string): string {
304
+ return renderSingleRole(node, lang, 'patient');
305
+ }
306
+
307
+ function renderBack(node: SemanticNode, lang: string): string {
308
+ const quantity = extractRoleValue(node, 'quantity');
309
+ const keyword = kw('back', lang);
310
+ if (quantity) return `${keyword} ${quantity}`;
311
+ return keyword;
312
+ }
313
+
314
+ function renderForward(node: SemanticNode, lang: string): string {
315
+ const quantity = extractRoleValue(node, 'quantity');
316
+ const keyword = kw('forward', lang);
317
+ if (quantity) return `${keyword} ${quantity}`;
318
+ return keyword;
319
+ }
320
+
321
+ function renderFocus(node: SemanticNode, lang: string): string {
322
+ const patient = extractRoleValue(node, 'patient') || '';
323
+ const keyword = kw('focus', lang);
324
+
325
+ if (!patient) return keyword;
326
+
327
+ if (SOV_LANGUAGES.has(lang)) {
328
+ const marker = getMarker('focus', 'patient', lang);
329
+ return marker ? `${patient} ${marker} ${keyword}` : `${patient} ${keyword}`;
330
+ }
331
+
332
+ return `${keyword} ${patient}`;
333
+ }
334
+
335
+ function renderClose(node: SemanticNode, lang: string): string {
336
+ return renderSingleRole(node, lang, 'patient');
337
+ }
338
+
339
+ function renderOpen(node: SemanticNode, lang: string): string {
340
+ return renderSingleRole(node, lang, 'patient');
341
+ }
342
+
343
+ function renderSearch(node: SemanticNode, lang: string): string {
344
+ const query = extractRoleValue(node, 'patient') || '';
345
+ const dest = extractRoleValue(node, 'destination');
346
+ const keyword = kw('search', lang);
347
+
348
+ if (!query) return keyword;
349
+
350
+ if (SOV_LANGUAGES.has(lang)) {
351
+ const parts: string[] = [];
352
+ if (dest) {
353
+ parts.push(dest);
354
+ const destMarker = getMarker('search', 'destination', lang);
355
+ if (destMarker) parts.push(destMarker);
356
+ }
357
+ parts.push(query);
358
+ const patientMarker = getMarker('search', 'patient', lang);
359
+ if (patientMarker) parts.push(patientMarker);
360
+ parts.push(keyword);
361
+ return parts.filter(Boolean).join(' ');
362
+ }
363
+
364
+ // SVO: keyword query [in dest]
365
+ const parts = [keyword, query];
366
+ if (dest) {
367
+ const destMarker = getMarker('search', 'destination', lang);
368
+ if (destMarker) parts.push(destMarker, dest);
369
+ }
370
+ return parts.join(' ');
371
+ }
372
+
373
+ function renderHelp(node: SemanticNode, lang: string): string {
374
+ const topic = extractRoleValue(node, 'patient');
375
+ const keyword = kw('help', lang);
376
+ if (topic) return `${keyword} ${topic}`;
377
+ return keyword;
378
+ }
379
+
380
+ // =============================================================================
381
+ // Public API
382
+ // =============================================================================
383
+
384
+ /**
385
+ * Render a voice SemanticNode to natural-language voice command text in the target language.
386
+ */
387
+ export function renderVoice(node: SemanticNode, language: string): string {
388
+ switch (node.action) {
389
+ case 'navigate':
390
+ return renderNavigate(node, language);
391
+ case 'click':
392
+ return renderClick(node, language);
393
+ case 'type':
394
+ return renderType(node, language);
395
+ case 'scroll':
396
+ return renderScroll(node, language);
397
+ case 'read':
398
+ return renderRead(node, language);
399
+ case 'zoom':
400
+ return renderZoom(node, language);
401
+ case 'select':
402
+ return renderSelect(node, language);
403
+ case 'back':
404
+ return renderBack(node, language);
405
+ case 'forward':
406
+ return renderForward(node, language);
407
+ case 'focus':
408
+ return renderFocus(node, language);
409
+ case 'close':
410
+ return renderClose(node, language);
411
+ case 'open':
412
+ return renderOpen(node, language);
413
+ case 'search':
414
+ return renderSearch(node, language);
415
+ case 'help':
416
+ return renderHelp(node, language);
417
+ default:
418
+ return `-- Unknown: ${node.action}`;
419
+ }
420
+ }
package/src/index.ts ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @lokascript/domain-voice — Multilingual Voice/Accessibility Commands
3
+ *
4
+ * A domain built on @lokascript/framework providing 14 voice/accessibility
5
+ * commands in 8 languages covering SVO, SOV, and VSO word orders.
6
+ * Commands compile to executable JavaScript for DOM manipulation,
7
+ * navigation, and accessibility actions.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createVoiceDSL } from '@lokascript/domain-voice';
12
+ *
13
+ * const voice = createVoiceDSL();
14
+ *
15
+ * // English (SVO)
16
+ * voice.compile('click submit', 'en');
17
+ * // → executable JS that finds and clicks the submit button
18
+ *
19
+ * // Japanese (SOV)
20
+ * voice.compile('送信 を クリック', 'ja');
21
+ *
22
+ * // Arabic (VSO)
23
+ * voice.compile('انقر على إرسال', 'ar');
24
+ *
25
+ * // Navigate
26
+ * voice.compile('navigate to home', 'en');
27
+ *
28
+ * // Scroll
29
+ * voice.compile('scroll down', 'en');
30
+ *
31
+ * // Read aloud
32
+ * voice.compile('read #article', 'en');
33
+ * ```
34
+ */
35
+
36
+ import { createMultilingualDSL, type MultilingualDSL } from '@lokascript/framework';
37
+ import { allSchemas } from './schemas/index';
38
+ import {
39
+ enProfile,
40
+ esProfile,
41
+ jaProfile,
42
+ arProfile,
43
+ koProfile,
44
+ zhProfile,
45
+ trProfile,
46
+ frProfile,
47
+ } from './profiles/index';
48
+ import {
49
+ EnglishVoiceTokenizer,
50
+ SpanishVoiceTokenizer,
51
+ JapaneseVoiceTokenizer,
52
+ ArabicVoiceTokenizer,
53
+ KoreanVoiceTokenizer,
54
+ ChineseVoiceTokenizer,
55
+ TurkishVoiceTokenizer,
56
+ FrenchVoiceTokenizer,
57
+ } from './tokenizers/index';
58
+ import { voiceCodeGenerator } from './generators/voice-generator';
59
+
60
+ /**
61
+ * Create a multilingual voice/accessibility DSL instance with all 8 supported languages.
62
+ */
63
+ export function createVoiceDSL(): MultilingualDSL {
64
+ return /*#__PURE__*/ createMultilingualDSL({
65
+ name: 'Voice',
66
+ schemas: allSchemas,
67
+ languages: [
68
+ {
69
+ code: 'en',
70
+ name: 'English',
71
+ nativeName: 'English',
72
+ tokenizer: EnglishVoiceTokenizer,
73
+ patternProfile: enProfile,
74
+ },
75
+ {
76
+ code: 'es',
77
+ name: 'Spanish',
78
+ nativeName: 'Español',
79
+ tokenizer: SpanishVoiceTokenizer,
80
+ patternProfile: esProfile,
81
+ },
82
+ {
83
+ code: 'ja',
84
+ name: 'Japanese',
85
+ nativeName: '日本語',
86
+ tokenizer: JapaneseVoiceTokenizer,
87
+ patternProfile: jaProfile,
88
+ },
89
+ {
90
+ code: 'ar',
91
+ name: 'Arabic',
92
+ nativeName: 'العربية',
93
+ tokenizer: ArabicVoiceTokenizer,
94
+ patternProfile: arProfile,
95
+ },
96
+ {
97
+ code: 'ko',
98
+ name: 'Korean',
99
+ nativeName: '한국어',
100
+ tokenizer: KoreanVoiceTokenizer,
101
+ patternProfile: koProfile,
102
+ },
103
+ {
104
+ code: 'zh',
105
+ name: 'Chinese',
106
+ nativeName: '中文',
107
+ tokenizer: ChineseVoiceTokenizer,
108
+ patternProfile: zhProfile,
109
+ },
110
+ {
111
+ code: 'tr',
112
+ name: 'Turkish',
113
+ nativeName: 'Türkçe',
114
+ tokenizer: TurkishVoiceTokenizer,
115
+ patternProfile: trProfile,
116
+ },
117
+ {
118
+ code: 'fr',
119
+ name: 'French',
120
+ nativeName: 'Français',
121
+ tokenizer: FrenchVoiceTokenizer,
122
+ patternProfile: frProfile,
123
+ },
124
+ ],
125
+ codeGenerator: voiceCodeGenerator,
126
+ });
127
+ }
128
+
129
+ // Re-export schemas
130
+ export {
131
+ allSchemas,
132
+ navigateSchema,
133
+ clickSchema,
134
+ typeSchema,
135
+ scrollSchema,
136
+ readSchema,
137
+ zoomSchema,
138
+ selectSchema,
139
+ backSchema,
140
+ forwardSchema,
141
+ focusSchema,
142
+ closeSchema,
143
+ openSchema,
144
+ searchSchema,
145
+ helpSchema,
146
+ } from './schemas/index';
147
+
148
+ // Re-export profiles
149
+ export {
150
+ enProfile,
151
+ esProfile,
152
+ jaProfile,
153
+ arProfile,
154
+ koProfile,
155
+ zhProfile,
156
+ trProfile,
157
+ frProfile,
158
+ allProfiles,
159
+ } from './profiles/index';
160
+
161
+ // Re-export generators
162
+ export { voiceCodeGenerator } from './generators/voice-generator';
163
+ export { renderVoice } from './generators/voice-renderer';
164
+
165
+ // Re-export tokenizers
166
+ export {
167
+ EnglishVoiceTokenizer,
168
+ SpanishVoiceTokenizer,
169
+ JapaneseVoiceTokenizer,
170
+ ArabicVoiceTokenizer,
171
+ KoreanVoiceTokenizer,
172
+ ChineseVoiceTokenizer,
173
+ TurkishVoiceTokenizer,
174
+ FrenchVoiceTokenizer,
175
+ } from './tokenizers/index';
176
+
177
+ // Re-export types and converters
178
+ export type { VoiceActionSpec } from './types';
179
+ export { toVoiceActionSpec } from './types';
180
+
181
+ // =============================================================================
182
+ // Domain Scan Config (for AOT / Vite plugin integration)
183
+ // =============================================================================
184
+
185
+ /** HTML attribute and script-type patterns for AOT scanning */
186
+ export const voiceScanConfig = {
187
+ attributes: ['data-voice', '_voice'] as const,
188
+ scriptTypes: ['text/voice'] as const,
189
+ defaultLanguage: 'en',
190
+ };