@willwade/aac-processors 0.2.7 → 0.2.9
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.
|
@@ -1137,9 +1137,11 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
break;
|
|
1139
1139
|
case 'Jump.ToKeyboard': {
|
|
1140
|
-
//
|
|
1140
|
+
// Prefer explicit keyboard page metadata when available.
|
|
1141
|
+
// Some Gridsets resolve the keyboard page in metadata
|
|
1142
|
+
// without preserving tree.keyboardGridName during parse.
|
|
1141
1143
|
const keyboardGridName = tree.keyboardGridName;
|
|
1142
|
-
const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1144
|
+
const keyboardPageId = tree.metadata?.defaultKeyboardPageId || gridNameToIdMap.get(keyboardGridName);
|
|
1143
1145
|
if (keyboardPageId && !navigationTarget) {
|
|
1144
1146
|
navigationTarget = keyboardPageId;
|
|
1145
1147
|
}
|
|
@@ -1699,6 +1701,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1699
1701
|
settingsData?.gridSetSettings?.keyboardGrid ||
|
|
1700
1702
|
settingsData?.GridsetSettings?.KeyboardGrid;
|
|
1701
1703
|
if (keyboardGridName && typeof keyboardGridName === 'string') {
|
|
1704
|
+
tree.keyboardGridName = keyboardGridName;
|
|
1702
1705
|
metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1703
1706
|
}
|
|
1704
1707
|
}
|
|
@@ -1708,6 +1711,22 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1708
1711
|
}
|
|
1709
1712
|
// Set metadata on tree
|
|
1710
1713
|
tree.metadata = metadata;
|
|
1714
|
+
if (metadata.defaultKeyboardPageId) {
|
|
1715
|
+
Object.values(tree.pages).forEach((page) => {
|
|
1716
|
+
page.buttons.forEach((button) => {
|
|
1717
|
+
if (button?.semanticAction?.platformData?.grid3?.commandId === 'Jump.ToKeyboard' &&
|
|
1718
|
+
!button.targetPageId) {
|
|
1719
|
+
button.targetPageId = metadata.defaultKeyboardPageId;
|
|
1720
|
+
if (button.semanticAction) {
|
|
1721
|
+
button.semanticAction.targetId = metadata.defaultKeyboardPageId;
|
|
1722
|
+
if (button.semanticAction.fallback?.type === 'NAVIGATE') {
|
|
1723
|
+
button.semanticAction.fallback.targetPageId = metadata.defaultKeyboardPageId;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1711
1730
|
return tree;
|
|
1712
1731
|
}
|
|
1713
1732
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -2207,6 +2226,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2207
2226
|
/**
|
|
2208
2227
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
2209
2228
|
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
2229
|
+
* It preserves the original grid structure and only updates button labels and messages.
|
|
2210
2230
|
*
|
|
2211
2231
|
* @param originalPath - Path to the original gridset file
|
|
2212
2232
|
* @param tree - Modified AACTree with pages to save
|
|
@@ -2223,91 +2243,202 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2223
2243
|
const AdmZip = (await import('adm-zip')).default;
|
|
2224
2244
|
const originalZip = new AdmZip(originalPath);
|
|
2225
2245
|
const outputZip = new AdmZip();
|
|
2226
|
-
//
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
return '';
|
|
2232
|
-
const normalizedStyle = { ...style };
|
|
2233
|
-
const styleKey = JSON.stringify(normalizedStyle);
|
|
2234
|
-
const existing = uniqueStyles.get(styleKey);
|
|
2235
|
-
if (existing)
|
|
2236
|
-
return existing.id;
|
|
2237
|
-
const styleId = `Style${styleIdCounter++}`;
|
|
2238
|
-
uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
|
|
2239
|
-
return styleId;
|
|
2240
|
-
};
|
|
2241
|
-
// Collect all styles from pages and buttons
|
|
2242
|
-
Object.values(tree.pages).forEach((page) => {
|
|
2243
|
-
addStyle(page.style);
|
|
2244
|
-
page.buttons.forEach((button) => {
|
|
2245
|
-
addStyle(button.style);
|
|
2246
|
-
});
|
|
2247
|
-
});
|
|
2246
|
+
// Create a map of pages by name for easy lookup
|
|
2247
|
+
const pagesByName = new Map();
|
|
2248
|
+
for (const page of Object.values(tree.pages)) {
|
|
2249
|
+
pagesByName.set(page.name, page);
|
|
2250
|
+
}
|
|
2248
2251
|
// Track which grid files we're modifying
|
|
2249
2252
|
const modifiedGridFiles = new Set();
|
|
2250
|
-
// Generate grid.xml files for pages in the tree
|
|
2253
|
+
// Generate updated grid.xml files for pages in the tree
|
|
2251
2254
|
const newGridFiles = new Map();
|
|
2255
|
+
// Create XML parser and builder
|
|
2256
|
+
const parser = new XMLParser({
|
|
2257
|
+
ignoreAttributes: false,
|
|
2258
|
+
attributeNamePrefix: '@_',
|
|
2259
|
+
});
|
|
2260
|
+
const gridBuilder = new XMLBuilder({
|
|
2261
|
+
ignoreAttributes: false,
|
|
2262
|
+
format: true,
|
|
2263
|
+
indentBy: ' ',
|
|
2264
|
+
suppressEmptyNode: true,
|
|
2265
|
+
// Preserve Grid 3 XML formatting requirements
|
|
2266
|
+
suppressBooleanAttributes: false,
|
|
2267
|
+
});
|
|
2252
2268
|
for (const page of Object.values(tree.pages)) {
|
|
2253
2269
|
const gridPath = `Grids/${page.name}/grid.xml`;
|
|
2254
2270
|
modifiedGridFiles.add(gridPath);
|
|
2255
|
-
//
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2271
|
+
// Try to get the original grid.xml file
|
|
2272
|
+
const originalEntry = originalZip.getEntry(gridPath);
|
|
2273
|
+
if (!originalEntry) {
|
|
2274
|
+
// If original doesn't exist, create a new basic grid
|
|
2275
|
+
const basicGrid = this.createBasicGridXml(page);
|
|
2276
|
+
newGridFiles.set(gridPath, basicGrid);
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
// Parse the original grid XML
|
|
2280
|
+
const originalContent = originalEntry.getData().toString('utf-8');
|
|
2281
|
+
const originalGrid = parser.parse(originalContent);
|
|
2282
|
+
if (!originalGrid.Grid) {
|
|
2283
|
+
// Invalid grid structure, create a basic one
|
|
2284
|
+
const basicGrid = this.createBasicGridXml(page);
|
|
2285
|
+
newGridFiles.set(gridPath, basicGrid);
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
// Create a map of buttons by their position for easy lookup
|
|
2289
|
+
const buttonsByPosition = new Map();
|
|
2290
|
+
for (const button of page.buttons) {
|
|
2291
|
+
const pos = this.findButtonPosition(page, button, 0);
|
|
2292
|
+
const key = `${pos.x},${pos.y}`;
|
|
2293
|
+
buttonsByPosition.set(key, button);
|
|
2294
|
+
}
|
|
2295
|
+
// Update cells in the original grid
|
|
2296
|
+
const originalCells = originalGrid.Grid.Cells?.Cell;
|
|
2297
|
+
if (originalCells) {
|
|
2298
|
+
const cellArray = Array.isArray(originalCells) ? originalCells : [originalCells];
|
|
2299
|
+
for (const cell of cellArray) {
|
|
2300
|
+
if (!cell.Content)
|
|
2301
|
+
continue;
|
|
2302
|
+
// Get cell position
|
|
2303
|
+
const x = parseInt(String(cell['@_X'] || cell['@_Column'] || '0'), 10);
|
|
2304
|
+
const y = parseInt(String(cell['@_Y'] || cell['@_Row'] || '0'), 10);
|
|
2305
|
+
const key = `${x},${y}`;
|
|
2306
|
+
// Check if there's a modified button for this position
|
|
2307
|
+
const modifiedButton = buttonsByPosition.get(key);
|
|
2308
|
+
if (modifiedButton) {
|
|
2309
|
+
// Check if this is an AutoContent/WordList cell
|
|
2310
|
+
const contentType = cell.Content.ContentType || cell.Content.contentType;
|
|
2311
|
+
const contentSubType = cell.Content.ContentSubType || cell.Content.contentsubtype;
|
|
2312
|
+
const isWordListCell = contentType === 'AutoContent' && contentSubType === 'WordList';
|
|
2313
|
+
const isPredictionCell = contentType === 'AutoContent' && contentSubType === 'Prediction';
|
|
2314
|
+
if (isWordListCell) {
|
|
2315
|
+
// For WordList cells, we need to add the word to the page's WordList
|
|
2316
|
+
// instead of modifying the cell directly. The cell will automatically
|
|
2317
|
+
// populate from the WordList.
|
|
2318
|
+
// Note: WordList updates are handled by collecting all new words
|
|
2319
|
+
// and adding them to the WordList.Items array later.
|
|
2320
|
+
continue; // Skip cell modification for WordList cells
|
|
2321
|
+
}
|
|
2322
|
+
if (isPredictionCell) {
|
|
2323
|
+
// Prediction cells are populated dynamically by Grid 3's prediction system.
|
|
2324
|
+
// They should remain as <CaptionAndImage xsi:nil="true" /> and not be modified.
|
|
2325
|
+
continue; // Skip cell modification for Prediction cells
|
|
2326
|
+
}
|
|
2327
|
+
// For regular cells, update the caption directly
|
|
2328
|
+
// CDATA wrapping for empty captions will be done in post-processing
|
|
2329
|
+
if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
|
|
2330
|
+
const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
|
|
2331
|
+
// Check if the label is a placeholder (generated during extraction)
|
|
2332
|
+
const isPlaceholderLabel = !modifiedButton.label ||
|
|
2333
|
+
modifiedButton.label.startsWith('Cell_') ||
|
|
2334
|
+
modifiedButton.label.startsWith('AutoContent_') ||
|
|
2335
|
+
modifiedButton.label.startsWith('Prediction ');
|
|
2336
|
+
if (!isPlaceholderLabel) {
|
|
2337
|
+
// Only update caption with real content, not placeholders
|
|
2338
|
+
captionAndImage.Caption = modifiedButton.label;
|
|
2339
|
+
// Remove xsi:nil attribute when adding content
|
|
2340
|
+
if (captionAndImage['@_xsi:nil'] || captionAndImage['xsi:nil']) {
|
|
2341
|
+
delete captionAndImage['@_xsi:nil'];
|
|
2342
|
+
delete captionAndImage['xsi:nil'];
|
|
2297
2343
|
}
|
|
2298
|
-
|
|
2299
|
-
}),
|
|
2344
|
+
}
|
|
2300
2345
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2346
|
+
// Update the message if different from label
|
|
2347
|
+
// But skip placeholder labels
|
|
2348
|
+
const isPlaceholderMessage = !modifiedButton.message ||
|
|
2349
|
+
modifiedButton.message.startsWith('Cell_') ||
|
|
2350
|
+
modifiedButton.message.startsWith('AutoContent_') ||
|
|
2351
|
+
modifiedButton.message.startsWith('Prediction ');
|
|
2352
|
+
if (!isPlaceholderMessage &&
|
|
2353
|
+
modifiedButton.message &&
|
|
2354
|
+
modifiedButton.message !== modifiedButton.label) {
|
|
2355
|
+
// For simple text content
|
|
2356
|
+
if (!cell.Content.Commands) {
|
|
2357
|
+
cell.Content['#text'] = modifiedButton.message;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
// Update image if present
|
|
2361
|
+
if (modifiedButton.image) {
|
|
2362
|
+
if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
|
|
2363
|
+
const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
|
|
2364
|
+
captionAndImage.Image = modifiedButton.image;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
// Update the page's WordList with new words from modified buttons
|
|
2371
|
+
// Collect all modified buttons that should be added to the WordList
|
|
2372
|
+
const newWordListItems = [];
|
|
2373
|
+
for (const button of page.buttons) {
|
|
2374
|
+
const pos = this.findButtonPosition(page, button, 0);
|
|
2375
|
+
// Check if this button corresponds to a WordList cell
|
|
2376
|
+
const cellArray = Array.isArray(originalGrid.Grid.Cells?.Cell)
|
|
2377
|
+
? originalGrid.Grid.Cells.Cell
|
|
2378
|
+
: originalGrid.Grid.Cells?.Cell
|
|
2379
|
+
? [originalGrid.Grid.Cells.Cell]
|
|
2380
|
+
: [];
|
|
2381
|
+
const cell = cellArray.find((c) => {
|
|
2382
|
+
const cellX = parseInt(String(c['@_X'] || '0'), 10);
|
|
2383
|
+
const cellY = parseInt(String(c['@_Y'] || '0'), 10);
|
|
2384
|
+
return cellX === pos.x && cellY === pos.y;
|
|
2385
|
+
});
|
|
2386
|
+
if (cell) {
|
|
2387
|
+
const contentType = cell.Content?.ContentType || cell.Content?.contentType;
|
|
2388
|
+
const contentSubType = cell.Content?.ContentSubType || cell.Content?.contentsubtype;
|
|
2389
|
+
const isWordListCell = contentType === 'AutoContent' && contentSubType === 'WordList';
|
|
2390
|
+
// Note: Prediction cells are already skipped earlier, so they won't reach here
|
|
2391
|
+
if (isWordListCell) {
|
|
2392
|
+
// Add this button to the WordList with proper Grid 3 format
|
|
2393
|
+
// Format: <Text><s><r>label</r></s></Text>
|
|
2394
|
+
newWordListItems.push({
|
|
2395
|
+
Text: {
|
|
2396
|
+
s: {
|
|
2397
|
+
r: button.label,
|
|
2398
|
+
},
|
|
2399
|
+
},
|
|
2400
|
+
Image: '', // No image for user-added words
|
|
2401
|
+
PartOfSpeech: 'Unknown',
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
// Add new items to the existing WordList
|
|
2407
|
+
if (newWordListItems.length > 0) {
|
|
2408
|
+
const existingWordList = originalGrid.Grid.WordList;
|
|
2409
|
+
if (existingWordList && existingWordList.Items) {
|
|
2410
|
+
const existingItems = existingWordList.Items.WordListItem || existingWordList.Items.wordlistitem || [];
|
|
2411
|
+
const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
|
|
2412
|
+
// Merge existing and new items
|
|
2413
|
+
const allItems = [...itemsArray, ...newWordListItems];
|
|
2414
|
+
// Update the WordList
|
|
2415
|
+
if (!originalGrid.Grid.WordList) {
|
|
2416
|
+
originalGrid.Grid.WordList = {};
|
|
2417
|
+
}
|
|
2418
|
+
if (!originalGrid.Grid.WordList.Items) {
|
|
2419
|
+
originalGrid.Grid.WordList.Items = {};
|
|
2420
|
+
}
|
|
2421
|
+
originalGrid.Grid.WordList.Items.WordListItem = allItems;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
// Build the updated grid XML and convert to Windows line endings
|
|
2425
|
+
let builtXml = gridBuilder.build(originalGrid);
|
|
2426
|
+
// Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
|
|
2427
|
+
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2428
|
+
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2429
|
+
// Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
2430
|
+
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2431
|
+
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
2432
|
+
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
2433
|
+
builtXml = builtXml.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2434
|
+
builtXml = builtXml.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2435
|
+
builtXml = builtXml.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2436
|
+
// Preserve CDATA in <r> tags for text parameters
|
|
2437
|
+
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
2438
|
+
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
2439
|
+
builtXml = builtXml.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2440
|
+
builtXml = builtXml.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2441
|
+
newGridFiles.set(gridPath, builtXml);
|
|
2311
2442
|
}
|
|
2312
2443
|
// Copy all files from original zip, replacing modified grid files
|
|
2313
2444
|
for (const entry of originalZip.getEntries()) {
|
|
@@ -2328,6 +2459,60 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2328
2459
|
const outputBuffer = outputZip.toBuffer();
|
|
2329
2460
|
await writeBinaryToPath(outputPath, outputBuffer);
|
|
2330
2461
|
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Create a basic grid XML for a page when original doesn't exist
|
|
2464
|
+
*/
|
|
2465
|
+
createBasicGridXml(page) {
|
|
2466
|
+
const gridData = {
|
|
2467
|
+
Grid: {
|
|
2468
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
2469
|
+
GridGuid: page.id,
|
|
2470
|
+
ColumnDefinitions: this.calculateColumnDefinitions(page),
|
|
2471
|
+
RowDefinitions: this.calculateRowDefinitions(page, false),
|
|
2472
|
+
AutoContentCommands: '',
|
|
2473
|
+
Cells: page.buttons.length > 0
|
|
2474
|
+
? {
|
|
2475
|
+
Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
|
|
2476
|
+
const position = this.findButtonPosition(page, button, btnIndex);
|
|
2477
|
+
const cell = {
|
|
2478
|
+
'@_X': position.x,
|
|
2479
|
+
'@_Y': position.y,
|
|
2480
|
+
Content: {
|
|
2481
|
+
CaptionAndImage: {
|
|
2482
|
+
Caption: button.label || '',
|
|
2483
|
+
},
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
if (button.image) {
|
|
2487
|
+
cell.Content.CaptionAndImage.Image = button.image;
|
|
2488
|
+
}
|
|
2489
|
+
if (position.columnSpan > 1) {
|
|
2490
|
+
cell['@_ColumnSpan'] = position.columnSpan;
|
|
2491
|
+
}
|
|
2492
|
+
if (position.rowSpan > 1) {
|
|
2493
|
+
cell['@_RowSpan'] = position.rowSpan;
|
|
2494
|
+
}
|
|
2495
|
+
return cell;
|
|
2496
|
+
}),
|
|
2497
|
+
}
|
|
2498
|
+
: undefined,
|
|
2499
|
+
},
|
|
2500
|
+
};
|
|
2501
|
+
const gridBuilder = new XMLBuilder({
|
|
2502
|
+
ignoreAttributes: false,
|
|
2503
|
+
format: true,
|
|
2504
|
+
indentBy: ' ',
|
|
2505
|
+
suppressEmptyNode: true,
|
|
2506
|
+
// Preserve Grid 3 XML formatting requirements
|
|
2507
|
+
suppressBooleanAttributes: false,
|
|
2508
|
+
});
|
|
2509
|
+
// Build the grid XML and convert to Windows line endings for Grid 3 compatibility
|
|
2510
|
+
let builtXml = gridBuilder.build(gridData);
|
|
2511
|
+
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2512
|
+
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2513
|
+
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2514
|
+
return builtXml;
|
|
2515
|
+
}
|
|
2331
2516
|
// Helper method to find button position with span information
|
|
2332
2517
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2333
2518
|
if (page.grid && page.grid.length > 0) {
|
|
@@ -54,12 +54,17 @@ declare class GridsetProcessor extends BaseProcessor {
|
|
|
54
54
|
/**
|
|
55
55
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
56
56
|
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
57
|
+
* It preserves the original grid structure and only updates button labels and messages.
|
|
57
58
|
*
|
|
58
59
|
* @param originalPath - Path to the original gridset file
|
|
59
60
|
* @param tree - Modified AACTree with pages to save
|
|
60
61
|
* @param outputPath - Path where the modified gridset should be saved
|
|
61
62
|
*/
|
|
62
63
|
saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Create a basic grid XML for a page when original doesn't exist
|
|
66
|
+
*/
|
|
67
|
+
private createBasicGridXml;
|
|
63
68
|
private findButtonPosition;
|
|
64
69
|
/**
|
|
65
70
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
@@ -1163,9 +1163,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1163
1163
|
}
|
|
1164
1164
|
break;
|
|
1165
1165
|
case 'Jump.ToKeyboard': {
|
|
1166
|
-
//
|
|
1166
|
+
// Prefer explicit keyboard page metadata when available.
|
|
1167
|
+
// Some Gridsets resolve the keyboard page in metadata
|
|
1168
|
+
// without preserving tree.keyboardGridName during parse.
|
|
1167
1169
|
const keyboardGridName = tree.keyboardGridName;
|
|
1168
|
-
const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1170
|
+
const keyboardPageId = tree.metadata?.defaultKeyboardPageId || gridNameToIdMap.get(keyboardGridName);
|
|
1169
1171
|
if (keyboardPageId && !navigationTarget) {
|
|
1170
1172
|
navigationTarget = keyboardPageId;
|
|
1171
1173
|
}
|
|
@@ -1725,6 +1727,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1725
1727
|
settingsData?.gridSetSettings?.keyboardGrid ||
|
|
1726
1728
|
settingsData?.GridsetSettings?.KeyboardGrid;
|
|
1727
1729
|
if (keyboardGridName && typeof keyboardGridName === 'string') {
|
|
1730
|
+
tree.keyboardGridName = keyboardGridName;
|
|
1728
1731
|
metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1729
1732
|
}
|
|
1730
1733
|
}
|
|
@@ -1734,6 +1737,22 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1734
1737
|
}
|
|
1735
1738
|
// Set metadata on tree
|
|
1736
1739
|
tree.metadata = metadata;
|
|
1740
|
+
if (metadata.defaultKeyboardPageId) {
|
|
1741
|
+
Object.values(tree.pages).forEach((page) => {
|
|
1742
|
+
page.buttons.forEach((button) => {
|
|
1743
|
+
if (button?.semanticAction?.platformData?.grid3?.commandId === 'Jump.ToKeyboard' &&
|
|
1744
|
+
!button.targetPageId) {
|
|
1745
|
+
button.targetPageId = metadata.defaultKeyboardPageId;
|
|
1746
|
+
if (button.semanticAction) {
|
|
1747
|
+
button.semanticAction.targetId = metadata.defaultKeyboardPageId;
|
|
1748
|
+
if (button.semanticAction.fallback?.type === 'NAVIGATE') {
|
|
1749
|
+
button.semanticAction.fallback.targetPageId = metadata.defaultKeyboardPageId;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1737
1756
|
return tree;
|
|
1738
1757
|
}
|
|
1739
1758
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -2233,6 +2252,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2233
2252
|
/**
|
|
2234
2253
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
2235
2254
|
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
2255
|
+
* It preserves the original grid structure and only updates button labels and messages.
|
|
2236
2256
|
*
|
|
2237
2257
|
* @param originalPath - Path to the original gridset file
|
|
2238
2258
|
* @param tree - Modified AACTree with pages to save
|
|
@@ -2249,91 +2269,202 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2249
2269
|
const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
|
|
2250
2270
|
const originalZip = new AdmZip(originalPath);
|
|
2251
2271
|
const outputZip = new AdmZip();
|
|
2252
|
-
//
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
return '';
|
|
2258
|
-
const normalizedStyle = { ...style };
|
|
2259
|
-
const styleKey = JSON.stringify(normalizedStyle);
|
|
2260
|
-
const existing = uniqueStyles.get(styleKey);
|
|
2261
|
-
if (existing)
|
|
2262
|
-
return existing.id;
|
|
2263
|
-
const styleId = `Style${styleIdCounter++}`;
|
|
2264
|
-
uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
|
|
2265
|
-
return styleId;
|
|
2266
|
-
};
|
|
2267
|
-
// Collect all styles from pages and buttons
|
|
2268
|
-
Object.values(tree.pages).forEach((page) => {
|
|
2269
|
-
addStyle(page.style);
|
|
2270
|
-
page.buttons.forEach((button) => {
|
|
2271
|
-
addStyle(button.style);
|
|
2272
|
-
});
|
|
2273
|
-
});
|
|
2272
|
+
// Create a map of pages by name for easy lookup
|
|
2273
|
+
const pagesByName = new Map();
|
|
2274
|
+
for (const page of Object.values(tree.pages)) {
|
|
2275
|
+
pagesByName.set(page.name, page);
|
|
2276
|
+
}
|
|
2274
2277
|
// Track which grid files we're modifying
|
|
2275
2278
|
const modifiedGridFiles = new Set();
|
|
2276
|
-
// Generate grid.xml files for pages in the tree
|
|
2279
|
+
// Generate updated grid.xml files for pages in the tree
|
|
2277
2280
|
const newGridFiles = new Map();
|
|
2281
|
+
// Create XML parser and builder
|
|
2282
|
+
const parser = new fast_xml_parser_1.XMLParser({
|
|
2283
|
+
ignoreAttributes: false,
|
|
2284
|
+
attributeNamePrefix: '@_',
|
|
2285
|
+
});
|
|
2286
|
+
const gridBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
2287
|
+
ignoreAttributes: false,
|
|
2288
|
+
format: true,
|
|
2289
|
+
indentBy: ' ',
|
|
2290
|
+
suppressEmptyNode: true,
|
|
2291
|
+
// Preserve Grid 3 XML formatting requirements
|
|
2292
|
+
suppressBooleanAttributes: false,
|
|
2293
|
+
});
|
|
2278
2294
|
for (const page of Object.values(tree.pages)) {
|
|
2279
2295
|
const gridPath = `Grids/${page.name}/grid.xml`;
|
|
2280
2296
|
modifiedGridFiles.add(gridPath);
|
|
2281
|
-
//
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2297
|
+
// Try to get the original grid.xml file
|
|
2298
|
+
const originalEntry = originalZip.getEntry(gridPath);
|
|
2299
|
+
if (!originalEntry) {
|
|
2300
|
+
// If original doesn't exist, create a new basic grid
|
|
2301
|
+
const basicGrid = this.createBasicGridXml(page);
|
|
2302
|
+
newGridFiles.set(gridPath, basicGrid);
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
// Parse the original grid XML
|
|
2306
|
+
const originalContent = originalEntry.getData().toString('utf-8');
|
|
2307
|
+
const originalGrid = parser.parse(originalContent);
|
|
2308
|
+
if (!originalGrid.Grid) {
|
|
2309
|
+
// Invalid grid structure, create a basic one
|
|
2310
|
+
const basicGrid = this.createBasicGridXml(page);
|
|
2311
|
+
newGridFiles.set(gridPath, basicGrid);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
// Create a map of buttons by their position for easy lookup
|
|
2315
|
+
const buttonsByPosition = new Map();
|
|
2316
|
+
for (const button of page.buttons) {
|
|
2317
|
+
const pos = this.findButtonPosition(page, button, 0);
|
|
2318
|
+
const key = `${pos.x},${pos.y}`;
|
|
2319
|
+
buttonsByPosition.set(key, button);
|
|
2320
|
+
}
|
|
2321
|
+
// Update cells in the original grid
|
|
2322
|
+
const originalCells = originalGrid.Grid.Cells?.Cell;
|
|
2323
|
+
if (originalCells) {
|
|
2324
|
+
const cellArray = Array.isArray(originalCells) ? originalCells : [originalCells];
|
|
2325
|
+
for (const cell of cellArray) {
|
|
2326
|
+
if (!cell.Content)
|
|
2327
|
+
continue;
|
|
2328
|
+
// Get cell position
|
|
2329
|
+
const x = parseInt(String(cell['@_X'] || cell['@_Column'] || '0'), 10);
|
|
2330
|
+
const y = parseInt(String(cell['@_Y'] || cell['@_Row'] || '0'), 10);
|
|
2331
|
+
const key = `${x},${y}`;
|
|
2332
|
+
// Check if there's a modified button for this position
|
|
2333
|
+
const modifiedButton = buttonsByPosition.get(key);
|
|
2334
|
+
if (modifiedButton) {
|
|
2335
|
+
// Check if this is an AutoContent/WordList cell
|
|
2336
|
+
const contentType = cell.Content.ContentType || cell.Content.contentType;
|
|
2337
|
+
const contentSubType = cell.Content.ContentSubType || cell.Content.contentsubtype;
|
|
2338
|
+
const isWordListCell = contentType === 'AutoContent' && contentSubType === 'WordList';
|
|
2339
|
+
const isPredictionCell = contentType === 'AutoContent' && contentSubType === 'Prediction';
|
|
2340
|
+
if (isWordListCell) {
|
|
2341
|
+
// For WordList cells, we need to add the word to the page's WordList
|
|
2342
|
+
// instead of modifying the cell directly. The cell will automatically
|
|
2343
|
+
// populate from the WordList.
|
|
2344
|
+
// Note: WordList updates are handled by collecting all new words
|
|
2345
|
+
// and adding them to the WordList.Items array later.
|
|
2346
|
+
continue; // Skip cell modification for WordList cells
|
|
2347
|
+
}
|
|
2348
|
+
if (isPredictionCell) {
|
|
2349
|
+
// Prediction cells are populated dynamically by Grid 3's prediction system.
|
|
2350
|
+
// They should remain as <CaptionAndImage xsi:nil="true" /> and not be modified.
|
|
2351
|
+
continue; // Skip cell modification for Prediction cells
|
|
2352
|
+
}
|
|
2353
|
+
// For regular cells, update the caption directly
|
|
2354
|
+
// CDATA wrapping for empty captions will be done in post-processing
|
|
2355
|
+
if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
|
|
2356
|
+
const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
|
|
2357
|
+
// Check if the label is a placeholder (generated during extraction)
|
|
2358
|
+
const isPlaceholderLabel = !modifiedButton.label ||
|
|
2359
|
+
modifiedButton.label.startsWith('Cell_') ||
|
|
2360
|
+
modifiedButton.label.startsWith('AutoContent_') ||
|
|
2361
|
+
modifiedButton.label.startsWith('Prediction ');
|
|
2362
|
+
if (!isPlaceholderLabel) {
|
|
2363
|
+
// Only update caption with real content, not placeholders
|
|
2364
|
+
captionAndImage.Caption = modifiedButton.label;
|
|
2365
|
+
// Remove xsi:nil attribute when adding content
|
|
2366
|
+
if (captionAndImage['@_xsi:nil'] || captionAndImage['xsi:nil']) {
|
|
2367
|
+
delete captionAndImage['@_xsi:nil'];
|
|
2368
|
+
delete captionAndImage['xsi:nil'];
|
|
2323
2369
|
}
|
|
2324
|
-
|
|
2325
|
-
}),
|
|
2370
|
+
}
|
|
2326
2371
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2372
|
+
// Update the message if different from label
|
|
2373
|
+
// But skip placeholder labels
|
|
2374
|
+
const isPlaceholderMessage = !modifiedButton.message ||
|
|
2375
|
+
modifiedButton.message.startsWith('Cell_') ||
|
|
2376
|
+
modifiedButton.message.startsWith('AutoContent_') ||
|
|
2377
|
+
modifiedButton.message.startsWith('Prediction ');
|
|
2378
|
+
if (!isPlaceholderMessage &&
|
|
2379
|
+
modifiedButton.message &&
|
|
2380
|
+
modifiedButton.message !== modifiedButton.label) {
|
|
2381
|
+
// For simple text content
|
|
2382
|
+
if (!cell.Content.Commands) {
|
|
2383
|
+
cell.Content['#text'] = modifiedButton.message;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
// Update image if present
|
|
2387
|
+
if (modifiedButton.image) {
|
|
2388
|
+
if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
|
|
2389
|
+
const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
|
|
2390
|
+
captionAndImage.Image = modifiedButton.image;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// Update the page's WordList with new words from modified buttons
|
|
2397
|
+
// Collect all modified buttons that should be added to the WordList
|
|
2398
|
+
const newWordListItems = [];
|
|
2399
|
+
for (const button of page.buttons) {
|
|
2400
|
+
const pos = this.findButtonPosition(page, button, 0);
|
|
2401
|
+
// Check if this button corresponds to a WordList cell
|
|
2402
|
+
const cellArray = Array.isArray(originalGrid.Grid.Cells?.Cell)
|
|
2403
|
+
? originalGrid.Grid.Cells.Cell
|
|
2404
|
+
: originalGrid.Grid.Cells?.Cell
|
|
2405
|
+
? [originalGrid.Grid.Cells.Cell]
|
|
2406
|
+
: [];
|
|
2407
|
+
const cell = cellArray.find((c) => {
|
|
2408
|
+
const cellX = parseInt(String(c['@_X'] || '0'), 10);
|
|
2409
|
+
const cellY = parseInt(String(c['@_Y'] || '0'), 10);
|
|
2410
|
+
return cellX === pos.x && cellY === pos.y;
|
|
2411
|
+
});
|
|
2412
|
+
if (cell) {
|
|
2413
|
+
const contentType = cell.Content?.ContentType || cell.Content?.contentType;
|
|
2414
|
+
const contentSubType = cell.Content?.ContentSubType || cell.Content?.contentsubtype;
|
|
2415
|
+
const isWordListCell = contentType === 'AutoContent' && contentSubType === 'WordList';
|
|
2416
|
+
// Note: Prediction cells are already skipped earlier, so they won't reach here
|
|
2417
|
+
if (isWordListCell) {
|
|
2418
|
+
// Add this button to the WordList with proper Grid 3 format
|
|
2419
|
+
// Format: <Text><s><r>label</r></s></Text>
|
|
2420
|
+
newWordListItems.push({
|
|
2421
|
+
Text: {
|
|
2422
|
+
s: {
|
|
2423
|
+
r: button.label,
|
|
2424
|
+
},
|
|
2425
|
+
},
|
|
2426
|
+
Image: '', // No image for user-added words
|
|
2427
|
+
PartOfSpeech: 'Unknown',
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
// Add new items to the existing WordList
|
|
2433
|
+
if (newWordListItems.length > 0) {
|
|
2434
|
+
const existingWordList = originalGrid.Grid.WordList;
|
|
2435
|
+
if (existingWordList && existingWordList.Items) {
|
|
2436
|
+
const existingItems = existingWordList.Items.WordListItem || existingWordList.Items.wordlistitem || [];
|
|
2437
|
+
const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
|
|
2438
|
+
// Merge existing and new items
|
|
2439
|
+
const allItems = [...itemsArray, ...newWordListItems];
|
|
2440
|
+
// Update the WordList
|
|
2441
|
+
if (!originalGrid.Grid.WordList) {
|
|
2442
|
+
originalGrid.Grid.WordList = {};
|
|
2443
|
+
}
|
|
2444
|
+
if (!originalGrid.Grid.WordList.Items) {
|
|
2445
|
+
originalGrid.Grid.WordList.Items = {};
|
|
2446
|
+
}
|
|
2447
|
+
originalGrid.Grid.WordList.Items.WordListItem = allItems;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
// Build the updated grid XML and convert to Windows line endings
|
|
2451
|
+
let builtXml = gridBuilder.build(originalGrid);
|
|
2452
|
+
// Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
|
|
2453
|
+
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2454
|
+
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2455
|
+
// Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
2456
|
+
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2457
|
+
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
2458
|
+
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
2459
|
+
builtXml = builtXml.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2460
|
+
builtXml = builtXml.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2461
|
+
builtXml = builtXml.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2462
|
+
// Preserve CDATA in <r> tags for text parameters
|
|
2463
|
+
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
2464
|
+
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
2465
|
+
builtXml = builtXml.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2466
|
+
builtXml = builtXml.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2467
|
+
newGridFiles.set(gridPath, builtXml);
|
|
2337
2468
|
}
|
|
2338
2469
|
// Copy all files from original zip, replacing modified grid files
|
|
2339
2470
|
for (const entry of originalZip.getEntries()) {
|
|
@@ -2354,6 +2485,60 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2354
2485
|
const outputBuffer = outputZip.toBuffer();
|
|
2355
2486
|
await writeBinaryToPath(outputPath, outputBuffer);
|
|
2356
2487
|
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Create a basic grid XML for a page when original doesn't exist
|
|
2490
|
+
*/
|
|
2491
|
+
createBasicGridXml(page) {
|
|
2492
|
+
const gridData = {
|
|
2493
|
+
Grid: {
|
|
2494
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
2495
|
+
GridGuid: page.id,
|
|
2496
|
+
ColumnDefinitions: this.calculateColumnDefinitions(page),
|
|
2497
|
+
RowDefinitions: this.calculateRowDefinitions(page, false),
|
|
2498
|
+
AutoContentCommands: '',
|
|
2499
|
+
Cells: page.buttons.length > 0
|
|
2500
|
+
? {
|
|
2501
|
+
Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
|
|
2502
|
+
const position = this.findButtonPosition(page, button, btnIndex);
|
|
2503
|
+
const cell = {
|
|
2504
|
+
'@_X': position.x,
|
|
2505
|
+
'@_Y': position.y,
|
|
2506
|
+
Content: {
|
|
2507
|
+
CaptionAndImage: {
|
|
2508
|
+
Caption: button.label || '',
|
|
2509
|
+
},
|
|
2510
|
+
},
|
|
2511
|
+
};
|
|
2512
|
+
if (button.image) {
|
|
2513
|
+
cell.Content.CaptionAndImage.Image = button.image;
|
|
2514
|
+
}
|
|
2515
|
+
if (position.columnSpan > 1) {
|
|
2516
|
+
cell['@_ColumnSpan'] = position.columnSpan;
|
|
2517
|
+
}
|
|
2518
|
+
if (position.rowSpan > 1) {
|
|
2519
|
+
cell['@_RowSpan'] = position.rowSpan;
|
|
2520
|
+
}
|
|
2521
|
+
return cell;
|
|
2522
|
+
}),
|
|
2523
|
+
}
|
|
2524
|
+
: undefined,
|
|
2525
|
+
},
|
|
2526
|
+
};
|
|
2527
|
+
const gridBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
2528
|
+
ignoreAttributes: false,
|
|
2529
|
+
format: true,
|
|
2530
|
+
indentBy: ' ',
|
|
2531
|
+
suppressEmptyNode: true,
|
|
2532
|
+
// Preserve Grid 3 XML formatting requirements
|
|
2533
|
+
suppressBooleanAttributes: false,
|
|
2534
|
+
});
|
|
2535
|
+
// Build the grid XML and convert to Windows line endings for Grid 3 compatibility
|
|
2536
|
+
let builtXml = gridBuilder.build(gridData);
|
|
2537
|
+
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2538
|
+
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2539
|
+
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2540
|
+
return builtXml;
|
|
2541
|
+
}
|
|
2357
2542
|
// Helper method to find button position with span information
|
|
2358
2543
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2359
2544
|
if (page.grid && page.grid.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"browser": "dist/browser/index.browser.js",
|