@willwade/aac-processors 0.1.0 → 0.1.2

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.
@@ -74,9 +74,9 @@ import {
74
74
  GridsetProcessor,
75
75
  ApplePanelsProcessor,
76
76
  AstericsGridProcessor,
77
- type AACTree,
78
- type AACPage,
79
- type AACButton
77
+ AACTree,
78
+ AACPage,
79
+ AACButton
80
80
  } from 'aac-processors';
81
81
 
82
82
  // UI Elements
@@ -93,11 +93,38 @@ const results = document.getElementById('results') as HTMLElement;
93
93
  const logPanel = document.getElementById('logPanel') as HTMLElement;
94
94
  const testResults = document.getElementById('testResults') as HTMLElement;
95
95
  const testList = document.getElementById('testList') as HTMLElement;
96
+ const tabButtons = document.querySelectorAll('.tab-btn') as NodeListOf<HTMLButtonElement>;
97
+ const inspectTab = document.getElementById('inspectTab') as HTMLElement;
98
+ const pagesetTab = document.getElementById('pagesetTab') as HTMLElement;
99
+ const templateSelect = document.getElementById('templateSelect') as HTMLSelectElement;
100
+ const formatSelect = document.getElementById('formatSelect') as HTMLSelectElement;
101
+ const createPagesetBtn = document.getElementById('createPagesetBtn') as HTMLButtonElement;
102
+ const previewPagesetBtn = document.getElementById('previewPagesetBtn') as HTMLButtonElement;
103
+ const convertToObfBtn = document.getElementById('convertToObfBtn') as HTMLButtonElement;
104
+ const convertToObzBtn = document.getElementById('convertToObzBtn') as HTMLButtonElement;
105
+ const conversionStatus = document.getElementById('conversionStatus') as HTMLElement;
106
+ const pagesetOutput = document.getElementById('pagesetOutput') as HTMLElement;
96
107
 
97
108
  // State
98
109
  let currentFile: File | null = null;
99
110
  let currentProcessor: any = null;
100
111
  let currentTree: AACTree | null = null;
112
+ let currentSourceLabel = 'pageset';
113
+
114
+ // Tabs
115
+ function setActiveTab(tabId: string) {
116
+ tabButtons.forEach((btn) => {
117
+ btn.classList.toggle('active', btn.dataset.tab === tabId);
118
+ });
119
+ inspectTab.classList.toggle('active', tabId === 'inspectTab');
120
+ pagesetTab.classList.toggle('active', tabId === 'pagesetTab');
121
+ }
122
+
123
+ tabButtons.forEach((btn) => {
124
+ btn.addEventListener('click', () => {
125
+ setActiveTab(btn.dataset.tab || 'inspectTab');
126
+ });
127
+ });
101
128
 
102
129
  // Logging
103
130
  function log(message: string, type: 'info' | 'success' | 'error' | 'warn' = 'info') {
@@ -109,6 +136,52 @@ function log(message: string, type: 'info' | 'success' | 'error' | 'warn' = 'inf
109
136
  console.log(`[${type.toUpperCase()}]`, message);
110
137
  }
111
138
 
139
+ function setConversionStatus(message: string, state: 'success' | 'warn' | 'info' = 'info') {
140
+ conversionStatus.textContent = message;
141
+ conversionStatus.classList.remove('success', 'warn');
142
+ if (state !== 'info') {
143
+ conversionStatus.classList.add(state);
144
+ }
145
+ }
146
+
147
+ function updateConvertButtons() {
148
+ const hasTree = !!currentTree;
149
+ convertToObfBtn.disabled = !hasTree;
150
+ convertToObzBtn.disabled = !hasTree;
151
+ if (!hasTree) {
152
+ setConversionStatus('No pageset loaded yet.', 'info');
153
+ } else {
154
+ setConversionStatus(`Ready to export: ${currentSourceLabel}`, 'success');
155
+ }
156
+ }
157
+
158
+ function updateStatsForTree(tree: AACTree, textCount?: number, loadTimeMs?: number) {
159
+ const pageCount = Object.keys(tree.pages).length;
160
+ const buttonCount = Object.values(tree.pages).reduce(
161
+ (sum: number, page: AACPage) => sum + page.buttons.length,
162
+ 0
163
+ );
164
+
165
+ document.getElementById('pageCount')!.textContent = pageCount.toString();
166
+ document.getElementById('buttonCount')!.textContent = buttonCount.toString();
167
+ document.getElementById('textCount')!.textContent = (textCount ?? 0).toString();
168
+ document.getElementById('loadTime')!.textContent =
169
+ loadTimeMs !== undefined ? `${loadTimeMs.toFixed(0)}ms` : '—';
170
+ stats.style.display = 'grid';
171
+ }
172
+
173
+ function collectTextCount(tree: AACTree): number {
174
+ const texts = new Set<string>();
175
+ Object.values(tree.pages).forEach((page) => {
176
+ if (page.name) texts.add(page.name);
177
+ page.buttons.forEach((button) => {
178
+ if (button.label) texts.add(button.label);
179
+ if (button.message) texts.add(button.message);
180
+ });
181
+ });
182
+ return texts.size;
183
+ }
184
+
112
185
  // Get file extension
113
186
  function getFileExtension(filename: string): string {
114
187
  const match = filename.toLowerCase().match(/\.\w+$/);
@@ -146,6 +219,7 @@ function handleFile(file: File) {
146
219
  fileDetails.textContent = `${file.name} • ${formatFileSize(file.size)}`;
147
220
  fileInfo.style.display = 'block';
148
221
  processBtn.disabled = false;
222
+ currentSourceLabel = file.name;
149
223
 
150
224
  log(`Using processor: ${currentProcessor.constructor.name}`, 'success');
151
225
  } catch (error) {
@@ -213,22 +287,13 @@ processBtn.addEventListener('click', async () => {
213
287
  log(`Extracted ${texts.length} texts`, 'success');
214
288
 
215
289
  // Update stats
216
- const pageCount = Object.keys(currentTree.pages).length;
217
- const buttonCount = Object.values(currentTree.pages).reduce(
218
- (sum: number, page: AACPage) => sum + page.buttons.length,
219
- 0
220
- );
221
-
222
- document.getElementById('pageCount')!.textContent = pageCount.toString();
223
- document.getElementById('buttonCount')!.textContent = buttonCount.toString();
224
- document.getElementById('textCount')!.textContent = texts.length.toString();
225
- document.getElementById('loadTime')!.textContent = `${loadTime.toFixed(0)}ms`;
226
- stats.style.display = 'grid';
290
+ updateStatsForTree(currentTree, texts.length, loadTime);
227
291
 
228
292
  // Display results
229
293
  displayResults(currentTree);
294
+ updateConvertButtons();
230
295
 
231
- log(`✅ Successfully processed ${pageCount} pages with ${buttonCount} buttons`, 'success');
296
+ log(`✅ Successfully processed ${Object.keys(currentTree.pages).length} pages`, 'success');
232
297
  } catch (error) {
233
298
  const errorMsg = (error as Error).message;
234
299
  log(`❌ Error: ${errorMsg}`, 'error');
@@ -343,12 +408,299 @@ clearBtn.addEventListener('click', () => {
343
408
  currentFile = null;
344
409
  currentProcessor = null;
345
410
  currentTree = null;
411
+ currentSourceLabel = 'pageset';
346
412
  fileInput.value = '';
347
413
  fileInfo.style.display = 'none';
348
414
  stats.style.display = 'none';
349
415
  results.innerHTML = '<p style="color: #999; text-align: center; padding: 40px;">Load a file to see its contents here</p>';
350
416
  testResults.style.display = 'none';
351
417
  logPanel.innerHTML = '<div class="log-entry log-info">Cleared. Ready to process files...</div>';
418
+ pagesetOutput.textContent = 'Generate or convert a pageset to preview the output JSON.';
419
+ updateConvertButtons();
420
+ });
421
+
422
+ function sanitizeFilename(name: string): string {
423
+ return name
424
+ .toLowerCase()
425
+ .replace(/[^a-z0-9]+/g, '-')
426
+ .replace(/(^-|-$)/g, '') || 'pageset';
427
+ }
428
+
429
+ function buildSampleTree(template: string): AACTree {
430
+ const tree = new AACTree();
431
+ tree.metadata = {
432
+ name: template === 'home' ? 'Home & Core Demo' : 'Starter Demo',
433
+ description: 'Generated in the AAC Processors browser demo',
434
+ locale: 'en',
435
+ };
436
+
437
+ if (template === 'home') {
438
+ const hello = new AACButton({ id: 'hello', label: 'Hello', message: 'Hello', action: { type: 'SPEAK' } });
439
+ const want = new AACButton({ id: 'want', label: 'I want', message: 'I want', action: { type: 'SPEAK' } });
440
+ const help = new AACButton({ id: 'help', label: 'Help', message: 'Help', action: { type: 'SPEAK' } });
441
+ const more = new AACButton({
442
+ id: 'more',
443
+ label: 'More',
444
+ targetPageId: 'core',
445
+ action: { type: 'NAVIGATE', targetPageId: 'core' },
446
+ });
447
+ const yes = new AACButton({ id: 'yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
448
+ const no = new AACButton({ id: 'no', label: 'No', message: 'No', action: { type: 'SPEAK' } });
449
+ const stop = new AACButton({ id: 'stop', label: 'Stop', message: 'Stop', action: { type: 'SPEAK' } });
450
+ const go = new AACButton({ id: 'go', label: 'Go', message: 'Go', action: { type: 'SPEAK' } });
451
+ const food = new AACButton({
452
+ id: 'food',
453
+ label: 'Food',
454
+ targetPageId: 'food',
455
+ action: { type: 'NAVIGATE', targetPageId: 'food' },
456
+ });
457
+
458
+ const homePage = new AACPage({
459
+ id: 'home',
460
+ name: 'Home',
461
+ buttons: [hello, want, help, more, yes, no, stop, go, food],
462
+ grid: [
463
+ [hello, want, help],
464
+ [more, yes, no],
465
+ [stop, go, food],
466
+ ],
467
+ });
468
+
469
+ const hungry = new AACButton({ id: 'hungry', label: 'Hungry', message: 'I am hungry', action: { type: 'SPEAK' } });
470
+ const drink = new AACButton({ id: 'drink', label: 'Drink', message: 'I want a drink', action: { type: 'SPEAK' } });
471
+ const snack = new AACButton({ id: 'snack', label: 'Snack', message: 'Snack', action: { type: 'SPEAK' } });
472
+ const backFood = new AACButton({
473
+ id: 'back-food',
474
+ label: 'Back',
475
+ targetPageId: 'home',
476
+ action: { type: 'NAVIGATE', targetPageId: 'home' },
477
+ });
478
+
479
+ const foodPage = new AACPage({
480
+ id: 'food',
481
+ name: 'Food',
482
+ buttons: [hungry, drink, snack, backFood],
483
+ grid: [
484
+ [hungry, drink],
485
+ [snack, backFood],
486
+ ],
487
+ });
488
+
489
+ const coreYes = new AACButton({ id: 'core-yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
490
+ const coreNo = new AACButton({ id: 'core-no', label: 'No', message: 'No', action: { type: 'SPEAK' } });
491
+ const coreStop = new AACButton({ id: 'core-stop', label: 'Stop', message: 'Stop', action: { type: 'SPEAK' } });
492
+ const coreGo = new AACButton({ id: 'core-go', label: 'Go', message: 'Go', action: { type: 'SPEAK' } });
493
+ const backCore = new AACButton({
494
+ id: 'back-core',
495
+ label: 'Back',
496
+ targetPageId: 'home',
497
+ action: { type: 'NAVIGATE', targetPageId: 'home' },
498
+ });
499
+
500
+ const corePage = new AACPage({
501
+ id: 'core',
502
+ name: 'Core Words',
503
+ buttons: [coreYes, coreNo, coreStop, coreGo, backCore],
504
+ grid: [
505
+ [coreYes, coreNo],
506
+ [coreStop, coreGo],
507
+ [backCore, null],
508
+ ],
509
+ });
510
+
511
+ tree.addPage(homePage);
512
+ tree.addPage(corePage);
513
+ tree.addPage(foodPage);
514
+ tree.rootId = 'home';
515
+ return tree;
516
+ }
517
+
518
+ const hello = new AACButton({ id: 'hello', label: 'Hello', message: 'Hello', action: { type: 'SPEAK' } });
519
+ const thanks = new AACButton({ id: 'thanks', label: 'Thanks', message: 'Thank you', action: { type: 'SPEAK' } });
520
+ const yes = new AACButton({ id: 'yes', label: 'Yes', message: 'Yes', action: { type: 'SPEAK' } });
521
+ const more = new AACButton({
522
+ id: 'more',
523
+ label: 'Feelings',
524
+ targetPageId: 'feelings',
525
+ action: { type: 'NAVIGATE', targetPageId: 'feelings' },
526
+ });
527
+
528
+ const homePage = new AACPage({
529
+ id: 'home',
530
+ name: 'Starter',
531
+ buttons: [hello, thanks, yes, more],
532
+ grid: [
533
+ [hello, thanks],
534
+ [yes, more],
535
+ ],
536
+ });
537
+
538
+ const happy = new AACButton({ id: 'happy', label: 'Happy', message: 'I feel happy', action: { type: 'SPEAK' } });
539
+ const sad = new AACButton({ id: 'sad', label: 'Sad', message: 'I feel sad', action: { type: 'SPEAK' } });
540
+ const back = new AACButton({
541
+ id: 'back',
542
+ label: 'Back',
543
+ targetPageId: 'home',
544
+ action: { type: 'NAVIGATE', targetPageId: 'home' },
545
+ });
546
+
547
+ const feelingsPage = new AACPage({
548
+ id: 'feelings',
549
+ name: 'Feelings',
550
+ buttons: [happy, sad, back],
551
+ grid: [
552
+ [happy, sad],
553
+ [back, null],
554
+ ],
555
+ });
556
+
557
+ tree.addPage(homePage);
558
+ tree.addPage(feelingsPage);
559
+ tree.rootId = 'home';
560
+ return tree;
561
+ }
562
+
563
+ function buildFallbackObfBoard(page: AACPage, metadata?: AACTree['metadata']) {
564
+ const rows = page.grid.length || 1;
565
+ const columns = page.grid.reduce((max, row) => Math.max(max, row.length), 0) || page.buttons.length;
566
+ const order: (string | null)[][] = [];
567
+ const positions = new Map<string, number>();
568
+
569
+ if (page.grid.length) {
570
+ page.grid.forEach((row, rowIndex) => {
571
+ const orderRow: (string | null)[] = [];
572
+ for (let colIndex = 0; colIndex < columns; colIndex++) {
573
+ const cell = row[colIndex] || null;
574
+ if (cell) {
575
+ const id = String(cell.id ?? '');
576
+ orderRow.push(id);
577
+ positions.set(id, rowIndex * columns + colIndex);
578
+ } else {
579
+ orderRow.push(null);
580
+ }
581
+ }
582
+ order.push(orderRow);
583
+ });
584
+ } else {
585
+ const fallbackRow = page.buttons.map((button, index) => {
586
+ const id = String(button.id ?? '');
587
+ positions.set(id, index);
588
+ return id;
589
+ });
590
+ order.push(fallbackRow);
591
+ }
592
+
593
+ return {
594
+ format: 'open-board-0.1',
595
+ id: page.id,
596
+ locale: metadata?.locale || page.locale || 'en',
597
+ name: page.name || metadata?.name || 'Board',
598
+ description_html: page.descriptionHtml || metadata?.description || '',
599
+ grid: { rows, columns, order },
600
+ buttons: page.buttons.map((button) => ({
601
+ id: button.id,
602
+ label: button.label,
603
+ vocalization: button.message || button.label,
604
+ load_board: button.targetPageId ? { path: button.targetPageId } : undefined,
605
+ box_id: positions.get(String(button.id ?? '')),
606
+ background_color: button.style?.backgroundColor,
607
+ border_color: button.style?.borderColor,
608
+ })),
609
+ };
610
+ }
611
+
612
+ async function buildObfExport(tree: AACTree, format: 'obf' | 'obz') {
613
+ const obfProcessor = new ObfProcessor();
614
+ const obfInternal = obfProcessor as ObfProcessor & {
615
+ createObfBoardFromPage?: (page: AACPage, fallbackName: string, metadata?: AACTree['metadata']) => any;
616
+ };
617
+
618
+ const boards = Object.values(tree.pages).map((page) => ({
619
+ pageId: page.id,
620
+ board: obfInternal.createObfBoardFromPage
621
+ ? obfInternal.createObfBoardFromPage(page, 'Board', tree.metadata)
622
+ : buildFallbackObfBoard(page, tree.metadata),
623
+ }));
624
+
625
+ if (format === 'obf') {
626
+ const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
627
+ const board =
628
+ boards.find((entry) => entry.pageId === rootPage?.id)?.board ?? boards[0]?.board ?? {};
629
+ const json = JSON.stringify(board, null, 2);
630
+ return { filename: `${sanitizeFilename(tree.metadata?.name || 'pageset')}.obf`, data: json };
631
+ }
632
+
633
+ const module = await import('jszip');
634
+ const JSZip = module.default || module;
635
+ const zip = new JSZip();
636
+ boards.forEach((entry) => {
637
+ zip.file(`${entry.pageId}.obf`, JSON.stringify(entry.board, null, 2));
638
+ });
639
+ const zipData = await zip.generateAsync({ type: 'uint8array' });
640
+ return { filename: `${sanitizeFilename(tree.metadata?.name || 'pageset')}.obz`, data: zipData };
641
+ }
642
+
643
+ function triggerDownload(data: Uint8Array | string, filename: string, mime: string) {
644
+ const blob = new Blob([data], { type: mime });
645
+ const url = URL.createObjectURL(blob);
646
+ const a = document.createElement('a');
647
+ a.href = url;
648
+ a.download = filename;
649
+ a.click();
650
+ URL.revokeObjectURL(url);
651
+ }
652
+
653
+ createPagesetBtn.addEventListener('click', async () => {
654
+ const template = templateSelect.value;
655
+ const format = formatSelect.value === 'obz' ? 'obz' : 'obf';
656
+ const tree = buildSampleTree(template);
657
+ currentTree = tree;
658
+ currentSourceLabel = `${tree.metadata?.name || 'sample pageset'}`;
659
+ updateConvertButtons();
660
+
661
+ const exportData = await buildObfExport(tree, format);
662
+ const isObf = typeof exportData.data === 'string';
663
+ triggerDownload(
664
+ exportData.data,
665
+ exportData.filename,
666
+ isObf ? 'application/json' : 'application/zip'
667
+ );
668
+
669
+ pagesetOutput.textContent = isObf
670
+ ? exportData.data
671
+ : `Generated OBZ with ${Object.keys(tree.pages).length} boards.`;
672
+
673
+ log(`Created sample pageset and exported ${exportData.filename}`, 'success');
674
+ setConversionStatus(`Exported ${exportData.filename}`, 'success');
675
+ });
676
+
677
+ previewPagesetBtn.addEventListener('click', () => {
678
+ const tree = buildSampleTree(templateSelect.value);
679
+ currentTree = tree;
680
+ currentSourceLabel = `${tree.metadata?.name || 'sample pageset'}`;
681
+ displayResults(tree);
682
+ updateStatsForTree(tree, collectTextCount(tree));
683
+ updateConvertButtons();
684
+ setActiveTab('inspectTab');
685
+ log('Previewing sample pageset in viewer', 'info');
686
+ });
687
+
688
+ convertToObfBtn.addEventListener('click', async () => {
689
+ if (!currentTree) return;
690
+ const exportData = await buildObfExport(currentTree, 'obf');
691
+ triggerDownload(exportData.data, exportData.filename, 'application/json');
692
+ pagesetOutput.textContent = exportData.data as string;
693
+ log(`Converted ${currentSourceLabel} to ${exportData.filename}`, 'success');
694
+ setConversionStatus(`Exported ${exportData.filename}`, 'success');
695
+ });
696
+
697
+ convertToObzBtn.addEventListener('click', async () => {
698
+ if (!currentTree) return;
699
+ const exportData = await buildObfExport(currentTree, 'obz');
700
+ triggerDownload(exportData.data, exportData.filename, 'application/zip');
701
+ pagesetOutput.textContent = `Generated OBZ with ${Object.keys(currentTree.pages).length} boards.`;
702
+ log(`Converted ${currentSourceLabel} to ${exportData.filename}`, 'success');
703
+ setConversionStatus(`Exported ${exportData.filename}`, 'success');
352
704
  });
353
705
 
354
706
  // Run compatibility tests
@@ -4,7 +4,11 @@ import path from 'path';
4
4
  export default defineConfig({
5
5
  resolve: {
6
6
  alias: {
7
- 'aac-processors': path.resolve(__dirname, '../../src/index.browser.ts')
7
+ 'aac-processors': path.resolve(__dirname, '../../src/index.browser.ts'),
8
+ stream: path.resolve(__dirname, 'node_modules/stream-browserify'),
9
+ events: path.resolve(__dirname, 'node_modules/events'),
10
+ timers: path.resolve(__dirname, 'node_modules/timers-browserify'),
11
+ util: path.resolve(__dirname, 'node_modules/util')
8
12
  }
9
13
  },
10
14
  optimizeDeps: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
5
5
  "main": "dist/index.js",
6
6
  "browser": "dist/browser/index.browser.js",
@@ -93,6 +93,8 @@
93
93
  "lint:fix": "eslint \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" --fix",
94
94
  "format": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" \"*.{js,ts,json,md}\"",
95
95
  "format:check": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" \"*.{js,ts,json,md}\"",
96
+ "smoke:browser": "node scripts/smoke-browser-bundle.js",
97
+ "verify:browser-build": "node scripts/verify-browser-build.js",
96
98
  "test": "npm run build && jest",
97
99
  "test:watch": "npm run build && jest --watch",
98
100
  "test:coverage": "npm run build && jest --coverage",
@@ -101,8 +103,8 @@
101
103
  "docs": "typedoc",
102
104
  "coverage:report": "node scripts/coverage-analysis.js",
103
105
  "type-check": "tsc --noEmit",
104
- "prepublishOnly": "npm run build",
105
- "prepack": "npm run build"
106
+ "prepublishOnly": "npm run build:all && npm run verify:browser-build",
107
+ "prepack": "npm run build:all && npm run verify:browser-build"
106
108
  },
107
109
  "keywords": [
108
110
  "aac",