@willwade/aac-processors 0.2.6 → 0.2.8
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 +145 -2
- package/dist/browser/processors/obfProcessor.js +69 -0
- package/dist/processors/gridsetProcessor.d.ts +9 -0
- package/dist/processors/gridsetProcessor.js +168 -2
- package/dist/processors/obfProcessor.d.ts +9 -0
- package/dist/processors/obfProcessor.js +92 -0
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -2204,6 +2223,130 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2204
2223
|
RowDefinition: Array(maxRows).fill({}),
|
|
2205
2224
|
};
|
|
2206
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
2228
|
+
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
2229
|
+
*
|
|
2230
|
+
* @param originalPath - Path to the original gridset file
|
|
2231
|
+
* @param tree - Modified AACTree with pages to save
|
|
2232
|
+
* @param outputPath - Path where the modified gridset should be saved
|
|
2233
|
+
*/
|
|
2234
|
+
async saveModifiedTree(originalPath, tree, outputPath) {
|
|
2235
|
+
const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
|
|
2236
|
+
if (Object.keys(tree.pages).length === 0) {
|
|
2237
|
+
// Empty tree, just copy the original
|
|
2238
|
+
const originalBuffer = await readBinaryFromInput(originalPath);
|
|
2239
|
+
await writeBinaryToPath(outputPath, originalBuffer);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
2243
|
+
const originalZip = new AdmZip(originalPath);
|
|
2244
|
+
const outputZip = new AdmZip();
|
|
2245
|
+
// Collect styles from the tree for grid.xml files
|
|
2246
|
+
const uniqueStyles = new Map();
|
|
2247
|
+
let styleIdCounter = 1;
|
|
2248
|
+
const addStyle = (style) => {
|
|
2249
|
+
if (!style)
|
|
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
|
+
});
|
|
2267
|
+
// Track which grid files we're modifying
|
|
2268
|
+
const modifiedGridFiles = new Set();
|
|
2269
|
+
// Generate grid.xml files for pages in the tree
|
|
2270
|
+
const newGridFiles = new Map();
|
|
2271
|
+
for (const page of Object.values(tree.pages)) {
|
|
2272
|
+
const gridPath = `Grids/${page.name}/grid.xml`;
|
|
2273
|
+
modifiedGridFiles.add(gridPath);
|
|
2274
|
+
// Build the grid XML content
|
|
2275
|
+
const gridData = {
|
|
2276
|
+
Grid: {
|
|
2277
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
2278
|
+
GridGuid: page.id,
|
|
2279
|
+
ColumnDefinitions: this.calculateColumnDefinitions(page),
|
|
2280
|
+
RowDefinitions: this.calculateRowDefinitions(page, false),
|
|
2281
|
+
AutoContentCommands: '',
|
|
2282
|
+
Cells: page.buttons.length > 0
|
|
2283
|
+
? {
|
|
2284
|
+
Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
|
|
2285
|
+
const buttonStyleId = button.style ? addStyle(button.style) : '';
|
|
2286
|
+
const position = this.findButtonPosition(page, button, btnIndex);
|
|
2287
|
+
const captionAndImage = {
|
|
2288
|
+
Caption: button.label || '',
|
|
2289
|
+
};
|
|
2290
|
+
// Handle image references
|
|
2291
|
+
if (button.image) {
|
|
2292
|
+
captionAndImage.Image = `${button.image}`;
|
|
2293
|
+
}
|
|
2294
|
+
const cell = {
|
|
2295
|
+
'@_Column': position.x,
|
|
2296
|
+
'@_Row': position.y,
|
|
2297
|
+
captionAndImage,
|
|
2298
|
+
};
|
|
2299
|
+
if (position.columnSpan > 1) {
|
|
2300
|
+
cell['@_ColumnSpan'] = position.columnSpan;
|
|
2301
|
+
}
|
|
2302
|
+
if (position.rowSpan > 1) {
|
|
2303
|
+
cell['@_RowSpan'] = position.rowSpan;
|
|
2304
|
+
}
|
|
2305
|
+
if (buttonStyleId) {
|
|
2306
|
+
cell.CellStyle = buttonStyleId;
|
|
2307
|
+
}
|
|
2308
|
+
if (button.message && button.message !== button.label) {
|
|
2309
|
+
// Use spoken message if different from label
|
|
2310
|
+
const spoken = button.message;
|
|
2311
|
+
const cellContent = {
|
|
2312
|
+
spoken,
|
|
2313
|
+
type: 'text',
|
|
2314
|
+
};
|
|
2315
|
+
cell['ContentCell'] = cellContent;
|
|
2316
|
+
}
|
|
2317
|
+
return cell;
|
|
2318
|
+
}),
|
|
2319
|
+
}
|
|
2320
|
+
: undefined,
|
|
2321
|
+
},
|
|
2322
|
+
};
|
|
2323
|
+
const gridBuilder = new XMLBuilder({
|
|
2324
|
+
ignoreAttributes: false,
|
|
2325
|
+
format: true,
|
|
2326
|
+
indentBy: ' ',
|
|
2327
|
+
suppressEmptyNode: true,
|
|
2328
|
+
});
|
|
2329
|
+
newGridFiles.set(gridPath, gridBuilder.build(gridData));
|
|
2330
|
+
}
|
|
2331
|
+
// Copy all files from original zip, replacing modified grid files
|
|
2332
|
+
for (const entry of originalZip.getEntries()) {
|
|
2333
|
+
if (entry.isDirectory)
|
|
2334
|
+
continue;
|
|
2335
|
+
// Skip grid.xml files that we're modifying
|
|
2336
|
+
if (modifiedGridFiles.has(entry.entryName)) {
|
|
2337
|
+
const newContent = newGridFiles.get(entry.entryName);
|
|
2338
|
+
if (newContent) {
|
|
2339
|
+
outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
|
|
2340
|
+
}
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
// Copy all other files as-is
|
|
2344
|
+
outputZip.addFile(entry.entryName, entry.getData());
|
|
2345
|
+
}
|
|
2346
|
+
// Write the output ZIP
|
|
2347
|
+
const outputBuffer = outputZip.toBuffer();
|
|
2348
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
2349
|
+
}
|
|
2207
2350
|
// Helper method to find button position with span information
|
|
2208
2351
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2209
2352
|
if (page.grid && page.grid.length > 0) {
|
|
@@ -616,6 +616,75 @@ class ObfProcessor extends BaseProcessor {
|
|
|
616
616
|
await writeBinaryToPath(outputPath, zipData);
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Save a modified tree while preserving all original files (images, sounds, assets)
|
|
621
|
+
* This method only updates the .obf files for pages in the tree, keeping everything else intact.
|
|
622
|
+
*
|
|
623
|
+
* @param originalPath - Path to the original OBF/OBZ file
|
|
624
|
+
* @param tree - Modified AACTree with pages to save
|
|
625
|
+
* @param outputPath - Path where the modified file should be saved
|
|
626
|
+
*/
|
|
627
|
+
async saveModifiedTree(originalPath, tree, outputPath) {
|
|
628
|
+
const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
|
|
629
|
+
// If output is .obf (single file), use regular save
|
|
630
|
+
if (outputPath.endsWith('.obf')) {
|
|
631
|
+
await this.saveFromTree(tree, outputPath);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (Object.keys(tree.pages).length === 0) {
|
|
635
|
+
// Empty tree, just copy the original
|
|
636
|
+
const originalBuffer = await readBinaryFromInput(originalPath);
|
|
637
|
+
await writeBinaryToPath(outputPath, originalBuffer);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
641
|
+
const originalZip = new AdmZip(originalPath);
|
|
642
|
+
const outputZip = new AdmZip();
|
|
643
|
+
const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
|
|
644
|
+
// Track which .obf files we're modifying
|
|
645
|
+
const modifiedObfFiles = new Set();
|
|
646
|
+
// Generate new .obf files for pages in the tree
|
|
647
|
+
const newObfFiles = new Map();
|
|
648
|
+
for (const page of Object.values(tree.pages)) {
|
|
649
|
+
const obfFilename = getPageFilename(page.id);
|
|
650
|
+
modifiedObfFiles.add(obfFilename);
|
|
651
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
652
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
653
|
+
newObfFiles.set(obfFilename, obfContent);
|
|
654
|
+
}
|
|
655
|
+
// Generate updated manifest if we have pages
|
|
656
|
+
if (Object.keys(tree.pages).length > 0) {
|
|
657
|
+
modifiedObfFiles.add('manifest.json');
|
|
658
|
+
const manifest = {
|
|
659
|
+
format: OBF_FORMAT_VERSION,
|
|
660
|
+
root: tree.metadata.defaultHomePageId,
|
|
661
|
+
paths: {
|
|
662
|
+
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
|
|
663
|
+
images: {},
|
|
664
|
+
sounds: {},
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
newObfFiles.set('manifest.json', JSON.stringify(manifest));
|
|
668
|
+
}
|
|
669
|
+
// Copy all files from original zip, replacing modified .obf files
|
|
670
|
+
for (const entry of originalZip.getEntries()) {
|
|
671
|
+
if (entry.isDirectory)
|
|
672
|
+
continue;
|
|
673
|
+
// Skip .obf files that we're modifying
|
|
674
|
+
if (modifiedObfFiles.has(entry.entryName)) {
|
|
675
|
+
const newContent = newObfFiles.get(entry.entryName);
|
|
676
|
+
if (newContent) {
|
|
677
|
+
outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
|
|
678
|
+
}
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// Copy all other files as-is (preserves images, sounds, etc.)
|
|
682
|
+
outputZip.addFile(entry.entryName, entry.getData());
|
|
683
|
+
}
|
|
684
|
+
// Write the output ZIP
|
|
685
|
+
const outputBuffer = outputZip.toBuffer();
|
|
686
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
687
|
+
}
|
|
619
688
|
/**
|
|
620
689
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
621
690
|
* Uses the generic implementation from BaseProcessor
|
|
@@ -51,6 +51,15 @@ declare class GridsetProcessor extends BaseProcessor {
|
|
|
51
51
|
saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
|
|
52
52
|
private calculateColumnDefinitions;
|
|
53
53
|
private calculateRowDefinitions;
|
|
54
|
+
/**
|
|
55
|
+
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
56
|
+
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
57
|
+
*
|
|
58
|
+
* @param originalPath - Path to the original gridset file
|
|
59
|
+
* @param tree - Modified AACTree with pages to save
|
|
60
|
+
* @param outputPath - Path where the modified gridset should be saved
|
|
61
|
+
*/
|
|
62
|
+
saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
|
|
54
63
|
private findButtonPosition;
|
|
55
64
|
/**
|
|
56
65
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.GridsetProcessor = void 0;
|
|
4
27
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
@@ -1140,9 +1163,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1140
1163
|
}
|
|
1141
1164
|
break;
|
|
1142
1165
|
case 'Jump.ToKeyboard': {
|
|
1143
|
-
//
|
|
1166
|
+
// Prefer explicit keyboard page metadata when available.
|
|
1167
|
+
// Some Gridsets resolve the keyboard page in metadata
|
|
1168
|
+
// without preserving tree.keyboardGridName during parse.
|
|
1144
1169
|
const keyboardGridName = tree.keyboardGridName;
|
|
1145
|
-
const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1170
|
+
const keyboardPageId = tree.metadata?.defaultKeyboardPageId || gridNameToIdMap.get(keyboardGridName);
|
|
1146
1171
|
if (keyboardPageId && !navigationTarget) {
|
|
1147
1172
|
navigationTarget = keyboardPageId;
|
|
1148
1173
|
}
|
|
@@ -1702,6 +1727,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1702
1727
|
settingsData?.gridSetSettings?.keyboardGrid ||
|
|
1703
1728
|
settingsData?.GridsetSettings?.KeyboardGrid;
|
|
1704
1729
|
if (keyboardGridName && typeof keyboardGridName === 'string') {
|
|
1730
|
+
tree.keyboardGridName = keyboardGridName;
|
|
1705
1731
|
metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1706
1732
|
}
|
|
1707
1733
|
}
|
|
@@ -1711,6 +1737,22 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1711
1737
|
}
|
|
1712
1738
|
// Set metadata on tree
|
|
1713
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
|
+
}
|
|
1714
1756
|
return tree;
|
|
1715
1757
|
}
|
|
1716
1758
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -2207,6 +2249,130 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2207
2249
|
RowDefinition: Array(maxRows).fill({}),
|
|
2208
2250
|
};
|
|
2209
2251
|
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
2254
|
+
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
2255
|
+
*
|
|
2256
|
+
* @param originalPath - Path to the original gridset file
|
|
2257
|
+
* @param tree - Modified AACTree with pages to save
|
|
2258
|
+
* @param outputPath - Path where the modified gridset should be saved
|
|
2259
|
+
*/
|
|
2260
|
+
async saveModifiedTree(originalPath, tree, outputPath) {
|
|
2261
|
+
const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
|
|
2262
|
+
if (Object.keys(tree.pages).length === 0) {
|
|
2263
|
+
// Empty tree, just copy the original
|
|
2264
|
+
const originalBuffer = await readBinaryFromInput(originalPath);
|
|
2265
|
+
await writeBinaryToPath(outputPath, originalBuffer);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
|
|
2269
|
+
const originalZip = new AdmZip(originalPath);
|
|
2270
|
+
const outputZip = new AdmZip();
|
|
2271
|
+
// Collect styles from the tree for grid.xml files
|
|
2272
|
+
const uniqueStyles = new Map();
|
|
2273
|
+
let styleIdCounter = 1;
|
|
2274
|
+
const addStyle = (style) => {
|
|
2275
|
+
if (!style)
|
|
2276
|
+
return '';
|
|
2277
|
+
const normalizedStyle = { ...style };
|
|
2278
|
+
const styleKey = JSON.stringify(normalizedStyle);
|
|
2279
|
+
const existing = uniqueStyles.get(styleKey);
|
|
2280
|
+
if (existing)
|
|
2281
|
+
return existing.id;
|
|
2282
|
+
const styleId = `Style${styleIdCounter++}`;
|
|
2283
|
+
uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
|
|
2284
|
+
return styleId;
|
|
2285
|
+
};
|
|
2286
|
+
// Collect all styles from pages and buttons
|
|
2287
|
+
Object.values(tree.pages).forEach((page) => {
|
|
2288
|
+
addStyle(page.style);
|
|
2289
|
+
page.buttons.forEach((button) => {
|
|
2290
|
+
addStyle(button.style);
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
// Track which grid files we're modifying
|
|
2294
|
+
const modifiedGridFiles = new Set();
|
|
2295
|
+
// Generate grid.xml files for pages in the tree
|
|
2296
|
+
const newGridFiles = new Map();
|
|
2297
|
+
for (const page of Object.values(tree.pages)) {
|
|
2298
|
+
const gridPath = `Grids/${page.name}/grid.xml`;
|
|
2299
|
+
modifiedGridFiles.add(gridPath);
|
|
2300
|
+
// Build the grid XML content
|
|
2301
|
+
const gridData = {
|
|
2302
|
+
Grid: {
|
|
2303
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
2304
|
+
GridGuid: page.id,
|
|
2305
|
+
ColumnDefinitions: this.calculateColumnDefinitions(page),
|
|
2306
|
+
RowDefinitions: this.calculateRowDefinitions(page, false),
|
|
2307
|
+
AutoContentCommands: '',
|
|
2308
|
+
Cells: page.buttons.length > 0
|
|
2309
|
+
? {
|
|
2310
|
+
Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
|
|
2311
|
+
const buttonStyleId = button.style ? addStyle(button.style) : '';
|
|
2312
|
+
const position = this.findButtonPosition(page, button, btnIndex);
|
|
2313
|
+
const captionAndImage = {
|
|
2314
|
+
Caption: button.label || '',
|
|
2315
|
+
};
|
|
2316
|
+
// Handle image references
|
|
2317
|
+
if (button.image) {
|
|
2318
|
+
captionAndImage.Image = `${button.image}`;
|
|
2319
|
+
}
|
|
2320
|
+
const cell = {
|
|
2321
|
+
'@_Column': position.x,
|
|
2322
|
+
'@_Row': position.y,
|
|
2323
|
+
captionAndImage,
|
|
2324
|
+
};
|
|
2325
|
+
if (position.columnSpan > 1) {
|
|
2326
|
+
cell['@_ColumnSpan'] = position.columnSpan;
|
|
2327
|
+
}
|
|
2328
|
+
if (position.rowSpan > 1) {
|
|
2329
|
+
cell['@_RowSpan'] = position.rowSpan;
|
|
2330
|
+
}
|
|
2331
|
+
if (buttonStyleId) {
|
|
2332
|
+
cell.CellStyle = buttonStyleId;
|
|
2333
|
+
}
|
|
2334
|
+
if (button.message && button.message !== button.label) {
|
|
2335
|
+
// Use spoken message if different from label
|
|
2336
|
+
const spoken = button.message;
|
|
2337
|
+
const cellContent = {
|
|
2338
|
+
spoken,
|
|
2339
|
+
type: 'text',
|
|
2340
|
+
};
|
|
2341
|
+
cell['ContentCell'] = cellContent;
|
|
2342
|
+
}
|
|
2343
|
+
return cell;
|
|
2344
|
+
}),
|
|
2345
|
+
}
|
|
2346
|
+
: undefined,
|
|
2347
|
+
},
|
|
2348
|
+
};
|
|
2349
|
+
const gridBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
2350
|
+
ignoreAttributes: false,
|
|
2351
|
+
format: true,
|
|
2352
|
+
indentBy: ' ',
|
|
2353
|
+
suppressEmptyNode: true,
|
|
2354
|
+
});
|
|
2355
|
+
newGridFiles.set(gridPath, gridBuilder.build(gridData));
|
|
2356
|
+
}
|
|
2357
|
+
// Copy all files from original zip, replacing modified grid files
|
|
2358
|
+
for (const entry of originalZip.getEntries()) {
|
|
2359
|
+
if (entry.isDirectory)
|
|
2360
|
+
continue;
|
|
2361
|
+
// Skip grid.xml files that we're modifying
|
|
2362
|
+
if (modifiedGridFiles.has(entry.entryName)) {
|
|
2363
|
+
const newContent = newGridFiles.get(entry.entryName);
|
|
2364
|
+
if (newContent) {
|
|
2365
|
+
outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
|
|
2366
|
+
}
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
// Copy all other files as-is
|
|
2370
|
+
outputZip.addFile(entry.entryName, entry.getData());
|
|
2371
|
+
}
|
|
2372
|
+
// Write the output ZIP
|
|
2373
|
+
const outputBuffer = outputZip.toBuffer();
|
|
2374
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
2375
|
+
}
|
|
2210
2376
|
// Helper method to find button position with span information
|
|
2211
2377
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2212
2378
|
if (page.grid && page.grid.length > 0) {
|
|
@@ -23,6 +23,15 @@ declare class ObfProcessor extends BaseProcessor {
|
|
|
23
23
|
private createObfBoardFromPage;
|
|
24
24
|
processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
|
|
25
25
|
saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Save a modified tree while preserving all original files (images, sounds, assets)
|
|
28
|
+
* This method only updates the .obf files for pages in the tree, keeping everything else intact.
|
|
29
|
+
*
|
|
30
|
+
* @param originalPath - Path to the original OBF/OBZ file
|
|
31
|
+
* @param tree - Modified AACTree with pages to save
|
|
32
|
+
* @param outputPath - Path where the modified file should be saved
|
|
33
|
+
*/
|
|
34
|
+
saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
|
|
26
35
|
/**
|
|
27
36
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
28
37
|
* Uses the generic implementation from BaseProcessor
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.ObfProcessor = void 0;
|
|
4
27
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
@@ -619,6 +642,75 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
619
642
|
await writeBinaryToPath(outputPath, zipData);
|
|
620
643
|
}
|
|
621
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Save a modified tree while preserving all original files (images, sounds, assets)
|
|
647
|
+
* This method only updates the .obf files for pages in the tree, keeping everything else intact.
|
|
648
|
+
*
|
|
649
|
+
* @param originalPath - Path to the original OBF/OBZ file
|
|
650
|
+
* @param tree - Modified AACTree with pages to save
|
|
651
|
+
* @param outputPath - Path where the modified file should be saved
|
|
652
|
+
*/
|
|
653
|
+
async saveModifiedTree(originalPath, tree, outputPath) {
|
|
654
|
+
const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
|
|
655
|
+
// If output is .obf (single file), use regular save
|
|
656
|
+
if (outputPath.endsWith('.obf')) {
|
|
657
|
+
await this.saveFromTree(tree, outputPath);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (Object.keys(tree.pages).length === 0) {
|
|
661
|
+
// Empty tree, just copy the original
|
|
662
|
+
const originalBuffer = await readBinaryFromInput(originalPath);
|
|
663
|
+
await writeBinaryToPath(outputPath, originalBuffer);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
|
|
667
|
+
const originalZip = new AdmZip(originalPath);
|
|
668
|
+
const outputZip = new AdmZip();
|
|
669
|
+
const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
|
|
670
|
+
// Track which .obf files we're modifying
|
|
671
|
+
const modifiedObfFiles = new Set();
|
|
672
|
+
// Generate new .obf files for pages in the tree
|
|
673
|
+
const newObfFiles = new Map();
|
|
674
|
+
for (const page of Object.values(tree.pages)) {
|
|
675
|
+
const obfFilename = getPageFilename(page.id);
|
|
676
|
+
modifiedObfFiles.add(obfFilename);
|
|
677
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
678
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
679
|
+
newObfFiles.set(obfFilename, obfContent);
|
|
680
|
+
}
|
|
681
|
+
// Generate updated manifest if we have pages
|
|
682
|
+
if (Object.keys(tree.pages).length > 0) {
|
|
683
|
+
modifiedObfFiles.add('manifest.json');
|
|
684
|
+
const manifest = {
|
|
685
|
+
format: OBF_FORMAT_VERSION,
|
|
686
|
+
root: tree.metadata.defaultHomePageId,
|
|
687
|
+
paths: {
|
|
688
|
+
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
|
|
689
|
+
images: {},
|
|
690
|
+
sounds: {},
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
newObfFiles.set('manifest.json', JSON.stringify(manifest));
|
|
694
|
+
}
|
|
695
|
+
// Copy all files from original zip, replacing modified .obf files
|
|
696
|
+
for (const entry of originalZip.getEntries()) {
|
|
697
|
+
if (entry.isDirectory)
|
|
698
|
+
continue;
|
|
699
|
+
// Skip .obf files that we're modifying
|
|
700
|
+
if (modifiedObfFiles.has(entry.entryName)) {
|
|
701
|
+
const newContent = newObfFiles.get(entry.entryName);
|
|
702
|
+
if (newContent) {
|
|
703
|
+
outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
// Copy all other files as-is (preserves images, sounds, etc.)
|
|
708
|
+
outputZip.addFile(entry.entryName, entry.getData());
|
|
709
|
+
}
|
|
710
|
+
// Write the output ZIP
|
|
711
|
+
const outputBuffer = outputZip.toBuffer();
|
|
712
|
+
await writeBinaryToPath(outputPath, outputBuffer);
|
|
713
|
+
}
|
|
622
714
|
/**
|
|
623
715
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
624
716
|
* Uses the generic implementation from BaseProcessor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|