@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.
- package/README.md +52 -852
- package/dist/browser/core/baseProcessor.js +241 -0
- package/dist/browser/core/stringCasing.js +179 -0
- package/dist/browser/core/treeStructure.js +255 -0
- package/dist/browser/index.browser.js +73 -0
- package/dist/browser/processors/applePanelsProcessor.js +582 -0
- package/dist/browser/processors/astericsGridProcessor.js +1509 -0
- package/dist/browser/processors/dotProcessor.js +221 -0
- package/dist/browser/processors/gridset/commands.js +962 -0
- package/dist/browser/processors/gridset/crypto.js +53 -0
- package/dist/browser/processors/gridset/password.js +43 -0
- package/dist/browser/processors/gridset/pluginTypes.js +277 -0
- package/dist/browser/processors/gridset/resolver.js +137 -0
- package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
- package/dist/browser/processors/gridset/symbols.js +421 -0
- package/dist/browser/processors/gridsetProcessor.js +2002 -0
- package/dist/browser/processors/obfProcessor.js +705 -0
- package/dist/browser/processors/opmlProcessor.js +274 -0
- package/dist/browser/types/aac.js +38 -0
- package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
- package/dist/browser/utilities/translation/translationProcessor.js +200 -0
- package/dist/browser/utils/io.js +95 -0
- package/dist/browser/validation/baseValidator.js +156 -0
- package/dist/browser/validation/gridsetValidator.js +355 -0
- package/dist/browser/validation/obfValidator.js +500 -0
- package/dist/browser/validation/validationTypes.js +46 -0
- package/dist/cli/index.js +5 -5
- package/dist/core/analyze.d.ts +2 -2
- package/dist/core/analyze.js +2 -2
- package/dist/core/baseProcessor.d.ts +5 -4
- package/dist/core/baseProcessor.js +22 -27
- package/dist/core/treeStructure.d.ts +5 -5
- package/dist/core/treeStructure.js +1 -4
- package/dist/index.browser.d.ts +37 -0
- package/dist/index.browser.js +99 -0
- package/dist/index.d.ts +1 -48
- package/dist/index.js +1 -136
- package/dist/index.node.d.ts +48 -0
- package/dist/index.node.js +152 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -4
- package/dist/processors/applePanelsProcessor.js +58 -62
- package/dist/processors/astericsGridProcessor.d.ts +7 -6
- package/dist/processors/astericsGridProcessor.js +31 -42
- package/dist/processors/dotProcessor.d.ts +5 -4
- package/dist/processors/dotProcessor.js +25 -33
- package/dist/processors/excelProcessor.d.ts +4 -3
- package/dist/processors/excelProcessor.js +6 -3
- package/dist/processors/gridset/crypto.d.ts +18 -0
- package/dist/processors/gridset/crypto.js +57 -0
- package/dist/processors/gridset/helpers.d.ts +1 -1
- package/dist/processors/gridset/helpers.js +18 -8
- package/dist/processors/gridset/password.d.ts +20 -3
- package/dist/processors/gridset/password.js +17 -3
- package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
- package/dist/processors/gridset/wordlistHelpers.js +21 -20
- package/dist/processors/gridsetProcessor.d.ts +7 -12
- package/dist/processors/gridsetProcessor.js +118 -77
- package/dist/processors/obfProcessor.d.ts +9 -7
- package/dist/processors/obfProcessor.js +131 -56
- package/dist/processors/obfsetProcessor.d.ts +5 -4
- package/dist/processors/obfsetProcessor.js +10 -16
- package/dist/processors/opmlProcessor.d.ts +5 -4
- package/dist/processors/opmlProcessor.js +27 -34
- package/dist/processors/snapProcessor.d.ts +8 -7
- package/dist/processors/snapProcessor.js +15 -12
- package/dist/processors/touchchatProcessor.d.ts +8 -7
- package/dist/processors/touchchatProcessor.js +22 -17
- package/dist/types/aac.d.ts +0 -2
- package/dist/types/aac.js +2 -0
- package/dist/utils/io.d.ts +12 -0
- package/dist/utils/io.js +107 -0
- package/dist/validation/gridsetValidator.js +7 -7
- package/dist/validation/snapValidator.js +28 -35
- package/docs/BROWSER_USAGE.md +618 -0
- package/examples/README.md +77 -0
- package/examples/browser-test-server.js +81 -0
- package/examples/browser-test.html +331 -0
- package/examples/vitedemo/QUICKSTART.md +74 -0
- package/examples/vitedemo/README.md +157 -0
- package/examples/vitedemo/index.html +376 -0
- package/examples/vitedemo/package-lock.json +1221 -0
- package/examples/vitedemo/package.json +18 -0
- package/examples/vitedemo/src/main.ts +519 -0
- package/examples/vitedemo/test-files/example.dot +14 -0
- package/examples/vitedemo/test-files/example.grd +1 -0
- package/examples/vitedemo/test-files/example.gridset +0 -0
- package/examples/vitedemo/test-files/example.obz +0 -0
- package/examples/vitedemo/test-files/example.opml +18 -0
- package/examples/vitedemo/test-files/simple.obf +53 -0
- package/examples/vitedemo/tsconfig.json +24 -0
- package/examples/vitedemo/vite.config.ts +34 -0
- 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
|
+
}
|