@willwade/aac-processors 0.2.8 → 0.2.10
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/dist/browser/processors/gridsetProcessor.js +243 -77
- package/dist/browser/processors/obfProcessor.js +106 -49
- package/dist/browser/utilities/analytics/morphology/index.js +0 -1
- package/dist/processors/gridsetProcessor.d.ts +5 -0
- package/dist/processors/gridsetProcessor.js +243 -77
- package/dist/processors/obfProcessor.d.ts +2 -1
- package/dist/processors/obfProcessor.js +106 -49
- package/dist/utilities/analytics/morphology/index.d.ts +0 -1
- package/dist/utilities/analytics/morphology/index.js +1 -3
- package/package.json +1 -1
|
@@ -2226,6 +2226,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2226
2226
|
/**
|
|
2227
2227
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
2228
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.
|
|
2229
2230
|
*
|
|
2230
2231
|
* @param originalPath - Path to the original gridset file
|
|
2231
2232
|
* @param tree - Modified AACTree with pages to save
|
|
@@ -2242,91 +2243,202 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2242
2243
|
const AdmZip = (await import('adm-zip')).default;
|
|
2243
2244
|
const originalZip = new AdmZip(originalPath);
|
|
2244
2245
|
const outputZip = new AdmZip();
|
|
2245
|
-
//
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
return '';
|
|
2251
|
-
const normalizedStyle = { ...style };
|
|
2252
|
-
const styleKey = JSON.stringify(normalizedStyle);
|
|
2253
|
-
const existing = uniqueStyles.get(styleKey);
|
|
2254
|
-
if (existing)
|
|
2255
|
-
return existing.id;
|
|
2256
|
-
const styleId = `Style${styleIdCounter++}`;
|
|
2257
|
-
uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
|
|
2258
|
-
return styleId;
|
|
2259
|
-
};
|
|
2260
|
-
// Collect all styles from pages and buttons
|
|
2261
|
-
Object.values(tree.pages).forEach((page) => {
|
|
2262
|
-
addStyle(page.style);
|
|
2263
|
-
page.buttons.forEach((button) => {
|
|
2264
|
-
addStyle(button.style);
|
|
2265
|
-
});
|
|
2266
|
-
});
|
|
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
|
+
}
|
|
2267
2251
|
// Track which grid files we're modifying
|
|
2268
2252
|
const modifiedGridFiles = new Set();
|
|
2269
|
-
// Generate grid.xml files for pages in the tree
|
|
2253
|
+
// Generate updated grid.xml files for pages in the tree
|
|
2270
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
|
+
});
|
|
2271
2268
|
for (const page of Object.values(tree.pages)) {
|
|
2272
2269
|
const gridPath = `Grids/${page.name}/grid.xml`;
|
|
2273
2270
|
modifiedGridFiles.add(gridPath);
|
|
2274
|
-
//
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
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'];
|
|
2316
2343
|
}
|
|
2317
|
-
|
|
2318
|
-
}),
|
|
2344
|
+
}
|
|
2319
2345
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
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);
|
|
2330
2442
|
}
|
|
2331
2443
|
// Copy all files from original zip, replacing modified grid files
|
|
2332
2444
|
for (const entry of originalZip.getEntries()) {
|
|
@@ -2347,6 +2459,60 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2347
2459
|
const outputBuffer = outputZip.toBuffer();
|
|
2348
2460
|
await writeBinaryToPath(outputPath, outputBuffer);
|
|
2349
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
|
+
}
|
|
2350
2516
|
// Helper method to find button position with span information
|
|
2351
2517
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2352
2518
|
if (page.grid && page.grid.length > 0) {
|