@willwade/aac-processors 0.2.9 → 0.2.11
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 +4 -1
- package/dist/browser/processors/gridset/cellHelpers.js +97 -0
- package/dist/browser/processors/gridset/gridCalculations.js +61 -0
- package/dist/browser/processors/gridset/helpers.js +4 -1
- package/dist/browser/processors/gridset/xmlFormatter.js +84 -0
- package/dist/browser/processors/gridsetProcessor.js +31 -92
- package/dist/browser/processors/obfProcessor.js +106 -49
- package/dist/browser/utilities/analytics/morphology/index.js +0 -1
- package/dist/browser/utils/sqlite.js +6 -2
- package/dist/processors/gridset/cellHelpers.d.ts +66 -0
- package/dist/processors/gridset/cellHelpers.js +102 -0
- package/dist/processors/gridset/gridCalculations.d.ts +45 -0
- package/dist/processors/gridset/gridCalculations.js +65 -0
- package/dist/processors/gridset/helpers.js +4 -1
- package/dist/processors/gridset/wordlistHelpers.js +9 -7
- package/dist/processors/gridset/xmlFormatter.d.ts +43 -0
- package/dist/processors/gridset/xmlFormatter.js +89 -0
- package/dist/processors/gridsetProcessor.js +31 -92
- package/dist/processors/obfProcessor.d.ts +2 -1
- package/dist/processors/obfProcessor.js +106 -49
- package/dist/utilities/analytics/morphology/index.d.ts +0 -1
- package/dist/utilities/analytics/morphology/index.js +1 -3
- package/dist/utils/sqlite.js +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid3 XML Formatter
|
|
3
|
+
*
|
|
4
|
+
* Utilities for formatting XML to match Grid 3's specific requirements.
|
|
5
|
+
* Grid 3 has strict formatting requirements including line endings, self-closing
|
|
6
|
+
* tag spacing, and specific tag expansion rules.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Format XML string to match Grid 3's requirements
|
|
10
|
+
*
|
|
11
|
+
* Grid 3 requires specific formatting:
|
|
12
|
+
* - Windows line endings (\r\n)
|
|
13
|
+
* - Space before /> in self-closing tags: <Element /> not <Element/>
|
|
14
|
+
* - Plain apostrophes instead of '
|
|
15
|
+
* - Specific tags expanded to full opening/closing format
|
|
16
|
+
* - CDATA for empty/whitespace captions and <r> tags
|
|
17
|
+
*
|
|
18
|
+
* @param xml - The XML string to format
|
|
19
|
+
* @returns Formatted XML string compatible with Grid 3
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const formatted = formatGrid3Xml('<Grid><Cell X="0"/></Grid>');
|
|
23
|
+
* // Returns: '<Grid>\r\n<Cell X="0" />\r\n</Grid>'
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatGrid3Xml(xml: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Format empty/whitespace captions with CDATA for Grid 3 compatibility
|
|
28
|
+
*
|
|
29
|
+
* Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text.
|
|
30
|
+
* Also handles <r> tags which need CDATA for spaces to prevent stripping.
|
|
31
|
+
*
|
|
32
|
+
* @param xml - The XML string to format
|
|
33
|
+
* @returns XML string with CDATA-wrapped empty content
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatEmptyCaptionsWithCdata(xml: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Complete XML formatting for Grid 3 compatibility
|
|
38
|
+
* Combines all Grid 3 XML formatting requirements
|
|
39
|
+
*
|
|
40
|
+
* @param xml - The XML string to format
|
|
41
|
+
* @returns Fully formatted XML string compatible with Grid 3
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatGrid3XmlComplete(xml: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Grid3 XML Formatter
|
|
4
|
+
*
|
|
5
|
+
* Utilities for formatting XML to match Grid 3's specific requirements.
|
|
6
|
+
* Grid 3 has strict formatting requirements including line endings, self-closing
|
|
7
|
+
* tag spacing, and specific tag expansion rules.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.formatGrid3Xml = formatGrid3Xml;
|
|
11
|
+
exports.formatEmptyCaptionsWithCdata = formatEmptyCaptionsWithCdata;
|
|
12
|
+
exports.formatGrid3XmlComplete = formatGrid3XmlComplete;
|
|
13
|
+
/**
|
|
14
|
+
* Tags that Grid 3 requires in full opening/closing format instead of self-closing
|
|
15
|
+
* Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
16
|
+
*/
|
|
17
|
+
const TAGS_NEEDING_EXPANSION = ['AudioDescription', 'VideoDescription'];
|
|
18
|
+
/**
|
|
19
|
+
* Format XML string to match Grid 3's requirements
|
|
20
|
+
*
|
|
21
|
+
* Grid 3 requires specific formatting:
|
|
22
|
+
* - Windows line endings (\r\n)
|
|
23
|
+
* - Space before /> in self-closing tags: <Element /> not <Element/>
|
|
24
|
+
* - Plain apostrophes instead of '
|
|
25
|
+
* - Specific tags expanded to full opening/closing format
|
|
26
|
+
* - CDATA for empty/whitespace captions and <r> tags
|
|
27
|
+
*
|
|
28
|
+
* @param xml - The XML string to format
|
|
29
|
+
* @returns Formatted XML string compatible with Grid 3
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const formatted = formatGrid3Xml('<Grid><Cell X="0"/></Grid>');
|
|
33
|
+
* // Returns: '<Grid>\r\n<Cell X="0" />\r\n</Grid>'
|
|
34
|
+
*/
|
|
35
|
+
function formatGrid3Xml(xml) {
|
|
36
|
+
let formatted = xml;
|
|
37
|
+
// Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
|
|
38
|
+
formatted = formatted.replace(/\n/g, '\r\n');
|
|
39
|
+
// Add space before /> in self-closing tags to match Grid 3's expected format
|
|
40
|
+
// Grid 3 original files use <Element /> not <Element/>
|
|
41
|
+
formatted = formatted.replace(/<(\w+)([^>]*)\/>/g, '<$1$2 />');
|
|
42
|
+
// Decode XML entities back to plain text to match Grid 3's expected format
|
|
43
|
+
// Grid 3 expects plain apostrophes, not '
|
|
44
|
+
formatted = formatted.replace(/'/g, "'");
|
|
45
|
+
formatted = formatted.replace(/"/g, '"');
|
|
46
|
+
formatted = formatted.replace(/</g, '<');
|
|
47
|
+
formatted = formatted.replace(/>/g, '>');
|
|
48
|
+
// Expand only specific self-closing tags that Grid 3 requires in full opening/closing format
|
|
49
|
+
// This must be done AFTER adding spaces, so we need to match the format with spaces
|
|
50
|
+
for (const tag of TAGS_NEEDING_EXPANSION) {
|
|
51
|
+
formatted = formatted.replace(new RegExp(`<${tag}(\\s+[^>]*)? />`, 'g'), `<${tag}$1></${tag}>`);
|
|
52
|
+
}
|
|
53
|
+
return formatted;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Format empty/whitespace captions with CDATA for Grid 3 compatibility
|
|
57
|
+
*
|
|
58
|
+
* Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text.
|
|
59
|
+
* Also handles <r> tags which need CDATA for spaces to prevent stripping.
|
|
60
|
+
*
|
|
61
|
+
* @param xml - The XML string to format
|
|
62
|
+
* @returns XML string with CDATA-wrapped empty content
|
|
63
|
+
*/
|
|
64
|
+
function formatEmptyCaptionsWithCdata(xml) {
|
|
65
|
+
let formatted = xml;
|
|
66
|
+
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
67
|
+
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
68
|
+
formatted = formatted.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
69
|
+
formatted = formatted.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
70
|
+
formatted = formatted.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
71
|
+
// Preserve CDATA in <r> tags for text parameters
|
|
72
|
+
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
73
|
+
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
74
|
+
formatted = formatted.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
75
|
+
formatted = formatted.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
76
|
+
return formatted;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Complete XML formatting for Grid 3 compatibility
|
|
80
|
+
* Combines all Grid 3 XML formatting requirements
|
|
81
|
+
*
|
|
82
|
+
* @param xml - The XML string to format
|
|
83
|
+
* @returns Fully formatted XML string compatible with Grid 3
|
|
84
|
+
*/
|
|
85
|
+
function formatGrid3XmlComplete(xml) {
|
|
86
|
+
let formatted = formatGrid3Xml(xml);
|
|
87
|
+
formatted = formatEmptyCaptionsWithCdata(formatted);
|
|
88
|
+
return formatted;
|
|
89
|
+
}
|
|
@@ -31,6 +31,9 @@ const resolver_1 = require("./gridset/resolver");
|
|
|
31
31
|
const translationProcessor_1 = require("../utilities/translation/translationProcessor");
|
|
32
32
|
const password_1 = require("./gridset/password");
|
|
33
33
|
const crypto_1 = require("./gridset/crypto");
|
|
34
|
+
const xmlFormatter_1 = require("./gridset/xmlFormatter");
|
|
35
|
+
const gridCalculations_1 = require("./gridset/gridCalculations");
|
|
36
|
+
const cellHelpers_1 = require("./gridset/cellHelpers");
|
|
34
37
|
const gridsetValidator_1 = require("../validation/gridsetValidator");
|
|
35
38
|
// New imports for enhanced Grid 3 support
|
|
36
39
|
const pluginTypes_1 = require("./gridset/pluginTypes");
|
|
@@ -2221,33 +2224,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2221
2224
|
}
|
|
2222
2225
|
// Helper method to calculate column definitions based on page layout
|
|
2223
2226
|
calculateColumnDefinitions(page) {
|
|
2224
|
-
|
|
2225
|
-
if (page.grid && page.grid.length > 0) {
|
|
2226
|
-
maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
|
|
2227
|
-
}
|
|
2228
|
-
else {
|
|
2229
|
-
// Fallback: estimate from button count
|
|
2230
|
-
maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
|
|
2231
|
-
}
|
|
2232
|
-
return {
|
|
2233
|
-
ColumnDefinition: Array(maxCols).fill({}),
|
|
2234
|
-
};
|
|
2227
|
+
return (0, gridCalculations_1.calculateColumnDefinitions)(page);
|
|
2235
2228
|
}
|
|
2236
2229
|
// Helper method to calculate row definitions based on page layout
|
|
2237
2230
|
calculateRowDefinitions(page, addWorkspaceOffset = false) {
|
|
2238
|
-
|
|
2239
|
-
const offset = addWorkspaceOffset ? 1 : 0;
|
|
2240
|
-
if (page.grid && page.grid.length > 0) {
|
|
2241
|
-
maxRows = Math.max(maxRows, page.grid.length + offset);
|
|
2242
|
-
}
|
|
2243
|
-
else {
|
|
2244
|
-
// Fallback: estimate from button count
|
|
2245
|
-
const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
|
|
2246
|
-
maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset;
|
|
2247
|
-
}
|
|
2248
|
-
return {
|
|
2249
|
-
RowDefinition: Array(maxRows).fill({}),
|
|
2250
|
-
};
|
|
2231
|
+
return (0, gridCalculations_1.calculateRowDefinitions)(page, addWorkspaceOffset);
|
|
2251
2232
|
}
|
|
2252
2233
|
/**
|
|
2253
2234
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
@@ -2393,6 +2374,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2393
2374
|
}
|
|
2394
2375
|
}
|
|
2395
2376
|
}
|
|
2377
|
+
// DO NOT create new cells - the system should only modify existing content
|
|
2378
|
+
// Personalized vocabulary is added to WordList cells via the WordList.Items array
|
|
2379
|
+
// Creating new cells would corrupt the grid structure
|
|
2396
2380
|
// Update the page's WordList with new words from modified buttons
|
|
2397
2381
|
// Collect all modified buttons that should be added to the WordList
|
|
2398
2382
|
const newWordListItems = [];
|
|
@@ -2405,9 +2389,18 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2405
2389
|
? [originalGrid.Grid.Cells.Cell]
|
|
2406
2390
|
: [];
|
|
2407
2391
|
const cell = cellArray.find((c) => {
|
|
2408
|
-
const
|
|
2409
|
-
|
|
2410
|
-
|
|
2392
|
+
const cellY = parseInt(String(c['@_Y'] || c['@_Row'] || '0'), 10);
|
|
2393
|
+
// Check Y position first
|
|
2394
|
+
if (cellY !== pos.y) {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
const cellX = c['@_X'] !== undefined ? parseInt(String(c['@_X']), 10) : undefined;
|
|
2398
|
+
// If cell has no X attribute (full-width cell), it matches any button at this Y
|
|
2399
|
+
if (cellX === undefined) {
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
// Otherwise, check exact X match
|
|
2403
|
+
return cellX === pos.x;
|
|
2411
2404
|
});
|
|
2412
2405
|
if (cell) {
|
|
2413
2406
|
const contentType = cell.Content?.ContentType || cell.Content?.contentType;
|
|
@@ -2416,11 +2409,14 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2416
2409
|
// Note: Prediction cells are already skipped earlier, so they won't reach here
|
|
2417
2410
|
if (isWordListCell) {
|
|
2418
2411
|
// Add this button to the WordList with proper Grid 3 format
|
|
2419
|
-
// Format: <Text><s><r>label</r></s></Text>
|
|
2412
|
+
// Format: <Text><p><s><r>label</r></s></p></Text>
|
|
2413
|
+
// Note: <p> wrapper is required by Grid 3's WordList format
|
|
2420
2414
|
newWordListItems.push({
|
|
2421
2415
|
Text: {
|
|
2422
|
-
|
|
2423
|
-
|
|
2416
|
+
p: {
|
|
2417
|
+
s: {
|
|
2418
|
+
r: button.label,
|
|
2419
|
+
},
|
|
2424
2420
|
},
|
|
2425
2421
|
},
|
|
2426
2422
|
Image: '', // No image for user-added words
|
|
@@ -2447,23 +2443,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2447
2443
|
originalGrid.Grid.WordList.Items.WordListItem = allItems;
|
|
2448
2444
|
}
|
|
2449
2445
|
}
|
|
2450
|
-
// Build the updated grid XML and
|
|
2446
|
+
// Build the updated grid XML and format for Grid 3 compatibility
|
|
2451
2447
|
let builtXml = gridBuilder.build(originalGrid);
|
|
2452
|
-
|
|
2453
|
-
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2454
|
-
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2455
|
-
// Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
2456
|
-
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2457
|
-
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
2458
|
-
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
2459
|
-
builtXml = builtXml.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2460
|
-
builtXml = builtXml.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2461
|
-
builtXml = builtXml.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2462
|
-
// Preserve CDATA in <r> tags for text parameters
|
|
2463
|
-
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
2464
|
-
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
2465
|
-
builtXml = builtXml.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2466
|
-
builtXml = builtXml.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2448
|
+
builtXml = (0, xmlFormatter_1.formatGrid3XmlComplete)(builtXml);
|
|
2467
2449
|
newGridFiles.set(gridPath, builtXml);
|
|
2468
2450
|
}
|
|
2469
2451
|
// Copy all files from original zip, replacing modified grid files
|
|
@@ -2532,57 +2514,14 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
2532
2514
|
// Preserve Grid 3 XML formatting requirements
|
|
2533
2515
|
suppressBooleanAttributes: false,
|
|
2534
2516
|
});
|
|
2535
|
-
// Build the grid XML and
|
|
2517
|
+
// Build the grid XML and format for Grid 3 compatibility
|
|
2536
2518
|
let builtXml = gridBuilder.build(gridData);
|
|
2537
|
-
builtXml =
|
|
2538
|
-
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2539
|
-
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2519
|
+
builtXml = (0, xmlFormatter_1.formatGrid3XmlComplete)(builtXml);
|
|
2540
2520
|
return builtXml;
|
|
2541
2521
|
}
|
|
2542
2522
|
// Helper method to find button position with span information
|
|
2543
2523
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2544
|
-
|
|
2545
|
-
// Search for button in grid layout and calculate span
|
|
2546
|
-
for (let y = 0; y < page.grid.length; y++) {
|
|
2547
|
-
for (let x = 0; x < page.grid[y].length; x++) {
|
|
2548
|
-
const current = page.grid[y][x];
|
|
2549
|
-
if (current && current.id === button.id) {
|
|
2550
|
-
// Calculate span by checking how far the same button extends
|
|
2551
|
-
let columnSpan = 1;
|
|
2552
|
-
let rowSpan = 1;
|
|
2553
|
-
// Check column span (rightward)
|
|
2554
|
-
while (x + columnSpan < page.grid[y].length) {
|
|
2555
|
-
const right = page.grid[y][x + columnSpan];
|
|
2556
|
-
if (right && right.id === button.id) {
|
|
2557
|
-
columnSpan++;
|
|
2558
|
-
}
|
|
2559
|
-
else {
|
|
2560
|
-
break;
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
// Check row span (downward)
|
|
2564
|
-
while (y + rowSpan < page.grid.length) {
|
|
2565
|
-
const below = page.grid[y + rowSpan][x];
|
|
2566
|
-
if (below && below.id === button.id) {
|
|
2567
|
-
rowSpan++;
|
|
2568
|
-
}
|
|
2569
|
-
else {
|
|
2570
|
-
break;
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
return { x, y, columnSpan, rowSpan };
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
// Fallback positioning
|
|
2579
|
-
const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
|
|
2580
|
-
return {
|
|
2581
|
-
x: fallbackIndex % gridCols,
|
|
2582
|
-
y: Math.floor(fallbackIndex / gridCols),
|
|
2583
|
-
columnSpan: 1,
|
|
2584
|
-
rowSpan: 1,
|
|
2585
|
-
};
|
|
2524
|
+
return (0, cellHelpers_1.findButtonPosition)(page, button, fallbackIndex);
|
|
2586
2525
|
}
|
|
2587
2526
|
/**
|
|
2588
2527
|
* Extract strings with metadata for aac-tools-platform compatibility
|
|
@@ -16,13 +16,14 @@ declare class ObfProcessor extends BaseProcessor {
|
|
|
16
16
|
*/
|
|
17
17
|
private extractImageAsDataUrl;
|
|
18
18
|
private getMimeTypeFromFilename;
|
|
19
|
+
private getPageFilename;
|
|
19
20
|
private processBoard;
|
|
20
21
|
extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
|
|
21
22
|
loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
|
|
22
23
|
private buildGridMetadata;
|
|
23
24
|
private createObfBoardFromPage;
|
|
24
25
|
processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
|
|
25
|
-
saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
|
|
26
|
+
saveFromTree(tree: AACTree, outputPath: string, embedData?: boolean): Promise<void>;
|
|
26
27
|
/**
|
|
27
28
|
* Save a modified tree while preserving all original files (images, sounds, assets)
|
|
28
29
|
* This method only updates the .obf files for pages in the tree, keeping everything else intact.
|
|
@@ -106,7 +106,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
106
106
|
// Images are typically stored in an 'images' folder or root
|
|
107
107
|
const possiblePaths = [
|
|
108
108
|
imageData.path, // Explicit path if provided
|
|
109
|
-
`images/${imageData.
|
|
109
|
+
`images/${imageData.path || imageId}`, // Standard images folder
|
|
110
110
|
imageData.id, // Just the ID
|
|
111
111
|
].filter(Boolean);
|
|
112
112
|
for (const imagePath of possiblePaths) {
|
|
@@ -152,14 +152,18 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
152
152
|
return 'image/png';
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
getPageFilename(id, metadata) {
|
|
156
|
+
if (metadata._obfPagePaths && id in metadata._obfPagePaths)
|
|
157
|
+
return metadata._obfPagePaths[id];
|
|
158
|
+
if (id.endsWith('.obf'))
|
|
159
|
+
return id;
|
|
160
|
+
return `${id}.obf`;
|
|
161
|
+
}
|
|
162
|
+
async processBoard(boardData, _boardPath) {
|
|
156
163
|
const sourceButtons = boardData.buttons || [];
|
|
157
164
|
// Calculate page ID first (used to make button IDs unique)
|
|
158
|
-
const pageId =
|
|
159
|
-
|
|
160
|
-
: boardData?.id
|
|
161
|
-
? String(boardData.id)
|
|
162
|
-
: _boardPath?.split(/[/\\]/).pop() || '';
|
|
165
|
+
const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
|
|
166
|
+
const images = boardData.images;
|
|
163
167
|
const buttons = await Promise.all(sourceButtons.map(async (btn) => {
|
|
164
168
|
const semanticAction = btn.load_board
|
|
165
169
|
? {
|
|
@@ -183,11 +187,16 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
183
187
|
// Resolve image if image_id is present
|
|
184
188
|
let resolvedImage;
|
|
185
189
|
let imageBuffer;
|
|
186
|
-
if (btn.image_id &&
|
|
187
|
-
resolvedImage =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
if (btn.image_id && images) {
|
|
191
|
+
resolvedImage = (await this.extractImageAsDataUrl(btn.image_id, images)) || undefined;
|
|
192
|
+
imageBuffer = (await this.extractImageAsBuffer(btn.image_id, images)) || undefined;
|
|
193
|
+
// save image data
|
|
194
|
+
if (images) {
|
|
195
|
+
const imageIndex = images?.findIndex((img) => img.id === btn.image_id);
|
|
196
|
+
if (imageIndex !== -1) {
|
|
197
|
+
images[imageIndex].data = resolvedImage;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
191
200
|
}
|
|
192
201
|
// Build parameters object for Grid3 export compatibility
|
|
193
202
|
const buttonParameters = {};
|
|
@@ -224,7 +233,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
224
233
|
parentId: null,
|
|
225
234
|
locale: boardData.locale,
|
|
226
235
|
descriptionHtml: boardData.description_html,
|
|
227
|
-
images
|
|
236
|
+
images,
|
|
228
237
|
sounds: boardData.sounds,
|
|
229
238
|
});
|
|
230
239
|
// Process grid layout if available
|
|
@@ -314,7 +323,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
314
323
|
return texts;
|
|
315
324
|
}
|
|
316
325
|
async loadIntoTree(filePathOrBuffer) {
|
|
317
|
-
const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
|
|
326
|
+
const { readBinaryFromInput, readTextFromInput, listDir, join, isDirectory } = this.options.fileAdapter;
|
|
318
327
|
// Detailed logging for debugging input
|
|
319
328
|
const bufferLength = typeof filePathOrBuffer === 'string'
|
|
320
329
|
? null
|
|
@@ -356,7 +365,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
356
365
|
const boardData = await tryParseObfJson(content);
|
|
357
366
|
if (boardData) {
|
|
358
367
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
359
|
-
const page = await this.processBoard(boardData, filePathOrBuffer
|
|
368
|
+
const page = await this.processBoard(boardData, filePathOrBuffer);
|
|
360
369
|
tree.addPage(page);
|
|
361
370
|
// Set metadata from root board
|
|
362
371
|
tree.metadata.format = 'obf';
|
|
@@ -380,22 +389,30 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
380
389
|
throw err;
|
|
381
390
|
}
|
|
382
391
|
}
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
// Determine if input is ZIP, directory, or OBF JSON string/buffer
|
|
393
|
+
let fileType = 'obf';
|
|
394
|
+
if (typeof filePathOrBuffer !== 'string') {
|
|
395
|
+
const bytes = await readBinaryFromInput(filePathOrBuffer);
|
|
396
|
+
if (bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b)
|
|
397
|
+
fileType = 'zip';
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
if (await isDirectory(filePathOrBuffer)) {
|
|
401
|
+
fileType = 'dir';
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
const lowered = filePathOrBuffer.toLowerCase();
|
|
405
|
+
if (lowered.endsWith('.zip') || lowered.endsWith('.obz'))
|
|
406
|
+
fileType = 'zip';
|
|
388
407
|
}
|
|
389
|
-
const bytes = await readBinaryFromInput(input);
|
|
390
|
-
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
|
391
408
|
}
|
|
392
409
|
// Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
|
|
393
|
-
if (
|
|
410
|
+
if (fileType === 'obf') {
|
|
394
411
|
const asJson = await tryParseObfJson(filePathOrBuffer);
|
|
395
412
|
if (!asJson)
|
|
396
413
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
397
414
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
398
|
-
const page = await this.processBoard(asJson, '[bufferOrString]'
|
|
415
|
+
const page = await this.processBoard(asJson, '[bufferOrString]');
|
|
399
416
|
tree.addPage(page);
|
|
400
417
|
// Set metadata from root board
|
|
401
418
|
tree.metadata.format = 'obf';
|
|
@@ -411,18 +428,31 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
411
428
|
tree.rootId = page.id;
|
|
412
429
|
return tree;
|
|
413
430
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
431
|
+
this.zipFile = {
|
|
432
|
+
readFile: async (name) => {
|
|
433
|
+
return await readBinaryFromInput(join(filePathOrBuffer, name));
|
|
434
|
+
},
|
|
435
|
+
listFiles: () => {
|
|
436
|
+
throw new Error('Not implemented for directory input');
|
|
437
|
+
},
|
|
438
|
+
writeFiles: () => {
|
|
439
|
+
throw new Error('Not implemented for directory input');
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
if (fileType === 'zip') {
|
|
443
|
+
try {
|
|
444
|
+
this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
console.error('[OBF] Error loading ZIP:', err);
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
420
450
|
}
|
|
421
451
|
// Store the ZIP file reference for image extraction
|
|
422
452
|
this.imageCache.clear(); // Clear cache for new file
|
|
423
|
-
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
453
|
+
console.log('[OBF] Detected zip archive or directory, extracting .obf files');
|
|
424
454
|
// List manifest and OBF files
|
|
425
|
-
const filesInZip = this.zipFile.listFiles();
|
|
455
|
+
const filesInZip = fileType === 'zip' ? this.zipFile.listFiles() : await listDir(filePathOrBuffer);
|
|
426
456
|
const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
|
|
427
457
|
let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
428
458
|
// Attempt to read manifest
|
|
@@ -456,7 +486,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
456
486
|
const content = await this.zipFile.readFile(entryName);
|
|
457
487
|
const boardData = await tryParseObfJson((0, io_1.decodeText)(content));
|
|
458
488
|
if (boardData) {
|
|
459
|
-
const page = await this.processBoard(boardData, entryName
|
|
489
|
+
const page = await this.processBoard(boardData, entryName);
|
|
460
490
|
tree.addPage(page);
|
|
461
491
|
// Set metadata if not already set (use first board as reference)
|
|
462
492
|
if (!tree.metadata.format) {
|
|
@@ -465,12 +495,16 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
465
495
|
tree.metadata.description = boardData.description_html;
|
|
466
496
|
tree.metadata.locale = boardData.locale;
|
|
467
497
|
tree.metadata.id = boardData.id;
|
|
498
|
+
tree.metadata._obfPagePaths = { [page.id]: entryName };
|
|
468
499
|
if (boardData.url)
|
|
469
500
|
tree.metadata.url = boardData.url;
|
|
470
501
|
if (boardData.locale)
|
|
471
502
|
tree.metadata.languages = [boardData.locale];
|
|
472
503
|
tree.rootId = page.id;
|
|
473
504
|
}
|
|
505
|
+
else {
|
|
506
|
+
tree.metadata._obfPagePaths[page.id] = entryName;
|
|
507
|
+
}
|
|
474
508
|
}
|
|
475
509
|
else {
|
|
476
510
|
console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
|
|
@@ -523,11 +557,18 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
523
557
|
}
|
|
524
558
|
return { rows: totalRows, columns: totalColumns, order, buttonPositions };
|
|
525
559
|
}
|
|
526
|
-
createObfBoardFromPage(page, fallbackName, metadata) {
|
|
560
|
+
createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
|
|
527
561
|
const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
|
|
528
562
|
const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
|
|
529
563
|
? metadata.name
|
|
530
564
|
: page.name || fallbackName;
|
|
565
|
+
let images = Array.isArray(page.images) ? page.images : [];
|
|
566
|
+
if (!embedData) {
|
|
567
|
+
images = images.map((image) => {
|
|
568
|
+
delete image.data;
|
|
569
|
+
return image;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
531
572
|
return {
|
|
532
573
|
format: OBF_FORMAT_VERSION,
|
|
533
574
|
id: page.id,
|
|
@@ -564,7 +605,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
564
605
|
hidden: button.visibility === 'Hidden' || false,
|
|
565
606
|
};
|
|
566
607
|
}),
|
|
567
|
-
images
|
|
608
|
+
images,
|
|
568
609
|
sounds: Array.isArray(page.sounds) ? page.sounds : [],
|
|
569
610
|
};
|
|
570
611
|
}
|
|
@@ -601,23 +642,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
601
642
|
await this.saveFromTree(tree, outputPath);
|
|
602
643
|
return await readBinaryFromInput(outputPath);
|
|
603
644
|
}
|
|
604
|
-
async saveFromTree(tree, outputPath) {
|
|
605
|
-
const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
|
|
645
|
+
async saveFromTree(tree, outputPath, embedData = false) {
|
|
646
|
+
const { writeTextToPath, writeBinaryToPath, pathExists, mkDir, join } = this.options.fileAdapter;
|
|
606
647
|
if (outputPath.endsWith('.obf')) {
|
|
607
648
|
// Save as single OBF JSON file
|
|
608
649
|
const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
|
|
609
650
|
if (!rootPage) {
|
|
610
651
|
throw new Error('No pages to save');
|
|
611
652
|
}
|
|
612
|
-
const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
|
|
653
|
+
const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata, embedData);
|
|
613
654
|
await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
|
|
614
655
|
}
|
|
615
656
|
else {
|
|
616
|
-
const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
|
|
617
657
|
const files = Object.values(tree.pages).map((page) => {
|
|
618
|
-
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
658
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata, embedData);
|
|
619
659
|
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
620
|
-
const name = getPageFilename(page.id);
|
|
660
|
+
const name = this.getPageFilename(page.id, tree.metadata);
|
|
621
661
|
return {
|
|
622
662
|
name,
|
|
623
663
|
data: new TextEncoder().encode(obfContent),
|
|
@@ -627,7 +667,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
627
667
|
format: OBF_FORMAT_VERSION,
|
|
628
668
|
root: tree.metadata.defaultHomePageId,
|
|
629
669
|
paths: {
|
|
630
|
-
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
|
|
670
|
+
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
|
|
671
|
+
id,
|
|
672
|
+
this.getPageFilename(page.id, tree.metadata),
|
|
673
|
+
])),
|
|
631
674
|
images: {}, //TODO Add support for saving images as files
|
|
632
675
|
sounds: {}, //TODO Add support for saving sounds as files
|
|
633
676
|
},
|
|
@@ -636,10 +679,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
636
679
|
name: 'manifest.json',
|
|
637
680
|
data: new TextEncoder().encode(JSON.stringify(manifest)),
|
|
638
681
|
});
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
682
|
+
if (outputPath.endsWith('.obz') || outputPath.endsWith('.zip')) {
|
|
683
|
+
console.log('[OBF] Saving to ZIP file:', outputPath);
|
|
684
|
+
const fileExists = await pathExists(outputPath);
|
|
685
|
+
this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
|
|
686
|
+
const zipData = await this.zipFile.writeFiles(files);
|
|
687
|
+
await writeBinaryToPath(outputPath, zipData);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
console.log('[OBF] Saving to directory:', outputPath);
|
|
691
|
+
if (!(await pathExists(outputPath)))
|
|
692
|
+
await mkDir(outputPath);
|
|
693
|
+
for (const file of files) {
|
|
694
|
+
const filePath = join(outputPath, file.name);
|
|
695
|
+
await writeBinaryToPath(filePath, file.data);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
643
698
|
}
|
|
644
699
|
}
|
|
645
700
|
/**
|
|
@@ -666,13 +721,12 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
666
721
|
const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
|
|
667
722
|
const originalZip = new AdmZip(originalPath);
|
|
668
723
|
const outputZip = new AdmZip();
|
|
669
|
-
const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
|
|
670
724
|
// Track which .obf files we're modifying
|
|
671
725
|
const modifiedObfFiles = new Set();
|
|
672
726
|
// Generate new .obf files for pages in the tree
|
|
673
727
|
const newObfFiles = new Map();
|
|
674
728
|
for (const page of Object.values(tree.pages)) {
|
|
675
|
-
const obfFilename = getPageFilename(page.id);
|
|
729
|
+
const obfFilename = this.getPageFilename(page.id, tree.metadata);
|
|
676
730
|
modifiedObfFiles.add(obfFilename);
|
|
677
731
|
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
678
732
|
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
@@ -685,7 +739,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
685
739
|
format: OBF_FORMAT_VERSION,
|
|
686
740
|
root: tree.metadata.defaultHomePageId,
|
|
687
741
|
paths: {
|
|
688
|
-
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
|
|
742
|
+
boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
|
|
743
|
+
id,
|
|
744
|
+
this.getPageFilename(page.id, tree.metadata),
|
|
745
|
+
])),
|
|
689
746
|
images: {},
|
|
690
747
|
sounds: {},
|
|
691
748
|
},
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { MorphologyEngine } from './engine';
|
|
2
|
-
export { Grid3VerbsParser } from './grid3VerbsParser';
|
|
3
2
|
export { WordFormGenerator } from './wordFormGenerator';
|
|
4
3
|
export type { MorphRuleSet, MorphRule, MorphWordForms, AstericsWordForm, VerbFormWithConditions, Grid3VerbFormsDetailed, } from './types';
|
|
5
4
|
export type { Grid3VerbForms } from './grid3VerbsParser';
|