@willwade/aac-processors 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +787 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +189 -0
- package/dist/cli/prettyPrint.d.ts +2 -0
- package/dist/cli/prettyPrint.js +28 -0
- package/dist/core/analyze.d.ts +6 -0
- package/dist/core/analyze.js +49 -0
- package/dist/core/baseProcessor.d.ts +94 -0
- package/dist/core/baseProcessor.js +208 -0
- package/dist/core/fileProcessor.d.ts +7 -0
- package/dist/core/fileProcessor.js +51 -0
- package/dist/core/stringCasing.d.ts +37 -0
- package/dist/core/stringCasing.js +174 -0
- package/dist/core/treeStructure.d.ts +190 -0
- package/dist/core/treeStructure.js +223 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +96 -0
- package/dist/optional/symbolTools.d.ts +28 -0
- package/dist/optional/symbolTools.js +126 -0
- package/dist/processors/applePanelsProcessor.d.ts +23 -0
- package/dist/processors/applePanelsProcessor.js +521 -0
- package/dist/processors/astericsGridProcessor.d.ts +49 -0
- package/dist/processors/astericsGridProcessor.js +1427 -0
- package/dist/processors/dotProcessor.d.ts +21 -0
- package/dist/processors/dotProcessor.js +191 -0
- package/dist/processors/excelProcessor.d.ts +145 -0
- package/dist/processors/excelProcessor.js +556 -0
- package/dist/processors/gridset/helpers.d.ts +4 -0
- package/dist/processors/gridset/helpers.js +48 -0
- package/dist/processors/gridset/resolver.d.ts +8 -0
- package/dist/processors/gridset/resolver.js +100 -0
- package/dist/processors/gridsetProcessor.d.ts +28 -0
- package/dist/processors/gridsetProcessor.js +1339 -0
- package/dist/processors/index.d.ts +14 -0
- package/dist/processors/index.js +42 -0
- package/dist/processors/obfProcessor.d.ts +21 -0
- package/dist/processors/obfProcessor.js +278 -0
- package/dist/processors/opmlProcessor.d.ts +21 -0
- package/dist/processors/opmlProcessor.js +235 -0
- package/dist/processors/snap/helpers.d.ts +4 -0
- package/dist/processors/snap/helpers.js +27 -0
- package/dist/processors/snapProcessor.d.ts +44 -0
- package/dist/processors/snapProcessor.js +586 -0
- package/dist/processors/touchchat/helpers.d.ts +4 -0
- package/dist/processors/touchchat/helpers.js +27 -0
- package/dist/processors/touchchatProcessor.d.ts +27 -0
- package/dist/processors/touchchatProcessor.js +768 -0
- package/dist/types/aac.d.ts +47 -0
- package/dist/types/aac.js +2 -0
- package/docs/.keep +1 -0
- package/docs/ApplePanels.md +309 -0
- package/docs/Grid3-XML-Format.md +1788 -0
- package/docs/TobiiDynavox-Snap-Details.md +394 -0
- package/docs/asterics-Grid-fileformat-details.md +443 -0
- package/docs/obf_.obz Open Board File Formats.md +432 -0
- package/docs/touchchat.md +520 -0
- package/examples/.coverage +0 -0
- package/examples/.keep +1 -0
- package/examples/README.md +31 -0
- package/examples/communikate.dot +2637 -0
- package/examples/demo.js +143 -0
- package/examples/example-images.gridset +0 -0
- package/examples/example.ce +0 -0
- package/examples/example.dot +14 -0
- package/examples/example.grd +1 -0
- package/examples/example.gridset +0 -0
- package/examples/example.obf +27 -0
- package/examples/example.obz +0 -0
- package/examples/example.opml +18 -0
- package/examples/example.spb +0 -0
- package/examples/example.sps +0 -0
- package/examples/example2.grd +1 -0
- package/examples/gemini_response.txt +845 -0
- package/examples/image-map.js +45 -0
- package/examples/package-lock.json +1326 -0
- package/examples/package.json +10 -0
- package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
- package/examples/styled-output/styled-example.ce +0 -0
- package/examples/styled-output/styled-example.gridset +0 -0
- package/examples/styled-output/styled-example.obf +37 -0
- package/examples/styled-output/styled-example.spb +0 -0
- package/examples/styling-example.ts +316 -0
- package/examples/translate.js +39 -0
- package/examples/translate_demo.js +254 -0
- package/examples/translation_cache.json +44894 -0
- package/examples/typescript-demo.ts +251 -0
- package/examples/unified-interface-demo.ts +183 -0
- package/package.json +106 -0
|
@@ -0,0 +1,521 @@
|
|
|
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.ApplePanelsProcessor = void 0;
|
|
7
|
+
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
|
+
const treeStructure_1 = require("../core/treeStructure");
|
|
9
|
+
// Removed unused import: FileProcessor
|
|
10
|
+
const plist_1 = __importDefault(require("plist"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super(options);
|
|
16
|
+
}
|
|
17
|
+
// Helper function to parse Apple Panels Rect format "{{x, y}, {width, height}}"
|
|
18
|
+
parseRect(rectString) {
|
|
19
|
+
if (!rectString)
|
|
20
|
+
return null;
|
|
21
|
+
// Parse format like "{{0, 0}, {100, 25}}"
|
|
22
|
+
const match = rectString.match(/\{\{(\d+),\s*(\d+)\},\s*\{(\d+),\s*(\d+)\}\}/);
|
|
23
|
+
if (!match)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
x: parseInt(match[1], 10),
|
|
27
|
+
y: parseInt(match[2], 10),
|
|
28
|
+
width: parseInt(match[3], 10),
|
|
29
|
+
height: parseInt(match[4], 10),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Convert pixel coordinates to grid coordinates (assuming 25px grid cells)
|
|
33
|
+
pixelToGrid(pixelX, pixelY, cellSize = 25) {
|
|
34
|
+
return {
|
|
35
|
+
gridX: Math.floor(pixelX / cellSize),
|
|
36
|
+
gridY: Math.floor(pixelY / cellSize),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
extractTexts(filePathOrBuffer) {
|
|
40
|
+
const tree = this.loadIntoTree(filePathOrBuffer);
|
|
41
|
+
const texts = [];
|
|
42
|
+
for (const pageId in tree.pages) {
|
|
43
|
+
const page = tree.pages[pageId];
|
|
44
|
+
if (page.name)
|
|
45
|
+
texts.push(page.name);
|
|
46
|
+
page.buttons.forEach((btn) => {
|
|
47
|
+
if (btn.label)
|
|
48
|
+
texts.push(btn.label);
|
|
49
|
+
if (btn.message && btn.message !== btn.label)
|
|
50
|
+
texts.push(btn.message);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return texts;
|
|
54
|
+
}
|
|
55
|
+
loadIntoTree(filePathOrBuffer) {
|
|
56
|
+
let content;
|
|
57
|
+
if (Buffer.isBuffer(filePathOrBuffer)) {
|
|
58
|
+
content = filePathOrBuffer.toString('utf8');
|
|
59
|
+
}
|
|
60
|
+
else if (typeof filePathOrBuffer === 'string') {
|
|
61
|
+
// Check if it's a .ascconfig folder or a direct .plist file
|
|
62
|
+
if (filePathOrBuffer.endsWith('.ascconfig')) {
|
|
63
|
+
// Read from proper Apple Panels structure: *.ascconfig/Contents/Resources/PanelDefinitions.plist
|
|
64
|
+
const panelDefsPath = `${filePathOrBuffer}/Contents/Resources/PanelDefinitions.plist`;
|
|
65
|
+
if (fs_1.default.existsSync(panelDefsPath)) {
|
|
66
|
+
content = fs_1.default.readFileSync(panelDefsPath, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
throw new Error(`Apple Panels file not found: ${panelDefsPath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Fallback: treat as direct .plist file
|
|
74
|
+
content = fs_1.default.readFileSync(filePathOrBuffer, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
throw new Error('Invalid input: expected string path or Buffer');
|
|
79
|
+
}
|
|
80
|
+
const parsedData = plist_1.default.parse(content);
|
|
81
|
+
// Handle both old format (panels array) and new Apple Panels format (Panels dict)
|
|
82
|
+
let panelsData = [];
|
|
83
|
+
if (Array.isArray(parsedData.panels)) {
|
|
84
|
+
// Old format
|
|
85
|
+
panelsData = parsedData.panels;
|
|
86
|
+
}
|
|
87
|
+
else if (parsedData.Panels) {
|
|
88
|
+
// Apple Panels format: convert Panels dict to array
|
|
89
|
+
const panelsDict = parsedData.Panels;
|
|
90
|
+
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
91
|
+
const panel = panelsDict[panelId];
|
|
92
|
+
return {
|
|
93
|
+
id: (panel.ID || panelId).replace(/^USER\./, ''), // Strip USER. prefix to maintain original IDs
|
|
94
|
+
name: panel.Name || 'Panel',
|
|
95
|
+
buttons: (panel.PanelObjects || [])
|
|
96
|
+
.filter((obj) => obj.PanelObjectType === 'Button')
|
|
97
|
+
.map((btn) => {
|
|
98
|
+
const firstAction = Array.isArray(btn.Actions) && btn.Actions.length > 0 ? btn.Actions[0] : undefined;
|
|
99
|
+
const isCharSequence = firstAction &&
|
|
100
|
+
(firstAction.ActionType === 'ActionPressKeyCharSequence' ||
|
|
101
|
+
firstAction.ActionType === 'ActionSendKeys');
|
|
102
|
+
const charString = isCharSequence
|
|
103
|
+
? (firstAction.ActionParam?.CharString ?? undefined)
|
|
104
|
+
: undefined;
|
|
105
|
+
const targetPanel = firstAction && firstAction.ActionType === 'ActionOpenPanel'
|
|
106
|
+
? firstAction.ActionParam?.PanelID?.replace(/^USER\./, '')
|
|
107
|
+
: undefined;
|
|
108
|
+
return {
|
|
109
|
+
label: btn.DisplayText || 'Button',
|
|
110
|
+
message: charString || btn.DisplayText || 'Button',
|
|
111
|
+
DisplayColor: btn.DisplayColor,
|
|
112
|
+
DisplayImageWeight: btn.DisplayImageWeight,
|
|
113
|
+
FontSize: btn.FontSize,
|
|
114
|
+
Rect: btn.Rect,
|
|
115
|
+
targetPanel,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const data = { panels: panelsData };
|
|
122
|
+
const tree = new treeStructure_1.AACTree();
|
|
123
|
+
data.panels.forEach((panel) => {
|
|
124
|
+
const page = new treeStructure_1.AACPage({
|
|
125
|
+
id: panel.id,
|
|
126
|
+
name: panel.name,
|
|
127
|
+
grid: [],
|
|
128
|
+
buttons: [],
|
|
129
|
+
parentId: null,
|
|
130
|
+
});
|
|
131
|
+
// Create a 2D grid to track button positions
|
|
132
|
+
const gridLayout = [];
|
|
133
|
+
const maxRows = 20; // Reasonable default for Apple Panels
|
|
134
|
+
const maxCols = 20;
|
|
135
|
+
for (let r = 0; r < maxRows; r++) {
|
|
136
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
137
|
+
}
|
|
138
|
+
panel.buttons.forEach((btn, idx) => {
|
|
139
|
+
// Create semantic action from Apple Panels button
|
|
140
|
+
let semanticAction;
|
|
141
|
+
if (btn.targetPanel) {
|
|
142
|
+
semanticAction = {
|
|
143
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
144
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
145
|
+
targetId: btn.targetPanel,
|
|
146
|
+
platformData: {
|
|
147
|
+
applePanels: {
|
|
148
|
+
actionType: 'ActionOpenPanel',
|
|
149
|
+
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
fallback: {
|
|
153
|
+
type: 'NAVIGATE',
|
|
154
|
+
targetPageId: btn.targetPanel,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
semanticAction = {
|
|
160
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
161
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
162
|
+
text: btn.message || btn.label,
|
|
163
|
+
platformData: {
|
|
164
|
+
applePanels: {
|
|
165
|
+
actionType: 'ActionPressKeyCharSequence',
|
|
166
|
+
parameters: {
|
|
167
|
+
CharString: btn.message || btn.label || '',
|
|
168
|
+
isStickyKey: false,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
fallback: {
|
|
173
|
+
type: 'SPEAK',
|
|
174
|
+
message: btn.message || btn.label,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const button = new treeStructure_1.AACButton({
|
|
179
|
+
id: `${panel.id}_btn_${idx}`,
|
|
180
|
+
label: btn.label,
|
|
181
|
+
message: btn.message || btn.label,
|
|
182
|
+
targetPageId: btn.targetPanel,
|
|
183
|
+
semanticAction: semanticAction,
|
|
184
|
+
style: {
|
|
185
|
+
backgroundColor: btn.DisplayColor,
|
|
186
|
+
fontSize: btn.FontSize,
|
|
187
|
+
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
page.addButton(button);
|
|
191
|
+
// Place button in grid layout using Rect position data
|
|
192
|
+
if (btn.Rect) {
|
|
193
|
+
const rect = this.parseRect(btn.Rect);
|
|
194
|
+
if (rect) {
|
|
195
|
+
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
196
|
+
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
197
|
+
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
198
|
+
// Place button in grid (handle width/height span)
|
|
199
|
+
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
200
|
+
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
201
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
202
|
+
gridLayout[r][c] = button;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Set the page's grid layout
|
|
210
|
+
page.grid = gridLayout;
|
|
211
|
+
tree.addPage(page);
|
|
212
|
+
});
|
|
213
|
+
return tree;
|
|
214
|
+
}
|
|
215
|
+
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
216
|
+
// Load the tree, apply translations, and save to new file
|
|
217
|
+
const tree = this.loadIntoTree(filePathOrBuffer);
|
|
218
|
+
// Apply translations to all text content
|
|
219
|
+
Object.values(tree.pages).forEach((page) => {
|
|
220
|
+
// Translate page names
|
|
221
|
+
if (page.name && translations.has(page.name)) {
|
|
222
|
+
page.name = translations.get(page.name);
|
|
223
|
+
}
|
|
224
|
+
// Translate button labels and messages
|
|
225
|
+
page.buttons.forEach((button) => {
|
|
226
|
+
if (button.label && translations.has(button.label)) {
|
|
227
|
+
button.label = translations.get(button.label);
|
|
228
|
+
}
|
|
229
|
+
if (button.message && translations.has(button.message)) {
|
|
230
|
+
button.message = translations.get(button.message);
|
|
231
|
+
}
|
|
232
|
+
if (button.semanticAction) {
|
|
233
|
+
const intentStr = String(button.semanticAction.intent);
|
|
234
|
+
if (intentStr === 'SPEAK_TEXT' || intentStr === 'INSERT_TEXT') {
|
|
235
|
+
const updatedText = button.message || button.label || '';
|
|
236
|
+
button.semanticAction.text = updatedText;
|
|
237
|
+
if (button.semanticAction.fallback) {
|
|
238
|
+
button.semanticAction.fallback.message = updatedText;
|
|
239
|
+
}
|
|
240
|
+
const platformParams = button.semanticAction.platformData?.applePanels?.parameters;
|
|
241
|
+
if (platformParams && typeof platformParams === 'object') {
|
|
242
|
+
if ('CharString' in platformParams) {
|
|
243
|
+
platformParams.CharString = updatedText;
|
|
244
|
+
}
|
|
245
|
+
if ('PanelID' in platformParams && button.targetPageId) {
|
|
246
|
+
platformParams.PanelID = `USER.${button.targetPageId}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// Save the translated tree to the requested location and return its content
|
|
254
|
+
this.saveFromTree(tree, outputPath);
|
|
255
|
+
if (outputPath.endsWith('.plist')) {
|
|
256
|
+
return fs_1.default.readFileSync(outputPath);
|
|
257
|
+
}
|
|
258
|
+
// In bundle mode, return the PanelDefinitions.plist content
|
|
259
|
+
const configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
|
|
260
|
+
const panelDefsPath = path_1.default.join(configPath, 'Contents', 'Resources', 'PanelDefinitions.plist');
|
|
261
|
+
return fs_1.default.readFileSync(panelDefsPath);
|
|
262
|
+
}
|
|
263
|
+
saveFromTree(tree, outputPath) {
|
|
264
|
+
// Support two output modes:
|
|
265
|
+
// 1) Single-file .plist (PanelDefinitions.plist content written directly)
|
|
266
|
+
// 2) Apple Panels bundle folder (*.ascconfig) with Contents/Resources structure
|
|
267
|
+
const isSinglePlist = outputPath.endsWith('.plist');
|
|
268
|
+
// Prepare folder structure only when exporting as bundle
|
|
269
|
+
let configPath = '';
|
|
270
|
+
let contentsPath = '';
|
|
271
|
+
let resourcesPath = '';
|
|
272
|
+
if (!isSinglePlist) {
|
|
273
|
+
configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
|
|
274
|
+
contentsPath = path_1.default.join(configPath, 'Contents');
|
|
275
|
+
resourcesPath = path_1.default.join(contentsPath, 'Resources');
|
|
276
|
+
if (!fs_1.default.existsSync(configPath))
|
|
277
|
+
fs_1.default.mkdirSync(configPath, { recursive: true });
|
|
278
|
+
if (!fs_1.default.existsSync(contentsPath))
|
|
279
|
+
fs_1.default.mkdirSync(contentsPath, { recursive: true });
|
|
280
|
+
if (!fs_1.default.existsSync(resourcesPath))
|
|
281
|
+
fs_1.default.mkdirSync(resourcesPath, { recursive: true });
|
|
282
|
+
// Create Info.plist (bundle mode only)
|
|
283
|
+
const infoPlist = {
|
|
284
|
+
ASCConfigurationDisplayName: 'AAC Processors Export',
|
|
285
|
+
ASCConfigurationIdentifier: `com.aacprocessors.${Date.now()}`,
|
|
286
|
+
ASCConfigurationProductSupportType: 'VirtualKeyboard',
|
|
287
|
+
ASCConfigurationVersion: '7.1',
|
|
288
|
+
CFBundleDevelopmentRegion: 'en',
|
|
289
|
+
CFBundleIdentifier: 'com.aacprocessors.panel.export',
|
|
290
|
+
CFBundleName: 'AAC Processors Panels',
|
|
291
|
+
CFBundleShortVersionString: '1.0',
|
|
292
|
+
CFBundleVersion: '1',
|
|
293
|
+
NSHumanReadableCopyright: 'Generated by AAC Processors',
|
|
294
|
+
};
|
|
295
|
+
const infoPlistContent = plist_1.default.build(infoPlist);
|
|
296
|
+
fs_1.default.writeFileSync(path_1.default.join(contentsPath, 'Info.plist'), infoPlistContent);
|
|
297
|
+
// Create AssetIndex.plist (empty)
|
|
298
|
+
const assetIndexContent = plist_1.default.build({});
|
|
299
|
+
fs_1.default.writeFileSync(path_1.default.join(resourcesPath, 'AssetIndex.plist'), assetIndexContent);
|
|
300
|
+
}
|
|
301
|
+
// Build PanelDefinitions content from tree
|
|
302
|
+
const panelsDict = {};
|
|
303
|
+
Object.values(tree.pages).forEach((page, pageIndex) => {
|
|
304
|
+
const panelId = `USER.${page.id}`;
|
|
305
|
+
// Detect actual grid dimensions from the source data
|
|
306
|
+
let gridCols = 4; // Default fallback
|
|
307
|
+
let gridRows = Math.ceil(page.buttons.length / gridCols);
|
|
308
|
+
if (page.grid && page.grid.length > 0) {
|
|
309
|
+
// Use actual grid dimensions from source
|
|
310
|
+
gridRows = page.grid.length;
|
|
311
|
+
gridCols = page.grid[0] ? page.grid[0].length : 4;
|
|
312
|
+
// Find the actual used area to avoid empty space
|
|
313
|
+
let maxUsedX = 0, maxUsedY = 0;
|
|
314
|
+
for (let y = 0; y < page.grid.length; y++) {
|
|
315
|
+
for (let x = 0; x < page.grid[y].length; x++) {
|
|
316
|
+
if (page.grid[y][x]) {
|
|
317
|
+
maxUsedX = Math.max(maxUsedX, x);
|
|
318
|
+
maxUsedY = Math.max(maxUsedY, y);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Use the actual used dimensions if they're reasonable
|
|
323
|
+
if (maxUsedX > 0 && maxUsedY > 0) {
|
|
324
|
+
gridCols = maxUsedX + 1;
|
|
325
|
+
gridRows = maxUsedY + 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
// Intelligent auto-layout: try to make a reasonable grid
|
|
330
|
+
const buttonCount = page.buttons.length;
|
|
331
|
+
if (buttonCount <= 6) {
|
|
332
|
+
gridCols = Math.min(buttonCount, 3); // 1-3 columns for small sets
|
|
333
|
+
}
|
|
334
|
+
else if (buttonCount <= 12) {
|
|
335
|
+
gridCols = 4; // 4 columns for medium sets
|
|
336
|
+
}
|
|
337
|
+
else if (buttonCount <= 24) {
|
|
338
|
+
gridCols = 6; // 6 columns for larger sets
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
gridCols = 8; // 8 columns for very large sets
|
|
342
|
+
}
|
|
343
|
+
gridRows = Math.ceil(buttonCount / gridCols);
|
|
344
|
+
}
|
|
345
|
+
const panelObjects = page.buttons.map((button, buttonIndex) => {
|
|
346
|
+
// Find button position in grid layout and convert to Rect format
|
|
347
|
+
let rect;
|
|
348
|
+
if (page.grid && page.grid.length > 0) {
|
|
349
|
+
// Search for button in actual grid layout
|
|
350
|
+
let found = false;
|
|
351
|
+
for (let y = 0; y < page.grid.length && !found; y++) {
|
|
352
|
+
for (let x = 0; x < page.grid[y].length && !found; x++) {
|
|
353
|
+
const gridButton = page.grid[y][x];
|
|
354
|
+
if (gridButton && gridButton.id === button.id) {
|
|
355
|
+
// Convert grid coordinates to pixel coordinates
|
|
356
|
+
const pixelX = x * 105; // 105px per column (100px button + 5px spacing)
|
|
357
|
+
const pixelY = y * 30; // 30px per row (25px button + 5px spacing)
|
|
358
|
+
rect = `{{${pixelX}, ${pixelY}}, {100, 25}}`;
|
|
359
|
+
found = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!found) {
|
|
364
|
+
// Button not found in grid, use auto-layout
|
|
365
|
+
const autoX = (buttonIndex % gridCols) * 105;
|
|
366
|
+
const autoY = Math.floor(buttonIndex / gridCols) * 30;
|
|
367
|
+
rect = `{{${autoX}, ${autoY}}, {100, 25}}`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// Use auto-layout with detected grid dimensions
|
|
372
|
+
const autoX = (buttonIndex % gridCols) * 105;
|
|
373
|
+
const autoY = Math.floor(buttonIndex / gridCols) * 30;
|
|
374
|
+
rect = `{{${autoX}, ${autoY}}, {100, 25}}`;
|
|
375
|
+
}
|
|
376
|
+
const buttonObj = {
|
|
377
|
+
ButtonType: 0,
|
|
378
|
+
DisplayText: button.label || 'Button',
|
|
379
|
+
FontSize: button.style?.fontSize || 12,
|
|
380
|
+
ID: `Button.${button.id}`,
|
|
381
|
+
PanelObjectType: 'Button',
|
|
382
|
+
Rect: rect,
|
|
383
|
+
};
|
|
384
|
+
if (button.style?.backgroundColor) {
|
|
385
|
+
buttonObj.DisplayColor = button.style.backgroundColor;
|
|
386
|
+
}
|
|
387
|
+
if (button.style?.fontWeight === 'bold') {
|
|
388
|
+
buttonObj.DisplayImageWeight = 'FontWeightBold';
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
buttonObj.DisplayImageWeight = 'FontWeightRegular';
|
|
392
|
+
}
|
|
393
|
+
// Add actions - prefer semantic action if available
|
|
394
|
+
buttonObj.Actions = [this.createApplePanelsAction(button)];
|
|
395
|
+
return buttonObj;
|
|
396
|
+
});
|
|
397
|
+
panelsDict[panelId] = {
|
|
398
|
+
DisplayOrder: pageIndex + 1,
|
|
399
|
+
GlidingLensSize: 5,
|
|
400
|
+
HasTransientPosition: false,
|
|
401
|
+
HideHome: false,
|
|
402
|
+
HideMinimize: false,
|
|
403
|
+
HidePanelAdjustments: false,
|
|
404
|
+
HideSwitchDock: false,
|
|
405
|
+
HideSwitchDockContextualButtons: false,
|
|
406
|
+
HideTitlebar: false,
|
|
407
|
+
ID: panelId,
|
|
408
|
+
Name: page.name || 'Panel',
|
|
409
|
+
PanelObjects: panelObjects,
|
|
410
|
+
ProductSupportType: 'All',
|
|
411
|
+
Rect: '{{15, 75}, {425, 55}}',
|
|
412
|
+
ScanStyle: 0,
|
|
413
|
+
ShowPanelLocationString: 'CustomPanelList',
|
|
414
|
+
UsesPinnedResizing: false,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
const panelDefinitions = {
|
|
418
|
+
Panels: panelsDict,
|
|
419
|
+
ToolbarOrdering: {
|
|
420
|
+
ToolbarIdentifiersAfterBasePanel: [],
|
|
421
|
+
ToolbarIdentifiersPriorToBasePanel: [],
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
const panelDefsContent = plist_1.default.build(panelDefinitions);
|
|
425
|
+
if (isSinglePlist) {
|
|
426
|
+
// Write single PanelDefinitions.plist file directly
|
|
427
|
+
const dir = path_1.default.dirname(outputPath);
|
|
428
|
+
if (!fs_1.default.existsSync(dir))
|
|
429
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
430
|
+
fs_1.default.writeFileSync(outputPath, panelDefsContent);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Write into bundle structure
|
|
434
|
+
fs_1.default.writeFileSync(path_1.default.join(resourcesPath, 'PanelDefinitions.plist'), panelDefsContent);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
createApplePanelsAction(button) {
|
|
438
|
+
// Use semantic action if available
|
|
439
|
+
if (button.semanticAction?.platformData?.applePanels) {
|
|
440
|
+
const applePanelsData = button.semanticAction.platformData.applePanels;
|
|
441
|
+
return {
|
|
442
|
+
ActionParam: applePanelsData.parameters,
|
|
443
|
+
ActionRecordedOffset: 0.0,
|
|
444
|
+
ActionType: applePanelsData.actionType,
|
|
445
|
+
ID: `Action.${button.id}`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// Handle semantic actions without Apple Panels specific data
|
|
449
|
+
if (button.semanticAction) {
|
|
450
|
+
const intentStr = String(button.semanticAction.intent);
|
|
451
|
+
switch (intentStr) {
|
|
452
|
+
case 'NAVIGATE_TO':
|
|
453
|
+
return {
|
|
454
|
+
ActionParam: {
|
|
455
|
+
PanelID: `USER.${button.semanticAction.targetId || button.targetPageId || ''}`,
|
|
456
|
+
},
|
|
457
|
+
ActionRecordedOffset: 0.0,
|
|
458
|
+
ActionType: 'ActionOpenPanel',
|
|
459
|
+
ID: `Action.${button.id}`,
|
|
460
|
+
};
|
|
461
|
+
case 'SPEAK_TEXT':
|
|
462
|
+
case 'INSERT_TEXT':
|
|
463
|
+
return {
|
|
464
|
+
ActionParam: {
|
|
465
|
+
CharString: button.semanticAction.text || button.message || button.label || '',
|
|
466
|
+
isStickyKey: false,
|
|
467
|
+
},
|
|
468
|
+
ActionRecordedOffset: 0.0,
|
|
469
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
470
|
+
ID: `Action.${button.id}`,
|
|
471
|
+
};
|
|
472
|
+
case 'SEND_KEYS':
|
|
473
|
+
return {
|
|
474
|
+
ActionParam: {
|
|
475
|
+
CharString: button.semanticAction.text || '',
|
|
476
|
+
isStickyKey: false,
|
|
477
|
+
},
|
|
478
|
+
ActionRecordedOffset: 0.0,
|
|
479
|
+
ActionType: 'ActionSendKeys',
|
|
480
|
+
ID: `Action.${button.id}`,
|
|
481
|
+
};
|
|
482
|
+
default:
|
|
483
|
+
// Fallback to speech for unknown semantic actions
|
|
484
|
+
return {
|
|
485
|
+
ActionParam: {
|
|
486
|
+
CharString: button.semanticAction.fallback?.message || button.message || button.label || '',
|
|
487
|
+
isStickyKey: false,
|
|
488
|
+
},
|
|
489
|
+
ActionRecordedOffset: 0.0,
|
|
490
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
491
|
+
ID: `Action.${button.id}`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Default SPEAK action if no semantic action
|
|
496
|
+
return {
|
|
497
|
+
ActionParam: {
|
|
498
|
+
CharString: button.message || button.label || '',
|
|
499
|
+
isStickyKey: false,
|
|
500
|
+
},
|
|
501
|
+
ActionRecordedOffset: 0.0,
|
|
502
|
+
ActionType: 'ActionPressKeyCharSequence',
|
|
503
|
+
ID: `Action.${button.id}`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
508
|
+
* Uses the generic implementation from BaseProcessor
|
|
509
|
+
*/
|
|
510
|
+
extractStringsWithMetadata(filePath) {
|
|
511
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
515
|
+
* Uses the generic implementation from BaseProcessor
|
|
516
|
+
*/
|
|
517
|
+
generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
518
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
exports.ApplePanelsProcessor = ApplePanelsProcessor;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
|
|
2
|
+
import { AACTree } from '../core/treeStructure';
|
|
3
|
+
declare class AstericsGridProcessor extends BaseProcessor {
|
|
4
|
+
private loadAudio;
|
|
5
|
+
constructor(options?: ProcessorOptions & {
|
|
6
|
+
loadAudio?: boolean;
|
|
7
|
+
});
|
|
8
|
+
extractTexts(filePathOrBuffer: string | Buffer): string[];
|
|
9
|
+
private extractRawTexts;
|
|
10
|
+
private extractActionTexts;
|
|
11
|
+
loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
|
|
12
|
+
private getLocalizedLabel;
|
|
13
|
+
private getLocalizedText;
|
|
14
|
+
private createButtonFromElement;
|
|
15
|
+
processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
|
|
16
|
+
private applyTranslationsToGridFile;
|
|
17
|
+
private applyTranslationsToAction;
|
|
18
|
+
saveFromTree(tree: AACTree, outputPath: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Add audio recording to a specific grid element
|
|
21
|
+
*/
|
|
22
|
+
addAudioToElement(filePath: string, elementId: string, audioData: Buffer, metadata?: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Create a copy of the grid file with audio recordings added
|
|
25
|
+
*/
|
|
26
|
+
createAudioEnhancedGridFile(sourceFilePath: string, targetFilePath: string, audioMappings: Map<string, {
|
|
27
|
+
audioData: Buffer;
|
|
28
|
+
metadata?: string;
|
|
29
|
+
}>): void;
|
|
30
|
+
/**
|
|
31
|
+
* Extract all element IDs from the grid file for audio mapping
|
|
32
|
+
*/
|
|
33
|
+
getElementIds(filePathOrBuffer: string | Buffer): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Check if an element has audio recording
|
|
36
|
+
*/
|
|
37
|
+
hasAudioRecording(filePathOrBuffer: string | Buffer, elementId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
40
|
+
* Uses the generic implementation from BaseProcessor
|
|
41
|
+
*/
|
|
42
|
+
extractStringsWithMetadata(filePath: string): Promise<ExtractStringsResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
45
|
+
* Uses the generic implementation from BaseProcessor
|
|
46
|
+
*/
|
|
47
|
+
generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
|
|
48
|
+
}
|
|
49
|
+
export { AstericsGridProcessor };
|