@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
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ npm install @willwade/aac-processors
|
|
|
13
13
|
## Dual Build Targets
|
|
14
14
|
|
|
15
15
|
### Node.js (default)
|
|
16
|
+
|
|
16
17
|
Full feature set, including filesystem access, SQLite-backed formats, and
|
|
17
18
|
ZIP/encrypted formats.
|
|
18
19
|
|
|
@@ -27,6 +28,7 @@ const texts = await snap.extractTexts('board.sps');
|
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
### Browser
|
|
31
|
+
|
|
30
32
|
Browser-safe entry that avoids Node-only dependencies. It expects `Buffer`,
|
|
31
33
|
`Uint8Array`, or `ArrayBuffer` inputs rather than file paths.
|
|
32
34
|
|
|
@@ -82,7 +84,8 @@ const translations = new Map([
|
|
|
82
84
|
|
|
83
85
|
await processor.processTexts('board.dot', translations, 'board-es.dot');
|
|
84
86
|
```
|
|
85
|
-
|
|
87
|
+
|
|
88
|
+
NB: Please use [https://aactools.co.uk](https://aactools.co.uk) for a far more comphrensive translation logic - where we do far far more than this...
|
|
86
89
|
|
|
87
90
|
## Documentation
|
|
88
91
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid3 Cell Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utilities for working with Grid 3 cells, including finding button positions
|
|
5
|
+
* and calculating cell spans in grid layouts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Find button position with span information
|
|
9
|
+
*
|
|
10
|
+
* Searches the page's grid layout for a button and calculates its position
|
|
11
|
+
* and span (how many columns/rows it occupies).
|
|
12
|
+
*
|
|
13
|
+
* @param page - The AAC page containing the button
|
|
14
|
+
* @param button - The button to locate
|
|
15
|
+
* @param fallbackIndex - Index to use if button not found in grid
|
|
16
|
+
* @returns Position and span information for the button
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const position = findButtonPosition(page, button, 0);
|
|
20
|
+
* console.log(`Button at ${position.x},${position.y} spans ${position.columnSpan}x${position.rowSpan}`);
|
|
21
|
+
*/
|
|
22
|
+
export function findButtonPosition(page, button, fallbackIndex) {
|
|
23
|
+
if (page.grid && page.grid.length > 0) {
|
|
24
|
+
// Search for button in grid layout and calculate span
|
|
25
|
+
for (let y = 0; y < page.grid.length; y++) {
|
|
26
|
+
for (let x = 0; x < page.grid[y].length; x++) {
|
|
27
|
+
const current = page.grid[y][x];
|
|
28
|
+
if (current && current.id === button.id) {
|
|
29
|
+
// Calculate span by checking how far the same button extends
|
|
30
|
+
let columnSpan = 1;
|
|
31
|
+
let rowSpan = 1;
|
|
32
|
+
// Check column span (rightward)
|
|
33
|
+
while (x + columnSpan < page.grid[y].length) {
|
|
34
|
+
const right = page.grid[y][x + columnSpan];
|
|
35
|
+
if (right && right.id === button.id) {
|
|
36
|
+
columnSpan++;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check row span (downward)
|
|
43
|
+
while (y + rowSpan < page.grid.length) {
|
|
44
|
+
const below = page.grid[y + rowSpan][x];
|
|
45
|
+
if (below && below.id === button.id) {
|
|
46
|
+
rowSpan++;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { x, y, columnSpan, rowSpan };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Fallback positioning
|
|
58
|
+
const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
|
|
59
|
+
return {
|
|
60
|
+
x: fallbackIndex % gridCols,
|
|
61
|
+
y: Math.floor(fallbackIndex / gridCols),
|
|
62
|
+
columnSpan: 1,
|
|
63
|
+
rowSpan: 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Calculate cell position key for Maps and Sets
|
|
68
|
+
*
|
|
69
|
+
* Creates a string key from X and Y coordinates for use as a Map key or Set entry.
|
|
70
|
+
*
|
|
71
|
+
* @param x - X coordinate
|
|
72
|
+
* @param y - Y coordinate
|
|
73
|
+
* @returns String key in format "x,y"
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const key = cellPositionKey(5, 3);
|
|
77
|
+
* console.log(key); // "5,3"
|
|
78
|
+
*/
|
|
79
|
+
export function cellPositionKey(x, y) {
|
|
80
|
+
return `${x},${y}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse cell position key
|
|
84
|
+
*
|
|
85
|
+
* Extracts X and Y coordinates from a position key string.
|
|
86
|
+
*
|
|
87
|
+
* @param key - Position key in format "x,y"
|
|
88
|
+
* @returns Object with x and y properties
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const pos = parseCellPositionKey("5,3");
|
|
92
|
+
* console.log(pos); // { x: 5, y: 3 }
|
|
93
|
+
*/
|
|
94
|
+
export function parseCellPositionKey(key) {
|
|
95
|
+
const [x, y] = key.split(',').map(Number);
|
|
96
|
+
return { x, y };
|
|
97
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid3 Grid Calculations
|
|
3
|
+
*
|
|
4
|
+
* Utilities for calculating grid dimensions and definitions
|
|
5
|
+
* based on page layout and button count.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Calculate column definitions based on page layout
|
|
9
|
+
*
|
|
10
|
+
* Analyzes the page's grid structure to determine the number of columns.
|
|
11
|
+
* If no grid exists, estimates from button count.
|
|
12
|
+
*
|
|
13
|
+
* @param page - The AAC page to analyze
|
|
14
|
+
* @returns Column definitions object for Grid 3 XML
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const columns = calculateColumnDefinitions(page);
|
|
18
|
+
* // Returns: { ColumnDefinition: [{}, {}, {}, {}] } for 4 columns
|
|
19
|
+
*/
|
|
20
|
+
export function calculateColumnDefinitions(page) {
|
|
21
|
+
let maxCols = 4; // Default minimum
|
|
22
|
+
if (page.grid && page.grid.length > 0) {
|
|
23
|
+
maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Fallback: estimate from button count
|
|
27
|
+
maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
ColumnDefinition: Array(maxCols).fill({}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Calculate row definitions based on page layout
|
|
35
|
+
*
|
|
36
|
+
* Analyzes the page's grid structure to determine the number of rows.
|
|
37
|
+
* If no grid exists, estimates from button count.
|
|
38
|
+
*
|
|
39
|
+
* @param page - The AAC page to analyze
|
|
40
|
+
* @param addWorkspaceOffset - Whether to add 1 row for workspace (default: false)
|
|
41
|
+
* @returns Row definitions object for Grid 3 XML
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const rows = calculateRowDefinitions(page, false);
|
|
45
|
+
* // Returns: { RowDefinition: [{}, {}, {}, {}] } for 4 rows
|
|
46
|
+
*/
|
|
47
|
+
export function calculateRowDefinitions(page, addWorkspaceOffset = false) {
|
|
48
|
+
let maxRows = 4; // Default minimum
|
|
49
|
+
const offset = addWorkspaceOffset ? 1 : 0;
|
|
50
|
+
if (page.grid && page.grid.length > 0) {
|
|
51
|
+
maxRows = Math.max(maxRows, page.grid.length + offset);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Fallback: estimate from button count
|
|
55
|
+
const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
|
|
56
|
+
maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
RowDefinition: Array(maxRows).fill({}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -149,7 +149,10 @@ export function getCommonDocumentsPath() {
|
|
|
149
149
|
// Query registry for Common Documents path
|
|
150
150
|
const child_process = getNodeRequire()('child_process');
|
|
151
151
|
const command = 'REG.EXE QUERY "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" /V "Common Documents"';
|
|
152
|
-
const output = child_process.execSync(command, {
|
|
152
|
+
const output = child_process.execSync(command, {
|
|
153
|
+
encoding: 'utf-8',
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
});
|
|
153
156
|
// Parse the output to extract the path
|
|
154
157
|
const match = output.match(/Common Documents\s+REG_SZ\s+(.+)/);
|
|
155
158
|
if (match && match[1]) {
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
* Tags that Grid 3 requires in full opening/closing format instead of self-closing
|
|
10
|
+
* Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
11
|
+
*/
|
|
12
|
+
const TAGS_NEEDING_EXPANSION = ['AudioDescription', 'VideoDescription'];
|
|
13
|
+
/**
|
|
14
|
+
* Format XML string to match Grid 3's requirements
|
|
15
|
+
*
|
|
16
|
+
* Grid 3 requires specific formatting:
|
|
17
|
+
* - Windows line endings (\r\n)
|
|
18
|
+
* - Space before /> in self-closing tags: <Element /> not <Element/>
|
|
19
|
+
* - Plain apostrophes instead of '
|
|
20
|
+
* - Specific tags expanded to full opening/closing format
|
|
21
|
+
* - CDATA for empty/whitespace captions and <r> tags
|
|
22
|
+
*
|
|
23
|
+
* @param xml - The XML string to format
|
|
24
|
+
* @returns Formatted XML string compatible with Grid 3
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const formatted = formatGrid3Xml('<Grid><Cell X="0"/></Grid>');
|
|
28
|
+
* // Returns: '<Grid>\r\n<Cell X="0" />\r\n</Grid>'
|
|
29
|
+
*/
|
|
30
|
+
export function formatGrid3Xml(xml) {
|
|
31
|
+
let formatted = xml;
|
|
32
|
+
// Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
|
|
33
|
+
formatted = formatted.replace(/\n/g, '\r\n');
|
|
34
|
+
// Add space before /> in self-closing tags to match Grid 3's expected format
|
|
35
|
+
// Grid 3 original files use <Element /> not <Element/>
|
|
36
|
+
formatted = formatted.replace(/<(\w+)([^>]*)\/>/g, '<$1$2 />');
|
|
37
|
+
// Decode XML entities back to plain text to match Grid 3's expected format
|
|
38
|
+
// Grid 3 expects plain apostrophes, not '
|
|
39
|
+
formatted = formatted.replace(/'/g, "'");
|
|
40
|
+
formatted = formatted.replace(/"/g, '"');
|
|
41
|
+
formatted = formatted.replace(/</g, '<');
|
|
42
|
+
formatted = formatted.replace(/>/g, '>');
|
|
43
|
+
// Expand only specific self-closing tags that Grid 3 requires in full opening/closing format
|
|
44
|
+
// This must be done AFTER adding spaces, so we need to match the format with spaces
|
|
45
|
+
for (const tag of TAGS_NEEDING_EXPANSION) {
|
|
46
|
+
formatted = formatted.replace(new RegExp(`<${tag}(\\s+[^>]*)? />`, 'g'), `<${tag}$1></${tag}>`);
|
|
47
|
+
}
|
|
48
|
+
return formatted;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Format empty/whitespace captions with CDATA for Grid 3 compatibility
|
|
52
|
+
*
|
|
53
|
+
* Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text.
|
|
54
|
+
* Also handles <r> tags which need CDATA for spaces to prevent stripping.
|
|
55
|
+
*
|
|
56
|
+
* @param xml - The XML string to format
|
|
57
|
+
* @returns XML string with CDATA-wrapped empty content
|
|
58
|
+
*/
|
|
59
|
+
export function formatEmptyCaptionsWithCdata(xml) {
|
|
60
|
+
let formatted = xml;
|
|
61
|
+
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
62
|
+
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
63
|
+
formatted = formatted.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
64
|
+
formatted = formatted.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
65
|
+
formatted = formatted.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
66
|
+
// Preserve CDATA in <r> tags for text parameters
|
|
67
|
+
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
68
|
+
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
69
|
+
formatted = formatted.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
70
|
+
formatted = formatted.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
71
|
+
return formatted;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Complete XML formatting for Grid 3 compatibility
|
|
75
|
+
* Combines all Grid 3 XML formatting requirements
|
|
76
|
+
*
|
|
77
|
+
* @param xml - The XML string to format
|
|
78
|
+
* @returns Fully formatted XML string compatible with Grid 3
|
|
79
|
+
*/
|
|
80
|
+
export function formatGrid3XmlComplete(xml) {
|
|
81
|
+
let formatted = formatGrid3Xml(xml);
|
|
82
|
+
formatted = formatEmptyCaptionsWithCdata(formatted);
|
|
83
|
+
return formatted;
|
|
84
|
+
}
|
|
@@ -5,6 +5,9 @@ import { resolveGrid3CellImage } from './gridset/resolver';
|
|
|
5
5
|
import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
|
|
6
6
|
import { getZipEntriesFromAdapter, resolveGridsetPassword, } from './gridset/password';
|
|
7
7
|
import { decryptGridsetEntry } from './gridset/crypto';
|
|
8
|
+
import { formatGrid3XmlComplete } from './gridset/xmlFormatter';
|
|
9
|
+
import { calculateColumnDefinitions as calcColumnDefs, calculateRowDefinitions as calcRowDefs, } from './gridset/gridCalculations';
|
|
10
|
+
import { findButtonPosition as findButtonPos } from './gridset/cellHelpers';
|
|
8
11
|
import { GridsetValidator } from '../validation/gridsetValidator';
|
|
9
12
|
// New imports for enhanced Grid 3 support
|
|
10
13
|
import { detectPluginCellType, Grid3CellType } from './gridset/pluginTypes';
|
|
@@ -2195,33 +2198,11 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2195
2198
|
}
|
|
2196
2199
|
// Helper method to calculate column definitions based on page layout
|
|
2197
2200
|
calculateColumnDefinitions(page) {
|
|
2198
|
-
|
|
2199
|
-
if (page.grid && page.grid.length > 0) {
|
|
2200
|
-
maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
|
|
2201
|
-
}
|
|
2202
|
-
else {
|
|
2203
|
-
// Fallback: estimate from button count
|
|
2204
|
-
maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
|
|
2205
|
-
}
|
|
2206
|
-
return {
|
|
2207
|
-
ColumnDefinition: Array(maxCols).fill({}),
|
|
2208
|
-
};
|
|
2201
|
+
return calcColumnDefs(page);
|
|
2209
2202
|
}
|
|
2210
2203
|
// Helper method to calculate row definitions based on page layout
|
|
2211
2204
|
calculateRowDefinitions(page, addWorkspaceOffset = false) {
|
|
2212
|
-
|
|
2213
|
-
const offset = addWorkspaceOffset ? 1 : 0;
|
|
2214
|
-
if (page.grid && page.grid.length > 0) {
|
|
2215
|
-
maxRows = Math.max(maxRows, page.grid.length + offset);
|
|
2216
|
-
}
|
|
2217
|
-
else {
|
|
2218
|
-
// Fallback: estimate from button count
|
|
2219
|
-
const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
|
|
2220
|
-
maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset;
|
|
2221
|
-
}
|
|
2222
|
-
return {
|
|
2223
|
-
RowDefinition: Array(maxRows).fill({}),
|
|
2224
|
-
};
|
|
2205
|
+
return calcRowDefs(page, addWorkspaceOffset);
|
|
2225
2206
|
}
|
|
2226
2207
|
/**
|
|
2227
2208
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
@@ -2367,6 +2348,9 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2367
2348
|
}
|
|
2368
2349
|
}
|
|
2369
2350
|
}
|
|
2351
|
+
// DO NOT create new cells - the system should only modify existing content
|
|
2352
|
+
// Personalized vocabulary is added to WordList cells via the WordList.Items array
|
|
2353
|
+
// Creating new cells would corrupt the grid structure
|
|
2370
2354
|
// Update the page's WordList with new words from modified buttons
|
|
2371
2355
|
// Collect all modified buttons that should be added to the WordList
|
|
2372
2356
|
const newWordListItems = [];
|
|
@@ -2379,9 +2363,18 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2379
2363
|
? [originalGrid.Grid.Cells.Cell]
|
|
2380
2364
|
: [];
|
|
2381
2365
|
const cell = cellArray.find((c) => {
|
|
2382
|
-
const
|
|
2383
|
-
|
|
2384
|
-
|
|
2366
|
+
const cellY = parseInt(String(c['@_Y'] || c['@_Row'] || '0'), 10);
|
|
2367
|
+
// Check Y position first
|
|
2368
|
+
if (cellY !== pos.y) {
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
const cellX = c['@_X'] !== undefined ? parseInt(String(c['@_X']), 10) : undefined;
|
|
2372
|
+
// If cell has no X attribute (full-width cell), it matches any button at this Y
|
|
2373
|
+
if (cellX === undefined) {
|
|
2374
|
+
return true;
|
|
2375
|
+
}
|
|
2376
|
+
// Otherwise, check exact X match
|
|
2377
|
+
return cellX === pos.x;
|
|
2385
2378
|
});
|
|
2386
2379
|
if (cell) {
|
|
2387
2380
|
const contentType = cell.Content?.ContentType || cell.Content?.contentType;
|
|
@@ -2390,11 +2383,14 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2390
2383
|
// Note: Prediction cells are already skipped earlier, so they won't reach here
|
|
2391
2384
|
if (isWordListCell) {
|
|
2392
2385
|
// Add this button to the WordList with proper Grid 3 format
|
|
2393
|
-
// Format: <Text><s><r>label</r></s></Text>
|
|
2386
|
+
// Format: <Text><p><s><r>label</r></s></p></Text>
|
|
2387
|
+
// Note: <p> wrapper is required by Grid 3's WordList format
|
|
2394
2388
|
newWordListItems.push({
|
|
2395
2389
|
Text: {
|
|
2396
|
-
|
|
2397
|
-
|
|
2390
|
+
p: {
|
|
2391
|
+
s: {
|
|
2392
|
+
r: button.label,
|
|
2393
|
+
},
|
|
2398
2394
|
},
|
|
2399
2395
|
},
|
|
2400
2396
|
Image: '', // No image for user-added words
|
|
@@ -2421,23 +2417,9 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2421
2417
|
originalGrid.Grid.WordList.Items.WordListItem = allItems;
|
|
2422
2418
|
}
|
|
2423
2419
|
}
|
|
2424
|
-
// Build the updated grid XML and
|
|
2420
|
+
// Build the updated grid XML and format for Grid 3 compatibility
|
|
2425
2421
|
let builtXml = gridBuilder.build(originalGrid);
|
|
2426
|
-
|
|
2427
|
-
builtXml = builtXml.replace(/\n/g, '\r\n');
|
|
2428
|
-
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2429
|
-
// Grid 3 cannot parse <AudioDescription /> - it requires <AudioDescription></AudioDescription>
|
|
2430
|
-
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2431
|
-
// Convert empty/whitespace captions to CDATA format for Grid 3 compatibility
|
|
2432
|
-
// Grid 3 requires <![CDATA[ ]]> for empty captions, not plain text
|
|
2433
|
-
builtXml = builtXml.replace(/<Caption><\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2434
|
-
builtXml = builtXml.replace(/<Caption> <\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2435
|
-
builtXml = builtXml.replace(/<Caption> {2}<\/Caption>/g, '<Caption><![CDATA[ ]]></Caption>');
|
|
2436
|
-
// Preserve CDATA in <r> tags for text parameters
|
|
2437
|
-
// Spaces in <r> tags must use CDATA or they get stripped during rendering
|
|
2438
|
-
// e.g., <r> </r> becomes <r><![CDATA[ ]]></r>
|
|
2439
|
-
builtXml = builtXml.replace(/<r> <\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2440
|
-
builtXml = builtXml.replace(/<r> {2}<\/r>/g, '<r><![CDATA[ ]]></r>');
|
|
2422
|
+
builtXml = formatGrid3XmlComplete(builtXml);
|
|
2441
2423
|
newGridFiles.set(gridPath, builtXml);
|
|
2442
2424
|
}
|
|
2443
2425
|
// Copy all files from original zip, replacing modified grid files
|
|
@@ -2506,57 +2488,14 @@ class GridsetProcessor extends BaseProcessor {
|
|
|
2506
2488
|
// Preserve Grid 3 XML formatting requirements
|
|
2507
2489
|
suppressBooleanAttributes: false,
|
|
2508
2490
|
});
|
|
2509
|
-
// Build the grid XML and
|
|
2491
|
+
// Build the grid XML and format for Grid 3 compatibility
|
|
2510
2492
|
let builtXml = gridBuilder.build(gridData);
|
|
2511
|
-
builtXml = builtXml
|
|
2512
|
-
// Expand self-closing tags to full opening/closing tags for Grid 3 compatibility
|
|
2513
|
-
builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2></$1>');
|
|
2493
|
+
builtXml = formatGrid3XmlComplete(builtXml);
|
|
2514
2494
|
return builtXml;
|
|
2515
2495
|
}
|
|
2516
2496
|
// Helper method to find button position with span information
|
|
2517
2497
|
findButtonPosition(page, button, fallbackIndex) {
|
|
2518
|
-
|
|
2519
|
-
// Search for button in grid layout and calculate span
|
|
2520
|
-
for (let y = 0; y < page.grid.length; y++) {
|
|
2521
|
-
for (let x = 0; x < page.grid[y].length; x++) {
|
|
2522
|
-
const current = page.grid[y][x];
|
|
2523
|
-
if (current && current.id === button.id) {
|
|
2524
|
-
// Calculate span by checking how far the same button extends
|
|
2525
|
-
let columnSpan = 1;
|
|
2526
|
-
let rowSpan = 1;
|
|
2527
|
-
// Check column span (rightward)
|
|
2528
|
-
while (x + columnSpan < page.grid[y].length) {
|
|
2529
|
-
const right = page.grid[y][x + columnSpan];
|
|
2530
|
-
if (right && right.id === button.id) {
|
|
2531
|
-
columnSpan++;
|
|
2532
|
-
}
|
|
2533
|
-
else {
|
|
2534
|
-
break;
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
// Check row span (downward)
|
|
2538
|
-
while (y + rowSpan < page.grid.length) {
|
|
2539
|
-
const below = page.grid[y + rowSpan][x];
|
|
2540
|
-
if (below && below.id === button.id) {
|
|
2541
|
-
rowSpan++;
|
|
2542
|
-
}
|
|
2543
|
-
else {
|
|
2544
|
-
break;
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
return { x, y, columnSpan, rowSpan };
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
// Fallback positioning
|
|
2553
|
-
const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
|
|
2554
|
-
return {
|
|
2555
|
-
x: fallbackIndex % gridCols,
|
|
2556
|
-
y: Math.floor(fallbackIndex / gridCols),
|
|
2557
|
-
columnSpan: 1,
|
|
2558
|
-
rowSpan: 1,
|
|
2559
|
-
};
|
|
2498
|
+
return findButtonPos(page, button, fallbackIndex);
|
|
2560
2499
|
}
|
|
2561
2500
|
/**
|
|
2562
2501
|
* Extract strings with metadata for aac-tools-platform compatibility
|