@willwade/aac-processors 0.0.3
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/LICENSE +674 -0
- package/README.md +787 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +189 -0
- package/dist/cli/prettyPrint.d.ts +2 -0
- package/dist/cli/prettyPrint.js +28 -0
- package/dist/core/analyze.d.ts +6 -0
- package/dist/core/analyze.js +49 -0
- package/dist/core/baseProcessor.d.ts +94 -0
- package/dist/core/baseProcessor.js +208 -0
- package/dist/core/fileProcessor.d.ts +7 -0
- package/dist/core/fileProcessor.js +51 -0
- package/dist/core/stringCasing.d.ts +37 -0
- package/dist/core/stringCasing.js +174 -0
- package/dist/core/treeStructure.d.ts +190 -0
- package/dist/core/treeStructure.js +223 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +96 -0
- package/dist/optional/symbolTools.d.ts +28 -0
- package/dist/optional/symbolTools.js +126 -0
- package/dist/processors/applePanelsProcessor.d.ts +23 -0
- package/dist/processors/applePanelsProcessor.js +521 -0
- package/dist/processors/astericsGridProcessor.d.ts +49 -0
- package/dist/processors/astericsGridProcessor.js +1427 -0
- package/dist/processors/dotProcessor.d.ts +21 -0
- package/dist/processors/dotProcessor.js +191 -0
- package/dist/processors/excelProcessor.d.ts +145 -0
- package/dist/processors/excelProcessor.js +556 -0
- package/dist/processors/gridset/helpers.d.ts +4 -0
- package/dist/processors/gridset/helpers.js +48 -0
- package/dist/processors/gridset/resolver.d.ts +8 -0
- package/dist/processors/gridset/resolver.js +100 -0
- package/dist/processors/gridsetProcessor.d.ts +28 -0
- package/dist/processors/gridsetProcessor.js +1339 -0
- package/dist/processors/index.d.ts +14 -0
- package/dist/processors/index.js +42 -0
- package/dist/processors/obfProcessor.d.ts +21 -0
- package/dist/processors/obfProcessor.js +278 -0
- package/dist/processors/opmlProcessor.d.ts +21 -0
- package/dist/processors/opmlProcessor.js +235 -0
- package/dist/processors/snap/helpers.d.ts +4 -0
- package/dist/processors/snap/helpers.js +27 -0
- package/dist/processors/snapProcessor.d.ts +44 -0
- package/dist/processors/snapProcessor.js +586 -0
- package/dist/processors/touchchat/helpers.d.ts +4 -0
- package/dist/processors/touchchat/helpers.js +27 -0
- package/dist/processors/touchchatProcessor.d.ts +27 -0
- package/dist/processors/touchchatProcessor.js +768 -0
- package/dist/types/aac.d.ts +47 -0
- package/dist/types/aac.js +2 -0
- package/docs/.keep +1 -0
- package/docs/ApplePanels.md +309 -0
- package/docs/Grid3-XML-Format.md +1788 -0
- package/docs/TobiiDynavox-Snap-Details.md +394 -0
- package/docs/asterics-Grid-fileformat-details.md +443 -0
- package/docs/obf_.obz Open Board File Formats.md +432 -0
- package/docs/touchchat.md +520 -0
- package/examples/.coverage +0 -0
- package/examples/.keep +1 -0
- package/examples/README.md +31 -0
- package/examples/communikate.dot +2637 -0
- package/examples/demo.js +143 -0
- package/examples/example-images.gridset +0 -0
- package/examples/example.ce +0 -0
- package/examples/example.dot +14 -0
- package/examples/example.grd +1 -0
- package/examples/example.gridset +0 -0
- package/examples/example.obf +27 -0
- package/examples/example.obz +0 -0
- package/examples/example.opml +18 -0
- package/examples/example.spb +0 -0
- package/examples/example.sps +0 -0
- package/examples/example2.grd +1 -0
- package/examples/gemini_response.txt +845 -0
- package/examples/image-map.js +45 -0
- package/examples/package-lock.json +1326 -0
- package/examples/package.json +10 -0
- package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
- package/examples/styled-output/styled-example.ce +0 -0
- package/examples/styled-output/styled-example.gridset +0 -0
- package/examples/styled-output/styled-example.obf +37 -0
- package/examples/styled-output/styled-example.spb +0 -0
- package/examples/styling-example.ts +316 -0
- package/examples/translate.js +39 -0
- package/examples/translate_demo.js +254 -0
- package/examples/translation_cache.json +44894 -0
- package/examples/typescript-demo.ts +251 -0
- package/examples/unified-interface-demo.ts +183 -0
- package/package.json +106 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const prettyPrint_1 = require("./prettyPrint");
|
|
9
|
+
const analyze_1 = require("../core/analyze");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
// Helper function to detect format from file/folder path
|
|
13
|
+
function detectFormat(filePath) {
|
|
14
|
+
// Check if it's a folder ending with .ascconfig
|
|
15
|
+
if (fs_1.default.existsSync(filePath) &&
|
|
16
|
+
fs_1.default.statSync(filePath).isDirectory() &&
|
|
17
|
+
filePath.endsWith('.ascconfig')) {
|
|
18
|
+
return 'ascconfig';
|
|
19
|
+
}
|
|
20
|
+
// Otherwise use file extension
|
|
21
|
+
return path_1.default.extname(filePath).slice(1);
|
|
22
|
+
}
|
|
23
|
+
// Helper function to parse filtering options from CLI arguments
|
|
24
|
+
function parseFilteringOptions(options) {
|
|
25
|
+
const processorOptions = {};
|
|
26
|
+
// Handle preserve all buttons flag
|
|
27
|
+
if (options.preserveAllButtons) {
|
|
28
|
+
processorOptions.preserveAllButtons = true;
|
|
29
|
+
return processorOptions; // If preserving all, ignore other options
|
|
30
|
+
}
|
|
31
|
+
// Handle specific exclusion flags
|
|
32
|
+
if (options.excludeNavigation !== undefined) {
|
|
33
|
+
processorOptions.excludeNavigationButtons = options.excludeNavigation;
|
|
34
|
+
}
|
|
35
|
+
if (options.excludeSystem !== undefined) {
|
|
36
|
+
processorOptions.excludeSystemButtons = options.excludeSystem;
|
|
37
|
+
}
|
|
38
|
+
// Handle custom button exclusion list
|
|
39
|
+
if (options.excludeButtons) {
|
|
40
|
+
const excludeList = options.excludeButtons
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((s) => s.trim().toLowerCase())
|
|
43
|
+
.filter((s) => s.length > 0);
|
|
44
|
+
if (excludeList.length > 0) {
|
|
45
|
+
processorOptions.customButtonFilter = (button) => {
|
|
46
|
+
const label = button.label?.toLowerCase() || '';
|
|
47
|
+
const message = button.message?.toLowerCase() || '';
|
|
48
|
+
// Exclude if button label or message contains any of the excluded terms
|
|
49
|
+
return !excludeList.some((term) => label.includes(term) || message.includes(term));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return processorOptions;
|
|
54
|
+
}
|
|
55
|
+
// Set version from package.json
|
|
56
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../package.json'), 'utf8'));
|
|
57
|
+
commander_1.program.version(packageJson.version);
|
|
58
|
+
commander_1.program
|
|
59
|
+
.command('analyze <file>')
|
|
60
|
+
.option('--format <format>', 'Format type (auto-detected if not specified)')
|
|
61
|
+
.option('--pretty', 'Pretty print output')
|
|
62
|
+
.option('--preserve-all-buttons', 'Preserve all buttons including navigation/system buttons')
|
|
63
|
+
.option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
|
|
64
|
+
.option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
|
|
65
|
+
.option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
|
|
66
|
+
.action((file, options) => {
|
|
67
|
+
try {
|
|
68
|
+
// Parse filtering options
|
|
69
|
+
const filteringOptions = parseFilteringOptions(options);
|
|
70
|
+
// Auto-detect format if not specified
|
|
71
|
+
const format = options.format || detectFormat(file);
|
|
72
|
+
const processor = (0, analyze_1.getProcessor)(format, filteringOptions);
|
|
73
|
+
const tree = processor.loadIntoTree(file);
|
|
74
|
+
const result = {
|
|
75
|
+
format,
|
|
76
|
+
tree,
|
|
77
|
+
filtering: filteringOptions,
|
|
78
|
+
};
|
|
79
|
+
if (options.pretty) {
|
|
80
|
+
console.log((0, prettyPrint_1.prettyPrintTree)(result.tree));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(JSON.stringify(result, null, 2));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Error analyzing file:', error instanceof Error ? error.message : String(error));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
commander_1.program
|
|
92
|
+
.command('extract <file>')
|
|
93
|
+
.option('--format <format>', 'Format type (auto-detected if not specified)')
|
|
94
|
+
.option('--verbose', 'Verbose output')
|
|
95
|
+
.option('--quiet', 'Quiet output')
|
|
96
|
+
.option('--preserve-all-buttons', 'Preserve all buttons including navigation/system buttons')
|
|
97
|
+
.option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
|
|
98
|
+
.option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
|
|
99
|
+
.option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
|
|
100
|
+
.action((file, options) => {
|
|
101
|
+
try {
|
|
102
|
+
// Parse filtering options
|
|
103
|
+
const filteringOptions = parseFilteringOptions(options);
|
|
104
|
+
// Auto-detect format if not specified
|
|
105
|
+
const format = options.format || detectFormat(file);
|
|
106
|
+
const processor = (0, analyze_1.getProcessor)(format, filteringOptions);
|
|
107
|
+
const texts = processor.extractTexts(file);
|
|
108
|
+
if (!options.quiet) {
|
|
109
|
+
if (options.verbose) {
|
|
110
|
+
console.log(`Extracting texts from ${file} (format: ${format})`);
|
|
111
|
+
console.log(`Found ${texts.length} text entries:`);
|
|
112
|
+
// Show filtering info in verbose mode
|
|
113
|
+
if (filteringOptions.preserveAllButtons) {
|
|
114
|
+
console.log('Filtering: All buttons preserved');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const filters = [];
|
|
118
|
+
if (filteringOptions.excludeNavigationButtons !== false)
|
|
119
|
+
filters.push('navigation');
|
|
120
|
+
if (filteringOptions.excludeSystemButtons !== false)
|
|
121
|
+
filters.push('system');
|
|
122
|
+
if (filteringOptions.customButtonFilter)
|
|
123
|
+
filters.push('custom');
|
|
124
|
+
if (filters.length > 0) {
|
|
125
|
+
console.log(`Filtering: Excluding ${filters.join(', ')} buttons`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Output the texts (one per line for easier processing)
|
|
131
|
+
texts.forEach((text) => console.log(text));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('Error extracting texts:', error instanceof Error ? error.message : String(error));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
commander_1.program
|
|
139
|
+
.command('convert <input> <output>')
|
|
140
|
+
.option('--format <format>', 'Output format (required)')
|
|
141
|
+
.option('--preserve-all-buttons', 'Preserve all buttons including navigation/system buttons')
|
|
142
|
+
.option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
|
|
143
|
+
.option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
|
|
144
|
+
.option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
|
|
145
|
+
.action((input, output, options) => {
|
|
146
|
+
try {
|
|
147
|
+
if (!options.format) {
|
|
148
|
+
console.error('Error: --format option is required for convert command');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
// Parse filtering options
|
|
152
|
+
const filteringOptions = parseFilteringOptions(options);
|
|
153
|
+
// Auto-detect input format
|
|
154
|
+
const inputFormat = detectFormat(input);
|
|
155
|
+
const inputProcessor = (0, analyze_1.getProcessor)(inputFormat, filteringOptions);
|
|
156
|
+
// Load the tree (handle both files and folders)
|
|
157
|
+
const tree = inputProcessor.loadIntoTree(input);
|
|
158
|
+
// Save using output format with same filtering options
|
|
159
|
+
const outputProcessor = (0, analyze_1.getProcessor)(options.format, filteringOptions);
|
|
160
|
+
outputProcessor.saveFromTree(tree, output);
|
|
161
|
+
// Show filtering summary
|
|
162
|
+
let filteringSummary = '';
|
|
163
|
+
if (filteringOptions.preserveAllButtons) {
|
|
164
|
+
filteringSummary = ' (all buttons preserved)';
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const filters = [];
|
|
168
|
+
if (filteringOptions.excludeNavigationButtons !== false)
|
|
169
|
+
filters.push('navigation');
|
|
170
|
+
if (filteringOptions.excludeSystemButtons !== false)
|
|
171
|
+
filters.push('system');
|
|
172
|
+
if (filteringOptions.customButtonFilter)
|
|
173
|
+
filters.push('custom');
|
|
174
|
+
if (filters.length > 0) {
|
|
175
|
+
filteringSummary = ` (filtered: ${filters.join(', ')} buttons)`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
console.log(`Successfully converted ${input} to ${output} (${options.format} format)${filteringSummary}`);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error('Error converting file:', error instanceof Error ? error.message : String(error));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// Show help if no command provided
|
|
186
|
+
if (process.argv.length <= 2) {
|
|
187
|
+
commander_1.program.help();
|
|
188
|
+
}
|
|
189
|
+
commander_1.program.parse(process.argv);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prettyPrintTree = prettyPrintTree;
|
|
4
|
+
function prettyPrintTree(tree) {
|
|
5
|
+
let output = '';
|
|
6
|
+
for (const pageId in tree.pages) {
|
|
7
|
+
const page = tree.pages[pageId];
|
|
8
|
+
output += `Page: ${page.name} (ID: ${page.id})\n`;
|
|
9
|
+
if (!page.buttons || page.buttons.length === 0) {
|
|
10
|
+
output += ' (no buttons)\n';
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
for (const btn of page.buttons) {
|
|
14
|
+
const intentStr = String(btn.semanticAction?.intent);
|
|
15
|
+
const isNavigate = intentStr === 'NAVIGATE_TO' || !!btn.targetPageId;
|
|
16
|
+
const buttonType = isNavigate ? 'NAVIGATE' : 'SPEAK';
|
|
17
|
+
output += ` - Button: ${JSON.stringify(btn.label)} [${buttonType}`;
|
|
18
|
+
if (isNavigate) {
|
|
19
|
+
const target = btn.semanticAction?.targetId || btn.targetPageId;
|
|
20
|
+
if (target)
|
|
21
|
+
output += ` to page: ${target}`;
|
|
22
|
+
}
|
|
23
|
+
output += ']\n';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return output;
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AACTree } from './treeStructure';
|
|
2
|
+
import { BaseProcessor, ProcessorOptions } from './baseProcessor';
|
|
3
|
+
export declare function getProcessor(format: string, options?: ProcessorOptions): BaseProcessor;
|
|
4
|
+
export declare function analyze(file: string, format: string): {
|
|
5
|
+
tree: AACTree;
|
|
6
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getProcessor = getProcessor;
|
|
4
|
+
exports.analyze = analyze;
|
|
5
|
+
const opmlProcessor_1 = require("../processors/opmlProcessor");
|
|
6
|
+
const obfProcessor_1 = require("../processors/obfProcessor");
|
|
7
|
+
const touchchatProcessor_1 = require("../processors/touchchatProcessor");
|
|
8
|
+
const gridsetProcessor_1 = require("../processors/gridsetProcessor");
|
|
9
|
+
const astericsGridProcessor_1 = require("../processors/astericsGridProcessor");
|
|
10
|
+
const snapProcessor_1 = require("../processors/snapProcessor");
|
|
11
|
+
const dotProcessor_1 = require("../processors/dotProcessor");
|
|
12
|
+
const excelProcessor_1 = require("../processors/excelProcessor");
|
|
13
|
+
const applePanelsProcessor_1 = require("../processors/applePanelsProcessor");
|
|
14
|
+
function getProcessor(format, options) {
|
|
15
|
+
const normalizedFormat = (format || '').toLowerCase();
|
|
16
|
+
switch (normalizedFormat) {
|
|
17
|
+
case 'opml':
|
|
18
|
+
return new opmlProcessor_1.OpmlProcessor(options);
|
|
19
|
+
case 'obf':
|
|
20
|
+
return new obfProcessor_1.ObfProcessor(options);
|
|
21
|
+
case 'touchchat':
|
|
22
|
+
case 'ce': // TouchChat file extension
|
|
23
|
+
return new touchchatProcessor_1.TouchChatProcessor(options);
|
|
24
|
+
case 'gridset':
|
|
25
|
+
return new gridsetProcessor_1.GridsetProcessor(options); // Grid3 format
|
|
26
|
+
case 'grd': // Asterics Grid file extension
|
|
27
|
+
return new astericsGridProcessor_1.AstericsGridProcessor(options);
|
|
28
|
+
case 'snap':
|
|
29
|
+
case 'sps': // Snap file extension
|
|
30
|
+
case 'spb': // Snap backup file extension
|
|
31
|
+
return new snapProcessor_1.SnapProcessor(options);
|
|
32
|
+
case 'dot':
|
|
33
|
+
return new dotProcessor_1.DotProcessor(options);
|
|
34
|
+
case 'excel':
|
|
35
|
+
case 'xlsx': // Excel file extension
|
|
36
|
+
return new excelProcessor_1.ExcelProcessor(options);
|
|
37
|
+
case 'applepanels':
|
|
38
|
+
case 'panels': // Apple Panels file extension
|
|
39
|
+
case 'ascconfig': // Apple Panels folder format
|
|
40
|
+
return new applePanelsProcessor_1.ApplePanelsProcessor(options);
|
|
41
|
+
default:
|
|
42
|
+
throw new Error('Unknown format: ' + format);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function analyze(file, format) {
|
|
46
|
+
const processor = getProcessor(format);
|
|
47
|
+
const tree = processor.loadIntoTree(file);
|
|
48
|
+
return { tree };
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AACTree, AACButton } from './treeStructure';
|
|
2
|
+
import { StringCasing } from './stringCasing';
|
|
3
|
+
export interface ProcessorOptions {
|
|
4
|
+
excludeNavigationButtons?: boolean;
|
|
5
|
+
excludeSystemButtons?: boolean;
|
|
6
|
+
customButtonFilter?: (button: AACButton) => boolean;
|
|
7
|
+
preserveAllButtons?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ExtractedString {
|
|
10
|
+
string: string;
|
|
11
|
+
vocabPlacementMeta: VocabPlacementMetadata;
|
|
12
|
+
}
|
|
13
|
+
export interface VocabPlacementMetadata {
|
|
14
|
+
vocabLocations: VocabLocation[];
|
|
15
|
+
}
|
|
16
|
+
export interface VocabLocation {
|
|
17
|
+
table: string;
|
|
18
|
+
id: number;
|
|
19
|
+
column: string;
|
|
20
|
+
casing: StringCasing;
|
|
21
|
+
}
|
|
22
|
+
export interface ProcessingError {
|
|
23
|
+
message: string;
|
|
24
|
+
step: 'EXTRACT' | 'PROCESS' | 'SAVE';
|
|
25
|
+
}
|
|
26
|
+
export interface ExtractStringsResult {
|
|
27
|
+
errors: ProcessingError[];
|
|
28
|
+
extractedStrings: ExtractedString[];
|
|
29
|
+
}
|
|
30
|
+
export interface TranslatedString {
|
|
31
|
+
sourcestringid: number;
|
|
32
|
+
overridestring: string;
|
|
33
|
+
translatedstring: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SourceString {
|
|
36
|
+
id: number;
|
|
37
|
+
sourcestring: string;
|
|
38
|
+
vocabplacementmetadata: VocabPlacementMetadata;
|
|
39
|
+
}
|
|
40
|
+
declare abstract class BaseProcessor {
|
|
41
|
+
protected options: ProcessorOptions;
|
|
42
|
+
constructor(options?: ProcessorOptions);
|
|
43
|
+
abstract extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
44
|
+
abstract loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
45
|
+
abstract processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
|
|
46
|
+
abstract saveFromTree(tree: AACTree, outputPath: string): void | Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Extract strings with metadata for external platform integration
|
|
49
|
+
* @param filePath - Path to the AAC file
|
|
50
|
+
* @returns Promise with extracted strings and any errors
|
|
51
|
+
*/
|
|
52
|
+
extractStringsWithMetadata?(filePath: string): Promise<ExtractStringsResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Generate translated download with external translation data
|
|
55
|
+
* @param filePath - Path to the original AAC file
|
|
56
|
+
* @param translatedStrings - Array of translated string data
|
|
57
|
+
* @param sourceStrings - Array of source string data with metadata
|
|
58
|
+
* @returns Promise with path to the generated translated file
|
|
59
|
+
*/
|
|
60
|
+
generateTranslatedDownload?(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
61
|
+
protected shouldFilterButton(button: AACButton): boolean;
|
|
62
|
+
protected filterPageButtons(buttons: AACButton[]): AACButton[];
|
|
63
|
+
/**
|
|
64
|
+
* Generic implementation for extracting strings with metadata
|
|
65
|
+
* Can be used by any processor that doesn't need format-specific logic
|
|
66
|
+
* @param filePath - Path to the AAC file
|
|
67
|
+
* @returns Promise with extracted strings and metadata
|
|
68
|
+
*/
|
|
69
|
+
protected extractStringsWithMetadataGeneric(filePath: string): Promise<ExtractStringsResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Generic implementation for generating translated downloads
|
|
72
|
+
* Can be used by any processor that doesn't need format-specific logic
|
|
73
|
+
* @param filePath - Path to the original AAC file
|
|
74
|
+
* @param translatedStrings - Array of translated string data
|
|
75
|
+
* @param sourceStrings - Array of source string data
|
|
76
|
+
* @returns Promise with path to the generated translated file
|
|
77
|
+
*/
|
|
78
|
+
protected generateTranslatedDownloadGeneric(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Helper method to add extracted strings to the map, handling duplicates
|
|
81
|
+
* @param extractedMap - Map to store extracted strings
|
|
82
|
+
* @param key - Lowercase key for deduplication
|
|
83
|
+
* @param originalString - Original string with proper casing
|
|
84
|
+
* @param vocabLocation - Metadata about where the string was found
|
|
85
|
+
*/
|
|
86
|
+
protected addToExtractedMap(extractedMap: Map<string, ExtractedString>, key: string, originalString: string, vocabLocation: VocabLocation): void;
|
|
87
|
+
/**
|
|
88
|
+
* Generate output path for translated file based on input file extension
|
|
89
|
+
* @param filePath - Original file path
|
|
90
|
+
* @returns Path for the translated output file
|
|
91
|
+
*/
|
|
92
|
+
protected generateTranslatedOutputPath(filePath: string): string;
|
|
93
|
+
}
|
|
94
|
+
export { BaseProcessor };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseProcessor = void 0;
|
|
4
|
+
const treeStructure_1 = require("./treeStructure");
|
|
5
|
+
const stringCasing_1 = require("./stringCasing");
|
|
6
|
+
class BaseProcessor {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
// Default configuration: exclude navigation/system buttons
|
|
9
|
+
this.options = {
|
|
10
|
+
excludeNavigationButtons: true,
|
|
11
|
+
excludeSystemButtons: true,
|
|
12
|
+
preserveAllButtons: false,
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// Helper method to determine if a button should be filtered out
|
|
17
|
+
shouldFilterButton(button) {
|
|
18
|
+
// If preserveAllButtons is true, never filter
|
|
19
|
+
if (this.options.preserveAllButtons) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// Apply custom filter if provided
|
|
23
|
+
if (this.options.customButtonFilter) {
|
|
24
|
+
return !this.options.customButtonFilter(button);
|
|
25
|
+
}
|
|
26
|
+
// Check semantic action-based filtering
|
|
27
|
+
if (button.semanticAction) {
|
|
28
|
+
const { category, intent } = button.semanticAction;
|
|
29
|
+
// Filter specific navigation intents (toolbar navigation only)
|
|
30
|
+
if (this.options.excludeNavigationButtons) {
|
|
31
|
+
const i = String(intent);
|
|
32
|
+
if (i === 'GO_BACK' || i === 'GO_HOME') {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Filter system/text editing buttons by category
|
|
37
|
+
if (this.options.excludeSystemButtons && category === treeStructure_1.AACSemanticCategory.TEXT_EDITING) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// Filter specific system intents
|
|
41
|
+
if (this.options.excludeSystemButtons) {
|
|
42
|
+
const i = String(intent);
|
|
43
|
+
if (i === 'DELETE_WORD' ||
|
|
44
|
+
i === 'DELETE_CHARACTER' ||
|
|
45
|
+
i === 'CLEAR_TEXT' ||
|
|
46
|
+
i === 'COPY_TEXT') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Fallback: check button labels for common navigation/system terms
|
|
52
|
+
// Only apply label-based filtering if button doesn't have semantic actions
|
|
53
|
+
if (!button.semanticAction &&
|
|
54
|
+
(this.options.excludeNavigationButtons || this.options.excludeSystemButtons)) {
|
|
55
|
+
const label = button.label?.toLowerCase() || '';
|
|
56
|
+
const message = button.message?.toLowerCase() || '';
|
|
57
|
+
// More conservative navigation terms (exclude "more" since it's often used for legitimate page navigation)
|
|
58
|
+
const navigationTerms = ['back', 'home', 'menu', 'settings'];
|
|
59
|
+
const systemTerms = ['delete', 'clear', 'copy', 'paste', 'undo', 'redo'];
|
|
60
|
+
if (this.options.excludeNavigationButtons &&
|
|
61
|
+
navigationTerms.some((term) => label.includes(term) || message.includes(term))) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (this.options.excludeSystemButtons &&
|
|
65
|
+
systemTerms.some((term) => label.includes(term) || message.includes(term))) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
// Helper method to filter buttons from a page
|
|
72
|
+
filterPageButtons(buttons) {
|
|
73
|
+
return buttons.filter((button) => !this.shouldFilterButton(button));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Generic implementation for extracting strings with metadata
|
|
77
|
+
* Can be used by any processor that doesn't need format-specific logic
|
|
78
|
+
* @param filePath - Path to the AAC file
|
|
79
|
+
* @returns Promise with extracted strings and metadata
|
|
80
|
+
*/
|
|
81
|
+
extractStringsWithMetadataGeneric(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
const tree = this.loadIntoTree(filePath);
|
|
84
|
+
const extractedMap = new Map();
|
|
85
|
+
// Process all pages and buttons
|
|
86
|
+
Object.values(tree.pages).forEach((page) => {
|
|
87
|
+
// Process page names
|
|
88
|
+
if (page.name && page.name.trim().length > 1 && !(0, stringCasing_1.isNumericOrEmpty)(page.name)) {
|
|
89
|
+
const key = page.name.trim().toLowerCase();
|
|
90
|
+
const vocabLocation = {
|
|
91
|
+
table: 'pages',
|
|
92
|
+
id: parseInt(page.id) || 0,
|
|
93
|
+
column: 'NAME',
|
|
94
|
+
casing: (0, stringCasing_1.detectCasing)(page.name),
|
|
95
|
+
};
|
|
96
|
+
this.addToExtractedMap(extractedMap, key, page.name.trim(), vocabLocation);
|
|
97
|
+
}
|
|
98
|
+
page.buttons.forEach((button) => {
|
|
99
|
+
// Process button labels
|
|
100
|
+
if (button.label && button.label.trim().length > 1 && !(0, stringCasing_1.isNumericOrEmpty)(button.label)) {
|
|
101
|
+
const key = button.label.trim().toLowerCase();
|
|
102
|
+
const vocabLocation = {
|
|
103
|
+
table: 'buttons',
|
|
104
|
+
id: parseInt(button.id) || 0,
|
|
105
|
+
column: 'LABEL',
|
|
106
|
+
casing: (0, stringCasing_1.detectCasing)(button.label),
|
|
107
|
+
};
|
|
108
|
+
this.addToExtractedMap(extractedMap, key, button.label.trim(), vocabLocation);
|
|
109
|
+
}
|
|
110
|
+
// Process button messages (if different from label)
|
|
111
|
+
if (button.message &&
|
|
112
|
+
button.message !== button.label &&
|
|
113
|
+
button.message.trim().length > 1 &&
|
|
114
|
+
!(0, stringCasing_1.isNumericOrEmpty)(button.message)) {
|
|
115
|
+
const key = button.message.trim().toLowerCase();
|
|
116
|
+
const vocabLocation = {
|
|
117
|
+
table: 'buttons',
|
|
118
|
+
id: parseInt(button.id) || 0,
|
|
119
|
+
column: 'MESSAGE',
|
|
120
|
+
casing: (0, stringCasing_1.detectCasing)(button.message),
|
|
121
|
+
};
|
|
122
|
+
this.addToExtractedMap(extractedMap, key, button.message.trim(), vocabLocation);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const extractedStrings = Array.from(extractedMap.values());
|
|
127
|
+
return Promise.resolve({ errors: [], extractedStrings });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return Promise.resolve({
|
|
131
|
+
errors: [
|
|
132
|
+
{
|
|
133
|
+
message: error instanceof Error ? error.message : 'Unknown extraction error',
|
|
134
|
+
step: 'EXTRACT',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
extractedStrings: [],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Generic implementation for generating translated downloads
|
|
143
|
+
* Can be used by any processor that doesn't need format-specific logic
|
|
144
|
+
* @param filePath - Path to the original AAC file
|
|
145
|
+
* @param translatedStrings - Array of translated string data
|
|
146
|
+
* @param sourceStrings - Array of source string data
|
|
147
|
+
* @returns Promise with path to the generated translated file
|
|
148
|
+
*/
|
|
149
|
+
generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings) {
|
|
150
|
+
try {
|
|
151
|
+
// Build translation map from the provided data
|
|
152
|
+
const translations = new Map();
|
|
153
|
+
sourceStrings.forEach((sourceString) => {
|
|
154
|
+
const translated = translatedStrings.find((ts) => ts.sourcestringid.toString() === sourceString.id.toString());
|
|
155
|
+
if (translated) {
|
|
156
|
+
const translatedText = translated.overridestring.length > 0
|
|
157
|
+
? translated.overridestring
|
|
158
|
+
: translated.translatedstring;
|
|
159
|
+
translations.set(sourceString.sourcestring, translatedText);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Generate output path based on file extension
|
|
163
|
+
const outputPath = this.generateTranslatedOutputPath(filePath);
|
|
164
|
+
// Use existing processTexts method
|
|
165
|
+
this.processTexts(filePath, translations, outputPath);
|
|
166
|
+
return Promise.resolve(outputPath);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return Promise.reject(new Error(`Failed to generate translated download: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Helper method to add extracted strings to the map, handling duplicates
|
|
174
|
+
* @param extractedMap - Map to store extracted strings
|
|
175
|
+
* @param key - Lowercase key for deduplication
|
|
176
|
+
* @param originalString - Original string with proper casing
|
|
177
|
+
* @param vocabLocation - Metadata about where the string was found
|
|
178
|
+
*/
|
|
179
|
+
addToExtractedMap(extractedMap, key, originalString, vocabLocation) {
|
|
180
|
+
const existing = extractedMap.get(key);
|
|
181
|
+
if (existing) {
|
|
182
|
+
existing.vocabPlacementMeta.vocabLocations.push(vocabLocation);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
extractedMap.set(key, {
|
|
186
|
+
string: originalString, // Use original casing for the string value
|
|
187
|
+
vocabPlacementMeta: {
|
|
188
|
+
vocabLocations: [vocabLocation],
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generate output path for translated file based on input file extension
|
|
195
|
+
* @param filePath - Original file path
|
|
196
|
+
* @returns Path for the translated output file
|
|
197
|
+
*/
|
|
198
|
+
generateTranslatedOutputPath(filePath) {
|
|
199
|
+
const lastDotIndex = filePath.lastIndexOf('.');
|
|
200
|
+
if (lastDotIndex === -1) {
|
|
201
|
+
return filePath + '_translated';
|
|
202
|
+
}
|
|
203
|
+
const basePath = filePath.substring(0, lastDotIndex);
|
|
204
|
+
const extension = filePath.substring(lastDotIndex);
|
|
205
|
+
return `${basePath}_translated${extension}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
exports.BaseProcessor = BaseProcessor;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type FileFormat = 'gridset' | 'coughdrop' | 'touchchat' | 'snap' | 'dot' | 'opml' | 'excel' | 'unknown';
|
|
2
|
+
declare class FileProcessor {
|
|
3
|
+
static readFile(filePath: string): Buffer;
|
|
4
|
+
static writeFile(filePath: string, data: Buffer | string): void;
|
|
5
|
+
static detectFormat(filePathOrBuffer: string | Buffer): FileFormat;
|
|
6
|
+
}
|
|
7
|
+
export default FileProcessor;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
class FileProcessor {
|
|
9
|
+
// Read a file and return its contents as a Buffer
|
|
10
|
+
static readFile(filePath) {
|
|
11
|
+
return fs_1.default.readFileSync(filePath);
|
|
12
|
+
}
|
|
13
|
+
// Write data (Buffer or string) to a file
|
|
14
|
+
static writeFile(filePath, data) {
|
|
15
|
+
fs_1.default.writeFileSync(filePath, data);
|
|
16
|
+
}
|
|
17
|
+
// Detect file format based on extension or magic bytes
|
|
18
|
+
static detectFormat(filePathOrBuffer) {
|
|
19
|
+
if (typeof filePathOrBuffer === 'string') {
|
|
20
|
+
const ext = path_1.default.extname(filePathOrBuffer).toLowerCase();
|
|
21
|
+
switch (ext) {
|
|
22
|
+
case '.gridset':
|
|
23
|
+
return 'gridset';
|
|
24
|
+
case '.obf':
|
|
25
|
+
case '.obz':
|
|
26
|
+
return 'coughdrop';
|
|
27
|
+
case '.ce':
|
|
28
|
+
case '.wfl':
|
|
29
|
+
case '.touchchat':
|
|
30
|
+
return 'touchchat';
|
|
31
|
+
case '.sps':
|
|
32
|
+
case '.spb':
|
|
33
|
+
return 'snap';
|
|
34
|
+
case '.dot':
|
|
35
|
+
return 'dot';
|
|
36
|
+
case '.opml':
|
|
37
|
+
return 'opml';
|
|
38
|
+
case '.xlsx':
|
|
39
|
+
return 'excel';
|
|
40
|
+
default:
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (Buffer.isBuffer(filePathOrBuffer)) {
|
|
45
|
+
// Optionally: inspect magic bytes here
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.default = FileProcessor;
|