@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.
Files changed (92) hide show
  1. package/README.md +52 -852
  2. package/dist/browser/core/baseProcessor.js +241 -0
  3. package/dist/browser/core/stringCasing.js +179 -0
  4. package/dist/browser/core/treeStructure.js +255 -0
  5. package/dist/browser/index.browser.js +73 -0
  6. package/dist/browser/processors/applePanelsProcessor.js +582 -0
  7. package/dist/browser/processors/astericsGridProcessor.js +1509 -0
  8. package/dist/browser/processors/dotProcessor.js +221 -0
  9. package/dist/browser/processors/gridset/commands.js +962 -0
  10. package/dist/browser/processors/gridset/crypto.js +53 -0
  11. package/dist/browser/processors/gridset/password.js +43 -0
  12. package/dist/browser/processors/gridset/pluginTypes.js +277 -0
  13. package/dist/browser/processors/gridset/resolver.js +137 -0
  14. package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
  15. package/dist/browser/processors/gridset/symbols.js +421 -0
  16. package/dist/browser/processors/gridsetProcessor.js +2002 -0
  17. package/dist/browser/processors/obfProcessor.js +705 -0
  18. package/dist/browser/processors/opmlProcessor.js +274 -0
  19. package/dist/browser/types/aac.js +38 -0
  20. package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
  21. package/dist/browser/utilities/translation/translationProcessor.js +200 -0
  22. package/dist/browser/utils/io.js +95 -0
  23. package/dist/browser/validation/baseValidator.js +156 -0
  24. package/dist/browser/validation/gridsetValidator.js +355 -0
  25. package/dist/browser/validation/obfValidator.js +500 -0
  26. package/dist/browser/validation/validationTypes.js +46 -0
  27. package/dist/cli/index.js +5 -5
  28. package/dist/core/analyze.d.ts +2 -2
  29. package/dist/core/analyze.js +2 -2
  30. package/dist/core/baseProcessor.d.ts +5 -4
  31. package/dist/core/baseProcessor.js +22 -27
  32. package/dist/core/treeStructure.d.ts +5 -5
  33. package/dist/core/treeStructure.js +1 -4
  34. package/dist/index.browser.d.ts +37 -0
  35. package/dist/index.browser.js +99 -0
  36. package/dist/index.d.ts +1 -48
  37. package/dist/index.js +1 -136
  38. package/dist/index.node.d.ts +48 -0
  39. package/dist/index.node.js +152 -0
  40. package/dist/processors/applePanelsProcessor.d.ts +5 -4
  41. package/dist/processors/applePanelsProcessor.js +58 -62
  42. package/dist/processors/astericsGridProcessor.d.ts +7 -6
  43. package/dist/processors/astericsGridProcessor.js +31 -42
  44. package/dist/processors/dotProcessor.d.ts +5 -4
  45. package/dist/processors/dotProcessor.js +25 -33
  46. package/dist/processors/excelProcessor.d.ts +4 -3
  47. package/dist/processors/excelProcessor.js +6 -3
  48. package/dist/processors/gridset/crypto.d.ts +18 -0
  49. package/dist/processors/gridset/crypto.js +57 -0
  50. package/dist/processors/gridset/helpers.d.ts +1 -1
  51. package/dist/processors/gridset/helpers.js +18 -8
  52. package/dist/processors/gridset/password.d.ts +20 -3
  53. package/dist/processors/gridset/password.js +17 -3
  54. package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
  55. package/dist/processors/gridset/wordlistHelpers.js +21 -20
  56. package/dist/processors/gridsetProcessor.d.ts +7 -12
  57. package/dist/processors/gridsetProcessor.js +118 -77
  58. package/dist/processors/obfProcessor.d.ts +9 -7
  59. package/dist/processors/obfProcessor.js +131 -56
  60. package/dist/processors/obfsetProcessor.d.ts +5 -4
  61. package/dist/processors/obfsetProcessor.js +10 -16
  62. package/dist/processors/opmlProcessor.d.ts +5 -4
  63. package/dist/processors/opmlProcessor.js +27 -34
  64. package/dist/processors/snapProcessor.d.ts +8 -7
  65. package/dist/processors/snapProcessor.js +15 -12
  66. package/dist/processors/touchchatProcessor.d.ts +8 -7
  67. package/dist/processors/touchchatProcessor.js +22 -17
  68. package/dist/types/aac.d.ts +0 -2
  69. package/dist/types/aac.js +2 -0
  70. package/dist/utils/io.d.ts +12 -0
  71. package/dist/utils/io.js +107 -0
  72. package/dist/validation/gridsetValidator.js +7 -7
  73. package/dist/validation/snapValidator.js +28 -35
  74. package/docs/BROWSER_USAGE.md +618 -0
  75. package/examples/README.md +77 -0
  76. package/examples/browser-test-server.js +81 -0
  77. package/examples/browser-test.html +331 -0
  78. package/examples/vitedemo/QUICKSTART.md +74 -0
  79. package/examples/vitedemo/README.md +157 -0
  80. package/examples/vitedemo/index.html +376 -0
  81. package/examples/vitedemo/package-lock.json +1221 -0
  82. package/examples/vitedemo/package.json +18 -0
  83. package/examples/vitedemo/src/main.ts +519 -0
  84. package/examples/vitedemo/test-files/example.dot +14 -0
  85. package/examples/vitedemo/test-files/example.grd +1 -0
  86. package/examples/vitedemo/test-files/example.gridset +0 -0
  87. package/examples/vitedemo/test-files/example.obz +0 -0
  88. package/examples/vitedemo/test-files/example.opml +18 -0
  89. package/examples/vitedemo/test-files/simple.obf +53 -0
  90. package/examples/vitedemo/tsconfig.json +24 -0
  91. package/examples/vitedemo/vite.config.ts +34 -0
  92. 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 };