@willwade/aac-processors 0.1.10 → 0.1.12

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 (52) hide show
  1. package/dist/browser/processors/gridset/resolver.js +10 -0
  2. package/dist/browser/processors/gridsetProcessor.js +128 -6
  3. package/dist/processors/gridset/resolver.js +10 -0
  4. package/dist/processors/gridsetProcessor.js +128 -6
  5. package/package.json +1 -3
  6. package/examples/.coverage +0 -0
  7. package/examples/.keep +0 -1
  8. package/examples/README.md +0 -55
  9. package/examples/browser-test.html +0 -331
  10. package/examples/communikate.dot +0 -2637
  11. package/examples/demo.js +0 -143
  12. package/examples/example-images.gridset +0 -0
  13. package/examples/example.ce +0 -0
  14. package/examples/example.dot +0 -14
  15. package/examples/example.grd +0 -1
  16. package/examples/example.gridset +0 -0
  17. package/examples/example.obf +0 -27
  18. package/examples/example.obz +0 -0
  19. package/examples/example.opml +0 -18
  20. package/examples/example.spb +0 -0
  21. package/examples/example.sps +0 -0
  22. package/examples/example2.grd +0 -1
  23. package/examples/obf/aboutme.json +0 -376
  24. package/examples/obf/array.json +0 -6
  25. package/examples/obf/hash.json +0 -4
  26. package/examples/obf/links.obz +0 -0
  27. package/examples/obf/simple.obf +0 -53
  28. package/examples/package-lock.json +0 -1326
  29. package/examples/package.json +0 -10
  30. package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
  31. package/examples/styled-output/styled-example.ce +0 -0
  32. package/examples/styled-output/styled-example.gridset +0 -0
  33. package/examples/styled-output/styled-example.obf +0 -37
  34. package/examples/styled-output/styled-example.spb +0 -0
  35. package/examples/styling-example.ts +0 -316
  36. package/examples/translate.js +0 -39
  37. package/examples/translate_demo.js +0 -254
  38. package/examples/typescript-demo.ts +0 -251
  39. package/examples/vitedemo/README.md +0 -164
  40. package/examples/vitedemo/index.html +0 -580
  41. package/examples/vitedemo/package-lock.json +0 -1751
  42. package/examples/vitedemo/package.json +0 -24
  43. package/examples/vitedemo/src/main.ts +0 -1001
  44. package/examples/vitedemo/src/vite-env.d.ts +0 -1
  45. package/examples/vitedemo/test-files/example.dot +0 -14
  46. package/examples/vitedemo/test-files/example.grd +0 -1
  47. package/examples/vitedemo/test-files/example.gridset +0 -0
  48. package/examples/vitedemo/test-files/example.obz +0 -0
  49. package/examples/vitedemo/test-files/example.opml +0 -18
  50. package/examples/vitedemo/test-files/simple.obf +0 -53
  51. package/examples/vitedemo/tsconfig.json +0 -24
  52. package/examples/vitedemo/vite.config.ts +0 -57
@@ -1,1001 +0,0 @@
1
- /**
2
- * AAC Processors Browser Demo
3
- *
4
- * This demo uses Vite to bundle AACProcessors for browser use.
5
- * It tests all browser-compatible processors with real file uploads.
6
- */
7
-
8
- // Polyfill Buffer for browser environment
9
- if (typeof (window as any).Buffer === 'undefined') {
10
- // Create a proper Buffer wrapper class that extends Uint8Array
11
- class BufferWrapper extends Uint8Array {
12
- constructor(data: any, byteOffset?: number, length?: number) {
13
- if (typeof data === 'number') {
14
- // Alloc case: data is the size
15
- super(data);
16
- } else if (Array.isArray(data)) {
17
- super(data);
18
- } else if (data instanceof ArrayBuffer) {
19
- super(data, byteOffset || 0, length);
20
- } else if (data instanceof Uint8Array) {
21
- super(data.buffer, data.byteOffset, data.length);
22
- } else if (typeof data === 'string') {
23
- const encoder = new TextEncoder();
24
- super(encoder.encode(data));
25
- } else {
26
- super(0);
27
- }
28
- }
29
-
30
- toString(encoding: string = 'utf8'): string {
31
- if (encoding === 'utf8' || encoding === 'utf-8') {
32
- const decoder = new TextDecoder('utf-8');
33
- return decoder.decode(this);
34
- }
35
- throw new Error(`Buffer.toString: encoding ${encoding} not supported`);
36
- }
37
-
38
- static from(data: any, encoding?: string): BufferWrapper {
39
- return new BufferWrapper(data);
40
- }
41
-
42
- static alloc(size: number): BufferWrapper {
43
- return new BufferWrapper(size);
44
- }
45
-
46
- static allocUnsafe(size: number): BufferWrapper {
47
- return new BufferWrapper(size);
48
- }
49
-
50
- static concat(list: Uint8Array[], totalLength?: number): BufferWrapper {
51
- const result = new Uint8Array(totalLength || list.reduce((sum, arr) => sum + arr.length, 0));
52
- let offset = 0;
53
- for (const arr of list) {
54
- result.set(arr, offset);
55
- offset += arr.length;
56
- }
57
- return new BufferWrapper(result.buffer, result.byteOffset, result.length);
58
- }
59
-
60
- static isBuffer(obj: any): boolean {
61
- return obj instanceof BufferWrapper;
62
- }
63
- }
64
-
65
- (window as any).Buffer = BufferWrapper as any;
66
- }
67
-
68
- import {
69
- configureSqlJs,
70
- getProcessor,
71
- getSupportedExtensions,
72
- DotProcessor,
73
- OpmlProcessor,
74
- ObfProcessor,
75
- GridsetProcessor,
76
- SnapProcessor,
77
- TouchChatProcessor,
78
- ApplePanelsProcessor,
79
- AstericsGridProcessor,
80
- AACTree,
81
- AACPage,
82
- AACButton
83
- } from 'aac-processors';
84
- import { validateFileOrBuffer, type ValidationResult } from 'aac-processors/validation';
85
-
86
- import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
87
-
88
- configureSqlJs({
89
- locateFile: () => sqlWasmUrl
90
- });
91
-
92
- // UI Elements
93
- const dropArea = document.getElementById('dropArea') as HTMLElement;
94
- const fileInput = document.getElementById('fileInput') as HTMLInputElement;
95
- const processBtn = document.getElementById('processBtn') as HTMLButtonElement;
96
- const validateBtn = document.getElementById('validateBtn') as HTMLButtonElement;
97
- const runTestsBtn = document.getElementById('runTestsBtn') as HTMLButtonElement;
98
- const clearBtn = document.getElementById('clearBtn') as HTMLButtonElement;
99
- const fileInfo = document.getElementById('fileInfo') as HTMLElement;
100
- const processorName = document.getElementById('processorName') as HTMLElement;
101
- const fileDetails = document.getElementById('fileDetails') as HTMLElement;
102
- const stats = document.getElementById('stats') as HTMLElement;
103
- const results = document.getElementById('results') as HTMLElement;
104
- const logPanel = document.getElementById('logPanel') as HTMLElement;
105
- const testResults = document.getElementById('testResults') as HTMLElement;
106
- const testList = document.getElementById('testList') as HTMLElement;
107
- const validationPanel = document.getElementById('validationPanel') as HTMLElement;
108
- const validationSummary = document.getElementById('validationSummary') as HTMLElement;
109
- const validationList = document.getElementById('validationList') as HTMLElement;
110
- const tabButtons = document.querySelectorAll('.tab-btn') as NodeListOf<HTMLButtonElement>;
111
- const inspectTab = document.getElementById('inspectTab') as HTMLElement;
112
- const pagesetTab = document.getElementById('pagesetTab') as HTMLElement;
113
- const templateSelect = document.getElementById('templateSelect') as HTMLSelectElement;
114
- const formatSelect = document.getElementById('formatSelect') as HTMLSelectElement;
115
- const createPagesetBtn = document.getElementById('createPagesetBtn') as HTMLButtonElement;
116
- const previewPagesetBtn = document.getElementById('previewPagesetBtn') as HTMLButtonElement;
117
- const convertToObfBtn = document.getElementById('convertToObfBtn') as HTMLButtonElement;
118
- const convertToObzBtn = document.getElementById('convertToObzBtn') as HTMLButtonElement;
119
- const conversionStatus = document.getElementById('conversionStatus') as HTMLElement;
120
- const pagesetOutput = document.getElementById('pagesetOutput') as HTMLElement;
121
-
122
- // State
123
- let currentFile: File | null = null;
124
- let currentProcessor: any = null;
125
- let currentTree: AACTree | null = null;
126
- let currentSourceLabel = 'pageset';
127
-
128
- // Tabs
129
- function setActiveTab(tabId: string) {
130
- tabButtons.forEach((btn) => {
131
- btn.classList.toggle('active', btn.dataset.tab === tabId);
132
- });
133
- inspectTab.classList.toggle('active', tabId === 'inspectTab');
134
- pagesetTab.classList.toggle('active', tabId === 'pagesetTab');
135
- }
136
-
137
- tabButtons.forEach((btn) => {
138
- btn.addEventListener('click', () => {
139
- setActiveTab(btn.dataset.tab || 'inspectTab');
140
- });
141
- });
142
-
143
- // Logging
144
- function log(message: string, type: 'info' | 'success' | 'error' | 'warn' = 'info') {
145
- const entry = document.createElement('div');
146
- entry.className = `log-entry log-${type}`;
147
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
148
- logPanel.appendChild(entry);
149
- logPanel.scrollTop = logPanel.scrollHeight;
150
- console.log(`[${type.toUpperCase()}]`, message);
151
- }
152
-
153
- function setConversionStatus(message: string, state: 'success' | 'warn' | 'info' = 'info') {
154
- conversionStatus.textContent = message;
155
- conversionStatus.classList.remove('success', 'warn');
156
- if (state !== 'info') {
157
- conversionStatus.classList.add(state);
158
- }
159
- }
160
-
161
- function updateConvertButtons() {
162
- const hasTree = !!currentTree;
163
- convertToObfBtn.disabled = !hasTree;
164
- convertToObzBtn.disabled = !hasTree;
165
- if (!hasTree) {
166
- setConversionStatus('No pageset loaded yet.', 'info');
167
- } else {
168
- setConversionStatus(`Ready to export: ${currentSourceLabel}`, 'success');
169
- }
170
- }
171
-
172
- function updateStatsForTree(tree: AACTree, textCount?: number, loadTimeMs?: number) {
173
- const pageCount = Object.keys(tree.pages).length;
174
- const buttonCount = Object.values(tree.pages).reduce(
175
- (sum: number, page: AACPage) => sum + page.buttons.length,
176
- 0
177
- );
178
-
179
- document.getElementById('pageCount')!.textContent = pageCount.toString();
180
- document.getElementById('buttonCount')!.textContent = buttonCount.toString();
181
- document.getElementById('textCount')!.textContent = (textCount ?? 0).toString();
182
- document.getElementById('loadTime')!.textContent =
183
- loadTimeMs !== undefined ? `${loadTimeMs.toFixed(0)}ms` : '—';
184
- stats.style.display = 'grid';
185
- }
186
-
187
- function collectTextCount(tree: AACTree): number {
188
- const texts = new Set<string>();
189
- Object.values(tree.pages).forEach((page) => {
190
- if (page.name) texts.add(page.name);
191
- page.buttons.forEach((button) => {
192
- if (button.label) texts.add(button.label);
193
- if (button.message) texts.add(button.message);
194
- });
195
- });
196
- return texts.size;
197
- }
198
-
199
- // Get file extension
200
- function getFileExtension(filename: string): string {
201
- const match = filename.toLowerCase().match(/\.\w+$/);
202
- return match ? match[0] : '';
203
- }
204
-
205
- // Format file size
206
- function formatFileSize(bytes: number): string {
207
- if (bytes < 1024) return bytes + ' B';
208
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
209
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
210
- }
211
-
212
- // Handle file selection
213
- function handleFile(file: File) {
214
- currentFile = file;
215
- const extension = getFileExtension(file.name);
216
-
217
- log(`Selected file: ${file.name} (${formatFileSize(file.size)})`, 'info');
218
-
219
- // Check if extension is supported
220
- if (!getSupportedExtensions().includes(extension)) {
221
- log(`Unsupported file type: ${extension}`, 'error');
222
- processorName.textContent = '❌ Unsupported file type';
223
- fileDetails.textContent = extension;
224
- fileInfo.style.display = 'block';
225
- processBtn.disabled = true;
226
- validateBtn.disabled = true;
227
- return;
228
- }
229
-
230
- // Get processor
231
- try {
232
- currentProcessor = getProcessor(extension);
233
- processorName.textContent = `✅ ${currentProcessor.constructor.name}`;
234
- fileDetails.textContent = `${file.name} • ${formatFileSize(file.size)}`;
235
- fileInfo.style.display = 'block';
236
- processBtn.disabled = false;
237
- validateBtn.disabled = false;
238
- currentSourceLabel = file.name;
239
-
240
- log(`Using processor: ${currentProcessor.constructor.name}`, 'success');
241
- } catch (error) {
242
- log(`Error getting processor: ${(error as Error).message}`, 'error');
243
- processBtn.disabled = true;
244
- validateBtn.disabled = true;
245
- }
246
- }
247
-
248
- // Drag and drop handlers
249
- dropArea.addEventListener('dragover', (e) => {
250
- e.preventDefault();
251
- dropArea.classList.add('dragover');
252
- });
253
-
254
- dropArea.addEventListener('dragleave', () => {
255
- dropArea.classList.remove('dragover');
256
- });
257
-
258
- dropArea.addEventListener('drop', (e) => {
259
- e.preventDefault();
260
- dropArea.classList.remove('dragover');
261
-
262
- const file = e.dataTransfer?.files[0];
263
- if (file) {
264
- fileInput.files = e.dataTransfer!.files;
265
- handleFile(file);
266
- }
267
- });
268
-
269
- dropArea.addEventListener('click', () => {
270
- fileInput.click();
271
- });
272
-
273
- fileInput.addEventListener('change', (e) => {
274
- const file = (e.target as HTMLInputElement).files?.[0];
275
- if (file) {
276
- handleFile(file);
277
- }
278
- });
279
-
280
- // Process file
281
- processBtn.addEventListener('click', async () => {
282
- if (!currentFile || !currentProcessor) return;
283
-
284
- const startTime = performance.now();
285
- log('Processing file...', 'info');
286
-
287
- try {
288
- processBtn.disabled = true;
289
- results.innerHTML = '<p style="text-align: center; padding: 40px;">⏳ Loading...</p>';
290
-
291
- // Read file as ArrayBuffer
292
- const arrayBuffer = await currentFile.arrayBuffer();
293
-
294
- // Load into tree
295
- log('Loading tree structure...', 'info');
296
- currentTree = await currentProcessor.loadIntoTree(arrayBuffer);
297
-
298
- const loadTime = performance.now() - startTime;
299
- log(`Tree loaded in ${loadTime.toFixed(0)}ms`, 'success');
300
-
301
- // Extract texts
302
- log('Extracting texts...', 'info');
303
- const texts = await currentProcessor.extractTexts(arrayBuffer);
304
- log(`Extracted ${texts.length} texts`, 'success');
305
-
306
- // Update stats
307
- updateStatsForTree(currentTree, texts.length, loadTime);
308
-
309
- // Display results
310
- displayResults(currentTree);
311
- updateConvertButtons();
312
-
313
- log(`✅ Successfully processed ${Object.keys(currentTree.pages).length} pages`, 'success');
314
- } catch (error) {
315
- const errorMsg = (error as Error).message;
316
- log(`❌ Error: ${errorMsg}`, 'error');
317
- results.innerHTML = `<p style="color: #f48771; text-align: center; padding: 40px;">
318
- ❌ Error: ${errorMsg}
319
- </p>`;
320
- } finally {
321
- processBtn.disabled = false;
322
- }
323
- });
324
-
325
- function collectValidationMessages(
326
- result: ValidationResult,
327
- prefix = ''
328
- ): Array<{ type: 'error' | 'warn'; message: string }> {
329
- const messages: Array<{ type: 'error' | 'warn'; message: string }> = [];
330
- const label = prefix ? `${prefix}: ` : '';
331
- result.results.forEach((check) => {
332
- if (!check.valid && check.error) {
333
- messages.push({ type: 'error', message: `${label}${check.description}: ${check.error}` });
334
- }
335
- if (check.warnings?.length) {
336
- check.warnings.forEach((warning) => {
337
- messages.push({ type: 'warn', message: `${label}${check.description}: ${warning}` });
338
- });
339
- }
340
- });
341
- result.sub_results?.forEach((sub) => {
342
- const nextPrefix = `${label}${sub.filename || sub.format}`;
343
- messages.push(...collectValidationMessages(sub, nextPrefix));
344
- });
345
- return messages;
346
- }
347
-
348
- function renderValidationResult(result: ValidationResult) {
349
- validationPanel.style.display = 'block';
350
- validationSummary.classList.remove('success', 'error');
351
- validationSummary.classList.add(result.valid ? 'success' : 'error');
352
- validationSummary.textContent = `${result.valid ? '✅ Valid' : '❌ Invalid'} • ${result.format.toUpperCase()} • ${result.errors} errors, ${result.warnings} warnings`;
353
-
354
- validationList.innerHTML = '';
355
- const messages = collectValidationMessages(result).slice(0, 30);
356
- if (messages.length === 0) {
357
- const empty = document.createElement('div');
358
- empty.className = 'validation-item';
359
- empty.textContent = 'No issues reported.';
360
- validationList.appendChild(empty);
361
- return;
362
- }
363
-
364
- messages.forEach((entry) => {
365
- const item = document.createElement('div');
366
- item.className = `validation-item ${entry.type}`;
367
- item.textContent = entry.message;
368
- validationList.appendChild(item);
369
- });
370
- }
371
-
372
- validateBtn.addEventListener('click', async () => {
373
- if (!currentFile) return;
374
- log('Validating file...', 'info');
375
-
376
- try {
377
- validateBtn.disabled = true;
378
- const arrayBuffer = await currentFile.arrayBuffer();
379
- const result = await validateFileOrBuffer(new Uint8Array(arrayBuffer), currentFile.name);
380
- renderValidationResult(result);
381
- log(
382
- `${result.valid ? '✅' : '❌'} Validation complete: ${result.errors} errors, ${result.warnings} warnings`,
383
- result.valid ? 'success' : 'warn'
384
- );
385
- } catch (error) {
386
- const errorMsg = (error as Error).message;
387
- validationPanel.style.display = 'block';
388
- validationSummary.classList.remove('success');
389
- validationSummary.classList.add('error');
390
- validationSummary.textContent = `❌ Validation failed: ${errorMsg}`;
391
- validationList.innerHTML = '';
392
- log(`❌ Validation failed: ${errorMsg}`, 'error');
393
- } finally {
394
- validateBtn.disabled = !currentFile;
395
- }
396
- });
397
-
398
- // Display results
399
- function displayResults(tree: AACTree) {
400
- results.innerHTML = '';
401
-
402
- const sortedPageIds = Object.keys(tree.pages).sort((a, b) => {
403
- // Show root page first
404
- if (a === tree.rootId) return -1;
405
- if (b === tree.rootId) return 1;
406
- return a.localeCompare(b);
407
- });
408
-
409
- sortedPageIds.forEach((pageId) => {
410
- const page = tree.pages[pageId];
411
- const pageCard = document.createElement('div');
412
- pageCard.className = 'page-card';
413
-
414
- const pageTitle = document.createElement('div');
415
- pageTitle.className = 'page-title';
416
- pageTitle.textContent = `${page.name} ${pageId === tree.rootId ? '🏠' : ''}`;
417
- pageCard.appendChild(pageTitle);
418
-
419
- if (page.buttons.length > 0) {
420
- const buttonGrid = document.createElement('div');
421
- buttonGrid.className = 'button-grid';
422
-
423
- page.buttons.forEach((button) => {
424
- const buttonItem = document.createElement('div');
425
- buttonItem.className = 'button-item';
426
-
427
- const label = document.createElement('div');
428
- label.className = 'button-label';
429
- label.textContent = button.label || '(no label)';
430
- buttonItem.appendChild(label);
431
-
432
- if (button.message) {
433
- const message = document.createElement('div');
434
- message.className = 'button-message';
435
- message.textContent = button.message;
436
- buttonItem.appendChild(message);
437
- }
438
-
439
- const type = document.createElement('div');
440
- type.className = 'button-type';
441
- type.textContent = button.type;
442
-
443
- switch (button.type) {
444
- case 'SPEAK':
445
- type.classList.add('type-speak');
446
- break;
447
- case 'NAVIGATE':
448
- type.classList.add('type-navigate');
449
- break;
450
- default:
451
- type.classList.add('type-other');
452
- }
453
-
454
- buttonItem.appendChild(type);
455
-
456
- // Click handler
457
- buttonItem.addEventListener('click', () => {
458
- if (button.type === 'SPEAK' && button.message) {
459
- log(`🔊 Speaking: "${button.message}"`, 'info');
460
- if ('speechSynthesis' in window) {
461
- const utterance = new SpeechSynthesisUtterance(button.message);
462
- speechSynthesis.speak(utterance);
463
- }
464
- } else if (button.type === 'NAVIGATE' && button.targetPageId) {
465
- const targetPage = tree.pages[button.targetPageId];
466
- if (targetPage) {
467
- log(`🔗 Navigating to: ${targetPage.name}`, 'info');
468
- // Scroll to page
469
- const targetCard = Array.from(results.querySelectorAll('.page-card')).find((card) =>
470
- card.querySelector('.page-title')?.textContent?.includes(targetPage.name)
471
- );
472
- if (targetCard) {
473
- targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
474
- targetCard.style.animation = 'highlight 1s';
475
- }
476
- }
477
- }
478
- });
479
-
480
- buttonGrid.appendChild(buttonItem);
481
- });
482
-
483
- pageCard.appendChild(buttonGrid);
484
- } else {
485
- const noButtons = document.createElement('p');
486
- noButtons.textContent = 'No buttons';
487
- noButtons.style.color = '#999';
488
- noButtons.style.fontSize = '12px';
489
- pageCard.appendChild(noButtons);
490
- }
491
-
492
- results.appendChild(pageCard);
493
- });
494
- }
495
-
496
- // Clear results
497
- clearBtn.addEventListener('click', () => {
498
- currentFile = null;
499
- currentProcessor = null;
500
- currentTree = null;
501
- currentSourceLabel = 'pageset';
502
- fileInput.value = '';
503
- fileInfo.style.display = 'none';
504
- stats.style.display = 'none';
505
- results.innerHTML = '<p style="color: #999; text-align: center; padding: 40px;">Load a file to see its contents here</p>';
506
- testResults.style.display = 'none';
507
- validationPanel.style.display = 'none';
508
- validationSummary.textContent = '';
509
- validationList.innerHTML = '';
510
- logPanel.innerHTML = '<div class="log-entry log-info">Cleared. Ready to process files...</div>';
511
- pagesetOutput.textContent = 'Generate or convert a pageset to preview the output JSON.';
512
- updateConvertButtons();
513
- });
514
-
515
- function sanitizeFilename(name: string): string {
516
- return name
517
- .toLowerCase()
518
- .replace(/[^a-z0-9]+/g, '-')
519
- .replace(/(^-|-$)/g, '') || 'pageset';
520
- }
521
-
522
- function buildSampleTree(template: string): AACTree {
523
- const tree = new AACTree();
524
- tree.metadata = {
525
- name: template === 'home' ? 'Home & Core Demo' : 'Starter Demo',
526
- description: 'Generated in the AAC Processors browser demo',
527
- locale: 'en',
528
- };
529
-
530
- if (template === 'home') {
531
- const hello = new AACButton({ id: 'hello', label: 'Hello', message: 'Hello', action: { type: 'SPEAK' } });
532
- const want = new AACButton({ id: 'want', label: 'I want', message: 'I want', action: { type: 'SPEAK' } });
533
- const help = new AACButton({ id: 'help', label: 'Help', message: 'Help', action: { type: 'SPEAK' } });
534
- const more = new AACButton({
535
- id: 'more',
536
- label: 'More',
537
- targetPageId: 'core',
538
- action: { type: 'NAVIGATE', targetPageId: 'core' },
539
- });
540
- const yes = new AACButton({ id: 'yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
541
- const no = new AACButton({ id: 'no', label: 'No', message: 'No', action: { type: 'SPEAK' } });
542
- const stop = new AACButton({ id: 'stop', label: 'Stop', message: 'Stop', action: { type: 'SPEAK' } });
543
- const go = new AACButton({ id: 'go', label: 'Go', message: 'Go', action: { type: 'SPEAK' } });
544
- const food = new AACButton({
545
- id: 'food',
546
- label: 'Food',
547
- targetPageId: 'food',
548
- action: { type: 'NAVIGATE', targetPageId: 'food' },
549
- });
550
-
551
- const homePage = new AACPage({
552
- id: 'home',
553
- name: 'Home',
554
- buttons: [hello, want, help, more, yes, no, stop, go, food],
555
- grid: [
556
- [hello, want, help],
557
- [more, yes, no],
558
- [stop, go, food],
559
- ],
560
- });
561
-
562
- const hungry = new AACButton({ id: 'hungry', label: 'Hungry', message: 'I am hungry', action: { type: 'SPEAK' } });
563
- const drink = new AACButton({ id: 'drink', label: 'Drink', message: 'I want a drink', action: { type: 'SPEAK' } });
564
- const snack = new AACButton({ id: 'snack', label: 'Snack', message: 'Snack', action: { type: 'SPEAK' } });
565
- const backFood = new AACButton({
566
- id: 'back-food',
567
- label: 'Back',
568
- targetPageId: 'home',
569
- action: { type: 'NAVIGATE', targetPageId: 'home' },
570
- });
571
-
572
- const foodPage = new AACPage({
573
- id: 'food',
574
- name: 'Food',
575
- buttons: [hungry, drink, snack, backFood],
576
- grid: [
577
- [hungry, drink],
578
- [snack, backFood],
579
- ],
580
- });
581
-
582
- const coreYes = new AACButton({ id: 'core-yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
583
- const coreNo = new AACButton({ id: 'core-no', label: 'No', message: 'No', action: { type: 'SPEAK' } });
584
- const coreStop = new AACButton({ id: 'core-stop', label: 'Stop', message: 'Stop', action: { type: 'SPEAK' } });
585
- const coreGo = new AACButton({ id: 'core-go', label: 'Go', message: 'Go', action: { type: 'SPEAK' } });
586
- const backCore = new AACButton({
587
- id: 'back-core',
588
- label: 'Back',
589
- targetPageId: 'home',
590
- action: { type: 'NAVIGATE', targetPageId: 'home' },
591
- });
592
-
593
- const corePage = new AACPage({
594
- id: 'core',
595
- name: 'Core Words',
596
- buttons: [coreYes, coreNo, coreStop, coreGo, backCore],
597
- grid: [
598
- [coreYes, coreNo],
599
- [coreStop, coreGo],
600
- [backCore, null],
601
- ],
602
- });
603
-
604
- tree.addPage(homePage);
605
- tree.addPage(corePage);
606
- tree.addPage(foodPage);
607
- tree.rootId = 'home';
608
- return tree;
609
- }
610
-
611
- const hello = new AACButton({ id: 'hello', label: 'Hello', message: 'Hello', action: { type: 'SPEAK' } });
612
- const thanks = new AACButton({ id: 'thanks', label: 'Thanks', message: 'Thank you', action: { type: 'SPEAK' } });
613
- const yes = new AACButton({ id: 'yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
614
- const more = new AACButton({
615
- id: 'more',
616
- label: 'Feelings',
617
- targetPageId: 'feelings',
618
- action: { type: 'NAVIGATE', targetPageId: 'feelings' },
619
- });
620
-
621
- const homePage = new AACPage({
622
- id: 'home',
623
- name: 'Starter',
624
- buttons: [hello, thanks, yes, more],
625
- grid: [
626
- [hello, thanks],
627
- [yes, more],
628
- ],
629
- });
630
-
631
- const happy = new AACButton({ id: 'happy', label: 'Happy', message: 'I feel happy', action: { type: 'SPEAK' } });
632
- const sad = new AACButton({ id: 'sad', label: 'Sad', message: 'I feel sad', action: { type: 'SPEAK' } });
633
- const back = new AACButton({
634
- id: 'back',
635
- label: 'Back',
636
- targetPageId: 'home',
637
- action: { type: 'NAVIGATE', targetPageId: 'home' },
638
- });
639
-
640
- const feelingsPage = new AACPage({
641
- id: 'feelings',
642
- name: 'Feelings',
643
- buttons: [happy, sad, back],
644
- grid: [
645
- [happy, sad],
646
- [back, null],
647
- ],
648
- });
649
-
650
- tree.addPage(homePage);
651
- tree.addPage(feelingsPage);
652
- tree.rootId = 'home';
653
- return tree;
654
- }
655
-
656
- function buildFallbackObfBoard(page: AACPage, metadata?: AACTree['metadata']) {
657
- const rows = page.grid.length || 1;
658
- const columns = page.grid.reduce((max, row) => Math.max(max, row.length), 0) || page.buttons.length;
659
- const order: (string | null)[][] = [];
660
- const positions = new Map<string, number>();
661
-
662
- if (page.grid.length) {
663
- page.grid.forEach((row, rowIndex) => {
664
- const orderRow: (string | null)[] = [];
665
- for (let colIndex = 0; colIndex < columns; colIndex++) {
666
- const cell = row[colIndex] || null;
667
- if (cell) {
668
- const id = String(cell.id ?? '');
669
- orderRow.push(id);
670
- positions.set(id, rowIndex * columns + colIndex);
671
- } else {
672
- orderRow.push(null);
673
- }
674
- }
675
- order.push(orderRow);
676
- });
677
- } else {
678
- const fallbackRow = page.buttons.map((button, index) => {
679
- const id = String(button.id ?? '');
680
- positions.set(id, index);
681
- return id;
682
- });
683
- order.push(fallbackRow);
684
- }
685
-
686
- return {
687
- format: 'open-board-0.1',
688
- id: page.id,
689
- locale: metadata?.locale || page.locale || 'en',
690
- name: page.name || metadata?.name || 'Board',
691
- description_html: page.descriptionHtml || metadata?.description || '',
692
- grid: { rows, columns, order },
693
- buttons: page.buttons.map((button) => ({
694
- id: button.id,
695
- label: button.label,
696
- vocalization: button.message || button.label,
697
- load_board: button.targetPageId ? { path: button.targetPageId } : undefined,
698
- box_id: positions.get(String(button.id ?? '')),
699
- background_color: button.style?.backgroundColor,
700
- border_color: button.style?.borderColor,
701
- })),
702
- };
703
- }
704
-
705
- async function buildObfExport(tree: AACTree, format: 'obf' | 'obz') {
706
- const obfProcessor = new ObfProcessor();
707
- const obfInternal = obfProcessor as ObfProcessor & {
708
- createObfBoardFromPage?: (page: AACPage, fallbackName: string, metadata?: AACTree['metadata']) => any;
709
- };
710
-
711
- const boards = Object.values(tree.pages).map((page) => ({
712
- pageId: page.id,
713
- board: obfInternal.createObfBoardFromPage
714
- ? obfInternal.createObfBoardFromPage(page, 'Board', tree.metadata)
715
- : buildFallbackObfBoard(page, tree.metadata),
716
- }));
717
-
718
- if (format === 'obf') {
719
- const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
720
- const board =
721
- boards.find((entry) => entry.pageId === rootPage?.id)?.board ?? boards[0]?.board ?? {};
722
- const json = JSON.stringify(board, null, 2);
723
- return { filename: `${sanitizeFilename(tree.metadata?.name || 'pageset')}.obf`, data: json };
724
- }
725
-
726
- const module = await import('jszip');
727
- const JSZip = module.default || module;
728
- const zip = new JSZip();
729
- boards.forEach((entry) => {
730
- zip.file(`${entry.pageId}.obf`, JSON.stringify(entry.board, null, 2));
731
- });
732
- const zipData = await zip.generateAsync({ type: 'uint8array' });
733
- return { filename: `${sanitizeFilename(tree.metadata?.name || 'pageset')}.obz`, data: zipData };
734
- }
735
-
736
- function triggerDownload(data: Uint8Array | string, filename: string, mime: string) {
737
- const blob = new Blob([data], { type: mime });
738
- const url = URL.createObjectURL(blob);
739
- const a = document.createElement('a');
740
- a.href = url;
741
- a.download = filename;
742
- a.click();
743
- URL.revokeObjectURL(url);
744
- }
745
-
746
- createPagesetBtn.addEventListener('click', async () => {
747
- const template = templateSelect.value;
748
- const format = formatSelect.value === 'obz' ? 'obz' : 'obf';
749
- const tree = buildSampleTree(template);
750
- currentTree = tree;
751
- currentSourceLabel = `${tree.metadata?.name || 'sample pageset'}`;
752
- updateConvertButtons();
753
-
754
- const exportData = await buildObfExport(tree, format);
755
- const isObf = typeof exportData.data === 'string';
756
- triggerDownload(
757
- exportData.data,
758
- exportData.filename,
759
- isObf ? 'application/json' : 'application/zip'
760
- );
761
-
762
- pagesetOutput.textContent = isObf
763
- ? exportData.data
764
- : `Generated OBZ with ${Object.keys(tree.pages).length} boards.`;
765
-
766
- log(`Created sample pageset and exported ${exportData.filename}`, 'success');
767
- setConversionStatus(`Exported ${exportData.filename}`, 'success');
768
- });
769
-
770
- previewPagesetBtn.addEventListener('click', () => {
771
- const tree = buildSampleTree(templateSelect.value);
772
- currentTree = tree;
773
- currentSourceLabel = `${tree.metadata?.name || 'sample pageset'}`;
774
- displayResults(tree);
775
- updateStatsForTree(tree, collectTextCount(tree));
776
- updateConvertButtons();
777
- setActiveTab('inspectTab');
778
- log('Previewing sample pageset in viewer', 'info');
779
- });
780
-
781
- convertToObfBtn.addEventListener('click', async () => {
782
- if (!currentTree) return;
783
- const exportData = await buildObfExport(currentTree, 'obf');
784
- triggerDownload(exportData.data, exportData.filename, 'application/json');
785
- pagesetOutput.textContent = exportData.data as string;
786
- log(`Converted ${currentSourceLabel} to ${exportData.filename}`, 'success');
787
- setConversionStatus(`Exported ${exportData.filename}`, 'success');
788
- });
789
-
790
- convertToObzBtn.addEventListener('click', async () => {
791
- if (!currentTree) return;
792
- const exportData = await buildObfExport(currentTree, 'obz');
793
- triggerDownload(exportData.data, exportData.filename, 'application/zip');
794
- pagesetOutput.textContent = `Generated OBZ with ${Object.keys(currentTree.pages).length} boards.`;
795
- log(`Converted ${currentSourceLabel} to ${exportData.filename}`, 'success');
796
- setConversionStatus(`Exported ${exportData.filename}`, 'success');
797
- });
798
-
799
- // Run compatibility tests
800
- runTestsBtn.addEventListener('click', async () => {
801
- log('Running compatibility tests...', 'info');
802
- testResults.style.display = 'block';
803
- testList.innerHTML = '';
804
-
805
- const tests: { name: string; fn: () => Promise<boolean> }[] = [
806
- {
807
- name: 'getProcessor() factory function',
808
- fn: async () => {
809
- const dotProc = getProcessor('.dot');
810
- const opmlProc = getProcessor('.opml');
811
- const obfProc = getProcessor('.obf');
812
- const gridsetProc = getProcessor('.gridset');
813
- const snapProc = getProcessor('.sps');
814
- const touchChatProc = getProcessor('.ce');
815
- return (
816
- dotProc instanceof DotProcessor &&
817
- opmlProc instanceof OpmlProcessor &&
818
- obfProc instanceof ObfProcessor &&
819
- gridsetProc instanceof GridsetProcessor &&
820
- snapProc instanceof SnapProcessor &&
821
- touchChatProc instanceof TouchChatProcessor
822
- );
823
- }
824
- },
825
- {
826
- name: 'getSupportedExtensions() returns all extensions',
827
- fn: async () => {
828
- const extensions = getSupportedExtensions();
829
- const expected = [
830
- '.dot',
831
- '.opml',
832
- '.obf',
833
- '.obz',
834
- '.gridset',
835
- '.spb',
836
- '.sps',
837
- '.ce',
838
- '.plist',
839
- '.grd'
840
- ];
841
- return expected.every((ext) => extensions.includes(ext));
842
- }
843
- },
844
- {
845
- name: 'DotProcessor instantiation',
846
- fn: async () => {
847
- try {
848
- new DotProcessor();
849
- return true;
850
- } catch {
851
- return false;
852
- }
853
- }
854
- },
855
- {
856
- name: 'OpmlProcessor instantiation',
857
- fn: async () => {
858
- try {
859
- new OpmlProcessor();
860
- return true;
861
- } catch {
862
- return false;
863
- }
864
- }
865
- },
866
- {
867
- name: 'ObfProcessor instantiation',
868
- fn: async () => {
869
- try {
870
- new ObfProcessor();
871
- return true;
872
- } catch {
873
- return false;
874
- }
875
- }
876
- },
877
- {
878
- name: 'GridsetProcessor instantiation',
879
- fn: async () => {
880
- try {
881
- new GridsetProcessor();
882
- return true;
883
- } catch {
884
- return false;
885
- }
886
- }
887
- },
888
- {
889
- name: 'SnapProcessor instantiation',
890
- fn: async () => {
891
- try {
892
- new SnapProcessor();
893
- return true;
894
- } catch {
895
- return false;
896
- }
897
- }
898
- },
899
- {
900
- name: 'TouchChatProcessor instantiation',
901
- fn: async () => {
902
- try {
903
- new TouchChatProcessor();
904
- return true;
905
- } catch {
906
- return false;
907
- }
908
- }
909
- },
910
- {
911
- name: 'ApplePanelsProcessor instantiation',
912
- fn: async () => {
913
- try {
914
- new ApplePanelsProcessor();
915
- return true;
916
- } catch {
917
- return false;
918
- }
919
- }
920
- },
921
- {
922
- name: 'AstericsGridProcessor instantiation',
923
- fn: async () => {
924
- try {
925
- new AstericsGridProcessor();
926
- return true;
927
- } catch {
928
- return false;
929
- }
930
- }
931
- },
932
- {
933
- name: 'Processors accept ArrayBuffer type',
934
- fn: async () => {
935
- try {
936
- const proc = new DotProcessor();
937
- const buffer = new Uint8Array([123, 125]); // Invalid but tests type acceptance
938
- await proc.loadIntoTree(buffer); // Will fail but tests that it accepts the type
939
- return true;
940
- } catch {
941
- return true; // Expected to fail with invalid data, but type was accepted
942
- }
943
- }
944
- }
945
- ];
946
-
947
- let passed = 0;
948
- let failed = 0;
949
-
950
- for (const test of tests) {
951
- const item = document.createElement('div');
952
- item.className = 'test-item';
953
-
954
- const status = document.createElement('div');
955
- status.className = 'test-status test-pending';
956
- status.textContent = '⏳';
957
-
958
- const name = document.createElement('div');
959
- name.className = 'test-name';
960
- name.textContent = test.name;
961
-
962
- item.appendChild(status);
963
- item.appendChild(name);
964
- testList.appendChild(item);
965
-
966
- try {
967
- const result = await test.fn();
968
- if (result) {
969
- status.className = 'test-status test-pass';
970
- status.textContent = '✓';
971
- passed++;
972
- log(`✓ ${test.name}`, 'success');
973
- } else {
974
- status.className = 'test-status test-fail';
975
- status.textContent = '✗';
976
- failed++;
977
- log(`✗ ${test.name}`, 'error');
978
- }
979
- } catch (error) {
980
- status.className = 'test-status test-fail';
981
- status.textContent = '✗';
982
- failed++;
983
- log(`✗ ${test.name}: ${(error as Error).message}`, 'error');
984
- }
985
- }
986
-
987
- log(`Tests complete: ${passed} passed, ${failed} failed`, passed === tests.length ? 'success' : 'warn');
988
-
989
- const summary = document.createElement('div');
990
- summary.style.marginTop = '15px';
991
- summary.style.paddingTop = '15px';
992
- summary.style.borderTop = '2px solid #e0e0e0';
993
- summary.style.fontWeight = '600';
994
- summary.textContent = `📊 Summary: ${passed}/${tests.length} tests passed`;
995
- testList.appendChild(summary);
996
- });
997
-
998
- // Log initialization
999
- log('✅ AAC Processors Browser Demo initialized', 'success');
1000
- log('📋 Supported extensions: ' + getSupportedExtensions().join(', '), 'info');
1001
- log('💡 Drop a file or click to upload', 'info');