@willwade/aac-processors 0.0.29 → 0.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.
Files changed (92) hide show
  1. package/README.md +52 -852
  2. package/dist/browser/core/baseProcessor.js +241 -0
  3. package/dist/browser/core/stringCasing.js +179 -0
  4. package/dist/browser/core/treeStructure.js +255 -0
  5. package/dist/browser/index.browser.js +73 -0
  6. package/dist/browser/processors/applePanelsProcessor.js +582 -0
  7. package/dist/browser/processors/astericsGridProcessor.js +1509 -0
  8. package/dist/browser/processors/dotProcessor.js +221 -0
  9. package/dist/browser/processors/gridset/commands.js +962 -0
  10. package/dist/browser/processors/gridset/crypto.js +53 -0
  11. package/dist/browser/processors/gridset/password.js +43 -0
  12. package/dist/browser/processors/gridset/pluginTypes.js +277 -0
  13. package/dist/browser/processors/gridset/resolver.js +137 -0
  14. package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
  15. package/dist/browser/processors/gridset/symbols.js +421 -0
  16. package/dist/browser/processors/gridsetProcessor.js +2002 -0
  17. package/dist/browser/processors/obfProcessor.js +705 -0
  18. package/dist/browser/processors/opmlProcessor.js +274 -0
  19. package/dist/browser/types/aac.js +38 -0
  20. package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
  21. package/dist/browser/utilities/translation/translationProcessor.js +200 -0
  22. package/dist/browser/utils/io.js +95 -0
  23. package/dist/browser/validation/baseValidator.js +156 -0
  24. package/dist/browser/validation/gridsetValidator.js +355 -0
  25. package/dist/browser/validation/obfValidator.js +500 -0
  26. package/dist/browser/validation/validationTypes.js +46 -0
  27. package/dist/cli/index.js +5 -5
  28. package/dist/core/analyze.d.ts +2 -2
  29. package/dist/core/analyze.js +2 -2
  30. package/dist/core/baseProcessor.d.ts +5 -4
  31. package/dist/core/baseProcessor.js +22 -27
  32. package/dist/core/treeStructure.d.ts +5 -5
  33. package/dist/core/treeStructure.js +1 -4
  34. package/dist/index.browser.d.ts +37 -0
  35. package/dist/index.browser.js +99 -0
  36. package/dist/index.d.ts +1 -48
  37. package/dist/index.js +1 -136
  38. package/dist/index.node.d.ts +48 -0
  39. package/dist/index.node.js +152 -0
  40. package/dist/processors/applePanelsProcessor.d.ts +5 -4
  41. package/dist/processors/applePanelsProcessor.js +58 -62
  42. package/dist/processors/astericsGridProcessor.d.ts +7 -6
  43. package/dist/processors/astericsGridProcessor.js +31 -42
  44. package/dist/processors/dotProcessor.d.ts +5 -4
  45. package/dist/processors/dotProcessor.js +25 -33
  46. package/dist/processors/excelProcessor.d.ts +4 -3
  47. package/dist/processors/excelProcessor.js +6 -3
  48. package/dist/processors/gridset/crypto.d.ts +18 -0
  49. package/dist/processors/gridset/crypto.js +57 -0
  50. package/dist/processors/gridset/helpers.d.ts +1 -1
  51. package/dist/processors/gridset/helpers.js +18 -8
  52. package/dist/processors/gridset/password.d.ts +20 -3
  53. package/dist/processors/gridset/password.js +17 -3
  54. package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
  55. package/dist/processors/gridset/wordlistHelpers.js +21 -20
  56. package/dist/processors/gridsetProcessor.d.ts +7 -12
  57. package/dist/processors/gridsetProcessor.js +118 -77
  58. package/dist/processors/obfProcessor.d.ts +9 -7
  59. package/dist/processors/obfProcessor.js +131 -56
  60. package/dist/processors/obfsetProcessor.d.ts +5 -4
  61. package/dist/processors/obfsetProcessor.js +10 -16
  62. package/dist/processors/opmlProcessor.d.ts +5 -4
  63. package/dist/processors/opmlProcessor.js +27 -34
  64. package/dist/processors/snapProcessor.d.ts +8 -7
  65. package/dist/processors/snapProcessor.js +15 -12
  66. package/dist/processors/touchchatProcessor.d.ts +8 -7
  67. package/dist/processors/touchchatProcessor.js +22 -17
  68. package/dist/types/aac.d.ts +0 -2
  69. package/dist/types/aac.js +2 -0
  70. package/dist/utils/io.d.ts +12 -0
  71. package/dist/utils/io.js +107 -0
  72. package/dist/validation/gridsetValidator.js +7 -7
  73. package/dist/validation/snapValidator.js +28 -35
  74. package/docs/BROWSER_USAGE.md +618 -0
  75. package/examples/README.md +77 -0
  76. package/examples/browser-test-server.js +81 -0
  77. package/examples/browser-test.html +331 -0
  78. package/examples/vitedemo/QUICKSTART.md +74 -0
  79. package/examples/vitedemo/README.md +157 -0
  80. package/examples/vitedemo/index.html +376 -0
  81. package/examples/vitedemo/package-lock.json +1221 -0
  82. package/examples/vitedemo/package.json +18 -0
  83. package/examples/vitedemo/src/main.ts +519 -0
  84. package/examples/vitedemo/test-files/example.dot +14 -0
  85. package/examples/vitedemo/test-files/example.grd +1 -0
  86. package/examples/vitedemo/test-files/example.gridset +0 -0
  87. package/examples/vitedemo/test-files/example.obz +0 -0
  88. package/examples/vitedemo/test-files/example.opml +18 -0
  89. package/examples/vitedemo/test-files/simple.obf +53 -0
  90. package/examples/vitedemo/tsconfig.json +24 -0
  91. package/examples/vitedemo/vite.config.ts +34 -0
  92. package/package.json +20 -4
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Base Processor for AAC File Formats
3
+ *
4
+ * This module provides base functionality for processing AAC (Augmentative and Alternative
5
+ * Communication) files across various formats (gridset, OBF, Snap, TouchChat, etc.).
6
+ *
7
+ * ## LLM-Based Translation with Symbol Preservation
8
+ *
9
+ * All processor formats support LLM-based translation that preserves symbol-to-word
10
+ * associations across languages. This is critical for AAC systems where visual symbols
11
+ * are attached to specific words.
12
+ *
13
+ * ### Usage Example:
14
+ *
15
+ * ```typescript
16
+ * import { extractAllButtonsForTranslation, createTranslationPrompt } from '../optional/translation/translationProcessor';
17
+ *
18
+ * // 1. Extract buttons from your format
19
+ * const buttons = extractAllButtonsForTranslation(myFormatButtons, (button) => ({
20
+ * pageId: button.pageId,
21
+ * pageName: button.pageName
22
+ * }));
23
+ *
24
+ * // 2. Create prompt for LLM
25
+ * const prompt = createTranslationPrompt(buttons, 'Spanish');
26
+ *
27
+ * // 3. Send to LLM (Gemini, GPT, etc.) and get response
28
+ * const llmResponse = await callLLMAPI(prompt);
29
+ *
30
+ * // 4. Apply translations to your format
31
+ * processor.processLLMTranslations(filePath, llmResponse, outputPath);
32
+ * ```
33
+ *
34
+ * ### Format-Specific Implementation:
35
+ *
36
+ * Each processor should implement:
37
+ * - `extractSymbolsForLLM()` - Uses extractAllButtonsForTranslation() utility
38
+ * - `processLLMTranslations()` - Applies translations using format-specific logic
39
+ *
40
+ * See `src/utilities/translation/translationProcessor.ts` for shared utilities.
41
+ */
42
+ import { AACSemanticCategory } from './treeStructure';
43
+ import { detectCasing, isNumericOrEmpty } from './stringCasing';
44
+ class BaseProcessor {
45
+ constructor(options = {}) {
46
+ // Default configuration: exclude navigation/system buttons
47
+ this.options = {
48
+ excludeNavigationButtons: true,
49
+ excludeSystemButtons: true,
50
+ preserveAllButtons: false,
51
+ ...options,
52
+ };
53
+ }
54
+ // Helper method to determine if a button should be filtered out
55
+ shouldFilterButton(button) {
56
+ // If preserveAllButtons is true, never filter
57
+ if (this.options.preserveAllButtons) {
58
+ return false;
59
+ }
60
+ // Apply custom filter if provided
61
+ if (this.options.customButtonFilter) {
62
+ return !this.options.customButtonFilter(button);
63
+ }
64
+ // Check semantic action-based filtering
65
+ if (button.semanticAction) {
66
+ const { category, intent } = button.semanticAction;
67
+ // Filter specific navigation intents (toolbar navigation only)
68
+ if (this.options.excludeNavigationButtons) {
69
+ const i = String(intent);
70
+ if (i === 'GO_BACK' || i === 'GO_HOME') {
71
+ return true;
72
+ }
73
+ }
74
+ // Filter system/text editing buttons by category
75
+ if (this.options.excludeSystemButtons && category === AACSemanticCategory.TEXT_EDITING) {
76
+ return true;
77
+ }
78
+ // Filter specific system intents
79
+ if (this.options.excludeSystemButtons) {
80
+ const i = String(intent);
81
+ if (i === 'DELETE_WORD' ||
82
+ i === 'DELETE_CHARACTER' ||
83
+ i === 'CLEAR_TEXT' ||
84
+ i === 'COPY_TEXT') {
85
+ return true;
86
+ }
87
+ }
88
+ }
89
+ // Fallback: check button labels for common navigation/system terms
90
+ // Only apply label-based filtering if button doesn't have semantic actions
91
+ if (!button.semanticAction &&
92
+ (this.options.excludeNavigationButtons || this.options.excludeSystemButtons)) {
93
+ const label = button.label?.toLowerCase() || '';
94
+ const message = button.message?.toLowerCase() || '';
95
+ // More conservative navigation terms (exclude "more" since it's often used for legitimate page navigation)
96
+ const navigationTerms = ['back', 'home', 'menu', 'settings'];
97
+ const systemTerms = ['delete', 'clear', 'copy', 'paste', 'undo', 'redo'];
98
+ if (this.options.excludeNavigationButtons &&
99
+ navigationTerms.some((term) => label.includes(term) || message.includes(term))) {
100
+ return true;
101
+ }
102
+ if (this.options.excludeSystemButtons &&
103
+ systemTerms.some((term) => label.includes(term) || message.includes(term))) {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ // Helper method to filter buttons from a page
110
+ filterPageButtons(buttons) {
111
+ return buttons.filter((button) => !this.shouldFilterButton(button));
112
+ }
113
+ /**
114
+ * Generic implementation for extracting strings with metadata
115
+ * Can be used by any processor that doesn't need format-specific logic
116
+ * @param filePath - Path to the AAC file
117
+ * @returns Promise with extracted strings and metadata
118
+ */
119
+ async extractStringsWithMetadataGeneric(filePath) {
120
+ try {
121
+ const tree = await this.loadIntoTree(filePath);
122
+ const extractedMap = new Map();
123
+ // Process all pages and buttons
124
+ Object.values(tree.pages).forEach((page) => {
125
+ // Process page names
126
+ if (page.name && page.name.trim().length > 1 && !isNumericOrEmpty(page.name)) {
127
+ const key = page.name.trim().toLowerCase();
128
+ const vocabLocation = {
129
+ table: 'pages',
130
+ id: page.id,
131
+ column: 'NAME',
132
+ casing: detectCasing(page.name),
133
+ };
134
+ this.addToExtractedMap(extractedMap, key, page.name.trim(), vocabLocation);
135
+ }
136
+ page.buttons.forEach((button) => {
137
+ // Process button labels
138
+ if (button.label && button.label.trim().length > 1 && !isNumericOrEmpty(button.label)) {
139
+ const key = button.label.trim().toLowerCase();
140
+ const vocabLocation = {
141
+ table: 'buttons',
142
+ id: button.id,
143
+ column: 'LABEL',
144
+ casing: detectCasing(button.label),
145
+ };
146
+ this.addToExtractedMap(extractedMap, key, button.label.trim(), vocabLocation);
147
+ }
148
+ // Process button messages (if different from label)
149
+ if (button.message &&
150
+ button.message !== button.label &&
151
+ button.message.trim().length > 1 &&
152
+ !isNumericOrEmpty(button.message)) {
153
+ const key = button.message.trim().toLowerCase();
154
+ const vocabLocation = {
155
+ table: 'buttons',
156
+ id: button.id,
157
+ column: 'MESSAGE',
158
+ casing: detectCasing(button.message),
159
+ };
160
+ this.addToExtractedMap(extractedMap, key, button.message.trim(), vocabLocation);
161
+ }
162
+ });
163
+ });
164
+ const extractedStrings = Array.from(extractedMap.values());
165
+ return { errors: [], extractedStrings };
166
+ }
167
+ catch (error) {
168
+ return {
169
+ errors: [
170
+ {
171
+ message: error instanceof Error ? error.message : 'Unknown extraction error',
172
+ step: 'EXTRACT',
173
+ },
174
+ ],
175
+ extractedStrings: [],
176
+ };
177
+ }
178
+ }
179
+ /**
180
+ * Generic implementation for generating translated downloads
181
+ * Can be used by any processor that doesn't need format-specific logic
182
+ * @param filePath - Path to the original AAC file
183
+ * @param translatedStrings - Array of translated string data
184
+ * @param sourceStrings - Array of source string data
185
+ * @returns Promise with path to the generated translated file
186
+ */
187
+ async generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings) {
188
+ // Build translation map from the provided data
189
+ const translations = new Map();
190
+ sourceStrings.forEach((sourceString) => {
191
+ const translated = translatedStrings.find((ts) => ts.sourcestringid.toString() === sourceString.id.toString());
192
+ if (translated) {
193
+ const translatedText = translated.overridestring.length > 0
194
+ ? translated.overridestring
195
+ : translated.translatedstring;
196
+ translations.set(sourceString.sourcestring, translatedText);
197
+ }
198
+ });
199
+ // Generate output path based on file extension
200
+ const outputPath = this.generateTranslatedOutputPath(filePath);
201
+ // Use existing processTexts method (now async)
202
+ await this.processTexts(filePath, translations, outputPath);
203
+ return outputPath;
204
+ }
205
+ /**
206
+ * Helper method to add extracted strings to the map, handling duplicates
207
+ * @param extractedMap - Map to store extracted strings
208
+ * @param key - Lowercase key for deduplication
209
+ * @param originalString - Original string with proper casing
210
+ * @param vocabLocation - Metadata about where the string was found
211
+ */
212
+ addToExtractedMap(extractedMap, key, originalString, vocabLocation) {
213
+ const existing = extractedMap.get(key);
214
+ if (existing) {
215
+ existing.vocabPlacementMeta.vocabLocations.push(vocabLocation);
216
+ }
217
+ else {
218
+ extractedMap.set(key, {
219
+ string: originalString, // Use original casing for the string value
220
+ vocabPlacementMeta: {
221
+ vocabLocations: [vocabLocation],
222
+ },
223
+ });
224
+ }
225
+ }
226
+ /**
227
+ * Generate output path for translated file based on input file extension
228
+ * @param filePath - Original file path
229
+ * @returns Path for the translated output file
230
+ */
231
+ generateTranslatedOutputPath(filePath) {
232
+ const lastDotIndex = filePath.lastIndexOf('.');
233
+ if (lastDotIndex === -1) {
234
+ return filePath + '_translated';
235
+ }
236
+ const basePath = filePath.substring(0, lastDotIndex);
237
+ const extension = filePath.substring(lastDotIndex);
238
+ return `${basePath}_translated${extension}`;
239
+ }
240
+ }
241
+ export { BaseProcessor };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * String casing utilities for AAC text processing
3
+ * Used for detecting and managing text casing across different AAC formats
4
+ */
5
+ export var StringCasing;
6
+ (function (StringCasing) {
7
+ StringCasing["LOWER"] = "lower";
8
+ StringCasing["SNAKE"] = "snake";
9
+ StringCasing["CONSTANT"] = "constant";
10
+ StringCasing["CAMEL"] = "camel";
11
+ StringCasing["UPPER"] = "upper";
12
+ StringCasing["KEBAB"] = "kebab";
13
+ StringCasing["CAPITAL"] = "capital";
14
+ StringCasing["HEADER"] = "header";
15
+ StringCasing["PASCAL"] = "pascal";
16
+ StringCasing["TITLE"] = "title";
17
+ StringCasing["SENTENCE"] = "sentence";
18
+ })(StringCasing || (StringCasing = {}));
19
+ /**
20
+ * Detects the casing pattern of a given text string
21
+ * @param text - The text to analyze for casing pattern
22
+ * @returns StringCasing enum value representing the detected casing
23
+ */
24
+ /**
25
+ * Infer the dominant casing style of a string (camel, pascal, snake, kebab, title, sentence, upper, lower).
26
+ */
27
+ export function detectCasing(text) {
28
+ if (!text || text.length === 0)
29
+ return StringCasing.LOWER;
30
+ // Remove leading/trailing whitespace for analysis
31
+ const trimmed = text.trim();
32
+ if (trimmed.length === 0)
33
+ return StringCasing.LOWER;
34
+ // Check for specific patterns
35
+ // CONSTANT_CASE (ALL_CAPS_WITH_UNDERSCORES)
36
+ if (/^[A-Z][A-Z0-9_]*$/.test(trimmed) && trimmed.includes('_')) {
37
+ return StringCasing.CONSTANT;
38
+ }
39
+ // snake_case (lowercase_with_underscores)
40
+ if (/^[a-z][a-z0-9_]*$/.test(trimmed) && trimmed.includes('_')) {
41
+ return StringCasing.SNAKE;
42
+ }
43
+ // kebab-case (lowercase-with-hyphens)
44
+ if (/^[a-z][a-z0-9-]*$/.test(trimmed) && trimmed.includes('-')) {
45
+ return StringCasing.KEBAB;
46
+ }
47
+ // camelCase (firstWordLowerCaseFollowingWordsCapitalized)
48
+ if (/^[a-z][a-zA-Z0-9]*$/.test(trimmed) && /[A-Z]/.test(trimmed)) {
49
+ return StringCasing.CAMEL;
50
+ }
51
+ // PascalCase (FirstWordAndFollowingWordsCapitalized)
52
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(trimmed) &&
53
+ /[a-z]/.test(trimmed) &&
54
+ /[A-Z].*[A-Z]/.test(trimmed)) {
55
+ return StringCasing.PASCAL;
56
+ }
57
+ // UPPER CASE (ALL UPPERCASE) - but only if more than one character
58
+ if (trimmed === trimmed.toUpperCase() && /[A-Z]/.test(trimmed) && trimmed.length > 1) {
59
+ return StringCasing.UPPER;
60
+ }
61
+ // lower case (all lowercase)
62
+ if (trimmed === trimmed.toLowerCase() && /[a-z]/.test(trimmed)) {
63
+ return StringCasing.LOWER;
64
+ }
65
+ // Title Case (First Letter Of Each Word Capitalized)
66
+ const words = trimmed.split(/\s+/);
67
+ if (words.length > 1 &&
68
+ words.every((word) => word.length > 0 &&
69
+ word[0] === word[0].toUpperCase() &&
70
+ (word.length === 1 || word.slice(1) === word.slice(1).toLowerCase()))) {
71
+ return StringCasing.TITLE;
72
+ }
73
+ // Header-Case (First-Letter-Of-Each-Word-Capitalized-With-Hyphens)
74
+ if (trimmed.includes('-')) {
75
+ const hyphenWords = trimmed.split('-');
76
+ if (hyphenWords.length > 1 &&
77
+ hyphenWords.every((word) => word.length > 0 &&
78
+ word[0] === word[0].toUpperCase() &&
79
+ (word.length === 1 || word.slice(1) === word.slice(1).toLowerCase()))) {
80
+ return StringCasing.HEADER;
81
+ }
82
+ }
83
+ // Sentence case (First letter capitalized, rest lowercase)
84
+ if (trimmed.length > 1 &&
85
+ trimmed[0] === trimmed[0].toUpperCase() &&
86
+ trimmed.slice(1) === trimmed.slice(1).toLowerCase()) {
87
+ return StringCasing.SENTENCE;
88
+ }
89
+ // Capital case (Just first letter capitalized, may have mixed case after)
90
+ if (trimmed[0] === trimmed[0].toUpperCase()) {
91
+ return StringCasing.CAPITAL;
92
+ }
93
+ // Default fallback
94
+ return StringCasing.LOWER;
95
+ }
96
+ /**
97
+ * Converts text to the specified casing
98
+ * @param text - The text to convert
99
+ * @param targetCasing - The desired casing format
100
+ * @returns The text converted to the target casing
101
+ */
102
+ /**
103
+ * Convert a string into the requested casing style.
104
+ * @param text Input string
105
+ * @param targetCasing Desired casing variant
106
+ */
107
+ export function convertCasing(text, targetCasing) {
108
+ if (!text || text.length === 0)
109
+ return text;
110
+ const trimmed = text.trim();
111
+ if (trimmed.length === 0)
112
+ return text;
113
+ switch (targetCasing) {
114
+ case StringCasing.LOWER:
115
+ return trimmed.toLowerCase();
116
+ case StringCasing.UPPER:
117
+ return trimmed.toUpperCase();
118
+ case StringCasing.CAPITAL:
119
+ return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
120
+ case StringCasing.SENTENCE:
121
+ return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
122
+ case StringCasing.TITLE:
123
+ return trimmed
124
+ .split(/\s+/)
125
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
126
+ .join(' ');
127
+ case StringCasing.CAMEL:
128
+ return trimmed
129
+ .split(/[\s_-]+/)
130
+ .map((word, index) => index === 0
131
+ ? word.toLowerCase()
132
+ : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
133
+ .join('');
134
+ case StringCasing.PASCAL:
135
+ return trimmed
136
+ .split(/[\s_-]+/)
137
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
138
+ .join('');
139
+ case StringCasing.SNAKE:
140
+ return trimmed
141
+ .split(/[\s-]+/)
142
+ .map((word) => word.toLowerCase())
143
+ .join('_');
144
+ case StringCasing.CONSTANT:
145
+ return trimmed
146
+ .split(/[\s-]+/)
147
+ .map((word) => word.toUpperCase())
148
+ .join('_');
149
+ case StringCasing.KEBAB:
150
+ return trimmed
151
+ .split(/[\s_]+/)
152
+ .map((word) => word.toLowerCase())
153
+ .join('-');
154
+ case StringCasing.HEADER:
155
+ return trimmed
156
+ .split(/[\s_]+/)
157
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
158
+ .join('-');
159
+ default:
160
+ return trimmed;
161
+ }
162
+ }
163
+ /**
164
+ * Utility function to check if text is primarily numeric or empty
165
+ * Used for filtering out non-meaningful text content
166
+ * @param text - The text to check
167
+ * @returns True if the text should be considered non-meaningful
168
+ */
169
+ /**
170
+ * Check whether a string is empty or represents a numeric value.
171
+ */
172
+ export function isNumericOrEmpty(text) {
173
+ const trimmed = text.trim();
174
+ if (trimmed.length <= 1)
175
+ return true;
176
+ // Check if the entire string is numeric
177
+ const numericValue = parseInt(trimmed, 10);
178
+ return !isNaN(numericValue) && numericValue.toString() === trimmed;
179
+ }
@@ -0,0 +1,255 @@
1
+ // Semantic action categories for cross-platform compatibility
2
+ export var AACSemanticCategory;
3
+ (function (AACSemanticCategory) {
4
+ AACSemanticCategory["COMMUNICATION"] = "communication";
5
+ AACSemanticCategory["NAVIGATION"] = "navigation";
6
+ AACSemanticCategory["TEXT_EDITING"] = "text_editing";
7
+ AACSemanticCategory["SYSTEM_CONTROL"] = "system_control";
8
+ AACSemanticCategory["MEDIA"] = "media";
9
+ AACSemanticCategory["ACCESSIBILITY"] = "accessibility";
10
+ AACSemanticCategory["CUSTOM"] = "custom";
11
+ })(AACSemanticCategory || (AACSemanticCategory = {}));
12
+ // Semantic intents within each category
13
+ export var AACSemanticIntent;
14
+ (function (AACSemanticIntent) {
15
+ // Communication
16
+ AACSemanticIntent["SPEAK_TEXT"] = "SPEAK_TEXT";
17
+ AACSemanticIntent["SPEAK_IMMEDIATE"] = "SPEAK_IMMEDIATE";
18
+ AACSemanticIntent["STOP_SPEECH"] = "STOP_SPEECH";
19
+ AACSemanticIntent["INSERT_TEXT"] = "INSERT_TEXT";
20
+ // Navigation
21
+ AACSemanticIntent["NAVIGATE_TO"] = "NAVIGATE_TO";
22
+ AACSemanticIntent["GO_BACK"] = "GO_BACK";
23
+ AACSemanticIntent["GO_HOME"] = "GO_HOME";
24
+ // Text Editing
25
+ AACSemanticIntent["DELETE_WORD"] = "DELETE_WORD";
26
+ AACSemanticIntent["DELETE_CHARACTER"] = "DELETE_CHARACTER";
27
+ AACSemanticIntent["CLEAR_TEXT"] = "CLEAR_TEXT";
28
+ AACSemanticIntent["COPY_TEXT"] = "COPY_TEXT";
29
+ AACSemanticIntent["PASTE_TEXT"] = "PASTE_TEXT";
30
+ // System Control
31
+ AACSemanticIntent["SEND_KEYS"] = "SEND_KEYS";
32
+ AACSemanticIntent["MOUSE_CLICK"] = "MOUSE_CLICK";
33
+ // Media
34
+ AACSemanticIntent["PLAY_SOUND"] = "PLAY_SOUND";
35
+ AACSemanticIntent["PLAY_VIDEO"] = "PLAY_VIDEO";
36
+ // Accessibility
37
+ AACSemanticIntent["SCAN_NEXT"] = "SCAN_NEXT";
38
+ AACSemanticIntent["SCAN_SELECT"] = "SCAN_SELECT";
39
+ // Custom
40
+ AACSemanticIntent["PLATFORM_SPECIFIC"] = "PLATFORM_SPECIFIC";
41
+ })(AACSemanticIntent || (AACSemanticIntent = {}));
42
+ /**
43
+ * Scanning types for accessibility
44
+ */
45
+ export var AACScanType;
46
+ (function (AACScanType) {
47
+ AACScanType["LINEAR"] = "linear";
48
+ AACScanType["ROW_COLUMN"] = "row-column";
49
+ AACScanType["COLUMN_ROW"] = "column-row";
50
+ AACScanType["BLOCK_ROW_COLUMN"] = "block-row-column";
51
+ AACScanType["BLOCK_COLUMN_ROW"] = "block-column-row";
52
+ })(AACScanType || (AACScanType = {}));
53
+ export class AACButton {
54
+ constructor({ id, label = '', message = '', targetPageId, semanticAction, audioRecording, style, contentType, contentSubType, image, resolvedImageEntry, symbolLibrary, symbolPath, x, y, columnSpan, rowSpan, scanBlocks, scanBlock, visibility, directActivate, parameters, predictions, semantic_id, clone_id,
55
+ // Legacy input support
56
+ type, action, }) {
57
+ this.id = id;
58
+ this.label = label;
59
+ this.message = message;
60
+ this.targetPageId = targetPageId;
61
+ this.semanticAction = semanticAction;
62
+ this.audioRecording = audioRecording;
63
+ this.style = style;
64
+ this.contentType = contentType;
65
+ this.contentSubType = contentSubType;
66
+ this.image = image;
67
+ this.resolvedImageEntry = resolvedImageEntry;
68
+ this.symbolLibrary = symbolLibrary;
69
+ this.symbolPath = symbolPath;
70
+ this.x = x;
71
+ this.y = y;
72
+ this.columnSpan = columnSpan;
73
+ this.rowSpan = rowSpan;
74
+ this.scanBlocks = scanBlocks;
75
+ this.scanBlock = scanBlock;
76
+ this.visibility = visibility;
77
+ this.directActivate = directActivate;
78
+ this.parameters = parameters;
79
+ this.predictions = predictions;
80
+ this.semantic_id = semantic_id;
81
+ this.clone_id = clone_id;
82
+ // Legacy mapping: if no semanticAction provided, derive from legacy `action` first
83
+ if (!this.semanticAction && action) {
84
+ if (action.type === 'NAVIGATE' && (action.targetPageId || this.targetPageId)) {
85
+ if (!this.targetPageId)
86
+ this.targetPageId = action.targetPageId;
87
+ this.semanticAction = {
88
+ category: AACSemanticCategory.NAVIGATION,
89
+ intent: AACSemanticIntent.NAVIGATE_TO,
90
+ targetId: this.targetPageId,
91
+ fallback: { type: 'NAVIGATE', targetPageId: this.targetPageId },
92
+ };
93
+ }
94
+ else if (action.type === 'SPEAK') {
95
+ const text = action.message || this.message || this.label || '';
96
+ if (!this.message)
97
+ this.message = text;
98
+ this.semanticAction = {
99
+ category: AACSemanticCategory.COMMUNICATION,
100
+ intent: AACSemanticIntent.SPEAK_TEXT,
101
+ text,
102
+ fallback: { type: 'SPEAK', message: text },
103
+ };
104
+ }
105
+ else {
106
+ this.semanticAction = {
107
+ category: AACSemanticCategory.SYSTEM_CONTROL,
108
+ intent: AACSemanticIntent.PLATFORM_SPECIFIC,
109
+ fallback: { type: 'ACTION' },
110
+ };
111
+ }
112
+ }
113
+ // Legacy mapping: if still no semanticAction and `type` provided
114
+ if (!this.semanticAction && type) {
115
+ if (type === 'NAVIGATE' && this.targetPageId) {
116
+ this.semanticAction = {
117
+ category: AACSemanticCategory.NAVIGATION,
118
+ intent: AACSemanticIntent.NAVIGATE_TO,
119
+ targetId: this.targetPageId,
120
+ fallback: { type: 'NAVIGATE', targetPageId: this.targetPageId },
121
+ };
122
+ }
123
+ else if (type === 'SPEAK') {
124
+ const text = this.message || this.label || '';
125
+ this.semanticAction = {
126
+ category: AACSemanticCategory.COMMUNICATION,
127
+ intent: AACSemanticIntent.SPEAK_TEXT,
128
+ text,
129
+ fallback: { type: 'SPEAK', message: text },
130
+ };
131
+ }
132
+ else {
133
+ this.semanticAction = {
134
+ category: AACSemanticCategory.SYSTEM_CONTROL,
135
+ intent: AACSemanticIntent.PLATFORM_SPECIFIC,
136
+ fallback: { type: 'ACTION' },
137
+ };
138
+ }
139
+ }
140
+ }
141
+ // Legacy compatibility properties
142
+ get type() {
143
+ if (this.semanticAction) {
144
+ const i = String(this.semanticAction.intent);
145
+ if (i === 'NAVIGATE_TO')
146
+ return 'NAVIGATE';
147
+ if (i === 'SPEAK_TEXT' || i === 'SPEAK_IMMEDIATE')
148
+ return 'SPEAK';
149
+ return 'ACTION';
150
+ }
151
+ if (this.targetPageId)
152
+ return 'NAVIGATE';
153
+ if (this.message)
154
+ return 'SPEAK';
155
+ return 'SPEAK';
156
+ }
157
+ get action() {
158
+ const t = this.type;
159
+ if (!t)
160
+ return null;
161
+ if (t === 'SPEAK' && !this.message && !this.label && !this.semanticAction) {
162
+ return null;
163
+ }
164
+ return { type: t, targetPageId: this.targetPageId, message: this.message };
165
+ }
166
+ }
167
+ export class AACPage {
168
+ constructor({ id, name = '', grid = [], buttons = [], parentId = null, style, locale, descriptionHtml, images, sounds, semantic_ids, clone_ids, scanningConfig, scanBlocksConfig, scanType, }) {
169
+ this.id = id;
170
+ this.name = name;
171
+ if (Array.isArray(grid)) {
172
+ this.grid = grid;
173
+ }
174
+ else if (grid && typeof grid === 'object' && 'columns' in grid && 'rows' in grid) {
175
+ const cols = grid.columns;
176
+ const rows = grid.rows;
177
+ this.grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
178
+ }
179
+ else {
180
+ this.grid = [];
181
+ }
182
+ this.buttons = buttons;
183
+ this.parentId = parentId;
184
+ this.style = style;
185
+ this.locale = locale;
186
+ this.descriptionHtml = descriptionHtml;
187
+ this.images = images;
188
+ this.sounds = sounds;
189
+ this.semantic_ids = semantic_ids;
190
+ this.clone_ids = clone_ids;
191
+ this.scanningConfig = scanningConfig;
192
+ this.scanBlocksConfig = scanBlocksConfig;
193
+ this.scanType = scanType;
194
+ }
195
+ addButton(button) {
196
+ this.buttons.push(button);
197
+ }
198
+ }
199
+ export class AACTree {
200
+ get rootId() {
201
+ return this.metadata.defaultHomePageId || null;
202
+ }
203
+ set rootId(id) {
204
+ this.metadata.defaultHomePageId = id || undefined;
205
+ }
206
+ get toolbarId() {
207
+ return this.metadata.toolbarId || null;
208
+ }
209
+ set toolbarId(id) {
210
+ this.metadata.toolbarId = id || undefined;
211
+ }
212
+ get dashboardId() {
213
+ return this.metadata.dashboardId || null;
214
+ }
215
+ set dashboardId(id) {
216
+ this.metadata.dashboardId = id || undefined;
217
+ }
218
+ constructor() {
219
+ this.pages = {};
220
+ this.metadata = {};
221
+ }
222
+ addPage(page) {
223
+ this.pages[page.id] = page;
224
+ if (!this.rootId)
225
+ this.rootId = page.id;
226
+ }
227
+ getPage(id) {
228
+ return this.pages[id];
229
+ }
230
+ traverse(callback) {
231
+ const queue = Object.keys(this.pages);
232
+ const visited = new Set();
233
+ while (queue.length > 0) {
234
+ const id = queue.shift();
235
+ if (!id || visited.has(id))
236
+ continue;
237
+ visited.add(id);
238
+ const page = this.pages[id];
239
+ if (page) {
240
+ callback(page);
241
+ // Add child pages to queue
242
+ page.buttons
243
+ .filter((b) => {
244
+ const i = String(b.semanticAction?.intent);
245
+ return i === 'NAVIGATE_TO' || !!b.semanticAction?.targetId || !!b.targetPageId;
246
+ })
247
+ .forEach((b) => {
248
+ const target = b.semanticAction?.targetId || b.targetPageId;
249
+ if (target)
250
+ queue.push(target);
251
+ });
252
+ }
253
+ }
254
+ }
255
+ }