@willwade/aac-processors 0.1.5 → 0.1.7
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/README.md +14 -0
- package/dist/browser/index.browser.js +15 -1
- package/dist/browser/processors/gridset/password.js +11 -0
- package/dist/browser/processors/gridsetProcessor.js +42 -46
- package/dist/browser/processors/obfProcessor.js +47 -63
- package/dist/browser/processors/snapProcessor.js +1031 -0
- package/dist/browser/processors/touchchatProcessor.js +1004 -0
- package/dist/browser/utils/io.js +36 -2
- package/dist/browser/utils/sqlite.js +109 -0
- package/dist/browser/utils/zip.js +54 -0
- package/dist/browser/validation/gridsetValidator.js +7 -27
- package/dist/browser/validation/obfValidator.js +9 -4
- package/dist/browser/validation/snapValidator.js +197 -0
- package/dist/browser/validation/touchChatValidator.js +201 -0
- package/dist/index.browser.d.ts +7 -0
- package/dist/index.browser.js +19 -2
- package/dist/processors/gridset/helpers.js +3 -4
- package/dist/processors/gridset/index.d.ts +1 -1
- package/dist/processors/gridset/index.js +3 -2
- package/dist/processors/gridset/password.d.ts +3 -2
- package/dist/processors/gridset/password.js +12 -0
- package/dist/processors/gridset/wordlistHelpers.js +107 -51
- package/dist/processors/gridsetProcessor.js +40 -44
- package/dist/processors/obfProcessor.js +46 -62
- package/dist/processors/snapProcessor.js +60 -54
- package/dist/processors/touchchatProcessor.js +38 -36
- package/dist/utils/io.d.ts +4 -0
- package/dist/utils/io.js +40 -2
- package/dist/utils/sqlite.d.ts +21 -0
- package/dist/utils/sqlite.js +137 -0
- package/dist/utils/zip.d.ts +7 -0
- package/dist/utils/zip.js +80 -0
- package/dist/validation/applePanelsValidator.js +11 -28
- package/dist/validation/astericsValidator.js +11 -30
- package/dist/validation/dotValidator.js +11 -30
- package/dist/validation/excelValidator.js +5 -6
- package/dist/validation/gridsetValidator.js +29 -26
- package/dist/validation/index.d.ts +2 -1
- package/dist/validation/index.js +9 -32
- package/dist/validation/obfValidator.js +8 -3
- package/dist/validation/obfsetValidator.js +11 -30
- package/dist/validation/opmlValidator.js +11 -30
- package/dist/validation/snapValidator.js +6 -9
- package/dist/validation/touchChatValidator.js +6 -7
- package/docs/BROWSER_USAGE.md +2 -10
- package/examples/README.md +3 -75
- package/examples/vitedemo/README.md +13 -7
- package/examples/vitedemo/index.html +51 -2
- package/examples/vitedemo/package-lock.json +9 -0
- package/examples/vitedemo/package.json +1 -0
- package/examples/vitedemo/src/main.ts +132 -2
- package/examples/vitedemo/src/vite-env.d.ts +1 -0
- package/examples/vitedemo/vite.config.ts +26 -7
- package/package.json +3 -1
- package/examples/browser-test-server.js +0 -81
package/README.md
CHANGED
|
@@ -30,6 +30,20 @@ const texts = await snap.extractTexts('board.sps');
|
|
|
30
30
|
Browser-safe entry that avoids Node-only dependencies. It expects `Buffer`,
|
|
31
31
|
`Uint8Array`, or `ArrayBuffer` inputs rather than file paths.
|
|
32
32
|
|
|
33
|
+
SQLite-backed formats (Snap `.sps/.spb` and TouchChat `.ce`) require a WASM
|
|
34
|
+
SQLite engine. Configure `sql.js` in your bundler before loading those formats:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { configureSqlJs, SnapProcessor } from '@willwade/aac-processors/browser';
|
|
38
|
+
|
|
39
|
+
configureSqlJs({
|
|
40
|
+
locateFile: (file) => new URL(`./${file}`, import.meta.url).toString(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const snap = new SnapProcessor();
|
|
44
|
+
const tree = await snap.loadIntoTree(snapUint8Array);
|
|
45
|
+
```
|
|
46
|
+
|
|
33
47
|
```ts
|
|
34
48
|
import { GridsetProcessor } from '@willwade/aac-processors/browser';
|
|
35
49
|
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* **NOTE: Gridset .gridsetx files**
|
|
7
7
|
* GridsetProcessor supports regular `.gridset` files in browser.
|
|
8
8
|
* Encrypted `.gridsetx` files require Node.js for crypto operations and are not supported in browser.
|
|
9
|
+
*
|
|
10
|
+
* **NOTE: SQLite-backed formats**
|
|
11
|
+
* Snap (.sps/.spb) and TouchChat (.ce) require a WASM-backed SQLite engine.
|
|
12
|
+
* Configure `sql.js` in browser builds via `configureSqlJs()` before loading these formats.
|
|
9
13
|
*/
|
|
10
14
|
// ===================================================================
|
|
11
15
|
// CORE TYPES
|
|
@@ -20,14 +24,19 @@ export { DotProcessor } from './processors/dotProcessor';
|
|
|
20
24
|
export { OpmlProcessor } from './processors/opmlProcessor';
|
|
21
25
|
export { ObfProcessor } from './processors/obfProcessor';
|
|
22
26
|
export { GridsetProcessor } from './processors/gridsetProcessor';
|
|
27
|
+
export { SnapProcessor } from './processors/snapProcessor';
|
|
28
|
+
export { TouchChatProcessor } from './processors/touchchatProcessor';
|
|
23
29
|
export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
24
30
|
export { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
25
31
|
import { DotProcessor } from './processors/dotProcessor';
|
|
26
32
|
import { OpmlProcessor } from './processors/opmlProcessor';
|
|
27
33
|
import { ObfProcessor } from './processors/obfProcessor';
|
|
28
34
|
import { GridsetProcessor } from './processors/gridsetProcessor';
|
|
35
|
+
import { SnapProcessor } from './processors/snapProcessor';
|
|
36
|
+
import { TouchChatProcessor } from './processors/touchchatProcessor';
|
|
29
37
|
import { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
30
38
|
import { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
39
|
+
export { configureSqlJs } from './utils/sqlite';
|
|
31
40
|
/**
|
|
32
41
|
* Factory function to get the appropriate processor for a file extension
|
|
33
42
|
* @param filePathOrExtension - File path or extension (e.g., '.dot', '/path/to/file.obf')
|
|
@@ -48,6 +57,11 @@ export function getProcessor(filePathOrExtension) {
|
|
|
48
57
|
return new ObfProcessor();
|
|
49
58
|
case '.gridset':
|
|
50
59
|
return new GridsetProcessor();
|
|
60
|
+
case '.spb':
|
|
61
|
+
case '.sps':
|
|
62
|
+
return new SnapProcessor();
|
|
63
|
+
case '.ce':
|
|
64
|
+
return new TouchChatProcessor();
|
|
51
65
|
case '.plist':
|
|
52
66
|
return new ApplePanelsProcessor();
|
|
53
67
|
case '.grd':
|
|
@@ -61,7 +75,7 @@ export function getProcessor(filePathOrExtension) {
|
|
|
61
75
|
* @returns Array of supported file extensions
|
|
62
76
|
*/
|
|
63
77
|
export function getSupportedExtensions() {
|
|
64
|
-
return ['.dot', '.opml', '.obf', '.obz', '.gridset', '.plist', '.grd'];
|
|
78
|
+
return ['.dot', '.opml', '.obf', '.obz', '.gridset', '.spb', '.sps', '.ce', '.plist', '.grd'];
|
|
65
79
|
}
|
|
66
80
|
/**
|
|
67
81
|
* Check if a file extension is supported
|
|
@@ -47,3 +47,14 @@ export function getZipEntriesWithPassword(zip, password) {
|
|
|
47
47
|
});
|
|
48
48
|
return entries;
|
|
49
49
|
}
|
|
50
|
+
export function getZipEntriesFromAdapter(zip, password) {
|
|
51
|
+
if (password) {
|
|
52
|
+
console.warn('Zip password support is handled at the archive level for .gridsetx files.');
|
|
53
|
+
}
|
|
54
|
+
return zip.listFiles().map((entryName) => ({
|
|
55
|
+
name: entryName,
|
|
56
|
+
entryName,
|
|
57
|
+
dir: false,
|
|
58
|
+
getData: () => zip.readFile(entryName),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
@@ -3,7 +3,7 @@ import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, }
|
|
|
3
3
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
4
4
|
import { resolveGrid3CellImage } from './gridset/resolver';
|
|
5
5
|
import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
|
|
6
|
-
import {
|
|
6
|
+
import { getZipEntriesFromAdapter, resolveGridsetPassword, } from './gridset/password';
|
|
7
7
|
import { decryptGridsetEntry } from './gridset/crypto';
|
|
8
8
|
import { GridsetValidator } from '../validation/gridsetValidator';
|
|
9
9
|
// New imports for enhanced Grid 3 support
|
|
@@ -13,32 +13,8 @@ import { parseSymbolReference } from './gridset/symbols';
|
|
|
13
13
|
import { isSymbolLibraryReference } from './gridset/resolver';
|
|
14
14
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
15
15
|
import { translateWithSymbols, extractSymbolsFromButton } from './gridset/symbolAlignment';
|
|
16
|
-
import { readBinaryFromInput, decodeText, writeBinaryToPath } from '../utils/io';
|
|
17
|
-
|
|
18
|
-
async function getJSZip() {
|
|
19
|
-
if (!JSZipModule) {
|
|
20
|
-
try {
|
|
21
|
-
// Try ES module import first (browser/Vite)
|
|
22
|
-
const module = await import('jszip');
|
|
23
|
-
JSZipModule = module.default || module;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
// Fall back to CommonJS require (Node.js)
|
|
27
|
-
try {
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
29
|
-
const module = require('jszip');
|
|
30
|
-
JSZipModule = module.default || module;
|
|
31
|
-
}
|
|
32
|
-
catch (err2) {
|
|
33
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (!JSZipModule) {
|
|
38
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
39
|
-
}
|
|
40
|
-
return JSZipModule;
|
|
41
|
-
}
|
|
16
|
+
import { readBinaryFromInput, decodeText, writeBinaryToPath, getNodeRequire, isNodeRuntime, } from '../utils/io';
|
|
17
|
+
import { openZipFromInput } from '../utils/zip';
|
|
42
18
|
class GridsetProcessor extends BaseProcessor {
|
|
43
19
|
constructor(options) {
|
|
44
20
|
super(options);
|
|
@@ -411,17 +387,15 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
411
387
|
}
|
|
412
388
|
async loadIntoTree(filePathOrBuffer) {
|
|
413
389
|
const tree = new AACTree();
|
|
414
|
-
let
|
|
390
|
+
let zipResult;
|
|
415
391
|
try {
|
|
416
|
-
|
|
417
|
-
const zipInput = readBinaryFromInput(filePathOrBuffer);
|
|
418
|
-
zip = await JSZip.loadAsync(zipInput);
|
|
392
|
+
zipResult = await openZipFromInput(readBinaryFromInput(filePathOrBuffer));
|
|
419
393
|
}
|
|
420
394
|
catch (error) {
|
|
421
395
|
throw new Error(`Invalid ZIP file format: ${error.message}`);
|
|
422
396
|
}
|
|
423
397
|
const password = this.getGridsetPassword(filePathOrBuffer);
|
|
424
|
-
const entries =
|
|
398
|
+
const entries = getZipEntriesFromAdapter(zipResult.zip, password);
|
|
425
399
|
const parser = new XMLParser({ ignoreAttributes: false });
|
|
426
400
|
const isEncryptedArchive = typeof filePathOrBuffer === 'string' && filePathOrBuffer.toLowerCase().endsWith('.gridsetx');
|
|
427
401
|
const encryptedContentPassword = this.getGridsetPassword(filePathOrBuffer);
|
|
@@ -432,14 +406,11 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
432
406
|
passwordProtected: !!password,
|
|
433
407
|
};
|
|
434
408
|
const readEntryBuffer = async (entry) => {
|
|
435
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument
|
|
436
409
|
const raw = await entry.getData();
|
|
437
410
|
if (!isEncryptedArchive) {
|
|
438
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
439
411
|
return raw;
|
|
440
412
|
}
|
|
441
|
-
|
|
442
|
-
return decryptGridsetEntry(raw, encryptedContentPassword);
|
|
413
|
+
return decryptGridsetEntry(Buffer.from(raw), encryptedContentPassword);
|
|
443
414
|
};
|
|
444
415
|
// Parse FileMap.xml if present to index dynamic files per grid
|
|
445
416
|
const fileMapIndex = new Map();
|
|
@@ -759,7 +730,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
759
730
|
const gridEntryPath = entry.entryName.replace(/\\/g, '/');
|
|
760
731
|
const baseDir = gridEntryPath.replace(/\/grid\.xml$/, '/');
|
|
761
732
|
const dynamicFiles = fileMapIndex.get(gridEntryPath) || [];
|
|
762
|
-
const resolvedImageEntry = resolveGrid3CellImage(
|
|
733
|
+
const resolvedImageEntry = resolveGrid3CellImage(null, {
|
|
763
734
|
baseDir,
|
|
764
735
|
imageName: declaredImageName,
|
|
765
736
|
x: cellX + 1,
|
|
@@ -1610,11 +1581,36 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1610
1581
|
return readBinaryFromInput(outputPath);
|
|
1611
1582
|
}
|
|
1612
1583
|
async saveFromTree(tree, outputPath) {
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1584
|
+
const useNodeZip = isNodeRuntime();
|
|
1585
|
+
let addText;
|
|
1586
|
+
let addBinary;
|
|
1587
|
+
let finalizeZip;
|
|
1588
|
+
if (useNodeZip) {
|
|
1589
|
+
const AdmZip = getNodeRequire()('adm-zip');
|
|
1590
|
+
const zip = new AdmZip();
|
|
1591
|
+
addText = (entryPath, content) => {
|
|
1592
|
+
zip.addFile(entryPath, Buffer.from(content, 'utf8'));
|
|
1593
|
+
};
|
|
1594
|
+
addBinary = (entryPath, content) => {
|
|
1595
|
+
zip.addFile(entryPath, Buffer.from(content));
|
|
1596
|
+
};
|
|
1597
|
+
finalizeZip = () => Promise.resolve(zip.toBuffer());
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
const module = await import('jszip');
|
|
1601
|
+
const JSZip = module.default || module;
|
|
1602
|
+
const zip = new JSZip();
|
|
1603
|
+
addText = (entryPath, content) => {
|
|
1604
|
+
zip.file(entryPath, content, { binary: false });
|
|
1605
|
+
};
|
|
1606
|
+
addBinary = (entryPath, content) => {
|
|
1607
|
+
zip.file(entryPath, content);
|
|
1608
|
+
};
|
|
1609
|
+
finalizeZip = async () => zip.generateAsync({ type: 'uint8array' });
|
|
1610
|
+
}
|
|
1615
1611
|
if (Object.keys(tree.pages).length === 0) {
|
|
1616
1612
|
// Create empty zip for empty tree
|
|
1617
|
-
const zipBuffer = await
|
|
1613
|
+
const zipBuffer = await finalizeZip();
|
|
1618
1614
|
writeBinaryToPath(outputPath, zipBuffer);
|
|
1619
1615
|
return;
|
|
1620
1616
|
}
|
|
@@ -1687,7 +1683,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1687
1683
|
suppressEmptyNode: true,
|
|
1688
1684
|
});
|
|
1689
1685
|
const settingsXmlContent = settingsBuilder.build(settingsData);
|
|
1690
|
-
|
|
1686
|
+
addText('Settings0/settings.xml', settingsXmlContent);
|
|
1691
1687
|
// Create Settings0/Styles/style.xml if there are styles
|
|
1692
1688
|
if (uniqueStyles.size > 0) {
|
|
1693
1689
|
const stylesArray = Array.from(uniqueStyles.values()).map(({ id, style }) => {
|
|
@@ -1720,7 +1716,7 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1720
1716
|
indentBy: ' ',
|
|
1721
1717
|
});
|
|
1722
1718
|
const styleXmlContent = styleBuilder.build(styleData);
|
|
1723
|
-
|
|
1719
|
+
addText('Settings0/Styles/styles.xml', styleXmlContent);
|
|
1724
1720
|
}
|
|
1725
1721
|
// Collect grid file paths for FileMap.xml
|
|
1726
1722
|
const gridFilePaths = [];
|
|
@@ -1861,14 +1857,14 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1861
1857
|
// Add to zip in Grids folder with proper Grid3 naming
|
|
1862
1858
|
const gridPath = `Grids/${page.name || page.id}/grid.xml`;
|
|
1863
1859
|
gridFilePaths.push(gridPath);
|
|
1864
|
-
|
|
1860
|
+
addText(gridPath, xmlContent);
|
|
1865
1861
|
});
|
|
1866
1862
|
// Write image files to ZIP
|
|
1867
1863
|
buttonImages.forEach((imgData) => {
|
|
1868
1864
|
if (imgData.imageData && imgData.imageData.length > 0) {
|
|
1869
1865
|
// Create image path in the grid's directory
|
|
1870
1866
|
const imagePath = `Grids/${imgData.pageName}/${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
|
|
1871
|
-
|
|
1867
|
+
addBinary(imagePath, imgData.imageData);
|
|
1872
1868
|
}
|
|
1873
1869
|
});
|
|
1874
1870
|
// Create FileMap.xml to map all grid files with their dynamic image files
|
|
@@ -1907,9 +1903,9 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
1907
1903
|
indentBy: ' ',
|
|
1908
1904
|
});
|
|
1909
1905
|
const fileMapXmlContent = fileMapBuilder.build(fileMapData);
|
|
1910
|
-
|
|
1906
|
+
addText('FileMap.xml', fileMapXmlContent);
|
|
1911
1907
|
// Write the zip file
|
|
1912
|
-
const zipBuffer = await
|
|
1908
|
+
const zipBuffer = await finalizeZip();
|
|
1913
1909
|
writeBinaryToPath(outputPath, zipBuffer);
|
|
1914
1910
|
}
|
|
1915
1911
|
// Helper method to calculate column definitions based on page layout
|
|
@@ -2,32 +2,8 @@ import { BaseProcessor, } from '../core/baseProcessor';
|
|
|
2
2
|
import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
|
|
3
3
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
4
4
|
import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
|
|
5
|
-
import { readBinaryFromInput, readTextFromInput, writeTextToPath, encodeBase64, } from '../utils/io';
|
|
6
|
-
|
|
7
|
-
async function getJSZipObf() {
|
|
8
|
-
if (!JSZipModuleObf) {
|
|
9
|
-
try {
|
|
10
|
-
// Try ES module import first (browser/Vite)
|
|
11
|
-
const module = await import('jszip');
|
|
12
|
-
JSZipModuleObf = module.default || module;
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
// Fall back to CommonJS require (Node.js)
|
|
16
|
-
try {
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
-
const module = require('jszip');
|
|
19
|
-
JSZipModuleObf = module.default || module;
|
|
20
|
-
}
|
|
21
|
-
catch (err2) {
|
|
22
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (!JSZipModuleObf) {
|
|
27
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
28
|
-
}
|
|
29
|
-
return JSZipModuleObf;
|
|
30
|
-
}
|
|
5
|
+
import { readBinaryFromInput, readTextFromInput, writeTextToPath, encodeBase64, decodeText, getNodeRequire, isNodeRuntime, } from '../utils/io';
|
|
6
|
+
import { openZipFromInput } from '../utils/zip';
|
|
31
7
|
const OBF_FORMAT_VERSION = 'open-board-0.1';
|
|
32
8
|
/**
|
|
33
9
|
* Map OBF hidden value to AAC standard visibility
|
|
@@ -65,10 +41,12 @@ class ObfProcessor extends BaseProcessor {
|
|
|
65
41
|
].filter(Boolean);
|
|
66
42
|
for (const imagePath of possiblePaths) {
|
|
67
43
|
try {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
const buffer = await this.zipFile.readFile(imagePath);
|
|
45
|
+
if (buffer) {
|
|
46
|
+
if (typeof Buffer !== 'undefined') {
|
|
47
|
+
return Buffer.from(buffer);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
72
50
|
}
|
|
73
51
|
}
|
|
74
52
|
catch (err) {
|
|
@@ -102,9 +80,8 @@ class ObfProcessor extends BaseProcessor {
|
|
|
102
80
|
].filter(Boolean);
|
|
103
81
|
for (const imagePath of possiblePaths) {
|
|
104
82
|
try {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
const buffer = await file.async('uint8array');
|
|
83
|
+
const buffer = await this.zipFile.readFile(imagePath);
|
|
84
|
+
if (buffer) {
|
|
108
85
|
const contentType = imageData.content_type ||
|
|
109
86
|
this.getMimeTypeFromFilename(imagePath);
|
|
110
87
|
const dataUrl = `data:${contentType};base64,${encodeBase64(buffer)}`;
|
|
@@ -407,36 +384,28 @@ class ObfProcessor extends BaseProcessor {
|
|
|
407
384
|
if (!isLikelyZip(filePathOrBuffer)) {
|
|
408
385
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
409
386
|
}
|
|
410
|
-
const JSZip = await getJSZipObf();
|
|
411
|
-
let zip;
|
|
412
387
|
try {
|
|
413
|
-
const
|
|
414
|
-
|
|
388
|
+
const zipResult = await openZipFromInput(filePathOrBuffer);
|
|
389
|
+
this.zipFile = zipResult.zip;
|
|
415
390
|
}
|
|
416
391
|
catch (err) {
|
|
417
|
-
console.error('[OBF] Error loading ZIP
|
|
392
|
+
console.error('[OBF] Error loading ZIP:', err);
|
|
418
393
|
throw err;
|
|
419
394
|
}
|
|
420
395
|
// Store the ZIP file reference for image extraction
|
|
421
|
-
this.zipFile = zip;
|
|
422
396
|
this.imageCache.clear(); // Clear cache for new file
|
|
423
397
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
424
398
|
// Collect all .obf entries
|
|
425
|
-
const obfEntries =
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return;
|
|
429
|
-
if (relativePath.toLowerCase().endsWith('.obf')) {
|
|
430
|
-
obfEntries.push({ name: relativePath, file });
|
|
431
|
-
}
|
|
432
|
-
});
|
|
399
|
+
const obfEntries = this.zipFile
|
|
400
|
+
.listFiles()
|
|
401
|
+
.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
433
402
|
// Process each .obf entry
|
|
434
|
-
for (const
|
|
403
|
+
for (const entryName of obfEntries) {
|
|
435
404
|
try {
|
|
436
|
-
const content = await
|
|
437
|
-
const boardData = tryParseObfJson(content);
|
|
405
|
+
const content = await this.zipFile.readFile(entryName);
|
|
406
|
+
const boardData = tryParseObfJson(decodeText(content));
|
|
438
407
|
if (boardData) {
|
|
439
|
-
const page = await this.processBoard(boardData,
|
|
408
|
+
const page = await this.processBoard(boardData, entryName);
|
|
440
409
|
tree.addPage(page);
|
|
441
410
|
// Set metadata if not already set (use first board as reference)
|
|
442
411
|
if (!tree.metadata.format) {
|
|
@@ -453,11 +422,11 @@ class ObfProcessor extends BaseProcessor {
|
|
|
453
422
|
}
|
|
454
423
|
}
|
|
455
424
|
else {
|
|
456
|
-
console.warn('[OBF] Skipped entry (not valid OBF JSON):',
|
|
425
|
+
console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
|
|
457
426
|
}
|
|
458
427
|
}
|
|
459
428
|
catch (err) {
|
|
460
|
-
console.warn('[OBF] Error processing entry:',
|
|
429
|
+
console.warn('[OBF] Error processing entry:', entryName, err);
|
|
461
430
|
}
|
|
462
431
|
}
|
|
463
432
|
return tree;
|
|
@@ -583,16 +552,31 @@ class ObfProcessor extends BaseProcessor {
|
|
|
583
552
|
}
|
|
584
553
|
else {
|
|
585
554
|
// Save as OBZ (zip with multiple OBF files)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
555
|
+
if (isNodeRuntime()) {
|
|
556
|
+
const AdmZip = getNodeRequire()('adm-zip');
|
|
557
|
+
const zip = new AdmZip();
|
|
558
|
+
Object.values(tree.pages).forEach((page) => {
|
|
559
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
560
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
561
|
+
zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
|
|
562
|
+
});
|
|
563
|
+
const zipBuffer = zip.toBuffer();
|
|
564
|
+
const { writeBinaryToPath } = await import('../utils/io');
|
|
565
|
+
writeBinaryToPath(outputPath, zipBuffer);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const module = await import('jszip');
|
|
569
|
+
const JSZip = module.default || module;
|
|
570
|
+
const zip = new JSZip();
|
|
571
|
+
Object.values(tree.pages).forEach((page) => {
|
|
572
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
573
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
574
|
+
zip.file(`${page.id}.obf`, obfContent);
|
|
575
|
+
});
|
|
576
|
+
const zipBuffer = await zip.generateAsync({ type: 'uint8array' });
|
|
577
|
+
const { writeBinaryToPath } = await import('../utils/io');
|
|
578
|
+
writeBinaryToPath(outputPath, zipBuffer);
|
|
579
|
+
}
|
|
596
580
|
}
|
|
597
581
|
}
|
|
598
582
|
/**
|