@willwade/aac-processors 0.2.10 → 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 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
- 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...
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, { encoding: 'utf-8', windowsHide: true });
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 &apos;
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 &apos;
39
+ formatted = formatted.replace(/&apos;/g, "'");
40
+ formatted = formatted.replace(/&quot;/g, '"');
41
+ formatted = formatted.replace(/&lt;/g, '<');
42
+ formatted = formatted.replace(/&gt;/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
- let maxCols = 4; // Default minimum
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
- let maxRows = 4; // Default minimum
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 cellX = parseInt(String(c['@_X'] || '0'), 10);
2383
- const cellY = parseInt(String(c['@_Y'] || '0'), 10);
2384
- return cellX === pos.x && cellY === pos.y;
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
- s: {
2397
- r: button.label,
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 convert to Windows line endings
2420
+ // Build the updated grid XML and format for Grid 3 compatibility
2425
2421
  let builtXml = gridBuilder.build(originalGrid);
2426
- // Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
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 convert to Windows line endings for Grid 3 compatibility
2491
+ // Build the grid XML and format for Grid 3 compatibility
2510
2492
  let builtXml = gridBuilder.build(gridData);
2511
- builtXml = builtXml.replace(/\n/g, '\r\n');
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
- if (page.grid && page.grid.length > 0) {
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
@@ -80,7 +80,9 @@ export async function openSqliteDatabase(input, options = {}) {
80
80
  throw new Error('SQLite file paths are not supported in browser environments.');
81
81
  }
82
82
  const Database = getBetterSqlite3();
83
- const db = new Database(input, { readonly: options.readonly ?? true });
83
+ const db = new Database(input, {
84
+ readonly: options.readonly ?? true,
85
+ });
84
86
  return { db };
85
87
  }
86
88
  const data = await readBinaryFromInput(input);
@@ -93,7 +95,9 @@ export async function openSqliteDatabase(input, options = {}) {
93
95
  const dbPath = join(tempDir, 'input.sqlite');
94
96
  await writeBinaryToPath(dbPath, data);
95
97
  const Database = getBetterSqlite3();
96
- const db = new Database(dbPath, { readonly: options.readonly ?? true });
98
+ const db = new Database(dbPath, {
99
+ readonly: options.readonly ?? true,
100
+ });
97
101
  const cleanup = async () => {
98
102
  try {
99
103
  db.close();
@@ -0,0 +1,66 @@
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
+ import type { AACPage, AACButton } from '../../core/treeStructure';
8
+ /**
9
+ * Cell position with span information
10
+ */
11
+ export interface CellPosition {
12
+ /** X coordinate (column) */
13
+ x: number;
14
+ /** Y coordinate (row) */
15
+ y: number;
16
+ /** Number of columns the cell spans */
17
+ columnSpan: number;
18
+ /** Number of rows the cell spans */
19
+ rowSpan: number;
20
+ }
21
+ /**
22
+ * Find button position with span information
23
+ *
24
+ * Searches the page's grid layout for a button and calculates its position
25
+ * and span (how many columns/rows it occupies).
26
+ *
27
+ * @param page - The AAC page containing the button
28
+ * @param button - The button to locate
29
+ * @param fallbackIndex - Index to use if button not found in grid
30
+ * @returns Position and span information for the button
31
+ *
32
+ * @example
33
+ * const position = findButtonPosition(page, button, 0);
34
+ * console.log(`Button at ${position.x},${position.y} spans ${position.columnSpan}x${position.rowSpan}`);
35
+ */
36
+ export declare function findButtonPosition(page: AACPage, button: AACButton, fallbackIndex: number): CellPosition;
37
+ /**
38
+ * Calculate cell position key for Maps and Sets
39
+ *
40
+ * Creates a string key from X and Y coordinates for use as a Map key or Set entry.
41
+ *
42
+ * @param x - X coordinate
43
+ * @param y - Y coordinate
44
+ * @returns String key in format "x,y"
45
+ *
46
+ * @example
47
+ * const key = cellPositionKey(5, 3);
48
+ * console.log(key); // "5,3"
49
+ */
50
+ export declare function cellPositionKey(x: number, y: number): string;
51
+ /**
52
+ * Parse cell position key
53
+ *
54
+ * Extracts X and Y coordinates from a position key string.
55
+ *
56
+ * @param key - Position key in format "x,y"
57
+ * @returns Object with x and y properties
58
+ *
59
+ * @example
60
+ * const pos = parseCellPositionKey("5,3");
61
+ * console.log(pos); // { x: 5, y: 3 }
62
+ */
63
+ export declare function parseCellPositionKey(key: string): {
64
+ x: number;
65
+ y: number;
66
+ };
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Grid3 Cell Helpers
4
+ *
5
+ * Utilities for working with Grid 3 cells, including finding button positions
6
+ * and calculating cell spans in grid layouts.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.findButtonPosition = findButtonPosition;
10
+ exports.cellPositionKey = cellPositionKey;
11
+ exports.parseCellPositionKey = parseCellPositionKey;
12
+ /**
13
+ * Find button position with span information
14
+ *
15
+ * Searches the page's grid layout for a button and calculates its position
16
+ * and span (how many columns/rows it occupies).
17
+ *
18
+ * @param page - The AAC page containing the button
19
+ * @param button - The button to locate
20
+ * @param fallbackIndex - Index to use if button not found in grid
21
+ * @returns Position and span information for the button
22
+ *
23
+ * @example
24
+ * const position = findButtonPosition(page, button, 0);
25
+ * console.log(`Button at ${position.x},${position.y} spans ${position.columnSpan}x${position.rowSpan}`);
26
+ */
27
+ function findButtonPosition(page, button, fallbackIndex) {
28
+ if (page.grid && page.grid.length > 0) {
29
+ // Search for button in grid layout and calculate span
30
+ for (let y = 0; y < page.grid.length; y++) {
31
+ for (let x = 0; x < page.grid[y].length; x++) {
32
+ const current = page.grid[y][x];
33
+ if (current && current.id === button.id) {
34
+ // Calculate span by checking how far the same button extends
35
+ let columnSpan = 1;
36
+ let rowSpan = 1;
37
+ // Check column span (rightward)
38
+ while (x + columnSpan < page.grid[y].length) {
39
+ const right = page.grid[y][x + columnSpan];
40
+ if (right && right.id === button.id) {
41
+ columnSpan++;
42
+ }
43
+ else {
44
+ break;
45
+ }
46
+ }
47
+ // Check row span (downward)
48
+ while (y + rowSpan < page.grid.length) {
49
+ const below = page.grid[y + rowSpan][x];
50
+ if (below && below.id === button.id) {
51
+ rowSpan++;
52
+ }
53
+ else {
54
+ break;
55
+ }
56
+ }
57
+ return { x, y, columnSpan, rowSpan };
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // Fallback positioning
63
+ const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
64
+ return {
65
+ x: fallbackIndex % gridCols,
66
+ y: Math.floor(fallbackIndex / gridCols),
67
+ columnSpan: 1,
68
+ rowSpan: 1,
69
+ };
70
+ }
71
+ /**
72
+ * Calculate cell position key for Maps and Sets
73
+ *
74
+ * Creates a string key from X and Y coordinates for use as a Map key or Set entry.
75
+ *
76
+ * @param x - X coordinate
77
+ * @param y - Y coordinate
78
+ * @returns String key in format "x,y"
79
+ *
80
+ * @example
81
+ * const key = cellPositionKey(5, 3);
82
+ * console.log(key); // "5,3"
83
+ */
84
+ function cellPositionKey(x, y) {
85
+ return `${x},${y}`;
86
+ }
87
+ /**
88
+ * Parse cell position key
89
+ *
90
+ * Extracts X and Y coordinates from a position key string.
91
+ *
92
+ * @param key - Position key in format "x,y"
93
+ * @returns Object with x and y properties
94
+ *
95
+ * @example
96
+ * const pos = parseCellPositionKey("5,3");
97
+ * console.log(pos); // { x: 5, y: 3 }
98
+ */
99
+ function parseCellPositionKey(key) {
100
+ const [x, y] = key.split(',').map(Number);
101
+ return { x, y };
102
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Grid3 Grid Calculations
3
+ *
4
+ * Utilities for calculating grid dimensions and definitions
5
+ * based on page layout and button count.
6
+ */
7
+ import type { AACPage } from '../../core/treeStructure';
8
+ /**
9
+ * Grid definition structure for Grid 3 XML
10
+ */
11
+ export interface GridDefinitions {
12
+ ColumnDefinition: any[];
13
+ }
14
+ export interface RowDefinitions {
15
+ RowDefinition: any[];
16
+ }
17
+ /**
18
+ * Calculate column definitions based on page layout
19
+ *
20
+ * Analyzes the page's grid structure to determine the number of columns.
21
+ * If no grid exists, estimates from button count.
22
+ *
23
+ * @param page - The AAC page to analyze
24
+ * @returns Column definitions object for Grid 3 XML
25
+ *
26
+ * @example
27
+ * const columns = calculateColumnDefinitions(page);
28
+ * // Returns: { ColumnDefinition: [{}, {}, {}, {}] } for 4 columns
29
+ */
30
+ export declare function calculateColumnDefinitions(page: AACPage): GridDefinitions;
31
+ /**
32
+ * Calculate row definitions based on page layout
33
+ *
34
+ * Analyzes the page's grid structure to determine the number of rows.
35
+ * If no grid exists, estimates from button count.
36
+ *
37
+ * @param page - The AAC page to analyze
38
+ * @param addWorkspaceOffset - Whether to add 1 row for workspace (default: false)
39
+ * @returns Row definitions object for Grid 3 XML
40
+ *
41
+ * @example
42
+ * const rows = calculateRowDefinitions(page, false);
43
+ * // Returns: { RowDefinition: [{}, {}, {}, {}] } for 4 rows
44
+ */
45
+ export declare function calculateRowDefinitions(page: AACPage, addWorkspaceOffset?: boolean): RowDefinitions;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Grid3 Grid Calculations
4
+ *
5
+ * Utilities for calculating grid dimensions and definitions
6
+ * based on page layout and button count.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.calculateColumnDefinitions = calculateColumnDefinitions;
10
+ exports.calculateRowDefinitions = calculateRowDefinitions;
11
+ /**
12
+ * Calculate column definitions based on page layout
13
+ *
14
+ * Analyzes the page's grid structure to determine the number of columns.
15
+ * If no grid exists, estimates from button count.
16
+ *
17
+ * @param page - The AAC page to analyze
18
+ * @returns Column definitions object for Grid 3 XML
19
+ *
20
+ * @example
21
+ * const columns = calculateColumnDefinitions(page);
22
+ * // Returns: { ColumnDefinition: [{}, {}, {}, {}] } for 4 columns
23
+ */
24
+ function calculateColumnDefinitions(page) {
25
+ let maxCols = 4; // Default minimum
26
+ if (page.grid && page.grid.length > 0) {
27
+ maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
28
+ }
29
+ else {
30
+ // Fallback: estimate from button count
31
+ maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
32
+ }
33
+ return {
34
+ ColumnDefinition: Array(maxCols).fill({}),
35
+ };
36
+ }
37
+ /**
38
+ * Calculate row definitions based on page layout
39
+ *
40
+ * Analyzes the page's grid structure to determine the number of rows.
41
+ * If no grid exists, estimates from button count.
42
+ *
43
+ * @param page - The AAC page to analyze
44
+ * @param addWorkspaceOffset - Whether to add 1 row for workspace (default: false)
45
+ * @returns Row definitions object for Grid 3 XML
46
+ *
47
+ * @example
48
+ * const rows = calculateRowDefinitions(page, false);
49
+ * // Returns: { RowDefinition: [{}, {}, {}, {}] } for 4 rows
50
+ */
51
+ function calculateRowDefinitions(page, addWorkspaceOffset = false) {
52
+ let maxRows = 4; // Default minimum
53
+ const offset = addWorkspaceOffset ? 1 : 0;
54
+ if (page.grid && page.grid.length > 0) {
55
+ maxRows = Math.max(maxRows, page.grid.length + offset);
56
+ }
57
+ else {
58
+ // Fallback: estimate from button count
59
+ const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
60
+ maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset;
61
+ }
62
+ return {
63
+ RowDefinition: Array(maxRows).fill({}),
64
+ };
65
+ }
@@ -167,7 +167,10 @@ function getCommonDocumentsPath() {
167
167
  // Query registry for Common Documents path
168
168
  const child_process = (0, io_1.getNodeRequire)()('child_process');
169
169
  const command = 'REG.EXE QUERY "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" /V "Common Documents"';
170
- const output = child_process.execSync(command, { encoding: 'utf-8', windowsHide: true });
170
+ const output = child_process.execSync(command, {
171
+ encoding: 'utf-8',
172
+ windowsHide: true,
173
+ });
171
174
  // Parse the output to extract the path
172
175
  const match = output.match(/Common Documents\s+REG_SZ\s+(.+)/);
173
176
  if (match && match[1]) {
@@ -69,9 +69,10 @@ function wordlistToXml(wordlist) {
69
69
  const items = wordlist.items.map((item) => ({
70
70
  WordListItem: {
71
71
  Text: {
72
- s: {
73
- '@_Image': item.image || '',
74
- r: item.text,
72
+ p: {
73
+ s: {
74
+ r: item.text,
75
+ },
75
76
  },
76
77
  },
77
78
  Image: item.image || '',
@@ -137,7 +138,7 @@ async function extractWordlists(gridsetBuffer, password = (0, password_1.resolve
137
138
  ? [itemsContainer.WordListItem]
138
139
  : [];
139
140
  const items = itemArray.map((item) => ({
140
- text: item.Text?.s?.r || item.text?.s?.r || '',
141
+ text: item.Text?.p?.s?.r || item.Text?.s?.r || item.text?.p?.s?.r || item.text?.s?.r || '',
141
142
  image: item.Image || item.image || undefined,
142
143
  partOfSpeech: item.PartOfSpeech || item.partOfSpeech || 'Unknown',
143
144
  }));
@@ -202,9 +203,10 @@ async function updateWordlist(gridsetBuffer, gridName, wordlist, password = (0,
202
203
  const items = wordlist.items.map((item) => ({
203
204
  WordListItem: {
204
205
  Text: {
205
- s: {
206
- '@_Image': item.image || '',
207
- r: item.text,
206
+ p: {
207
+ s: {
208
+ r: item.text,
209
+ },
208
210
  },
209
211
  },
210
212
  Image: item.image || '',
@@ -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 &apos;
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 &apos;
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 &apos;
44
+ formatted = formatted.replace(/&apos;/g, "'");
45
+ formatted = formatted.replace(/&quot;/g, '"');
46
+ formatted = formatted.replace(/&lt;/g, '<');
47
+ formatted = formatted.replace(/&gt;/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
- let maxCols = 4; // Default minimum
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
- let maxRows = 4; // Default minimum
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 cellX = parseInt(String(c['@_X'] || '0'), 10);
2409
- const cellY = parseInt(String(c['@_Y'] || '0'), 10);
2410
- return cellX === pos.x && cellY === pos.y;
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
- s: {
2423
- r: button.label,
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 convert to Windows line endings
2446
+ // Build the updated grid XML and format for Grid 3 compatibility
2451
2447
  let builtXml = gridBuilder.build(originalGrid);
2452
- // Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility
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 convert to Windows line endings for Grid 3 compatibility
2517
+ // Build the grid XML and format for Grid 3 compatibility
2536
2518
  let builtXml = gridBuilder.build(gridData);
2537
- builtXml = builtXml.replace(/\n/g, '\r\n');
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
- if (page.grid && page.grid.length > 0) {
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
@@ -85,7 +85,9 @@ async function openSqliteDatabase(input, options = {}) {
85
85
  throw new Error('SQLite file paths are not supported in browser environments.');
86
86
  }
87
87
  const Database = getBetterSqlite3();
88
- const db = new Database(input, { readonly: options.readonly ?? true });
88
+ const db = new Database(input, {
89
+ readonly: options.readonly ?? true,
90
+ });
89
91
  return { db };
90
92
  }
91
93
  const data = await readBinaryFromInput(input);
@@ -98,7 +100,9 @@ async function openSqliteDatabase(input, options = {}) {
98
100
  const dbPath = join(tempDir, 'input.sqlite');
99
101
  await writeBinaryToPath(dbPath, data);
100
102
  const Database = getBetterSqlite3();
101
- const db = new Database(dbPath, { readonly: options.readonly ?? true });
103
+ const db = new Database(dbPath, {
104
+ readonly: options.readonly ?? true,
105
+ });
102
106
  const cleanup = async () => {
103
107
  try {
104
108
  db.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
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",