@willwade/aac-processors 0.0.29 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -852
- package/dist/browser/core/baseProcessor.js +241 -0
- package/dist/browser/core/stringCasing.js +179 -0
- package/dist/browser/core/treeStructure.js +255 -0
- package/dist/browser/index.browser.js +73 -0
- package/dist/browser/processors/applePanelsProcessor.js +582 -0
- package/dist/browser/processors/astericsGridProcessor.js +1509 -0
- package/dist/browser/processors/dotProcessor.js +221 -0
- package/dist/browser/processors/gridset/commands.js +962 -0
- package/dist/browser/processors/gridset/crypto.js +53 -0
- package/dist/browser/processors/gridset/password.js +43 -0
- package/dist/browser/processors/gridset/pluginTypes.js +277 -0
- package/dist/browser/processors/gridset/resolver.js +137 -0
- package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
- package/dist/browser/processors/gridset/symbols.js +421 -0
- package/dist/browser/processors/gridsetProcessor.js +2002 -0
- package/dist/browser/processors/obfProcessor.js +705 -0
- package/dist/browser/processors/opmlProcessor.js +274 -0
- package/dist/browser/types/aac.js +38 -0
- package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
- package/dist/browser/utilities/translation/translationProcessor.js +200 -0
- package/dist/browser/utils/io.js +95 -0
- package/dist/browser/validation/baseValidator.js +156 -0
- package/dist/browser/validation/gridsetValidator.js +355 -0
- package/dist/browser/validation/obfValidator.js +500 -0
- package/dist/browser/validation/validationTypes.js +46 -0
- package/dist/cli/index.js +5 -5
- package/dist/core/analyze.d.ts +2 -2
- package/dist/core/analyze.js +2 -2
- package/dist/core/baseProcessor.d.ts +5 -4
- package/dist/core/baseProcessor.js +22 -27
- package/dist/core/treeStructure.d.ts +5 -5
- package/dist/core/treeStructure.js +1 -4
- package/dist/index.browser.d.ts +37 -0
- package/dist/index.browser.js +99 -0
- package/dist/index.d.ts +1 -48
- package/dist/index.js +1 -136
- package/dist/index.node.d.ts +48 -0
- package/dist/index.node.js +152 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -4
- package/dist/processors/applePanelsProcessor.js +58 -62
- package/dist/processors/astericsGridProcessor.d.ts +7 -6
- package/dist/processors/astericsGridProcessor.js +31 -42
- package/dist/processors/dotProcessor.d.ts +5 -4
- package/dist/processors/dotProcessor.js +25 -33
- package/dist/processors/excelProcessor.d.ts +4 -3
- package/dist/processors/excelProcessor.js +6 -3
- package/dist/processors/gridset/crypto.d.ts +18 -0
- package/dist/processors/gridset/crypto.js +57 -0
- package/dist/processors/gridset/helpers.d.ts +1 -1
- package/dist/processors/gridset/helpers.js +18 -8
- package/dist/processors/gridset/password.d.ts +20 -3
- package/dist/processors/gridset/password.js +17 -3
- package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
- package/dist/processors/gridset/wordlistHelpers.js +21 -20
- package/dist/processors/gridsetProcessor.d.ts +7 -12
- package/dist/processors/gridsetProcessor.js +118 -77
- package/dist/processors/obfProcessor.d.ts +9 -7
- package/dist/processors/obfProcessor.js +131 -56
- package/dist/processors/obfsetProcessor.d.ts +5 -4
- package/dist/processors/obfsetProcessor.js +10 -16
- package/dist/processors/opmlProcessor.d.ts +5 -4
- package/dist/processors/opmlProcessor.js +27 -34
- package/dist/processors/snapProcessor.d.ts +8 -7
- package/dist/processors/snapProcessor.js +15 -12
- package/dist/processors/touchchatProcessor.d.ts +8 -7
- package/dist/processors/touchchatProcessor.js +22 -17
- package/dist/types/aac.d.ts +0 -2
- package/dist/types/aac.js +2 -0
- package/dist/utils/io.d.ts +12 -0
- package/dist/utils/io.js +107 -0
- package/dist/validation/gridsetValidator.js +7 -7
- package/dist/validation/snapValidator.js +28 -35
- package/docs/BROWSER_USAGE.md +618 -0
- package/examples/README.md +77 -0
- package/examples/browser-test-server.js +81 -0
- package/examples/browser-test.html +331 -0
- package/examples/vitedemo/QUICKSTART.md +74 -0
- package/examples/vitedemo/README.md +157 -0
- package/examples/vitedemo/index.html +376 -0
- package/examples/vitedemo/package-lock.json +1221 -0
- package/examples/vitedemo/package.json +18 -0
- package/examples/vitedemo/src/main.ts +519 -0
- package/examples/vitedemo/test-files/example.dot +14 -0
- package/examples/vitedemo/test-files/example.grd +1 -0
- package/examples/vitedemo/test-files/example.gridset +0 -0
- package/examples/vitedemo/test-files/example.obz +0 -0
- package/examples/vitedemo/test-files/example.opml +18 -0
- package/examples/vitedemo/test-files/simple.obf +53 -0
- package/examples/vitedemo/tsconfig.json +24 -0
- package/examples/vitedemo/vite.config.ts +34 -0
- package/package.json +20 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AACProcessors Browser Entry
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe exports only (no Node-only dependencies).
|
|
5
|
+
*
|
|
6
|
+
* **NOTE: Gridset .gridsetx files**
|
|
7
|
+
* GridsetProcessor supports regular `.gridset` files in browser.
|
|
8
|
+
* Encrypted `.gridsetx` files require Node.js for crypto operations and are not supported in browser.
|
|
9
|
+
*/
|
|
10
|
+
// ===================================================================
|
|
11
|
+
// CORE TYPES
|
|
12
|
+
// ===================================================================
|
|
13
|
+
export * from './core/treeStructure';
|
|
14
|
+
export * from './core/baseProcessor';
|
|
15
|
+
export * from './core/stringCasing';
|
|
16
|
+
// ===================================================================
|
|
17
|
+
// BROWSER-SAFE PROCESSORS
|
|
18
|
+
// ===================================================================
|
|
19
|
+
export { DotProcessor } from './processors/dotProcessor';
|
|
20
|
+
export { OpmlProcessor } from './processors/opmlProcessor';
|
|
21
|
+
export { ObfProcessor } from './processors/obfProcessor';
|
|
22
|
+
export { GridsetProcessor } from './processors/gridsetProcessor';
|
|
23
|
+
export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
24
|
+
export { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
25
|
+
import { DotProcessor } from './processors/dotProcessor';
|
|
26
|
+
import { OpmlProcessor } from './processors/opmlProcessor';
|
|
27
|
+
import { ObfProcessor } from './processors/obfProcessor';
|
|
28
|
+
import { GridsetProcessor } from './processors/gridsetProcessor';
|
|
29
|
+
import { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
30
|
+
import { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
31
|
+
/**
|
|
32
|
+
* Factory function to get the appropriate processor for a file extension
|
|
33
|
+
* @param filePathOrExtension - File path or extension (e.g., '.dot', '/path/to/file.obf')
|
|
34
|
+
* @returns The appropriate processor instance
|
|
35
|
+
* @throws Error if the file extension is not supported
|
|
36
|
+
*/
|
|
37
|
+
export function getProcessor(filePathOrExtension) {
|
|
38
|
+
const extension = filePathOrExtension.includes('.')
|
|
39
|
+
? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
|
|
40
|
+
: filePathOrExtension;
|
|
41
|
+
switch (extension.toLowerCase()) {
|
|
42
|
+
case '.dot':
|
|
43
|
+
return new DotProcessor();
|
|
44
|
+
case '.opml':
|
|
45
|
+
return new OpmlProcessor();
|
|
46
|
+
case '.obf':
|
|
47
|
+
case '.obz':
|
|
48
|
+
return new ObfProcessor();
|
|
49
|
+
case '.gridset':
|
|
50
|
+
return new GridsetProcessor();
|
|
51
|
+
case '.plist':
|
|
52
|
+
return new ApplePanelsProcessor();
|
|
53
|
+
case '.grd':
|
|
54
|
+
return new AstericsGridProcessor();
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported file extension: ${extension}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get all supported file extensions
|
|
61
|
+
* @returns Array of supported file extensions
|
|
62
|
+
*/
|
|
63
|
+
export function getSupportedExtensions() {
|
|
64
|
+
return ['.dot', '.opml', '.obf', '.obz', '.gridset', '.plist', '.grd'];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if a file extension is supported
|
|
68
|
+
* @param extension - File extension to check
|
|
69
|
+
* @returns True if the extension is supported
|
|
70
|
+
*/
|
|
71
|
+
export function isExtensionSupported(extension) {
|
|
72
|
+
return getSupportedExtensions().includes(extension.toLowerCase());
|
|
73
|
+
}
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { BaseProcessor, } from '../core/baseProcessor';
|
|
2
|
+
import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
|
|
3
|
+
// Removed unused import: FileProcessor
|
|
4
|
+
import plist from 'plist';
|
|
5
|
+
import { ValidationFailureError, buildValidationResultFromMessage, } from '../validation/validationTypes';
|
|
6
|
+
import { getBasename, getFs, getPath, readBinaryFromInput, readTextFromInput, writeTextToPath, } from '../utils/io';
|
|
7
|
+
function isNormalizedPanel(panel) {
|
|
8
|
+
return typeof panel.id === 'string';
|
|
9
|
+
}
|
|
10
|
+
function normalizePanel(panel, fallbackId) {
|
|
11
|
+
const rawId = panel.ID || fallbackId;
|
|
12
|
+
const buttons = Array.isArray(panel.PanelObjects)
|
|
13
|
+
? panel.PanelObjects.filter((obj) => obj.PanelObjectType === 'Button')
|
|
14
|
+
: [];
|
|
15
|
+
const normalizedButtons = buttons.map((btn) => {
|
|
16
|
+
const firstAction = Array.isArray(btn.Actions) && btn.Actions.length > 0 ? btn.Actions[0] : undefined;
|
|
17
|
+
const isCharSequence = firstAction &&
|
|
18
|
+
(firstAction.ActionType === 'ActionPressKeyCharSequence' ||
|
|
19
|
+
firstAction.ActionType === 'ActionSendKeys');
|
|
20
|
+
const charString = isCharSequence ? firstAction?.ActionParam?.CharString : undefined;
|
|
21
|
+
const targetPanel = firstAction && firstAction.ActionType === 'ActionOpenPanel'
|
|
22
|
+
? firstAction.ActionParam?.PanelID?.replace(/^USER\./, '')
|
|
23
|
+
: undefined;
|
|
24
|
+
return {
|
|
25
|
+
label: btn.DisplayText || 'Button',
|
|
26
|
+
message: charString || btn.DisplayText || 'Button',
|
|
27
|
+
DisplayColor: btn.DisplayColor,
|
|
28
|
+
DisplayImageWeight: btn.DisplayImageWeight,
|
|
29
|
+
FontSize: btn.FontSize,
|
|
30
|
+
Rect: btn.Rect,
|
|
31
|
+
targetPanel,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
id: rawId.replace(/^USER\./, ''),
|
|
36
|
+
name: panel.Name || 'Panel',
|
|
37
|
+
buttons: normalizedButtons,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function normalizeActionParameters(input) {
|
|
41
|
+
if (typeof input === 'object' && input !== null) {
|
|
42
|
+
return { ...input };
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
class ApplePanelsProcessor extends BaseProcessor {
|
|
47
|
+
constructor(options) {
|
|
48
|
+
super(options);
|
|
49
|
+
}
|
|
50
|
+
// Helper function to parse Apple Panels Rect format "{{x, y}, {width, height}}"
|
|
51
|
+
parseRect(rectString) {
|
|
52
|
+
if (!rectString)
|
|
53
|
+
return null;
|
|
54
|
+
// Parse format like "{{0, 0}, {100, 25}}"
|
|
55
|
+
const match = rectString.match(/\{\{(\d+),\s*(\d+)\},\s*\{(\d+),\s*(\d+)\}\}/);
|
|
56
|
+
if (!match)
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
x: parseInt(match[1], 10),
|
|
60
|
+
y: parseInt(match[2], 10),
|
|
61
|
+
width: parseInt(match[3], 10),
|
|
62
|
+
height: parseInt(match[4], 10),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Convert pixel coordinates to grid coordinates (assuming 25px grid cells)
|
|
66
|
+
pixelToGrid(pixelX, pixelY, cellSize = 25) {
|
|
67
|
+
return {
|
|
68
|
+
gridX: Math.floor(pixelX / cellSize),
|
|
69
|
+
gridY: Math.floor(pixelY / cellSize),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async extractTexts(filePathOrBuffer) {
|
|
73
|
+
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
74
|
+
const texts = [];
|
|
75
|
+
for (const pageId in tree.pages) {
|
|
76
|
+
const page = tree.pages[pageId];
|
|
77
|
+
if (page.name)
|
|
78
|
+
texts.push(page.name);
|
|
79
|
+
page.buttons.forEach((btn) => {
|
|
80
|
+
if (btn.label)
|
|
81
|
+
texts.push(btn.label);
|
|
82
|
+
if (btn.message && btn.message !== btn.label)
|
|
83
|
+
texts.push(btn.message);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return texts;
|
|
87
|
+
}
|
|
88
|
+
async loadIntoTree(filePathOrBuffer) {
|
|
89
|
+
await Promise.resolve();
|
|
90
|
+
const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.plist';
|
|
91
|
+
let buffer;
|
|
92
|
+
try {
|
|
93
|
+
if (typeof filePathOrBuffer === 'string') {
|
|
94
|
+
const fs = getFs();
|
|
95
|
+
const path = getPath();
|
|
96
|
+
if (filePathOrBuffer.endsWith('.ascconfig')) {
|
|
97
|
+
const panelDefsPath = path.join(filePathOrBuffer, 'Contents', 'Resources', 'PanelDefinitions.plist');
|
|
98
|
+
if (fs.existsSync(panelDefsPath)) {
|
|
99
|
+
buffer = fs.readFileSync(panelDefsPath);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const validation = buildValidationResultFromMessage({
|
|
103
|
+
filename,
|
|
104
|
+
filesize: 0,
|
|
105
|
+
format: 'applepanels',
|
|
106
|
+
message: `Apple Panels file not found: ${panelDefsPath}`,
|
|
107
|
+
type: 'missing',
|
|
108
|
+
description: 'PanelDefinitions.plist',
|
|
109
|
+
});
|
|
110
|
+
throw new ValidationFailureError('Apple Panels file not found', validation);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
buffer = fs.readFileSync(filePathOrBuffer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
buffer = readBinaryFromInput(filePathOrBuffer);
|
|
119
|
+
}
|
|
120
|
+
const content = readTextFromInput(buffer);
|
|
121
|
+
const parsedData = plist.parse(content);
|
|
122
|
+
let panelsData = [];
|
|
123
|
+
if (Array.isArray(parsedData.panels)) {
|
|
124
|
+
panelsData = parsedData.panels.map((panel, index) => {
|
|
125
|
+
if (isNormalizedPanel(panel)) {
|
|
126
|
+
return panel;
|
|
127
|
+
}
|
|
128
|
+
const panelData = panel || {
|
|
129
|
+
PanelObjects: [],
|
|
130
|
+
};
|
|
131
|
+
return normalizePanel(panelData, `panel_${index}`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (parsedData.Panels) {
|
|
135
|
+
const panelsDict = parsedData.Panels;
|
|
136
|
+
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
137
|
+
const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
|
|
138
|
+
return normalizePanel(rawPanel, panelId);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (panelsData.length === 0) {
|
|
142
|
+
const validation = buildValidationResultFromMessage({
|
|
143
|
+
filename,
|
|
144
|
+
filesize: buffer.byteLength,
|
|
145
|
+
format: 'applepanels',
|
|
146
|
+
message: 'No panels found in Apple Panels file',
|
|
147
|
+
type: 'structure',
|
|
148
|
+
description: 'Panels definition',
|
|
149
|
+
});
|
|
150
|
+
throw new ValidationFailureError('Apple Panels has no panels', validation);
|
|
151
|
+
}
|
|
152
|
+
const data = { panels: panelsData };
|
|
153
|
+
const tree = new AACTree();
|
|
154
|
+
tree.metadata.format = 'applepanels';
|
|
155
|
+
data.panels.forEach((panel) => {
|
|
156
|
+
const page = new AACPage({
|
|
157
|
+
id: panel.id,
|
|
158
|
+
name: panel.name,
|
|
159
|
+
grid: [],
|
|
160
|
+
buttons: [],
|
|
161
|
+
parentId: null,
|
|
162
|
+
});
|
|
163
|
+
const gridLayout = [];
|
|
164
|
+
const maxRows = 20;
|
|
165
|
+
const maxCols = 20;
|
|
166
|
+
for (let r = 0; r < maxRows; r++) {
|
|
167
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
168
|
+
}
|
|
169
|
+
panel.buttons.forEach((btn, idx) => {
|
|
170
|
+
let semanticAction;
|
|
171
|
+
if (btn.targetPanel) {
|
|
172
|
+
semanticAction = {
|
|
173
|
+
category: AACSemanticCategory.NAVIGATION,
|
|
174
|
+
intent: AACSemanticIntent.NAVIGATE_TO,
|
|
175
|
+
targetId: btn.targetPanel,
|
|
176
|
+
platformData: {
|
|
177
|
+
applePanels: {
|
|
178
|
+
actionType: 'ActionOpenPanel',
|
|
179
|
+
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
fallback: {
|
|
183
|
+
type: 'NAVIGATE',
|
|
184
|
+
targetPageId: btn.targetPanel,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
semanticAction = {
|
|
190
|
+
category: AACSemanticCategory.COMMUNICATION,
|
|
191
|
+
intent: AACSemanticIntent.SPEAK_TEXT,
|
|
192
|
+
text: btn.message || btn.label,
|
|
193
|
+
platformData: {
|
|
194
|
+
applePanels: {
|
|
195
|
+
actionType: 'ActionPressKeyCharSequence',
|
|
196
|
+
parameters: {
|
|
197
|
+
CharString: btn.message || btn.label || '',
|
|
198
|
+
isStickyKey: false,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
fallback: {
|
|
203
|
+
type: 'SPEAK',
|
|
204
|
+
message: btn.message || btn.label,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const button = new AACButton({
|
|
209
|
+
id: `${panel.id}_btn_${idx}`,
|
|
210
|
+
label: btn.label,
|
|
211
|
+
message: btn.message || btn.label,
|
|
212
|
+
targetPageId: btn.targetPanel,
|
|
213
|
+
semanticAction: semanticAction,
|
|
214
|
+
style: {
|
|
215
|
+
backgroundColor: btn.DisplayColor,
|
|
216
|
+
fontSize: btn.FontSize,
|
|
217
|
+
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
page.addButton(button);
|
|
221
|
+
if (btn.Rect) {
|
|
222
|
+
const rect = this.parseRect(btn.Rect);
|
|
223
|
+
if (rect) {
|
|
224
|
+
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
225
|
+
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
226
|
+
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
227
|
+
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
228
|
+
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
229
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
230
|
+
gridLayout[r][c] = button;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
page.grid = gridLayout;
|
|
238
|
+
tree.addPage(page);
|
|
239
|
+
});
|
|
240
|
+
return tree;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (err instanceof ValidationFailureError) {
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
const validation = buildValidationResultFromMessage({
|
|
247
|
+
filename,
|
|
248
|
+
filesize: typeof filePathOrBuffer === 'string'
|
|
249
|
+
? (() => {
|
|
250
|
+
const fs = getFs();
|
|
251
|
+
return fs.existsSync(filePathOrBuffer) ? fs.statSync(filePathOrBuffer).size : 0;
|
|
252
|
+
})()
|
|
253
|
+
: readBinaryFromInput(filePathOrBuffer).byteLength,
|
|
254
|
+
format: 'applepanels',
|
|
255
|
+
message: err?.message || 'Failed to parse Apple Panels file',
|
|
256
|
+
type: 'parse',
|
|
257
|
+
description: 'Parse Apple Panels plist',
|
|
258
|
+
});
|
|
259
|
+
throw new ValidationFailureError('Failed to load Apple Panels file', validation, err);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
263
|
+
// Load the tree, apply translations, and save to new file
|
|
264
|
+
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
265
|
+
// Apply translations to all text content
|
|
266
|
+
Object.values(tree.pages).forEach((page) => {
|
|
267
|
+
// Translate page names
|
|
268
|
+
if (page.name && translations.has(page.name)) {
|
|
269
|
+
const translatedName = translations.get(page.name);
|
|
270
|
+
if (translatedName !== undefined) {
|
|
271
|
+
page.name = translatedName;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Translate button labels and messages
|
|
275
|
+
page.buttons.forEach((button) => {
|
|
276
|
+
if (button.label && translations.has(button.label)) {
|
|
277
|
+
const translatedLabel = translations.get(button.label);
|
|
278
|
+
if (translatedLabel !== undefined) {
|
|
279
|
+
button.label = translatedLabel;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (button.message && translations.has(button.message)) {
|
|
283
|
+
const translatedMessage = translations.get(button.message);
|
|
284
|
+
if (translatedMessage !== undefined) {
|
|
285
|
+
button.message = translatedMessage;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (button.semanticAction) {
|
|
289
|
+
const intentStr = String(button.semanticAction.intent);
|
|
290
|
+
if (intentStr === 'SPEAK_TEXT' || intentStr === 'INSERT_TEXT') {
|
|
291
|
+
const updatedText = button.message || button.label || '';
|
|
292
|
+
button.semanticAction.text = updatedText;
|
|
293
|
+
if (button.semanticAction.fallback) {
|
|
294
|
+
button.semanticAction.fallback.message = updatedText;
|
|
295
|
+
}
|
|
296
|
+
const platformParams = button.semanticAction.platformData?.applePanels?.parameters;
|
|
297
|
+
if (platformParams && typeof platformParams === 'object') {
|
|
298
|
+
if ('CharString' in platformParams) {
|
|
299
|
+
platformParams.CharString = updatedText;
|
|
300
|
+
}
|
|
301
|
+
if ('PanelID' in platformParams && button.targetPageId) {
|
|
302
|
+
platformParams.PanelID = `USER.${button.targetPageId}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// Save the translated tree to the requested location and return its content
|
|
310
|
+
await this.saveFromTree(tree, outputPath);
|
|
311
|
+
if (outputPath.endsWith('.plist')) {
|
|
312
|
+
return readBinaryFromInput(outputPath);
|
|
313
|
+
}
|
|
314
|
+
const path = getPath();
|
|
315
|
+
const configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
|
|
316
|
+
const panelDefsPath = path.join(configPath, 'Contents', 'Resources', 'PanelDefinitions.plist');
|
|
317
|
+
return readBinaryFromInput(panelDefsPath);
|
|
318
|
+
}
|
|
319
|
+
async saveFromTree(tree, outputPath) {
|
|
320
|
+
await Promise.resolve();
|
|
321
|
+
// Support two output modes:
|
|
322
|
+
// 1) Single-file .plist (PanelDefinitions.plist content written directly)
|
|
323
|
+
// 2) Apple Panels bundle folder (*.ascconfig) with Contents/Resources structure
|
|
324
|
+
const isSinglePlist = outputPath.endsWith('.plist');
|
|
325
|
+
// Prepare folder structure only when exporting as bundle
|
|
326
|
+
let configPath = '';
|
|
327
|
+
let contentsPath = '';
|
|
328
|
+
let resourcesPath = '';
|
|
329
|
+
if (!isSinglePlist) {
|
|
330
|
+
const fs = getFs();
|
|
331
|
+
const path = getPath();
|
|
332
|
+
configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
|
|
333
|
+
contentsPath = path.join(configPath, 'Contents');
|
|
334
|
+
resourcesPath = path.join(contentsPath, 'Resources');
|
|
335
|
+
if (!fs.existsSync(configPath))
|
|
336
|
+
fs.mkdirSync(configPath, { recursive: true });
|
|
337
|
+
if (!fs.existsSync(contentsPath))
|
|
338
|
+
fs.mkdirSync(contentsPath, { recursive: true });
|
|
339
|
+
if (!fs.existsSync(resourcesPath))
|
|
340
|
+
fs.mkdirSync(resourcesPath, { recursive: true });
|
|
341
|
+
// Create Info.plist (bundle mode only)
|
|
342
|
+
const infoPlist = {
|
|
343
|
+
ASCConfigurationDisplayName: tree.metadata?.name || 'AAC Processors Export',
|
|
344
|
+
ASCConfigurationIdentifier: `com.aacprocessors.${Date.now()}`,
|
|
345
|
+
ASCConfigurationProductSupportType: 'VirtualKeyboard',
|
|
346
|
+
ASCConfigurationVersion: tree.metadata?.version || '7.1',
|
|
347
|
+
CFBundleDevelopmentRegion: tree.metadata?.locale || 'en',
|
|
348
|
+
CFBundleIdentifier: 'com.aacprocessors.panel.export',
|
|
349
|
+
CFBundleName: tree.metadata?.name || 'AAC Processors Panels',
|
|
350
|
+
CFBundleShortVersionString: tree.metadata?.version || '1.0',
|
|
351
|
+
CFBundleVersion: '1',
|
|
352
|
+
NSHumanReadableCopyright: tree.metadata?.copyright ||
|
|
353
|
+
`Generated by AAC Processors${tree.metadata?.author ? ` - Author: ${tree.metadata.author}` : ''}`,
|
|
354
|
+
};
|
|
355
|
+
const infoPlistContent = plist.build(infoPlist);
|
|
356
|
+
writeTextToPath(path.join(contentsPath, 'Info.plist'), infoPlistContent);
|
|
357
|
+
// Create AssetIndex.plist (empty)
|
|
358
|
+
const assetIndexContent = plist.build({});
|
|
359
|
+
writeTextToPath(path.join(resourcesPath, 'AssetIndex.plist'), assetIndexContent);
|
|
360
|
+
}
|
|
361
|
+
// Build PanelDefinitions content from tree
|
|
362
|
+
const panelsDict = {};
|
|
363
|
+
Object.values(tree.pages).forEach((page, pageIndex) => {
|
|
364
|
+
const panelId = `USER.${page.id}`;
|
|
365
|
+
// Detect actual grid dimensions from the source data
|
|
366
|
+
let gridCols = 4; // Default fallback
|
|
367
|
+
if (page.grid && page.grid.length > 0) {
|
|
368
|
+
// Use actual grid dimensions from source
|
|
369
|
+
gridCols = page.grid[0] ? page.grid[0].length : 4;
|
|
370
|
+
// Find the actual used area to avoid empty space
|
|
371
|
+
let maxUsedX = 0, maxUsedY = 0;
|
|
372
|
+
for (let y = 0; y < page.grid.length; y++) {
|
|
373
|
+
for (let x = 0; x < page.grid[y].length; x++) {
|
|
374
|
+
if (page.grid[y][x]) {
|
|
375
|
+
maxUsedX = Math.max(maxUsedX, x);
|
|
376
|
+
maxUsedY = Math.max(maxUsedY, y);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Use the actual used dimensions if they're reasonable
|
|
381
|
+
if (maxUsedX > 0 && maxUsedY > 0) {
|
|
382
|
+
gridCols = maxUsedX + 1;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// Intelligent auto-layout: try to make a reasonable grid
|
|
387
|
+
const buttonCount = page.buttons.length;
|
|
388
|
+
if (buttonCount <= 6) {
|
|
389
|
+
gridCols = Math.min(buttonCount, 3); // 1-3 columns for small sets
|
|
390
|
+
}
|
|
391
|
+
else if (buttonCount <= 12) {
|
|
392
|
+
gridCols = 4; // 4 columns for medium sets
|
|
393
|
+
}
|
|
394
|
+
else if (buttonCount <= 24) {
|
|
395
|
+
gridCols = 6; // 6 columns for larger sets
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
gridCols = 8; // 8 columns for very large sets
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const panelObjects = page.buttons.map((button, buttonIndex) => {
|
|
402
|
+
// Find button position in grid layout and convert to Rect format
|
|
403
|
+
let rect;
|
|
404
|
+
if (page.grid && page.grid.length > 0) {
|
|
405
|
+
// Search for button in actual grid layout
|
|
406
|
+
let found = false;
|
|
407
|
+
for (let y = 0; y < page.grid.length && !found; y++) {
|
|
408
|
+
for (let x = 0; x < page.grid[y].length && !found; x++) {
|
|
409
|
+
const gridButton = page.grid[y][x];
|
|
410
|
+
if (gridButton && gridButton.id === button.id) {
|
|
411
|
+
// Convert grid coordinates to pixel coordinates
|
|
412
|
+
const pixelX = x * 105; // 105px per column (100px button + 5px spacing)
|
|
413
|
+
const pixelY = y * 30; // 30px per row (25px button + 5px spacing)
|
|
414
|
+
rect = `{{${pixelX}, ${pixelY}}, {100, 25}}`;
|
|
415
|
+
found = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!found) {
|
|
420
|
+
// Button not found in grid, use auto-layout
|
|
421
|
+
const autoX = (buttonIndex % gridCols) * 105;
|
|
422
|
+
const autoY = Math.floor(buttonIndex / gridCols) * 30;
|
|
423
|
+
rect = `{{${autoX}, ${autoY}}, {100, 25}}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Use auto-layout with detected grid dimensions
|
|
428
|
+
const autoX = (buttonIndex % gridCols) * 105;
|
|
429
|
+
const autoY = Math.floor(buttonIndex / gridCols) * 30;
|
|
430
|
+
rect = `{{${autoX}, ${autoY}}, {100, 25}}`;
|
|
431
|
+
}
|
|
432
|
+
const buttonObj = {
|
|
433
|
+
ButtonType: 0,
|
|
434
|
+
DisplayText: button.label || 'Button',
|
|
435
|
+
FontSize: button.style?.fontSize || 12,
|
|
436
|
+
ID: `Button.${button.id}`,
|
|
437
|
+
PanelObjectType: 'Button',
|
|
438
|
+
Rect: rect ?? '{{0, 0}, {100, 25}}',
|
|
439
|
+
Actions: [],
|
|
440
|
+
};
|
|
441
|
+
if (button.style?.backgroundColor) {
|
|
442
|
+
buttonObj.DisplayColor = button.style.backgroundColor;
|
|
443
|
+
}
|
|
444
|
+
if (button.style?.fontWeight === 'bold') {
|
|
445
|
+
buttonObj.DisplayImageWeight = 'FontWeightBold';
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
buttonObj.DisplayImageWeight = 'FontWeightRegular';
|
|
449
|
+
}
|
|
450
|
+
// Add actions - prefer semantic action if available
|
|
451
|
+
buttonObj.Actions = [this.createApplePanelsAction(button)];
|
|
452
|
+
return buttonObj;
|
|
453
|
+
});
|
|
454
|
+
panelsDict[panelId] = {
|
|
455
|
+
DisplayOrder: pageIndex + 1,
|
|
456
|
+
GlidingLensSize: 5,
|
|
457
|
+
HasTransientPosition: false,
|
|
458
|
+
HideHome: false,
|
|
459
|
+
HideMinimize: false,
|
|
460
|
+
HidePanelAdjustments: false,
|
|
461
|
+
HideSwitchDock: false,
|
|
462
|
+
HideSwitchDockContextualButtons: false,
|
|
463
|
+
HideTitlebar: false,
|
|
464
|
+
ID: panelId,
|
|
465
|
+
Name: page.name || 'Panel',
|
|
466
|
+
PanelObjects: panelObjects,
|
|
467
|
+
ProductSupportType: 'All',
|
|
468
|
+
Rect: '{{15, 75}, {425, 55}}',
|
|
469
|
+
ScanStyle: 0,
|
|
470
|
+
ShowPanelLocationString: 'CustomPanelList',
|
|
471
|
+
UsesPinnedResizing: false,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
const panelsValue = Object.fromEntries(Object.entries(panelsDict).map(([key, value]) => [key, value]));
|
|
475
|
+
const panelDefinitions = {
|
|
476
|
+
Panels: panelsValue,
|
|
477
|
+
ToolbarOrdering: {
|
|
478
|
+
ToolbarIdentifiersAfterBasePanel: [],
|
|
479
|
+
ToolbarIdentifiersPriorToBasePanel: [],
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const panelDefsContent = plist.build(panelDefinitions);
|
|
483
|
+
if (isSinglePlist) {
|
|
484
|
+
// Write single PanelDefinitions.plist file directly
|
|
485
|
+
const fs = getFs();
|
|
486
|
+
const path = getPath();
|
|
487
|
+
const dir = path.dirname(outputPath);
|
|
488
|
+
if (!fs.existsSync(dir))
|
|
489
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
490
|
+
writeTextToPath(outputPath, panelDefsContent);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// Write into bundle structure
|
|
494
|
+
const path = getPath();
|
|
495
|
+
writeTextToPath(path.join(resourcesPath, 'PanelDefinitions.plist'), panelDefsContent);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
createApplePanelsAction(button) {
|
|
499
|
+
// Use semantic action if available
|
|
500
|
+
if (button.semanticAction?.platformData?.applePanels) {
|
|
501
|
+
const applePanelsData = button.semanticAction.platformData.applePanels;
|
|
502
|
+
return {
|
|
503
|
+
ActionParam: normalizeActionParameters(applePanelsData.parameters),
|
|
504
|
+
ActionRecordedOffset: 0.0,
|
|
505
|
+
ActionType: applePanelsData.actionType,
|
|
506
|
+
ID: `Action.${button.id}`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
// Handle semantic actions without Apple Panels specific data
|
|
510
|
+
if (button.semanticAction) {
|
|
511
|
+
const intentStr = String(button.semanticAction.intent);
|
|
512
|
+
switch (intentStr) {
|
|
513
|
+
case 'NAVIGATE_TO':
|
|
514
|
+
return {
|
|
515
|
+
ActionParam: {
|
|
516
|
+
PanelID: `USER.${button.semanticAction.targetId || button.targetPageId || ''}`,
|
|
517
|
+
},
|
|
518
|
+
ActionRecordedOffset: 0.0,
|
|
519
|
+
ActionType: 'ActionOpenPanel',
|
|
520
|
+
ID: `Action.${button.id}`,
|
|
521
|
+
};
|
|
522
|
+
case 'SPEAK_TEXT':
|
|
523
|
+
case 'INSERT_TEXT':
|
|
524
|
+
return {
|
|
525
|
+
ActionParam: {
|
|
526
|
+
CharString: button.semanticAction.text || button.message || button.label || '',
|
|
527
|
+
isStickyKey: false,
|
|
528
|
+
},
|
|
529
|
+
ActionRecordedOffset: 0.0,
|
|
530
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
531
|
+
ID: `Action.${button.id}`,
|
|
532
|
+
};
|
|
533
|
+
case 'SEND_KEYS':
|
|
534
|
+
return {
|
|
535
|
+
ActionParam: {
|
|
536
|
+
CharString: button.semanticAction.text || '',
|
|
537
|
+
isStickyKey: false,
|
|
538
|
+
},
|
|
539
|
+
ActionRecordedOffset: 0.0,
|
|
540
|
+
ActionType: 'ActionSendKeys',
|
|
541
|
+
ID: `Action.${button.id}`,
|
|
542
|
+
};
|
|
543
|
+
default:
|
|
544
|
+
// Fallback to speech for unknown semantic actions
|
|
545
|
+
return {
|
|
546
|
+
ActionParam: {
|
|
547
|
+
CharString: button.semanticAction.fallback?.message || button.message || button.label || '',
|
|
548
|
+
isStickyKey: false,
|
|
549
|
+
},
|
|
550
|
+
ActionRecordedOffset: 0.0,
|
|
551
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
552
|
+
ID: `Action.${button.id}`,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Default SPEAK action if no semantic action
|
|
557
|
+
return {
|
|
558
|
+
ActionParam: {
|
|
559
|
+
CharString: button.message || button.label || '',
|
|
560
|
+
isStickyKey: false,
|
|
561
|
+
},
|
|
562
|
+
ActionRecordedOffset: 0.0,
|
|
563
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
564
|
+
ID: `Action.${button.id}`,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
569
|
+
* Uses the generic implementation from BaseProcessor
|
|
570
|
+
*/
|
|
571
|
+
extractStringsWithMetadata(filePath) {
|
|
572
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
576
|
+
* Uses the generic implementation from BaseProcessor
|
|
577
|
+
*/
|
|
578
|
+
generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
579
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
export { ApplePanelsProcessor };
|