@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,14 @@
|
|
|
1
|
+
export { ApplePanelsProcessor } from './applePanelsProcessor';
|
|
2
|
+
export { DotProcessor } from './dotProcessor';
|
|
3
|
+
export { ExcelProcessor } from './excelProcessor';
|
|
4
|
+
export { GridsetProcessor } from './gridsetProcessor';
|
|
5
|
+
export { ObfProcessor } from './obfProcessor';
|
|
6
|
+
export { OpmlProcessor } from './opmlProcessor';
|
|
7
|
+
export { SnapProcessor } from './snapProcessor';
|
|
8
|
+
export { TouchChatProcessor } from './touchchatProcessor';
|
|
9
|
+
export { AstericsGridProcessor } from './astericsGridProcessor';
|
|
10
|
+
export { getPageTokenImageMap, getAllowedImageEntries, openImage } from './gridset/helpers';
|
|
11
|
+
export { getPageTokenImageMap as getGridsetPageTokenImageMap, getAllowedImageEntries as getGridsetAllowedImageEntries, openImage as openGridsetImage, } from './gridset/helpers';
|
|
12
|
+
export { resolveGrid3CellImage } from './gridset/resolver';
|
|
13
|
+
export { getPageTokenImageMap as getSnapPageTokenImageMap, getAllowedImageEntries as getSnapAllowedImageEntries, openImage as openSnapImage, } from './snap/helpers';
|
|
14
|
+
export { getPageTokenImageMap as getTouchChatPageTokenImageMap, getAllowedImageEntries as getTouchChatAllowedImageEntries, openImage as openTouchChatImage, } from './touchchat/helpers';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openTouchChatImage = exports.getTouchChatAllowedImageEntries = exports.getTouchChatPageTokenImageMap = exports.openSnapImage = exports.getSnapAllowedImageEntries = exports.getSnapPageTokenImageMap = exports.resolveGrid3CellImage = exports.openGridsetImage = exports.getGridsetAllowedImageEntries = exports.getGridsetPageTokenImageMap = exports.openImage = exports.getAllowedImageEntries = exports.getPageTokenImageMap = exports.AstericsGridProcessor = exports.TouchChatProcessor = exports.SnapProcessor = exports.OpmlProcessor = exports.ObfProcessor = exports.GridsetProcessor = exports.ExcelProcessor = exports.DotProcessor = exports.ApplePanelsProcessor = void 0;
|
|
4
|
+
var applePanelsProcessor_1 = require("./applePanelsProcessor");
|
|
5
|
+
Object.defineProperty(exports, "ApplePanelsProcessor", { enumerable: true, get: function () { return applePanelsProcessor_1.ApplePanelsProcessor; } });
|
|
6
|
+
var dotProcessor_1 = require("./dotProcessor");
|
|
7
|
+
Object.defineProperty(exports, "DotProcessor", { enumerable: true, get: function () { return dotProcessor_1.DotProcessor; } });
|
|
8
|
+
var excelProcessor_1 = require("./excelProcessor");
|
|
9
|
+
Object.defineProperty(exports, "ExcelProcessor", { enumerable: true, get: function () { return excelProcessor_1.ExcelProcessor; } });
|
|
10
|
+
var gridsetProcessor_1 = require("./gridsetProcessor");
|
|
11
|
+
Object.defineProperty(exports, "GridsetProcessor", { enumerable: true, get: function () { return gridsetProcessor_1.GridsetProcessor; } });
|
|
12
|
+
var obfProcessor_1 = require("./obfProcessor");
|
|
13
|
+
Object.defineProperty(exports, "ObfProcessor", { enumerable: true, get: function () { return obfProcessor_1.ObfProcessor; } });
|
|
14
|
+
var opmlProcessor_1 = require("./opmlProcessor");
|
|
15
|
+
Object.defineProperty(exports, "OpmlProcessor", { enumerable: true, get: function () { return opmlProcessor_1.OpmlProcessor; } });
|
|
16
|
+
var snapProcessor_1 = require("./snapProcessor");
|
|
17
|
+
Object.defineProperty(exports, "SnapProcessor", { enumerable: true, get: function () { return snapProcessor_1.SnapProcessor; } });
|
|
18
|
+
var touchchatProcessor_1 = require("./touchchatProcessor");
|
|
19
|
+
Object.defineProperty(exports, "TouchChatProcessor", { enumerable: true, get: function () { return touchchatProcessor_1.TouchChatProcessor; } });
|
|
20
|
+
var astericsGridProcessor_1 = require("./astericsGridProcessor");
|
|
21
|
+
Object.defineProperty(exports, "AstericsGridProcessor", { enumerable: true, get: function () { return astericsGridProcessor_1.AstericsGridProcessor; } });
|
|
22
|
+
// Gridset (Grid 3) helpers
|
|
23
|
+
var helpers_1 = require("./gridset/helpers");
|
|
24
|
+
Object.defineProperty(exports, "getPageTokenImageMap", { enumerable: true, get: function () { return helpers_1.getPageTokenImageMap; } });
|
|
25
|
+
Object.defineProperty(exports, "getAllowedImageEntries", { enumerable: true, get: function () { return helpers_1.getAllowedImageEntries; } });
|
|
26
|
+
Object.defineProperty(exports, "openImage", { enumerable: true, get: function () { return helpers_1.openImage; } });
|
|
27
|
+
var helpers_2 = require("./gridset/helpers");
|
|
28
|
+
Object.defineProperty(exports, "getGridsetPageTokenImageMap", { enumerable: true, get: function () { return helpers_2.getPageTokenImageMap; } });
|
|
29
|
+
Object.defineProperty(exports, "getGridsetAllowedImageEntries", { enumerable: true, get: function () { return helpers_2.getAllowedImageEntries; } });
|
|
30
|
+
Object.defineProperty(exports, "openGridsetImage", { enumerable: true, get: function () { return helpers_2.openImage; } });
|
|
31
|
+
var resolver_1 = require("./gridset/resolver");
|
|
32
|
+
Object.defineProperty(exports, "resolveGrid3CellImage", { enumerable: true, get: function () { return resolver_1.resolveGrid3CellImage; } });
|
|
33
|
+
// Snap helpers (stubs)
|
|
34
|
+
var helpers_3 = require("./snap/helpers");
|
|
35
|
+
Object.defineProperty(exports, "getSnapPageTokenImageMap", { enumerable: true, get: function () { return helpers_3.getPageTokenImageMap; } });
|
|
36
|
+
Object.defineProperty(exports, "getSnapAllowedImageEntries", { enumerable: true, get: function () { return helpers_3.getAllowedImageEntries; } });
|
|
37
|
+
Object.defineProperty(exports, "openSnapImage", { enumerable: true, get: function () { return helpers_3.openImage; } });
|
|
38
|
+
// TouchChat helpers (stubs)
|
|
39
|
+
var helpers_4 = require("./touchchat/helpers");
|
|
40
|
+
Object.defineProperty(exports, "getTouchChatPageTokenImageMap", { enumerable: true, get: function () { return helpers_4.getPageTokenImageMap; } });
|
|
41
|
+
Object.defineProperty(exports, "getTouchChatAllowedImageEntries", { enumerable: true, get: function () { return helpers_4.getAllowedImageEntries; } });
|
|
42
|
+
Object.defineProperty(exports, "openTouchChatImage", { enumerable: true, get: function () { return helpers_4.openImage; } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
|
|
2
|
+
import { AACTree } from '../core/treeStructure';
|
|
3
|
+
declare class ObfProcessor extends BaseProcessor {
|
|
4
|
+
constructor(options?: ProcessorOptions);
|
|
5
|
+
private processBoard;
|
|
6
|
+
extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
7
|
+
loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
8
|
+
processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
|
|
9
|
+
saveFromTree(tree: AACTree, outputPath: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
12
|
+
* Uses the generic implementation from BaseProcessor
|
|
13
|
+
*/
|
|
14
|
+
extractStringsWithMetadata(filePath: string): Promise<ExtractStringsResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
17
|
+
* Uses the generic implementation from BaseProcessor
|
|
18
|
+
*/
|
|
19
|
+
generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export { ObfProcessor };
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
exports.ObfProcessor = void 0;
|
|
7
|
+
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
|
+
const treeStructure_1 = require("../core/treeStructure");
|
|
9
|
+
// Removed unused import: FileProcessor
|
|
10
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options);
|
|
15
|
+
}
|
|
16
|
+
processBoard(boardData, _boardPath) {
|
|
17
|
+
const buttons = (boardData.buttons || []).map((btn) => {
|
|
18
|
+
const semanticAction = btn.load_board
|
|
19
|
+
? {
|
|
20
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
21
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
22
|
+
targetId: btn.load_board.path,
|
|
23
|
+
fallback: {
|
|
24
|
+
type: 'NAVIGATE',
|
|
25
|
+
targetPageId: btn.load_board.path,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
30
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
31
|
+
text: String(btn?.vocalization || btn?.label || ''),
|
|
32
|
+
fallback: {
|
|
33
|
+
type: 'SPEAK',
|
|
34
|
+
message: String(btn?.vocalization || btn?.label || ''),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
return new treeStructure_1.AACButton({
|
|
38
|
+
id: String(btn?.id || ''),
|
|
39
|
+
label: String(btn?.label || ''),
|
|
40
|
+
message: String(btn?.vocalization || btn?.label || ''),
|
|
41
|
+
style: {
|
|
42
|
+
backgroundColor: btn.background_color,
|
|
43
|
+
borderColor: btn.border_color,
|
|
44
|
+
},
|
|
45
|
+
semanticAction,
|
|
46
|
+
targetPageId: btn.load_board?.path,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const page = new treeStructure_1.AACPage({
|
|
50
|
+
id: String(boardData?.id || ''),
|
|
51
|
+
name: String(boardData?.name || ''),
|
|
52
|
+
grid: [],
|
|
53
|
+
buttons,
|
|
54
|
+
parentId: null,
|
|
55
|
+
});
|
|
56
|
+
// Process grid layout if available
|
|
57
|
+
if (boardData.grid) {
|
|
58
|
+
const rows = boardData.grid.rows;
|
|
59
|
+
const cols = boardData.grid.columns;
|
|
60
|
+
const grid = Array(rows)
|
|
61
|
+
.fill(null)
|
|
62
|
+
.map(() => Array(cols).fill(null));
|
|
63
|
+
for (const btn of boardData.buttons) {
|
|
64
|
+
if (typeof btn.box_id === 'number') {
|
|
65
|
+
const row = Math.floor(btn.box_id / cols);
|
|
66
|
+
const col = btn.box_id % cols;
|
|
67
|
+
if (row < rows && col < cols) {
|
|
68
|
+
const aacBtn = buttons.find((b) => b.id === btn.id);
|
|
69
|
+
if (aacBtn) {
|
|
70
|
+
grid[row][col] = aacBtn;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
page.grid = grid;
|
|
76
|
+
}
|
|
77
|
+
return page;
|
|
78
|
+
}
|
|
79
|
+
extractTexts(filePathOrBuffer) {
|
|
80
|
+
const tree = this.loadIntoTree(filePathOrBuffer);
|
|
81
|
+
const texts = [];
|
|
82
|
+
for (const pageId in tree.pages) {
|
|
83
|
+
const page = tree.pages[pageId];
|
|
84
|
+
if (page.name)
|
|
85
|
+
texts.push(page.name);
|
|
86
|
+
page.buttons.forEach((btn) => {
|
|
87
|
+
if (typeof btn.label === 'string')
|
|
88
|
+
texts.push(btn.label);
|
|
89
|
+
if (typeof btn.message === 'string' && btn.message !== btn.label)
|
|
90
|
+
texts.push(btn.message);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return texts;
|
|
94
|
+
}
|
|
95
|
+
loadIntoTree(filePathOrBuffer) {
|
|
96
|
+
// Detailed logging for debugging input
|
|
97
|
+
console.log('[OBF] loadIntoTree called with:', {
|
|
98
|
+
type: typeof filePathOrBuffer,
|
|
99
|
+
isBuffer: Buffer.isBuffer(filePathOrBuffer),
|
|
100
|
+
value: typeof filePathOrBuffer === 'string'
|
|
101
|
+
? filePathOrBuffer
|
|
102
|
+
: '[Buffer of length ' + filePathOrBuffer.length + ']',
|
|
103
|
+
});
|
|
104
|
+
const tree = new treeStructure_1.AACTree();
|
|
105
|
+
// Helper: try to parse JSON OBF
|
|
106
|
+
function tryParseObfJson(data) {
|
|
107
|
+
try {
|
|
108
|
+
const str = typeof data === 'string' ? data : data.toString('utf8');
|
|
109
|
+
// Check for empty or whitespace-only content
|
|
110
|
+
if (!str.trim()) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const obj = JSON.parse(str);
|
|
114
|
+
if (obj && typeof obj === 'object' && 'id' in obj && 'buttons' in obj) {
|
|
115
|
+
return obj;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// Log parsing errors for debugging but don't throw
|
|
120
|
+
console.warn(`Failed to parse OBF JSON: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// If input is a string path and ends with .obf, treat as JSON
|
|
125
|
+
if (typeof filePathOrBuffer === 'string' && filePathOrBuffer.endsWith('.obf')) {
|
|
126
|
+
const fs = require('fs');
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync(filePathOrBuffer, 'utf8');
|
|
129
|
+
const boardData = tryParseObfJson(content);
|
|
130
|
+
if (boardData) {
|
|
131
|
+
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
132
|
+
const page = this.processBoard(boardData, filePathOrBuffer);
|
|
133
|
+
tree.addPage(page);
|
|
134
|
+
return tree;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
throw new Error('Invalid OBF JSON content');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error('[OBF] Error reading .obf file:', err);
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// If input is a buffer or string that parses as OBF JSON
|
|
146
|
+
const asJson = tryParseObfJson(filePathOrBuffer);
|
|
147
|
+
if (asJson) {
|
|
148
|
+
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
149
|
+
const page = this.processBoard(asJson, '[bufferOrString]');
|
|
150
|
+
tree.addPage(page);
|
|
151
|
+
return tree;
|
|
152
|
+
}
|
|
153
|
+
// Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
|
|
154
|
+
function isLikelyZip(input) {
|
|
155
|
+
if (typeof input === 'string')
|
|
156
|
+
return input.endsWith('.zip') || input.endsWith('.obz');
|
|
157
|
+
if (Buffer.isBuffer(input) && input.length >= 2) {
|
|
158
|
+
return input[0] === 0x50 && input[1] === 0x4b; // 'PK'
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (!isLikelyZip(filePathOrBuffer)) {
|
|
163
|
+
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
164
|
+
}
|
|
165
|
+
let zip;
|
|
166
|
+
try {
|
|
167
|
+
zip = new adm_zip_1.default(filePathOrBuffer);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error('[OBF] Error instantiating AdmZip with input:', err);
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
174
|
+
zip.getEntries().forEach((entry) => {
|
|
175
|
+
if (entry.entryName.endsWith('.obf')) {
|
|
176
|
+
const content = entry.getData().toString('utf8');
|
|
177
|
+
const boardData = tryParseObfJson(content);
|
|
178
|
+
if (boardData) {
|
|
179
|
+
const page = this.processBoard(boardData, entry.entryName);
|
|
180
|
+
tree.addPage(page);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.warn('[OBF] Skipped entry (not valid OBF JSON):', entry.entryName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return tree;
|
|
188
|
+
}
|
|
189
|
+
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
190
|
+
// Load the tree, apply translations, and save to new file
|
|
191
|
+
const tree = this.loadIntoTree(filePathOrBuffer);
|
|
192
|
+
// Apply translations to all text content
|
|
193
|
+
Object.values(tree.pages).forEach((page) => {
|
|
194
|
+
// Translate page names
|
|
195
|
+
if (page.name && translations.has(page.name)) {
|
|
196
|
+
page.name = translations.get(page.name);
|
|
197
|
+
}
|
|
198
|
+
// Translate button labels and messages
|
|
199
|
+
page.buttons.forEach((button) => {
|
|
200
|
+
if (button.label && translations.has(button.label)) {
|
|
201
|
+
button.label = translations.get(button.label);
|
|
202
|
+
}
|
|
203
|
+
if (button.message && translations.has(button.message)) {
|
|
204
|
+
button.message = translations.get(button.message);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// Save the translated tree and return its content
|
|
209
|
+
this.saveFromTree(tree, outputPath);
|
|
210
|
+
return fs_1.default.readFileSync(outputPath);
|
|
211
|
+
}
|
|
212
|
+
saveFromTree(tree, outputPath) {
|
|
213
|
+
if (outputPath.endsWith('.obf')) {
|
|
214
|
+
// Save as single OBF JSON file
|
|
215
|
+
const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
|
|
216
|
+
if (!rootPage) {
|
|
217
|
+
throw new Error('No pages to save');
|
|
218
|
+
}
|
|
219
|
+
const obfBoard = {
|
|
220
|
+
id: rootPage.id,
|
|
221
|
+
name: rootPage.name || 'Exported Board',
|
|
222
|
+
buttons: rootPage.buttons.map((button) => ({
|
|
223
|
+
id: button.id,
|
|
224
|
+
label: button.label,
|
|
225
|
+
vocalization: button.message || button.label,
|
|
226
|
+
load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
|
|
227
|
+
? {
|
|
228
|
+
path: button.targetPageId,
|
|
229
|
+
}
|
|
230
|
+
: undefined,
|
|
231
|
+
background_color: button.style?.backgroundColor,
|
|
232
|
+
border_color: button.style?.borderColor,
|
|
233
|
+
})),
|
|
234
|
+
};
|
|
235
|
+
fs_1.default.writeFileSync(outputPath, JSON.stringify(obfBoard, null, 2));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Save as OBZ (zip with multiple OBF files)
|
|
239
|
+
const zip = new adm_zip_1.default();
|
|
240
|
+
Object.values(tree.pages).forEach((page) => {
|
|
241
|
+
const obfBoard = {
|
|
242
|
+
id: page.id,
|
|
243
|
+
name: page.name || 'Board',
|
|
244
|
+
buttons: page.buttons.map((button) => ({
|
|
245
|
+
id: button.id,
|
|
246
|
+
label: button.label,
|
|
247
|
+
vocalization: button.message || button.label,
|
|
248
|
+
load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
|
|
249
|
+
? {
|
|
250
|
+
path: button.targetPageId,
|
|
251
|
+
}
|
|
252
|
+
: undefined,
|
|
253
|
+
background_color: button.style?.backgroundColor,
|
|
254
|
+
border_color: button.style?.borderColor,
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
257
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
258
|
+
zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
|
|
259
|
+
});
|
|
260
|
+
zip.writeZip(outputPath);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
265
|
+
* Uses the generic implementation from BaseProcessor
|
|
266
|
+
*/
|
|
267
|
+
async extractStringsWithMetadata(filePath) {
|
|
268
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
272
|
+
* Uses the generic implementation from BaseProcessor
|
|
273
|
+
*/
|
|
274
|
+
async generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
275
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.ObfProcessor = ObfProcessor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
|
|
2
|
+
import { AACTree } from '../core/treeStructure';
|
|
3
|
+
declare class OpmlProcessor extends BaseProcessor {
|
|
4
|
+
constructor(options?: ProcessorOptions);
|
|
5
|
+
private processOutline;
|
|
6
|
+
extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
7
|
+
loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
8
|
+
processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
|
|
9
|
+
saveFromTree(tree: AACTree, outputPath: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
12
|
+
* Uses the generic implementation from BaseProcessor
|
|
13
|
+
*/
|
|
14
|
+
extractStringsWithMetadata(filePath: string): Promise<ExtractStringsResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
17
|
+
* Uses the generic implementation from BaseProcessor
|
|
18
|
+
*/
|
|
19
|
+
generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export { OpmlProcessor };
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
exports.OpmlProcessor = void 0;
|
|
7
|
+
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
|
+
const treeStructure_1 = require("../core/treeStructure");
|
|
9
|
+
// Removed unused import: FileProcessor
|
|
10
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
class OpmlProcessor extends baseProcessor_1.BaseProcessor {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options);
|
|
15
|
+
}
|
|
16
|
+
processOutline(outline, parentId = null) {
|
|
17
|
+
if (!outline || typeof outline !== 'object') {
|
|
18
|
+
return { page: null, childPages: [] };
|
|
19
|
+
}
|
|
20
|
+
const text = outline['@_text'] ||
|
|
21
|
+
(outline._attributes && outline._attributes.text) ||
|
|
22
|
+
outline.text;
|
|
23
|
+
if (!text || typeof text !== 'string') {
|
|
24
|
+
// Skip invalid outlines
|
|
25
|
+
return { page: null, childPages: [] };
|
|
26
|
+
}
|
|
27
|
+
const page = new treeStructure_1.AACPage({
|
|
28
|
+
id: text.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
29
|
+
name: text,
|
|
30
|
+
grid: [],
|
|
31
|
+
buttons: [],
|
|
32
|
+
parentId,
|
|
33
|
+
});
|
|
34
|
+
const childPages = [];
|
|
35
|
+
if (outline.outline) {
|
|
36
|
+
const children = Array.isArray(outline.outline) ? outline.outline : [outline.outline];
|
|
37
|
+
children.forEach((child) => {
|
|
38
|
+
const childText = child['@_text'] || (child._attributes && child._attributes.text) || child.text;
|
|
39
|
+
if (childText && typeof childText === 'string') {
|
|
40
|
+
const button = new treeStructure_1.AACButton({
|
|
41
|
+
id: `nav_${page.id}_${childText}`,
|
|
42
|
+
label: childText,
|
|
43
|
+
message: '',
|
|
44
|
+
targetPageId: childText.replace(/[^a-zA-Z0-9]/g, '_'),
|
|
45
|
+
});
|
|
46
|
+
page.addButton(button);
|
|
47
|
+
const { page: childPage, childPages: grandChildren } = this.processOutline(child, page.id);
|
|
48
|
+
if (childPage && childPage.id)
|
|
49
|
+
childPages.push(childPage, ...grandChildren);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!page || !page.id)
|
|
54
|
+
return { page: null, childPages: [] };
|
|
55
|
+
return { page, childPages };
|
|
56
|
+
}
|
|
57
|
+
extractTexts(filePathOrBuffer) {
|
|
58
|
+
const content = typeof filePathOrBuffer === 'string'
|
|
59
|
+
? fs_1.default.readFileSync(filePathOrBuffer, 'utf8')
|
|
60
|
+
: filePathOrBuffer.toString('utf8');
|
|
61
|
+
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
62
|
+
const data = parser.parse(content);
|
|
63
|
+
const texts = [];
|
|
64
|
+
function processNode(node) {
|
|
65
|
+
// Handle different attribute formats
|
|
66
|
+
let textValue;
|
|
67
|
+
if (node && node._attributes && typeof node._attributes.text === 'string') {
|
|
68
|
+
textValue = node._attributes.text;
|
|
69
|
+
}
|
|
70
|
+
else if (node && typeof node['@_text'] === 'string') {
|
|
71
|
+
textValue = node['@_text'];
|
|
72
|
+
}
|
|
73
|
+
else if (node && typeof node.text === 'string') {
|
|
74
|
+
textValue = node.text;
|
|
75
|
+
}
|
|
76
|
+
if (textValue) {
|
|
77
|
+
texts.push(textValue);
|
|
78
|
+
}
|
|
79
|
+
if (node && node.outline) {
|
|
80
|
+
const children = Array.isArray(node.outline) ? node.outline : [node.outline];
|
|
81
|
+
children.forEach(processNode);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const outlines = Array.isArray(data.opml.body.outline)
|
|
85
|
+
? data.opml.body.outline
|
|
86
|
+
: [data.opml.body.outline];
|
|
87
|
+
outlines.forEach(processNode);
|
|
88
|
+
return texts;
|
|
89
|
+
}
|
|
90
|
+
loadIntoTree(filePathOrBuffer) {
|
|
91
|
+
const content = typeof filePathOrBuffer === 'string'
|
|
92
|
+
? fs_1.default.readFileSync(filePathOrBuffer, 'utf8')
|
|
93
|
+
: filePathOrBuffer.toString('utf8');
|
|
94
|
+
if (!content || !content.trim()) {
|
|
95
|
+
throw new Error('Empty OPML content');
|
|
96
|
+
}
|
|
97
|
+
// Validate XML before parsing, fast-xml-parser is permissive by default
|
|
98
|
+
const validationResult = fast_xml_parser_1.XMLValidator.validate(content);
|
|
99
|
+
if (validationResult !== true) {
|
|
100
|
+
const reason = validationResult?.err?.msg || JSON.stringify(validationResult);
|
|
101
|
+
throw new Error(`Invalid OPML XML: ${reason}`);
|
|
102
|
+
}
|
|
103
|
+
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
104
|
+
let data;
|
|
105
|
+
try {
|
|
106
|
+
data = parser.parse(content);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
throw new Error(`Invalid OPML XML: ${e?.message || String(e)}`);
|
|
110
|
+
}
|
|
111
|
+
const tree = new treeStructure_1.AACTree();
|
|
112
|
+
// Handle case where body.outline might not exist or be in different formats
|
|
113
|
+
const bodyOutline = data.opml?.body?.outline;
|
|
114
|
+
if (!bodyOutline) {
|
|
115
|
+
return tree; // Return empty tree if no outline data
|
|
116
|
+
}
|
|
117
|
+
const outlines = Array.isArray(bodyOutline) ? bodyOutline : [bodyOutline];
|
|
118
|
+
let firstRootId = null;
|
|
119
|
+
outlines.forEach((outline) => {
|
|
120
|
+
const { page, childPages } = this.processOutline(outline);
|
|
121
|
+
if (page && page.id) {
|
|
122
|
+
tree.addPage(page);
|
|
123
|
+
if (!firstRootId)
|
|
124
|
+
firstRootId = page.id;
|
|
125
|
+
}
|
|
126
|
+
childPages.forEach((childPage) => {
|
|
127
|
+
if (childPage && childPage.id)
|
|
128
|
+
tree.addPage(childPage);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
// Set rootId to first root page, or fallback to first page if any exist
|
|
132
|
+
if (firstRootId) {
|
|
133
|
+
tree.rootId = firstRootId;
|
|
134
|
+
}
|
|
135
|
+
else if (Object.keys(tree.pages).length > 0) {
|
|
136
|
+
tree.rootId = Object.keys(tree.pages)[0];
|
|
137
|
+
}
|
|
138
|
+
return tree;
|
|
139
|
+
}
|
|
140
|
+
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
141
|
+
const content = typeof filePathOrBuffer === 'string'
|
|
142
|
+
? fs_1.default.readFileSync(filePathOrBuffer, 'utf8')
|
|
143
|
+
: filePathOrBuffer.toString('utf8');
|
|
144
|
+
let translatedContent = content;
|
|
145
|
+
// Apply translations to text attributes in OPML outline elements
|
|
146
|
+
translations.forEach((translation, originalText) => {
|
|
147
|
+
if (typeof originalText === 'string' && typeof translation === 'string') {
|
|
148
|
+
// Replace text attributes in outline elements
|
|
149
|
+
const textAttrRegex = new RegExp(`text="${originalText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g');
|
|
150
|
+
translatedContent = translatedContent.replace(textAttrRegex, `text="${translation}"`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const resultBuffer = Buffer.from(translatedContent, 'utf8');
|
|
154
|
+
// Save to output path
|
|
155
|
+
fs_1.default.writeFileSync(outputPath, resultBuffer);
|
|
156
|
+
return resultBuffer;
|
|
157
|
+
}
|
|
158
|
+
saveFromTree(tree, outputPath) {
|
|
159
|
+
// Helper to recursively build outline nodes with cycle detection
|
|
160
|
+
function buildOutline(page, visited = new Set()) {
|
|
161
|
+
// Prevent infinite recursion by tracking visited pages
|
|
162
|
+
if (visited.has(page.id)) {
|
|
163
|
+
return {
|
|
164
|
+
'@_text': `${page.name || page.id} (circular reference)`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
visited.add(page.id);
|
|
168
|
+
const outline = {
|
|
169
|
+
'@_text': page.name || page.id,
|
|
170
|
+
};
|
|
171
|
+
// Find child pages (by NAVIGATE buttons)
|
|
172
|
+
const childOutlines = page.buttons
|
|
173
|
+
.filter((b) => b.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO &&
|
|
174
|
+
!!b.targetPageId &&
|
|
175
|
+
!!tree.pages[b.targetPageId])
|
|
176
|
+
.map((b) => buildOutline(tree.pages[b.targetPageId], new Set(visited))); // Pass copy of visited set
|
|
177
|
+
if (childOutlines.length)
|
|
178
|
+
outline.outline = childOutlines;
|
|
179
|
+
return outline;
|
|
180
|
+
}
|
|
181
|
+
// Find root pages (no parentId or not navigated to by any button)
|
|
182
|
+
const navigatedIds = new Set();
|
|
183
|
+
Object.values(tree.pages).forEach((page) => {
|
|
184
|
+
page.buttons.forEach((b) => {
|
|
185
|
+
if (b.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && b.targetPageId)
|
|
186
|
+
navigatedIds.add(b.targetPageId);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
let rootPages = Object.values(tree.pages).filter((page) => !navigatedIds.has(page.id));
|
|
190
|
+
// If no rootPages, fall back to tree.rootId
|
|
191
|
+
const treeRootId = tree.rootId;
|
|
192
|
+
if ((!rootPages || rootPages.length === 0) && treeRootId && tree.pages[treeRootId]) {
|
|
193
|
+
rootPages = [tree.pages[treeRootId]];
|
|
194
|
+
}
|
|
195
|
+
else if (treeRootId) {
|
|
196
|
+
rootPages = rootPages.sort((a, b) => a.id === treeRootId ? -1 : b.id === treeRootId ? 1 : 0);
|
|
197
|
+
}
|
|
198
|
+
// Build outlines
|
|
199
|
+
const outlines = rootPages.map((page) => buildOutline(page));
|
|
200
|
+
// Compose OPML document
|
|
201
|
+
const opmlObj = {
|
|
202
|
+
opml: {
|
|
203
|
+
'@_version': '2.0',
|
|
204
|
+
head: { title: 'Exported OPML' },
|
|
205
|
+
body: { outline: outlines },
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
// Convert to XML
|
|
209
|
+
const { XMLBuilder } = require('fast-xml-parser');
|
|
210
|
+
const builder = new XMLBuilder({
|
|
211
|
+
ignoreAttributes: false,
|
|
212
|
+
format: true,
|
|
213
|
+
indentBy: ' ',
|
|
214
|
+
suppressEmptyNode: false,
|
|
215
|
+
attributeNamePrefix: '@_',
|
|
216
|
+
});
|
|
217
|
+
const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(opmlObj);
|
|
218
|
+
fs_1.default.writeFileSync(outputPath, xml, 'utf8');
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
222
|
+
* Uses the generic implementation from BaseProcessor
|
|
223
|
+
*/
|
|
224
|
+
extractStringsWithMetadata(filePath) {
|
|
225
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
229
|
+
* Uses the generic implementation from BaseProcessor
|
|
230
|
+
*/
|
|
231
|
+
generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
232
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.OpmlProcessor = OpmlProcessor;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AACTree } from '../../core/treeStructure';
|
|
2
|
+
export declare function getPageTokenImageMap(tree: AACTree, pageId: string): Map<string, string>;
|
|
3
|
+
export declare function getAllowedImageEntries(_tree: AACTree): Set<string>;
|
|
4
|
+
export declare function openImage(_dbOrFile: string | Buffer, _entryPath: string): Buffer | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPageTokenImageMap = getPageTokenImageMap;
|
|
4
|
+
exports.getAllowedImageEntries = getAllowedImageEntries;
|
|
5
|
+
exports.openImage = openImage;
|
|
6
|
+
// Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
|
|
7
|
+
// NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
|
|
8
|
+
// therefore return empty collections until image resolution is implemented.
|
|
9
|
+
function getPageTokenImageMap(tree, pageId) {
|
|
10
|
+
const map = new Map();
|
|
11
|
+
const page = tree.getPage(pageId);
|
|
12
|
+
if (!page)
|
|
13
|
+
return map;
|
|
14
|
+
for (const btn of page.buttons) {
|
|
15
|
+
if (btn.resolvedImageEntry)
|
|
16
|
+
map.set(btn.id, String(btn.resolvedImageEntry));
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
20
|
+
function getAllowedImageEntries(_tree) {
|
|
21
|
+
// No known image entry paths for Snap yet
|
|
22
|
+
return new Set();
|
|
23
|
+
}
|
|
24
|
+
function openImage(_dbOrFile, _entryPath) {
|
|
25
|
+
// Not implemented for Snap yet
|
|
26
|
+
return null;
|
|
27
|
+
}
|