@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.
- package/dist/generators/voice-generator.d.ts +8 -0
- package/dist/generators/voice-renderer.d.ts +14 -0
- package/dist/index.cjs +1864 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +1803 -0
- package/dist/index.js.map +1 -0
- package/dist/profiles/index.d.ts +20 -0
- package/dist/schemas/index.d.ts +16 -0
- package/dist/tokenizers/index.d.ts +16 -0
- package/dist/types.d.ts +23 -0
- package/package.json +48 -0
- package/src/__test__/voice-domain.test.ts +1232 -0
- package/src/generators/voice-generator.ts +313 -0
- package/src/generators/voice-renderer.ts +420 -0
- package/src/index.ts +190 -0
- package/src/profiles/index.ts +235 -0
- package/src/schemas/index.ts +515 -0
- package/src/tokenizers/index.ts +644 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice/Accessibility Code Generator
|
|
3
|
+
*
|
|
4
|
+
* Transforms semantic AST nodes into executable JavaScript snippets
|
|
5
|
+
* for DOM manipulation, navigation, and accessibility actions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CodeGenerator, SemanticNode } from '@lokascript/framework';
|
|
9
|
+
import { extractRoleValue } from '@lokascript/framework';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// String Escaping
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
function esc(s: string): string {
|
|
16
|
+
return s
|
|
17
|
+
.replace(/\\/g, '\\\\')
|
|
18
|
+
.replace(/'/g, "\\'")
|
|
19
|
+
.replace(/"/g, '\\"')
|
|
20
|
+
.replace(/`/g, '\\`')
|
|
21
|
+
.replace(/\n/g, '\\n')
|
|
22
|
+
.replace(/\r/g, '\\r')
|
|
23
|
+
.replace(/\$/g, '\\$');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Shared Element-Finding Helper (injected into generated code)
|
|
28
|
+
// Self-initializing guard: safe to include multiple times, defines only once.
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
const FIND_EL = [
|
|
32
|
+
`if(!window._findEl){window._findEl=function(q,root){`,
|
|
33
|
+
` root=root||document;`,
|
|
34
|
+
` if(q.startsWith('#')||q.startsWith('.')||q.includes('[')){`,
|
|
35
|
+
` try{return root.querySelector(q)}catch(e){}`,
|
|
36
|
+
` }`,
|
|
37
|
+
` var el=root.querySelector('[aria-label="'+q+'"]');`,
|
|
38
|
+
` if(el)return el;`,
|
|
39
|
+
` el=root.querySelector('[role="'+q+'"]');`,
|
|
40
|
+
` if(el)return el;`,
|
|
41
|
+
` var c=root.querySelectorAll('button,a,input,[role="button"],[tabindex]');`,
|
|
42
|
+
` var lq=q.toLowerCase();`,
|
|
43
|
+
` for(var i=0;i<c.length;i++){`,
|
|
44
|
+
` if((c[i].textContent||'').toLowerCase().includes(lq))return c[i];`,
|
|
45
|
+
` if((c[i].getAttribute('aria-label')||'').toLowerCase().includes(lq))return c[i];`,
|
|
46
|
+
` }`,
|
|
47
|
+
` return null;`,
|
|
48
|
+
`}}`,
|
|
49
|
+
].join('');
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// i18n Word Sets (all 8 languages)
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
const SELECT_ALL_WORDS = new Set(['all', 'todo', '全て', '全部', 'الكل', '전체', 'hepsi', 'tout']);
|
|
56
|
+
const TAB_WORDS = new Set(['tab', 'pestaña', 'タブ', '탭', '标签', 'sekme', 'onglet']);
|
|
57
|
+
const DIALOG_WORDS = new Set([
|
|
58
|
+
'dialog',
|
|
59
|
+
'modal',
|
|
60
|
+
'diálogo',
|
|
61
|
+
'ダイアログ',
|
|
62
|
+
'대화상자',
|
|
63
|
+
'对话框',
|
|
64
|
+
'diyalog',
|
|
65
|
+
'dialogue',
|
|
66
|
+
]);
|
|
67
|
+
const PAGE_WORDS = new Set(['page', 'página', 'ページ', 'الصفحة', '페이지', '页面', 'sayfa']);
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Per-Command Generators
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
function generateNavigate(node: SemanticNode): string {
|
|
74
|
+
const dest = extractRoleValue(node, 'destination');
|
|
75
|
+
if (!dest) return '// navigate: missing destination';
|
|
76
|
+
if (dest.startsWith('/') || dest.startsWith('http')) {
|
|
77
|
+
return `window.location.href = '${esc(dest)}';`;
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
FIND_EL,
|
|
81
|
+
`var el = _findEl('${esc(dest)}');`,
|
|
82
|
+
`if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });`,
|
|
83
|
+
`else window.location.hash = '${esc(dest)}';`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateClick(node: SemanticNode): string {
|
|
88
|
+
const patient = extractRoleValue(node, 'patient');
|
|
89
|
+
if (!patient) return '// click: missing target';
|
|
90
|
+
return [FIND_EL, `var el = _findEl('${esc(patient)}');`, `if (el) el.click();`].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateType(node: SemanticNode): string {
|
|
94
|
+
const text = extractRoleValue(node, 'patient');
|
|
95
|
+
if (!text) return '// type: missing text';
|
|
96
|
+
const dest = extractRoleValue(node, 'destination');
|
|
97
|
+
const target = dest
|
|
98
|
+
? `_findEl('${esc(dest)}') || document.activeElement`
|
|
99
|
+
: `document.activeElement`;
|
|
100
|
+
return [
|
|
101
|
+
FIND_EL,
|
|
102
|
+
`var el = ${target};`,
|
|
103
|
+
`if (el && ('value' in el || el.isContentEditable)) {`,
|
|
104
|
+
` if ('value' in el) { el.value = (el.value || '') + '${esc(text)}'; }`,
|
|
105
|
+
` else { el.textContent = (el.textContent || '') + '${esc(text)}'; }`,
|
|
106
|
+
` el.dispatchEvent(new Event('input', { bubbles: true }));`,
|
|
107
|
+
`}`,
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateScroll(node: SemanticNode): string {
|
|
112
|
+
const manner = (extractRoleValue(node, 'manner') || 'down').toLowerCase();
|
|
113
|
+
const quantity = extractRoleValue(node, 'quantity');
|
|
114
|
+
const px = quantity ? parseInt(quantity, 10) || 300 : 300;
|
|
115
|
+
|
|
116
|
+
switch (manner) {
|
|
117
|
+
case 'up':
|
|
118
|
+
return `window.scrollBy({ top: -${px}, behavior: 'smooth' });`;
|
|
119
|
+
case 'down':
|
|
120
|
+
return `window.scrollBy({ top: ${px}, behavior: 'smooth' });`;
|
|
121
|
+
case 'left':
|
|
122
|
+
return `window.scrollBy({ left: -${px}, behavior: 'smooth' });`;
|
|
123
|
+
case 'right':
|
|
124
|
+
return `window.scrollBy({ left: ${px}, behavior: 'smooth' });`;
|
|
125
|
+
case 'top':
|
|
126
|
+
return `window.scrollTo({ top: 0, behavior: 'smooth' });`;
|
|
127
|
+
case 'bottom':
|
|
128
|
+
return `window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });`;
|
|
129
|
+
default:
|
|
130
|
+
return `window.scrollBy({ top: ${px}, behavior: 'smooth' });`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function generateRead(node: SemanticNode): string {
|
|
135
|
+
const patient = extractRoleValue(node, 'patient');
|
|
136
|
+
if (!patient) return '// read: missing target';
|
|
137
|
+
return [
|
|
138
|
+
FIND_EL,
|
|
139
|
+
`var el = _findEl('${esc(patient)}');`,
|
|
140
|
+
`if (el && el.textContent) {`,
|
|
141
|
+
` var utterance = new SpeechSynthesisUtterance(el.textContent);`,
|
|
142
|
+
` speechSynthesis.speak(utterance);`,
|
|
143
|
+
`}`,
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function generateZoom(node: SemanticNode): string {
|
|
148
|
+
const manner = (extractRoleValue(node, 'manner') || 'in').toLowerCase();
|
|
149
|
+
if (manner === 'reset') {
|
|
150
|
+
return [
|
|
151
|
+
`document.documentElement.dataset.zoom = '1';`,
|
|
152
|
+
`document.documentElement.style.transform = '';`,
|
|
153
|
+
].join('\n');
|
|
154
|
+
}
|
|
155
|
+
const factor = manner === 'out' ? 0.9 : 1.1;
|
|
156
|
+
return [
|
|
157
|
+
`var s = parseFloat(document.documentElement.dataset.zoom || '1');`,
|
|
158
|
+
`s = Math.round(s * ${factor} * 100) / 100;`,
|
|
159
|
+
`document.documentElement.dataset.zoom = s;`,
|
|
160
|
+
`document.documentElement.style.transform = 'scale(' + s + ')';`,
|
|
161
|
+
`document.documentElement.style.transformOrigin = 'top left';`,
|
|
162
|
+
].join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function generateSelect(node: SemanticNode): string {
|
|
166
|
+
const patient = extractRoleValue(node, 'patient');
|
|
167
|
+
if (!patient) return '// select: missing target';
|
|
168
|
+
const target = SELECT_ALL_WORDS.has(patient) ? 'document.body' : `_findEl('${esc(patient)}')`;
|
|
169
|
+
const lines = SELECT_ALL_WORDS.has(patient) ? [] : [FIND_EL];
|
|
170
|
+
lines.push(
|
|
171
|
+
`var el = ${target};`,
|
|
172
|
+
`if (el) {`,
|
|
173
|
+
` var range = document.createRange();`,
|
|
174
|
+
` range.selectNodeContents(el);`,
|
|
175
|
+
` var sel = window.getSelection();`,
|
|
176
|
+
` sel.removeAllRanges();`,
|
|
177
|
+
` sel.addRange(range);`,
|
|
178
|
+
`}`
|
|
179
|
+
);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function generateBack(node: SemanticNode): string {
|
|
184
|
+
const quantity = extractRoleValue(node, 'quantity');
|
|
185
|
+
const n = quantity ? parseInt(quantity, 10) || 1 : 1;
|
|
186
|
+
return `history.go(-${n});`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function generateForward(node: SemanticNode): string {
|
|
190
|
+
const quantity = extractRoleValue(node, 'quantity');
|
|
191
|
+
const n = quantity ? parseInt(quantity, 10) || 1 : 1;
|
|
192
|
+
return `history.go(${n});`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function generateFocus(node: SemanticNode): string {
|
|
196
|
+
const patient = extractRoleValue(node, 'patient');
|
|
197
|
+
if (!patient) return '// focus: missing target';
|
|
198
|
+
return [FIND_EL, `var el = _findEl('${esc(patient)}');`, `if (el) el.focus();`].join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function generateClose(node: SemanticNode): string {
|
|
202
|
+
const patient = extractRoleValue(node, 'patient') || '';
|
|
203
|
+
if (TAB_WORDS.has(patient)) {
|
|
204
|
+
return `window.close();`;
|
|
205
|
+
}
|
|
206
|
+
if (DIALOG_WORDS.has(patient)) {
|
|
207
|
+
return `var d = document.querySelector('dialog[open]'); if (d) d.close();`;
|
|
208
|
+
}
|
|
209
|
+
// Default: try to close any open dialog or modal
|
|
210
|
+
return [
|
|
211
|
+
`var d = document.querySelector('dialog[open]');`,
|
|
212
|
+
`if (d) { d.close(); }`,
|
|
213
|
+
`else {`,
|
|
214
|
+
` var m = document.querySelector('[role="dialog"], .modal.show, .modal.open');`,
|
|
215
|
+
` if (m) m.remove();`,
|
|
216
|
+
`}`,
|
|
217
|
+
].join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function generateOpen(node: SemanticNode): string {
|
|
221
|
+
const patient = extractRoleValue(node, 'patient');
|
|
222
|
+
if (!patient) return '// open: missing target';
|
|
223
|
+
if (patient.startsWith('/') || patient.startsWith('http')) {
|
|
224
|
+
return `window.open('${esc(patient)}', '_blank');`;
|
|
225
|
+
}
|
|
226
|
+
return [FIND_EL, `var el = _findEl('${esc(patient)}');`, `if (el) el.click();`].join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function generateSearch(node: SemanticNode): string {
|
|
230
|
+
const query = extractRoleValue(node, 'patient');
|
|
231
|
+
if (!query) return '// search: missing query';
|
|
232
|
+
const dest = extractRoleValue(node, 'destination');
|
|
233
|
+
if (dest && PAGE_WORDS.has(dest)) {
|
|
234
|
+
// window.find() is non-standard but no standard alternative exists for "find in page"
|
|
235
|
+
return `window.find('${esc(query)}');`;
|
|
236
|
+
}
|
|
237
|
+
const selector = dest
|
|
238
|
+
? `'${esc(dest)}'`
|
|
239
|
+
: `'input[type="search"], [role="searchbox"], input[name="q"], input[name="search"]'`;
|
|
240
|
+
return [
|
|
241
|
+
`var searchInput = document.querySelector(${selector});`,
|
|
242
|
+
`if (searchInput) {`,
|
|
243
|
+
` searchInput.value = '${esc(query)}';`,
|
|
244
|
+
` searchInput.dispatchEvent(new Event('input', { bubbles: true }));`,
|
|
245
|
+
` if (searchInput.form) searchInput.form.submit();`,
|
|
246
|
+
`}`,
|
|
247
|
+
].join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function generateHelp(node: SemanticNode): string {
|
|
251
|
+
const topic = extractRoleValue(node, 'patient');
|
|
252
|
+
const commands = [
|
|
253
|
+
'navigate',
|
|
254
|
+
'click',
|
|
255
|
+
'type',
|
|
256
|
+
'scroll',
|
|
257
|
+
'read',
|
|
258
|
+
'zoom',
|
|
259
|
+
'select',
|
|
260
|
+
'back',
|
|
261
|
+
'forward',
|
|
262
|
+
'focus',
|
|
263
|
+
'close',
|
|
264
|
+
'open',
|
|
265
|
+
'search',
|
|
266
|
+
'help',
|
|
267
|
+
];
|
|
268
|
+
if (topic) {
|
|
269
|
+
return `console.log('Help: ${esc(topic)}');`;
|
|
270
|
+
}
|
|
271
|
+
return `console.log('Available commands: ${commands.join(', ')}');`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// Public Code Generator
|
|
276
|
+
// =============================================================================
|
|
277
|
+
|
|
278
|
+
export const voiceCodeGenerator: CodeGenerator = {
|
|
279
|
+
generate(node: SemanticNode): string {
|
|
280
|
+
switch (node.action) {
|
|
281
|
+
case 'navigate':
|
|
282
|
+
return generateNavigate(node);
|
|
283
|
+
case 'click':
|
|
284
|
+
return generateClick(node);
|
|
285
|
+
case 'type':
|
|
286
|
+
return generateType(node);
|
|
287
|
+
case 'scroll':
|
|
288
|
+
return generateScroll(node);
|
|
289
|
+
case 'read':
|
|
290
|
+
return generateRead(node);
|
|
291
|
+
case 'zoom':
|
|
292
|
+
return generateZoom(node);
|
|
293
|
+
case 'select':
|
|
294
|
+
return generateSelect(node);
|
|
295
|
+
case 'back':
|
|
296
|
+
return generateBack(node);
|
|
297
|
+
case 'forward':
|
|
298
|
+
return generateForward(node);
|
|
299
|
+
case 'focus':
|
|
300
|
+
return generateFocus(node);
|
|
301
|
+
case 'close':
|
|
302
|
+
return generateClose(node);
|
|
303
|
+
case 'open':
|
|
304
|
+
return generateOpen(node);
|
|
305
|
+
case 'search':
|
|
306
|
+
return generateSearch(node);
|
|
307
|
+
case 'help':
|
|
308
|
+
return generateHelp(node);
|
|
309
|
+
default:
|
|
310
|
+
return `// Unknown voice command: ${node.action}`;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
};
|