@willwade/aac-processors 0.1.18 → 0.1.20
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/index.browser.js +9 -9
- package/dist/browser/processors/gridset/helpers.js +4 -2
- package/dist/browser/processors/gridsetProcessor.js +4 -1
- package/dist/browser/processors/obfProcessor.js +50 -28
- package/dist/browser/processors/snapProcessor.js +61 -3
- package/dist/browser/processors/touchchatProcessor.js +3 -1
- package/dist/browser/validation/touchChatValidator.js +3 -3
- package/dist/core/baseProcessor.d.ts +4 -0
- package/dist/core/fileProcessor.js +5 -0
- package/dist/index.browser.d.ts +2 -2
- package/dist/index.browser.js +9 -9
- package/dist/index.node.d.ts +2 -2
- package/dist/index.node.js +11 -11
- package/dist/processors/gridset/helpers.d.ts +5 -1
- package/dist/processors/gridset/helpers.js +4 -2
- package/dist/processors/gridset/imageDebug.d.ts +5 -1
- package/dist/processors/gridset/imageDebug.js +4 -2
- package/dist/processors/gridset/wordlistHelpers.d.ts +5 -1
- package/dist/processors/gridset/wordlistHelpers.js +4 -2
- package/dist/processors/gridsetProcessor.js +4 -1
- package/dist/processors/obfProcessor.js +50 -28
- package/dist/processors/snapProcessor.js +60 -2
- package/dist/processors/touchchatProcessor.js +3 -1
- package/dist/validation/touchChatValidator.d.ts +5 -1
- package/dist/validation/touchChatValidator.js +2 -2
- package/package.json +1 -1
|
@@ -48,29 +48,29 @@ export { configureSqlJs } from './utils/sqlite';
|
|
|
48
48
|
* @returns The appropriate processor instance
|
|
49
49
|
* @throws Error if the file extension is not supported
|
|
50
50
|
*/
|
|
51
|
-
export function getProcessor(filePathOrExtension) {
|
|
51
|
+
export function getProcessor(filePathOrExtension, options) {
|
|
52
52
|
const extension = filePathOrExtension.includes('.')
|
|
53
53
|
? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
|
|
54
54
|
: filePathOrExtension;
|
|
55
55
|
switch (extension.toLowerCase()) {
|
|
56
56
|
case '.dot':
|
|
57
|
-
return new DotProcessor();
|
|
57
|
+
return new DotProcessor(options);
|
|
58
58
|
case '.opml':
|
|
59
|
-
return new OpmlProcessor();
|
|
59
|
+
return new OpmlProcessor(options);
|
|
60
60
|
case '.obf':
|
|
61
61
|
case '.obz':
|
|
62
|
-
return new ObfProcessor();
|
|
62
|
+
return new ObfProcessor(options);
|
|
63
63
|
case '.gridset':
|
|
64
|
-
return new GridsetProcessor();
|
|
64
|
+
return new GridsetProcessor(options);
|
|
65
65
|
case '.spb':
|
|
66
66
|
case '.sps':
|
|
67
|
-
return new SnapProcessor();
|
|
67
|
+
return new SnapProcessor(options);
|
|
68
68
|
case '.ce':
|
|
69
|
-
return new TouchChatProcessor();
|
|
69
|
+
return new TouchChatProcessor(options);
|
|
70
70
|
case '.plist':
|
|
71
|
-
return new ApplePanelsProcessor();
|
|
71
|
+
return new ApplePanelsProcessor(options);
|
|
72
72
|
case '.grd':
|
|
73
|
-
return new AstericsGridProcessor();
|
|
73
|
+
return new AstericsGridProcessor(options);
|
|
74
74
|
default:
|
|
75
75
|
throw new Error(`Unsupported file extension: ${extension}`);
|
|
76
76
|
}
|
|
@@ -52,9 +52,11 @@ export function getAllowedImageEntries(tree) {
|
|
|
52
52
|
* @param entryPath Entry name inside the zip
|
|
53
53
|
* @returns Image data buffer or null if not found
|
|
54
54
|
*/
|
|
55
|
-
export async function openImage(gridsetBuffer, entryPath, password = resolveGridsetPasswordFromEnv()) {
|
|
55
|
+
export async function openImage(gridsetBuffer, entryPath, password = resolveGridsetPasswordFromEnv(), zipAdapter) {
|
|
56
56
|
try {
|
|
57
|
-
const { zip } =
|
|
57
|
+
const { zip } = zipAdapter
|
|
58
|
+
? await zipAdapter(gridsetBuffer)
|
|
59
|
+
: await openZipFromInput(gridsetBuffer);
|
|
58
60
|
const entries = getZipEntriesFromAdapter(zip, password);
|
|
59
61
|
const want = normalizeZipPath(entryPath);
|
|
60
62
|
const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
|
|
@@ -398,7 +398,10 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
398
398
|
const tree = new AACTree();
|
|
399
399
|
let zipResult;
|
|
400
400
|
try {
|
|
401
|
-
|
|
401
|
+
const zipInput = readBinaryFromInput(filePathOrBuffer);
|
|
402
|
+
zipResult = this.options.zipAdapter
|
|
403
|
+
? await this.options.zipAdapter(zipInput)
|
|
404
|
+
: await openZipFromInput(zipInput);
|
|
402
405
|
}
|
|
403
406
|
catch (error) {
|
|
404
407
|
throw new Error(`Invalid ZIP file format: ${error.message}`);
|
|
@@ -120,13 +120,9 @@ class ObfProcessor extends BaseProcessor {
|
|
|
120
120
|
return 'image/png';
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
async processBoard(boardData, _boardPath) {
|
|
123
|
+
async processBoard(boardData, _boardPath, isZipEntry) {
|
|
124
124
|
const sourceButtons = boardData.buttons || [];
|
|
125
125
|
// Calculate page ID first (used to make button IDs unique)
|
|
126
|
-
const isZipEntry = _boardPath &&
|
|
127
|
-
_boardPath.endsWith('.obf') &&
|
|
128
|
-
!_boardPath.includes('/') &&
|
|
129
|
-
!_boardPath.includes('\\');
|
|
130
126
|
const pageId = isZipEntry
|
|
131
127
|
? _boardPath // Zip entry - use filename to match navigation paths
|
|
132
128
|
: boardData?.id
|
|
@@ -328,7 +324,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
328
324
|
const boardData = tryParseObfJson(content);
|
|
329
325
|
if (boardData) {
|
|
330
326
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
331
|
-
const page = await this.processBoard(boardData, filePathOrBuffer);
|
|
327
|
+
const page = await this.processBoard(boardData, filePathOrBuffer, false);
|
|
332
328
|
tree.addPage(page);
|
|
333
329
|
// Set metadata from root board
|
|
334
330
|
tree.metadata.format = 'obf';
|
|
@@ -352,11 +348,22 @@ class ObfProcessor extends BaseProcessor {
|
|
|
352
348
|
throw err;
|
|
353
349
|
}
|
|
354
350
|
}
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
351
|
+
// Detect likely zip signature first
|
|
352
|
+
function isLikelyZip(input) {
|
|
353
|
+
if (typeof input === 'string') {
|
|
354
|
+
const lowered = input.toLowerCase();
|
|
355
|
+
return lowered.endsWith('.zip') || lowered.endsWith('.obz');
|
|
356
|
+
}
|
|
357
|
+
const bytes = readBinaryFromInput(input);
|
|
358
|
+
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
359
|
+
}
|
|
360
|
+
// Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
|
|
361
|
+
if (!isLikelyZip(filePathOrBuffer)) {
|
|
362
|
+
const asJson = tryParseObfJson(filePathOrBuffer);
|
|
363
|
+
if (!asJson)
|
|
364
|
+
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
358
365
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
359
|
-
const page = await this.processBoard(asJson, '[bufferOrString]');
|
|
366
|
+
const page = await this.processBoard(asJson, '[bufferOrString]', false);
|
|
360
367
|
tree.addPage(page);
|
|
361
368
|
// Set metadata from root board
|
|
362
369
|
tree.metadata.format = 'obf';
|
|
@@ -372,20 +379,10 @@ class ObfProcessor extends BaseProcessor {
|
|
|
372
379
|
tree.rootId = page.id;
|
|
373
380
|
return tree;
|
|
374
381
|
}
|
|
375
|
-
// Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
|
|
376
|
-
function isLikelyZip(input) {
|
|
377
|
-
if (typeof input === 'string') {
|
|
378
|
-
const lowered = input.toLowerCase();
|
|
379
|
-
return lowered.endsWith('.zip') || lowered.endsWith('.obz');
|
|
380
|
-
}
|
|
381
|
-
const bytes = readBinaryFromInput(input);
|
|
382
|
-
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
383
|
-
}
|
|
384
|
-
if (!isLikelyZip(filePathOrBuffer)) {
|
|
385
|
-
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
386
|
-
}
|
|
387
382
|
try {
|
|
388
|
-
const zipResult =
|
|
383
|
+
const zipResult = this.options.zipAdapter
|
|
384
|
+
? await this.options.zipAdapter(filePathOrBuffer)
|
|
385
|
+
: await openZipFromInput(filePathOrBuffer);
|
|
389
386
|
this.zipFile = zipResult.zip;
|
|
390
387
|
}
|
|
391
388
|
catch (err) {
|
|
@@ -395,17 +392,42 @@ class ObfProcessor extends BaseProcessor {
|
|
|
395
392
|
// Store the ZIP file reference for image extraction
|
|
396
393
|
this.imageCache.clear(); // Clear cache for new file
|
|
397
394
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
398
|
-
//
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
395
|
+
// List manifest and OBF files
|
|
396
|
+
const filesInZip = this.zipFile.listFiles();
|
|
397
|
+
const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
|
|
398
|
+
let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
399
|
+
// Attempt to read manifest
|
|
400
|
+
if (manifestFile && manifestFile.length === 1) {
|
|
401
|
+
try {
|
|
402
|
+
const content = await this.zipFile.readFile(manifestFile[0]);
|
|
403
|
+
const data = decodeText(content);
|
|
404
|
+
const str = typeof data === 'string' ? data : readTextFromInput(data);
|
|
405
|
+
if (!str.trim())
|
|
406
|
+
throw new Error('Manifest object missing');
|
|
407
|
+
const manifestObject = JSON.parse(str);
|
|
408
|
+
if (!manifestObject)
|
|
409
|
+
throw new Error('Manifest object is empty');
|
|
410
|
+
// Replace OBF file list
|
|
411
|
+
if (manifestObject.paths && manifestObject.paths.boards) {
|
|
412
|
+
obfEntries = Object.values(manifestObject.paths.boards);
|
|
413
|
+
}
|
|
414
|
+
// Move root board to top of list
|
|
415
|
+
if (manifestObject.root) {
|
|
416
|
+
obfEntries = obfEntries.filter((item) => item !== manifestObject.root);
|
|
417
|
+
obfEntries.unshift(manifestObject.root);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
console.warn('[OBF] Error processing mainfest', err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
402
424
|
// Process each .obf entry
|
|
403
425
|
for (const entryName of obfEntries) {
|
|
404
426
|
try {
|
|
405
427
|
const content = await this.zipFile.readFile(entryName);
|
|
406
428
|
const boardData = tryParseObfJson(decodeText(content));
|
|
407
429
|
if (boardData) {
|
|
408
|
-
const page = await this.processBoard(boardData, entryName);
|
|
430
|
+
const page = await this.processBoard(boardData, entryName, true);
|
|
409
431
|
tree.addPage(page);
|
|
410
432
|
// Set metadata if not already set (use first board as reference)
|
|
411
433
|
if (!tree.metadata.format) {
|
|
@@ -2,8 +2,28 @@ 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 { SnapValidator } from '../validation/snapValidator';
|
|
5
|
-
import { getFs, getNodeRequire, getPath, isNodeRuntime } from '../utils/io';
|
|
5
|
+
import { getFs, getNodeRequire, getPath, isNodeRuntime, getOs } from '../utils/io';
|
|
6
6
|
import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
|
|
7
|
+
import { openZipFromInput } from '../utils/zip';
|
|
8
|
+
/**
|
|
9
|
+
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
|
|
10
|
+
* Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
|
|
11
|
+
* This function works in both environments.
|
|
12
|
+
*/
|
|
13
|
+
function arrayBufferToBase64(data) {
|
|
14
|
+
// Node.js environment - Buffer has built-in base64 encoding
|
|
15
|
+
if (typeof Buffer !== 'undefined' && data instanceof Buffer) {
|
|
16
|
+
return data.toString('base64');
|
|
17
|
+
}
|
|
18
|
+
// Browser environment - use btoa with binary string conversion
|
|
19
|
+
const bytes = new Uint8Array(data);
|
|
20
|
+
let binary = '';
|
|
21
|
+
const len = bytes.byteLength;
|
|
22
|
+
for (let i = 0; i < len; i++) {
|
|
23
|
+
binary += String.fromCharCode(bytes[i]);
|
|
24
|
+
}
|
|
25
|
+
return btoa(binary);
|
|
26
|
+
}
|
|
7
27
|
/**
|
|
8
28
|
* Map Snap Visible value to AAC standard visibility
|
|
9
29
|
* Snap: 0 = hidden, 1 (or non-zero) = visible
|
|
@@ -48,8 +68,37 @@ class SnapProcessor extends BaseProcessor {
|
|
|
48
68
|
await Promise.resolve();
|
|
49
69
|
const tree = new AACTree();
|
|
50
70
|
let dbResult = null;
|
|
71
|
+
let cleanupTempZip = null;
|
|
51
72
|
try {
|
|
52
|
-
|
|
73
|
+
// Handle .sub.zip files (Snap pageset backups containing .sps files)
|
|
74
|
+
let inputFile = filePathOrBuffer;
|
|
75
|
+
if (typeof filePathOrBuffer === 'string') {
|
|
76
|
+
const fileName = getPath().basename(filePathOrBuffer).toLowerCase();
|
|
77
|
+
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
|
|
78
|
+
const fs = getFs();
|
|
79
|
+
const path = getPath();
|
|
80
|
+
const os = getOs();
|
|
81
|
+
// Extract .sub.zip to find the embedded .sps file
|
|
82
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
|
|
83
|
+
const { zip } = await openZipFromInput(filePathOrBuffer);
|
|
84
|
+
// Find the .sps file in the archive
|
|
85
|
+
const files = zip.listFiles();
|
|
86
|
+
const spsFile = files.find((f) => f.endsWith('.sps'));
|
|
87
|
+
if (!spsFile) {
|
|
88
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
89
|
+
throw new Error('No .sps file found in .sub.zip archive');
|
|
90
|
+
}
|
|
91
|
+
// Extract the .sps file
|
|
92
|
+
const spsData = await zip.readFile(spsFile);
|
|
93
|
+
const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
|
|
94
|
+
fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
|
|
95
|
+
inputFile = extractedSpsPath;
|
|
96
|
+
cleanupTempZip = () => {
|
|
97
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
dbResult = await openSqliteDatabase(inputFile, { readonly: true });
|
|
53
102
|
const db = dbResult.db;
|
|
54
103
|
const getTableColumns = (tableName) => {
|
|
55
104
|
try {
|
|
@@ -445,7 +494,7 @@ class SnapProcessor extends BaseProcessor {
|
|
|
445
494
|
if (isPng || isJpeg) {
|
|
446
495
|
// Actual PNG/JPEG image - can be displayed
|
|
447
496
|
const mimeType = isPng ? 'image/png' : 'image/jpeg';
|
|
448
|
-
const base64 = data
|
|
497
|
+
const base64 = arrayBufferToBase64(data);
|
|
449
498
|
buttonImage = `data:${mimeType};base64,${base64}`;
|
|
450
499
|
buttonParameters.image_id = imageData.Identifier;
|
|
451
500
|
}
|
|
@@ -639,6 +688,15 @@ class SnapProcessor extends BaseProcessor {
|
|
|
639
688
|
else if (dbResult?.db) {
|
|
640
689
|
dbResult.db.close();
|
|
641
690
|
}
|
|
691
|
+
// Clean up temporary extracted .sps file from .sub.zip
|
|
692
|
+
if (cleanupTempZip) {
|
|
693
|
+
try {
|
|
694
|
+
cleanupTempZip();
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
642
700
|
}
|
|
643
701
|
}
|
|
644
702
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -64,7 +64,9 @@ class TouchChatProcessor extends BaseProcessor {
|
|
|
64
64
|
this.sourceFile = filePathOrBuffer;
|
|
65
65
|
// Step 1: Unzip
|
|
66
66
|
const zipInput = readBinaryFromInput(filePathOrBuffer);
|
|
67
|
-
const { zip } =
|
|
67
|
+
const { zip } = this.options.zipAdapter
|
|
68
|
+
? await this.options.zipAdapter(zipInput)
|
|
69
|
+
: await openZipFromInput(zipInput);
|
|
68
70
|
const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
|
|
69
71
|
if (!vocabEntry) {
|
|
70
72
|
throw new Error('No .c4v vocab DB found in TouchChat export');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
4
4
|
import * as xml2js from 'xml2js';
|
|
5
5
|
import { BaseValidator } from './baseValidator';
|
|
6
|
-
import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
|
|
6
|
+
import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array, } from '../utils/io';
|
|
7
7
|
import { openZipFromInput } from '../utils/zip';
|
|
8
8
|
import { openSqliteDatabase } from '../utils/sqlite';
|
|
9
9
|
/**
|
|
@@ -27,14 +27,14 @@ export class TouchChatValidator extends BaseValidator {
|
|
|
27
27
|
/**
|
|
28
28
|
* Check if content is TouchChat format
|
|
29
29
|
*/
|
|
30
|
-
static async identifyFormat(content, filename) {
|
|
30
|
+
static async identifyFormat(content, filename, zipAdapter) {
|
|
31
31
|
const name = filename.toLowerCase();
|
|
32
32
|
if (name.endsWith('.ce')) {
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
// Try to parse as ZIP and check for .c4v database
|
|
36
36
|
try {
|
|
37
|
-
const { zip } = await openZipFromInput(content);
|
|
37
|
+
const { zip } = zipAdapter ? await zipAdapter(content) : await openZipFromInput(content);
|
|
38
38
|
const entries = zip.listFiles();
|
|
39
39
|
if (entries.some((entry) => entry.toLowerCase().endsWith('.c4v'))) {
|
|
40
40
|
return true;
|
|
@@ -43,6 +43,7 @@ import { AACTree, AACButton } from './treeStructure';
|
|
|
43
43
|
import { StringCasing } from './stringCasing';
|
|
44
44
|
import { ValidationResult } from '../validation/validationTypes';
|
|
45
45
|
import { BinaryOutput, ProcessorInput } from '../utils/io';
|
|
46
|
+
import type { ZipAdapter } from '../utils/zip';
|
|
46
47
|
export interface ProcessorOptions {
|
|
47
48
|
excludeNavigationButtons?: boolean;
|
|
48
49
|
excludeSystemButtons?: boolean;
|
|
@@ -52,6 +53,9 @@ export interface ProcessorOptions {
|
|
|
52
53
|
grid3SymbolDir?: string;
|
|
53
54
|
grid3Path?: string;
|
|
54
55
|
grid3Locale?: string;
|
|
56
|
+
zipAdapter?: (input: ProcessorInput) => Promise<{
|
|
57
|
+
zip: ZipAdapter;
|
|
58
|
+
}>;
|
|
55
59
|
}
|
|
56
60
|
export interface ExtractedString {
|
|
57
61
|
string: string;
|
|
@@ -18,6 +18,11 @@ class FileProcessor {
|
|
|
18
18
|
static detectFormat(filePathOrBuffer) {
|
|
19
19
|
if (typeof filePathOrBuffer === 'string') {
|
|
20
20
|
const ext = path_1.default.extname(filePathOrBuffer).toLowerCase();
|
|
21
|
+
const fileName = path_1.default.basename(filePathOrBuffer).toLowerCase();
|
|
22
|
+
// Handle double extensions like .sub.zip
|
|
23
|
+
if (fileName.endsWith('.sub.zip') || ext === '.sub') {
|
|
24
|
+
return 'snap';
|
|
25
|
+
}
|
|
21
26
|
switch (ext) {
|
|
22
27
|
case '.gridset':
|
|
23
28
|
case '.gridsetx':
|
package/dist/index.browser.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { TouchChatProcessor } from './processors/touchchatProcessor';
|
|
|
23
23
|
export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
24
24
|
export { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
25
25
|
export * as Metrics from './metrics';
|
|
26
|
-
import { BaseProcessor } from './core/baseProcessor';
|
|
26
|
+
import { BaseProcessor, ProcessorOptions } from './core/baseProcessor';
|
|
27
27
|
export { configureSqlJs } from './utils/sqlite';
|
|
28
28
|
/**
|
|
29
29
|
* Factory function to get the appropriate processor for a file extension
|
|
@@ -31,7 +31,7 @@ export { configureSqlJs } from './utils/sqlite';
|
|
|
31
31
|
* @returns The appropriate processor instance
|
|
32
32
|
* @throws Error if the file extension is not supported
|
|
33
33
|
*/
|
|
34
|
-
export declare function getProcessor(filePathOrExtension: string): BaseProcessor;
|
|
34
|
+
export declare function getProcessor(filePathOrExtension: string, options?: ProcessorOptions): BaseProcessor;
|
|
35
35
|
/**
|
|
36
36
|
* Get all supported file extensions
|
|
37
37
|
* @returns Array of supported file extensions
|
package/dist/index.browser.js
CHANGED
|
@@ -89,29 +89,29 @@ Object.defineProperty(exports, "configureSqlJs", { enumerable: true, get: functi
|
|
|
89
89
|
* @returns The appropriate processor instance
|
|
90
90
|
* @throws Error if the file extension is not supported
|
|
91
91
|
*/
|
|
92
|
-
function getProcessor(filePathOrExtension) {
|
|
92
|
+
function getProcessor(filePathOrExtension, options) {
|
|
93
93
|
const extension = filePathOrExtension.includes('.')
|
|
94
94
|
? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
|
|
95
95
|
: filePathOrExtension;
|
|
96
96
|
switch (extension.toLowerCase()) {
|
|
97
97
|
case '.dot':
|
|
98
|
-
return new dotProcessor_2.DotProcessor();
|
|
98
|
+
return new dotProcessor_2.DotProcessor(options);
|
|
99
99
|
case '.opml':
|
|
100
|
-
return new opmlProcessor_2.OpmlProcessor();
|
|
100
|
+
return new opmlProcessor_2.OpmlProcessor(options);
|
|
101
101
|
case '.obf':
|
|
102
102
|
case '.obz':
|
|
103
|
-
return new obfProcessor_2.ObfProcessor();
|
|
103
|
+
return new obfProcessor_2.ObfProcessor(options);
|
|
104
104
|
case '.gridset':
|
|
105
|
-
return new gridsetProcessor_2.GridsetProcessor();
|
|
105
|
+
return new gridsetProcessor_2.GridsetProcessor(options);
|
|
106
106
|
case '.spb':
|
|
107
107
|
case '.sps':
|
|
108
|
-
return new snapProcessor_2.SnapProcessor();
|
|
108
|
+
return new snapProcessor_2.SnapProcessor(options);
|
|
109
109
|
case '.ce':
|
|
110
|
-
return new touchchatProcessor_2.TouchChatProcessor();
|
|
110
|
+
return new touchchatProcessor_2.TouchChatProcessor(options);
|
|
111
111
|
case '.plist':
|
|
112
|
-
return new applePanelsProcessor_2.ApplePanelsProcessor();
|
|
112
|
+
return new applePanelsProcessor_2.ApplePanelsProcessor(options);
|
|
113
113
|
case '.grd':
|
|
114
|
-
return new astericsGridProcessor_2.AstericsGridProcessor();
|
|
114
|
+
return new astericsGridProcessor_2.AstericsGridProcessor(options);
|
|
115
115
|
default:
|
|
116
116
|
throw new Error(`Unsupported file extension: ${extension}`);
|
|
117
117
|
}
|
package/dist/index.node.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export * as Opml from './opml';
|
|
|
23
23
|
export * as ApplePanels from './applePanels';
|
|
24
24
|
export * as AstericsGrid from './astericsGrid';
|
|
25
25
|
export * as Translation from './translation';
|
|
26
|
-
import { BaseProcessor } from './core/baseProcessor';
|
|
26
|
+
import { BaseProcessor, ProcessorOptions } from './core/baseProcessor';
|
|
27
27
|
/**
|
|
28
28
|
* Factory function to get the appropriate processor for a file extension
|
|
29
29
|
* @param filePathOrExtension - File path or extension (e.g., '.dot', '/path/to/file.obf')
|
|
@@ -34,7 +34,7 @@ import { BaseProcessor } from './core/baseProcessor';
|
|
|
34
34
|
* const processor = getProcessor('/path/to/file.gridset');
|
|
35
35
|
* const tree = processor.loadIntoTree('/path/to/file.gridset');
|
|
36
36
|
*/
|
|
37
|
-
export declare function getProcessor(filePathOrExtension: string): BaseProcessor;
|
|
37
|
+
export declare function getProcessor(filePathOrExtension: string, options?: ProcessorOptions): BaseProcessor;
|
|
38
38
|
/**
|
|
39
39
|
* Get all supported file extensions
|
|
40
40
|
* @returns Array of supported file extensions
|
package/dist/index.node.js
CHANGED
|
@@ -88,35 +88,35 @@ const obfsetProcessor_1 = require("./processors/obfsetProcessor");
|
|
|
88
88
|
* const processor = getProcessor('/path/to/file.gridset');
|
|
89
89
|
* const tree = processor.loadIntoTree('/path/to/file.gridset');
|
|
90
90
|
*/
|
|
91
|
-
function getProcessor(filePathOrExtension) {
|
|
91
|
+
function getProcessor(filePathOrExtension, options) {
|
|
92
92
|
// Extract extension from file path
|
|
93
93
|
const extension = filePathOrExtension.includes('.')
|
|
94
94
|
? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
|
|
95
95
|
: filePathOrExtension;
|
|
96
96
|
switch (extension.toLowerCase()) {
|
|
97
97
|
case '.dot':
|
|
98
|
-
return new dotProcessor_1.DotProcessor();
|
|
98
|
+
return new dotProcessor_1.DotProcessor(options);
|
|
99
99
|
case '.xlsx':
|
|
100
|
-
return new excelProcessor_1.ExcelProcessor();
|
|
100
|
+
return new excelProcessor_1.ExcelProcessor(options);
|
|
101
101
|
case '.opml':
|
|
102
|
-
return new opmlProcessor_1.OpmlProcessor();
|
|
102
|
+
return new opmlProcessor_1.OpmlProcessor(options);
|
|
103
103
|
case '.obf':
|
|
104
104
|
case '.obz':
|
|
105
|
-
return new obfProcessor_1.ObfProcessor();
|
|
105
|
+
return new obfProcessor_1.ObfProcessor(options);
|
|
106
106
|
case '.obfset':
|
|
107
|
-
return new obfsetProcessor_1.ObfsetProcessor();
|
|
107
|
+
return new obfsetProcessor_1.ObfsetProcessor(options);
|
|
108
108
|
case '.gridset':
|
|
109
109
|
case '.gridsetx':
|
|
110
|
-
return new gridsetProcessor_1.GridsetProcessor();
|
|
110
|
+
return new gridsetProcessor_1.GridsetProcessor(options);
|
|
111
111
|
case '.spb':
|
|
112
112
|
case '.sps':
|
|
113
|
-
return new snapProcessor_1.SnapProcessor();
|
|
113
|
+
return new snapProcessor_1.SnapProcessor(options);
|
|
114
114
|
case '.ce':
|
|
115
|
-
return new touchchatProcessor_1.TouchChatProcessor();
|
|
115
|
+
return new touchchatProcessor_1.TouchChatProcessor(options);
|
|
116
116
|
case '.plist':
|
|
117
|
-
return new applePanelsProcessor_1.ApplePanelsProcessor();
|
|
117
|
+
return new applePanelsProcessor_1.ApplePanelsProcessor(options);
|
|
118
118
|
case '.grd':
|
|
119
|
-
return new astericsGridProcessor_1.AstericsGridProcessor();
|
|
119
|
+
return new astericsGridProcessor_1.AstericsGridProcessor(options);
|
|
120
120
|
default:
|
|
121
121
|
throw new Error(`Unsupported file extension: ${extension}`);
|
|
122
122
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AACTree, AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
|
|
2
|
+
import { type ZipAdapter } from '../../utils/zip';
|
|
3
|
+
import type { ProcessorInput } from '../../utils/io';
|
|
2
4
|
/**
|
|
3
5
|
* Build a map of button IDs to resolved image entry paths for a specific page.
|
|
4
6
|
* Helpful when rewriting zip entry names or validating images referenced in a grid.
|
|
@@ -15,7 +17,9 @@ export declare function getAllowedImageEntries(tree: AACTree): Set<string>;
|
|
|
15
17
|
* @param entryPath Entry name inside the zip
|
|
16
18
|
* @returns Image data buffer or null if not found
|
|
17
19
|
*/
|
|
18
|
-
export declare function openImage(gridsetBuffer: Uint8Array, entryPath: string, password?: string | undefined
|
|
20
|
+
export declare function openImage(gridsetBuffer: Uint8Array, entryPath: string, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
|
|
21
|
+
zip: ZipAdapter;
|
|
22
|
+
}>): Promise<Uint8Array | null>;
|
|
19
23
|
/**
|
|
20
24
|
* Generate a random GUID for Grid3 elements
|
|
21
25
|
* Grid3 uses GUIDs for grid identification
|
|
@@ -96,9 +96,11 @@ function getAllowedImageEntries(tree) {
|
|
|
96
96
|
* @param entryPath Entry name inside the zip
|
|
97
97
|
* @returns Image data buffer or null if not found
|
|
98
98
|
*/
|
|
99
|
-
async function openImage(gridsetBuffer, entryPath, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
|
|
99
|
+
async function openImage(gridsetBuffer, entryPath, password = (0, password_1.resolveGridsetPasswordFromEnv)(), zipAdapter) {
|
|
100
100
|
try {
|
|
101
|
-
const { zip } =
|
|
101
|
+
const { zip } = zipAdapter
|
|
102
|
+
? await zipAdapter(gridsetBuffer)
|
|
103
|
+
: await (0, zip_1.openZipFromInput)(gridsetBuffer);
|
|
102
104
|
const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
|
|
103
105
|
const want = normalizeZipPath(entryPath);
|
|
104
106
|
const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* These utilities help developers understand why images might not be resolving
|
|
5
5
|
* correctly in Grid3 gridsets.
|
|
6
6
|
*/
|
|
7
|
+
import { type ZipAdapter } from '../../utils/zip';
|
|
8
|
+
import { type ProcessorInput } from '../../utils/io';
|
|
7
9
|
export interface ImageIssue {
|
|
8
10
|
gridName: string;
|
|
9
11
|
cellX: number;
|
|
@@ -34,7 +36,9 @@ export interface ImageAuditResult {
|
|
|
34
36
|
* console.log(`Cell (${issue.cellX}, ${issue.cellY}): ${issue.suggestion}`);
|
|
35
37
|
* });
|
|
36
38
|
*/
|
|
37
|
-
export declare function auditGridsetImages(gridsetBuffer: Uint8Array, password?: string | undefined
|
|
39
|
+
export declare function auditGridsetImages(gridsetBuffer: Uint8Array, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
|
|
40
|
+
zip: ZipAdapter;
|
|
41
|
+
}>): Promise<ImageAuditResult>;
|
|
38
42
|
/**
|
|
39
43
|
* Get a human-readable summary of image audit results
|
|
40
44
|
*/
|
|
@@ -26,7 +26,7 @@ const io_1 = require("../../utils/io");
|
|
|
26
26
|
* console.log(`Cell (${issue.cellX}, ${issue.cellY}): ${issue.suggestion}`);
|
|
27
27
|
* });
|
|
28
28
|
*/
|
|
29
|
-
async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resolveGridsetPasswordFromEnv)()) {
|
|
29
|
+
async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resolveGridsetPasswordFromEnv)(), zipAdapter) {
|
|
30
30
|
const issues = [];
|
|
31
31
|
const availableImages = new Set();
|
|
32
32
|
let totalCells = 0;
|
|
@@ -34,7 +34,9 @@ async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resol
|
|
|
34
34
|
let resolvedImages = 0;
|
|
35
35
|
let unresolvedImages = 0;
|
|
36
36
|
try {
|
|
37
|
-
const { zip } =
|
|
37
|
+
const { zip } = zipAdapter
|
|
38
|
+
? await zipAdapter(gridsetBuffer)
|
|
39
|
+
: await (0, zip_1.openZipFromInput)(gridsetBuffer);
|
|
38
40
|
const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
|
|
39
41
|
const parser = new fast_xml_parser_1.XMLParser();
|
|
40
42
|
// Collect all image files in the gridset
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Note: Wordlists are only supported in Grid3 format. Other AAC formats
|
|
9
9
|
* do not have equivalent wordlist functionality.
|
|
10
10
|
*/
|
|
11
|
+
import { type ZipAdapter } from '../../utils/zip';
|
|
12
|
+
import { type ProcessorInput } from '../../utils/io';
|
|
11
13
|
/**
|
|
12
14
|
* Represents a single item in a wordlist
|
|
13
15
|
*/
|
|
@@ -64,7 +66,9 @@ export declare function wordlistToXml(wordlist: WordList): string;
|
|
|
64
66
|
* console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
|
|
65
67
|
* });
|
|
66
68
|
*/
|
|
67
|
-
export declare function extractWordlists(gridsetBuffer: Uint8Array, password?: string | undefined
|
|
69
|
+
export declare function extractWordlists(gridsetBuffer: Uint8Array, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
|
|
70
|
+
zip: ZipAdapter;
|
|
71
|
+
}>): Promise<Map<string, WordList>>;
|
|
68
72
|
/**
|
|
69
73
|
* Updates or adds a wordlist to a specific grid in a gridset
|
|
70
74
|
*
|
|
@@ -127,11 +127,13 @@ function wordlistToXml(wordlist) {
|
|
|
127
127
|
* console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
|
|
128
128
|
* });
|
|
129
129
|
*/
|
|
130
|
-
async function extractWordlists(gridsetBuffer, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
|
|
130
|
+
async function extractWordlists(gridsetBuffer, password = (0, password_1.resolveGridsetPasswordFromEnv)(), zipAdapter) {
|
|
131
131
|
const wordlists = new Map();
|
|
132
132
|
const parser = new fast_xml_parser_1.XMLParser();
|
|
133
133
|
try {
|
|
134
|
-
const { zip } =
|
|
134
|
+
const { zip } = zipAdapter
|
|
135
|
+
? await zipAdapter(gridsetBuffer)
|
|
136
|
+
: await (0, zip_1.openZipFromInput)(gridsetBuffer);
|
|
135
137
|
const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
|
|
136
138
|
// Process each grid file
|
|
137
139
|
for (const entry of entries) {
|
|
@@ -424,7 +424,10 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
424
424
|
const tree = new treeStructure_1.AACTree();
|
|
425
425
|
let zipResult;
|
|
426
426
|
try {
|
|
427
|
-
|
|
427
|
+
const zipInput = (0, io_1.readBinaryFromInput)(filePathOrBuffer);
|
|
428
|
+
zipResult = this.options.zipAdapter
|
|
429
|
+
? await this.options.zipAdapter(zipInput)
|
|
430
|
+
: await (0, zip_1.openZipFromInput)(zipInput);
|
|
428
431
|
}
|
|
429
432
|
catch (error) {
|
|
430
433
|
throw new Error(`Invalid ZIP file format: ${error.message}`);
|
|
@@ -146,13 +146,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
146
146
|
return 'image/png';
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
async processBoard(boardData, _boardPath) {
|
|
149
|
+
async processBoard(boardData, _boardPath, isZipEntry) {
|
|
150
150
|
const sourceButtons = boardData.buttons || [];
|
|
151
151
|
// Calculate page ID first (used to make button IDs unique)
|
|
152
|
-
const isZipEntry = _boardPath &&
|
|
153
|
-
_boardPath.endsWith('.obf') &&
|
|
154
|
-
!_boardPath.includes('/') &&
|
|
155
|
-
!_boardPath.includes('\\');
|
|
156
152
|
const pageId = isZipEntry
|
|
157
153
|
? _boardPath // Zip entry - use filename to match navigation paths
|
|
158
154
|
: boardData?.id
|
|
@@ -354,7 +350,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
354
350
|
const boardData = tryParseObfJson(content);
|
|
355
351
|
if (boardData) {
|
|
356
352
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
357
|
-
const page = await this.processBoard(boardData, filePathOrBuffer);
|
|
353
|
+
const page = await this.processBoard(boardData, filePathOrBuffer, false);
|
|
358
354
|
tree.addPage(page);
|
|
359
355
|
// Set metadata from root board
|
|
360
356
|
tree.metadata.format = 'obf';
|
|
@@ -378,11 +374,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
378
374
|
throw err;
|
|
379
375
|
}
|
|
380
376
|
}
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
377
|
+
// Detect likely zip signature first
|
|
378
|
+
function isLikelyZip(input) {
|
|
379
|
+
if (typeof input === 'string') {
|
|
380
|
+
const lowered = input.toLowerCase();
|
|
381
|
+
return lowered.endsWith('.zip') || lowered.endsWith('.obz');
|
|
382
|
+
}
|
|
383
|
+
const bytes = (0, io_1.readBinaryFromInput)(input);
|
|
384
|
+
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
385
|
+
}
|
|
386
|
+
// Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
|
|
387
|
+
if (!isLikelyZip(filePathOrBuffer)) {
|
|
388
|
+
const asJson = tryParseObfJson(filePathOrBuffer);
|
|
389
|
+
if (!asJson)
|
|
390
|
+
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
384
391
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
385
|
-
const page = await this.processBoard(asJson, '[bufferOrString]');
|
|
392
|
+
const page = await this.processBoard(asJson, '[bufferOrString]', false);
|
|
386
393
|
tree.addPage(page);
|
|
387
394
|
// Set metadata from root board
|
|
388
395
|
tree.metadata.format = 'obf';
|
|
@@ -398,20 +405,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
398
405
|
tree.rootId = page.id;
|
|
399
406
|
return tree;
|
|
400
407
|
}
|
|
401
|
-
// Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
|
|
402
|
-
function isLikelyZip(input) {
|
|
403
|
-
if (typeof input === 'string') {
|
|
404
|
-
const lowered = input.toLowerCase();
|
|
405
|
-
return lowered.endsWith('.zip') || lowered.endsWith('.obz');
|
|
406
|
-
}
|
|
407
|
-
const bytes = (0, io_1.readBinaryFromInput)(input);
|
|
408
|
-
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
409
|
-
}
|
|
410
|
-
if (!isLikelyZip(filePathOrBuffer)) {
|
|
411
|
-
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
412
|
-
}
|
|
413
408
|
try {
|
|
414
|
-
const zipResult =
|
|
409
|
+
const zipResult = this.options.zipAdapter
|
|
410
|
+
? await this.options.zipAdapter(filePathOrBuffer)
|
|
411
|
+
: await (0, zip_1.openZipFromInput)(filePathOrBuffer);
|
|
415
412
|
this.zipFile = zipResult.zip;
|
|
416
413
|
}
|
|
417
414
|
catch (err) {
|
|
@@ -421,17 +418,42 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
421
418
|
// Store the ZIP file reference for image extraction
|
|
422
419
|
this.imageCache.clear(); // Clear cache for new file
|
|
423
420
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
424
|
-
//
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
421
|
+
// List manifest and OBF files
|
|
422
|
+
const filesInZip = this.zipFile.listFiles();
|
|
423
|
+
const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
|
|
424
|
+
let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
425
|
+
// Attempt to read manifest
|
|
426
|
+
if (manifestFile && manifestFile.length === 1) {
|
|
427
|
+
try {
|
|
428
|
+
const content = await this.zipFile.readFile(manifestFile[0]);
|
|
429
|
+
const data = (0, io_1.decodeText)(content);
|
|
430
|
+
const str = typeof data === 'string' ? data : (0, io_1.readTextFromInput)(data);
|
|
431
|
+
if (!str.trim())
|
|
432
|
+
throw new Error('Manifest object missing');
|
|
433
|
+
const manifestObject = JSON.parse(str);
|
|
434
|
+
if (!manifestObject)
|
|
435
|
+
throw new Error('Manifest object is empty');
|
|
436
|
+
// Replace OBF file list
|
|
437
|
+
if (manifestObject.paths && manifestObject.paths.boards) {
|
|
438
|
+
obfEntries = Object.values(manifestObject.paths.boards);
|
|
439
|
+
}
|
|
440
|
+
// Move root board to top of list
|
|
441
|
+
if (manifestObject.root) {
|
|
442
|
+
obfEntries = obfEntries.filter((item) => item !== manifestObject.root);
|
|
443
|
+
obfEntries.unshift(manifestObject.root);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
console.warn('[OBF] Error processing mainfest', err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
428
450
|
// Process each .obf entry
|
|
429
451
|
for (const entryName of obfEntries) {
|
|
430
452
|
try {
|
|
431
453
|
const content = await this.zipFile.readFile(entryName);
|
|
432
454
|
const boardData = tryParseObfJson((0, io_1.decodeText)(content));
|
|
433
455
|
if (boardData) {
|
|
434
|
-
const page = await this.processBoard(boardData, entryName);
|
|
456
|
+
const page = await this.processBoard(boardData, entryName, true);
|
|
435
457
|
tree.addPage(page);
|
|
436
458
|
// Set metadata if not already set (use first board as reference)
|
|
437
459
|
if (!tree.metadata.format) {
|
|
@@ -7,6 +7,26 @@ const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
|
|
|
7
7
|
const snapValidator_1 = require("../validation/snapValidator");
|
|
8
8
|
const io_1 = require("../utils/io");
|
|
9
9
|
const sqlite_1 = require("../utils/sqlite");
|
|
10
|
+
const zip_1 = require("../utils/zip");
|
|
11
|
+
/**
|
|
12
|
+
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
|
|
13
|
+
* Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
|
|
14
|
+
* This function works in both environments.
|
|
15
|
+
*/
|
|
16
|
+
function arrayBufferToBase64(data) {
|
|
17
|
+
// Node.js environment - Buffer has built-in base64 encoding
|
|
18
|
+
if (typeof Buffer !== 'undefined' && data instanceof Buffer) {
|
|
19
|
+
return data.toString('base64');
|
|
20
|
+
}
|
|
21
|
+
// Browser environment - use btoa with binary string conversion
|
|
22
|
+
const bytes = new Uint8Array(data);
|
|
23
|
+
let binary = '';
|
|
24
|
+
const len = bytes.byteLength;
|
|
25
|
+
for (let i = 0; i < len; i++) {
|
|
26
|
+
binary += String.fromCharCode(bytes[i]);
|
|
27
|
+
}
|
|
28
|
+
return btoa(binary);
|
|
29
|
+
}
|
|
10
30
|
/**
|
|
11
31
|
* Map Snap Visible value to AAC standard visibility
|
|
12
32
|
* Snap: 0 = hidden, 1 (or non-zero) = visible
|
|
@@ -51,8 +71,37 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
51
71
|
await Promise.resolve();
|
|
52
72
|
const tree = new treeStructure_1.AACTree();
|
|
53
73
|
let dbResult = null;
|
|
74
|
+
let cleanupTempZip = null;
|
|
54
75
|
try {
|
|
55
|
-
|
|
76
|
+
// Handle .sub.zip files (Snap pageset backups containing .sps files)
|
|
77
|
+
let inputFile = filePathOrBuffer;
|
|
78
|
+
if (typeof filePathOrBuffer === 'string') {
|
|
79
|
+
const fileName = (0, io_1.getPath)().basename(filePathOrBuffer).toLowerCase();
|
|
80
|
+
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
|
|
81
|
+
const fs = (0, io_1.getFs)();
|
|
82
|
+
const path = (0, io_1.getPath)();
|
|
83
|
+
const os = (0, io_1.getOs)();
|
|
84
|
+
// Extract .sub.zip to find the embedded .sps file
|
|
85
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
|
|
86
|
+
const { zip } = await (0, zip_1.openZipFromInput)(filePathOrBuffer);
|
|
87
|
+
// Find the .sps file in the archive
|
|
88
|
+
const files = zip.listFiles();
|
|
89
|
+
const spsFile = files.find((f) => f.endsWith('.sps'));
|
|
90
|
+
if (!spsFile) {
|
|
91
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
92
|
+
throw new Error('No .sps file found in .sub.zip archive');
|
|
93
|
+
}
|
|
94
|
+
// Extract the .sps file
|
|
95
|
+
const spsData = await zip.readFile(spsFile);
|
|
96
|
+
const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
|
|
97
|
+
fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
|
|
98
|
+
inputFile = extractedSpsPath;
|
|
99
|
+
cleanupTempZip = () => {
|
|
100
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
dbResult = await (0, sqlite_1.openSqliteDatabase)(inputFile, { readonly: true });
|
|
56
105
|
const db = dbResult.db;
|
|
57
106
|
const getTableColumns = (tableName) => {
|
|
58
107
|
try {
|
|
@@ -448,7 +497,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
448
497
|
if (isPng || isJpeg) {
|
|
449
498
|
// Actual PNG/JPEG image - can be displayed
|
|
450
499
|
const mimeType = isPng ? 'image/png' : 'image/jpeg';
|
|
451
|
-
const base64 = data
|
|
500
|
+
const base64 = arrayBufferToBase64(data);
|
|
452
501
|
buttonImage = `data:${mimeType};base64,${base64}`;
|
|
453
502
|
buttonParameters.image_id = imageData.Identifier;
|
|
454
503
|
}
|
|
@@ -642,6 +691,15 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
642
691
|
else if (dbResult?.db) {
|
|
643
692
|
dbResult.db.close();
|
|
644
693
|
}
|
|
694
|
+
// Clean up temporary extracted .sps file from .sub.zip
|
|
695
|
+
if (cleanupTempZip) {
|
|
696
|
+
try {
|
|
697
|
+
cleanupTempZip();
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
645
703
|
}
|
|
646
704
|
}
|
|
647
705
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -67,7 +67,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
67
67
|
this.sourceFile = filePathOrBuffer;
|
|
68
68
|
// Step 1: Unzip
|
|
69
69
|
const zipInput = (0, io_1.readBinaryFromInput)(filePathOrBuffer);
|
|
70
|
-
const { zip } =
|
|
70
|
+
const { zip } = this.options.zipAdapter
|
|
71
|
+
? await this.options.zipAdapter(zipInput)
|
|
72
|
+
: await (0, zip_1.openZipFromInput)(zipInput);
|
|
71
73
|
const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
|
|
72
74
|
if (!vocabEntry) {
|
|
73
75
|
throw new Error('No .c4v vocab DB found in TouchChat export');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BaseValidator } from './baseValidator';
|
|
2
2
|
import { ValidationResult } from './validationTypes';
|
|
3
|
+
import { type ProcessorInput } from '../utils/io';
|
|
4
|
+
import { type ZipAdapter } from '../utils/zip';
|
|
3
5
|
/**
|
|
4
6
|
* Validator for TouchChat files (.ce)
|
|
5
7
|
* TouchChat files are ZIP archives that contain a .c4v SQLite database.
|
|
@@ -14,7 +16,9 @@ export declare class TouchChatValidator extends BaseValidator {
|
|
|
14
16
|
/**
|
|
15
17
|
* Check if content is TouchChat format
|
|
16
18
|
*/
|
|
17
|
-
static identifyFormat(content: any, filename: string
|
|
19
|
+
static identifyFormat(content: any, filename: string, zipAdapter?: (input: ProcessorInput) => Promise<{
|
|
20
|
+
zip: ZipAdapter;
|
|
21
|
+
}>): Promise<boolean>;
|
|
18
22
|
/**
|
|
19
23
|
* Main validation method
|
|
20
24
|
*/
|
|
@@ -53,14 +53,14 @@ class TouchChatValidator extends baseValidator_1.BaseValidator {
|
|
|
53
53
|
/**
|
|
54
54
|
* Check if content is TouchChat format
|
|
55
55
|
*/
|
|
56
|
-
static async identifyFormat(content, filename) {
|
|
56
|
+
static async identifyFormat(content, filename, zipAdapter) {
|
|
57
57
|
const name = filename.toLowerCase();
|
|
58
58
|
if (name.endsWith('.ce')) {
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
61
61
|
// Try to parse as ZIP and check for .c4v database
|
|
62
62
|
try {
|
|
63
|
-
const { zip } = await (0, zip_1.openZipFromInput)(content);
|
|
63
|
+
const { zip } = zipAdapter ? await zipAdapter(content) : await (0, zip_1.openZipFromInput)(content);
|
|
64
64
|
const entries = zip.listFiles();
|
|
65
65
|
if (entries.some((entry) => entry.toLowerCase().endsWith('.c4v'))) {
|
|
66
66
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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",
|