@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.
Files changed (89) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +787 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +189 -0
  5. package/dist/cli/prettyPrint.d.ts +2 -0
  6. package/dist/cli/prettyPrint.js +28 -0
  7. package/dist/core/analyze.d.ts +6 -0
  8. package/dist/core/analyze.js +49 -0
  9. package/dist/core/baseProcessor.d.ts +94 -0
  10. package/dist/core/baseProcessor.js +208 -0
  11. package/dist/core/fileProcessor.d.ts +7 -0
  12. package/dist/core/fileProcessor.js +51 -0
  13. package/dist/core/stringCasing.d.ts +37 -0
  14. package/dist/core/stringCasing.js +174 -0
  15. package/dist/core/treeStructure.d.ts +190 -0
  16. package/dist/core/treeStructure.js +223 -0
  17. package/dist/index.d.ts +23 -0
  18. package/dist/index.js +96 -0
  19. package/dist/optional/symbolTools.d.ts +28 -0
  20. package/dist/optional/symbolTools.js +126 -0
  21. package/dist/processors/applePanelsProcessor.d.ts +23 -0
  22. package/dist/processors/applePanelsProcessor.js +521 -0
  23. package/dist/processors/astericsGridProcessor.d.ts +49 -0
  24. package/dist/processors/astericsGridProcessor.js +1427 -0
  25. package/dist/processors/dotProcessor.d.ts +21 -0
  26. package/dist/processors/dotProcessor.js +191 -0
  27. package/dist/processors/excelProcessor.d.ts +145 -0
  28. package/dist/processors/excelProcessor.js +556 -0
  29. package/dist/processors/gridset/helpers.d.ts +4 -0
  30. package/dist/processors/gridset/helpers.js +48 -0
  31. package/dist/processors/gridset/resolver.d.ts +8 -0
  32. package/dist/processors/gridset/resolver.js +100 -0
  33. package/dist/processors/gridsetProcessor.d.ts +28 -0
  34. package/dist/processors/gridsetProcessor.js +1339 -0
  35. package/dist/processors/index.d.ts +14 -0
  36. package/dist/processors/index.js +42 -0
  37. package/dist/processors/obfProcessor.d.ts +21 -0
  38. package/dist/processors/obfProcessor.js +278 -0
  39. package/dist/processors/opmlProcessor.d.ts +21 -0
  40. package/dist/processors/opmlProcessor.js +235 -0
  41. package/dist/processors/snap/helpers.d.ts +4 -0
  42. package/dist/processors/snap/helpers.js +27 -0
  43. package/dist/processors/snapProcessor.d.ts +44 -0
  44. package/dist/processors/snapProcessor.js +586 -0
  45. package/dist/processors/touchchat/helpers.d.ts +4 -0
  46. package/dist/processors/touchchat/helpers.js +27 -0
  47. package/dist/processors/touchchatProcessor.d.ts +27 -0
  48. package/dist/processors/touchchatProcessor.js +768 -0
  49. package/dist/types/aac.d.ts +47 -0
  50. package/dist/types/aac.js +2 -0
  51. package/docs/.keep +1 -0
  52. package/docs/ApplePanels.md +309 -0
  53. package/docs/Grid3-XML-Format.md +1788 -0
  54. package/docs/TobiiDynavox-Snap-Details.md +394 -0
  55. package/docs/asterics-Grid-fileformat-details.md +443 -0
  56. package/docs/obf_.obz Open Board File Formats.md +432 -0
  57. package/docs/touchchat.md +520 -0
  58. package/examples/.coverage +0 -0
  59. package/examples/.keep +1 -0
  60. package/examples/README.md +31 -0
  61. package/examples/communikate.dot +2637 -0
  62. package/examples/demo.js +143 -0
  63. package/examples/example-images.gridset +0 -0
  64. package/examples/example.ce +0 -0
  65. package/examples/example.dot +14 -0
  66. package/examples/example.grd +1 -0
  67. package/examples/example.gridset +0 -0
  68. package/examples/example.obf +27 -0
  69. package/examples/example.obz +0 -0
  70. package/examples/example.opml +18 -0
  71. package/examples/example.spb +0 -0
  72. package/examples/example.sps +0 -0
  73. package/examples/example2.grd +1 -0
  74. package/examples/gemini_response.txt +845 -0
  75. package/examples/image-map.js +45 -0
  76. package/examples/package-lock.json +1326 -0
  77. package/examples/package.json +10 -0
  78. package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
  79. package/examples/styled-output/styled-example.ce +0 -0
  80. package/examples/styled-output/styled-example.gridset +0 -0
  81. package/examples/styled-output/styled-example.obf +37 -0
  82. package/examples/styled-output/styled-example.spb +0 -0
  83. package/examples/styling-example.ts +316 -0
  84. package/examples/translate.js +39 -0
  85. package/examples/translate_demo.js +254 -0
  86. package/examples/translation_cache.json +44894 -0
  87. package/examples/typescript-demo.ts +251 -0
  88. package/examples/unified-interface-demo.ts +183 -0
  89. 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
+ }