@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.
@@ -80,7 +80,7 @@ class ObfProcessor extends BaseProcessor {
80
80
  // Images are typically stored in an 'images' folder or root
81
81
  const possiblePaths = [
82
82
  imageData.path, // Explicit path if provided
83
- `images/${imageData.filename || imageId}`, // Standard images folder
83
+ `images/${imageData.path || imageId}`, // Standard images folder
84
84
  imageData.id, // Just the ID
85
85
  ].filter(Boolean);
86
86
  for (const imagePath of possiblePaths) {
@@ -126,14 +126,18 @@ class ObfProcessor extends BaseProcessor {
126
126
  return 'image/png';
127
127
  }
128
128
  }
129
- async processBoard(boardData, _boardPath, isZipEntry) {
129
+ getPageFilename(id, metadata) {
130
+ if (metadata._obfPagePaths && id in metadata._obfPagePaths)
131
+ return metadata._obfPagePaths[id];
132
+ if (id.endsWith('.obf'))
133
+ return id;
134
+ return `${id}.obf`;
135
+ }
136
+ async processBoard(boardData, _boardPath) {
130
137
  const sourceButtons = boardData.buttons || [];
131
138
  // Calculate page ID first (used to make button IDs unique)
132
- const pageId = isZipEntry
133
- ? _boardPath // Zip entry - use filename to match navigation paths
134
- : boardData?.id
135
- ? String(boardData.id)
136
- : _boardPath?.split(/[/\\]/).pop() || '';
139
+ const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
140
+ const images = boardData.images;
137
141
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
138
142
  const semanticAction = btn.load_board
139
143
  ? {
@@ -157,11 +161,16 @@ class ObfProcessor extends BaseProcessor {
157
161
  // Resolve image if image_id is present
158
162
  let resolvedImage;
159
163
  let imageBuffer;
160
- if (btn.image_id && boardData.images) {
161
- resolvedImage =
162
- (await this.extractImageAsDataUrl(btn.image_id, boardData.images)) || undefined;
163
- imageBuffer =
164
- (await this.extractImageAsBuffer(btn.image_id, boardData.images)) || undefined;
164
+ if (btn.image_id && images) {
165
+ resolvedImage = (await this.extractImageAsDataUrl(btn.image_id, images)) || undefined;
166
+ imageBuffer = (await this.extractImageAsBuffer(btn.image_id, images)) || undefined;
167
+ // save image data
168
+ if (images) {
169
+ const imageIndex = images?.findIndex((img) => img.id === btn.image_id);
170
+ if (imageIndex !== -1) {
171
+ images[imageIndex].data = resolvedImage;
172
+ }
173
+ }
165
174
  }
166
175
  // Build parameters object for Grid3 export compatibility
167
176
  const buttonParameters = {};
@@ -198,7 +207,7 @@ class ObfProcessor extends BaseProcessor {
198
207
  parentId: null,
199
208
  locale: boardData.locale,
200
209
  descriptionHtml: boardData.description_html,
201
- images: boardData.images,
210
+ images,
202
211
  sounds: boardData.sounds,
203
212
  });
204
213
  // Process grid layout if available
@@ -288,7 +297,7 @@ class ObfProcessor extends BaseProcessor {
288
297
  return texts;
289
298
  }
290
299
  async loadIntoTree(filePathOrBuffer) {
291
- const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
300
+ const { readBinaryFromInput, readTextFromInput, listDir, join, isDirectory } = this.options.fileAdapter;
292
301
  // Detailed logging for debugging input
293
302
  const bufferLength = typeof filePathOrBuffer === 'string'
294
303
  ? null
@@ -330,7 +339,7 @@ class ObfProcessor extends BaseProcessor {
330
339
  const boardData = await tryParseObfJson(content);
331
340
  if (boardData) {
332
341
  console.log('[OBF] Detected .obf file, parsed as JSON');
333
- const page = await this.processBoard(boardData, filePathOrBuffer, false);
342
+ const page = await this.processBoard(boardData, filePathOrBuffer);
334
343
  tree.addPage(page);
335
344
  // Set metadata from root board
336
345
  tree.metadata.format = 'obf';
@@ -354,22 +363,30 @@ class ObfProcessor extends BaseProcessor {
354
363
  throw err;
355
364
  }
356
365
  }
357
- // Detect likely zip signature first
358
- async function isLikelyZip(input) {
359
- if (typeof input === 'string') {
360
- const lowered = input.toLowerCase();
361
- return lowered.endsWith('.zip') || lowered.endsWith('.obz');
366
+ // Determine if input is ZIP, directory, or OBF JSON string/buffer
367
+ let fileType = 'obf';
368
+ if (typeof filePathOrBuffer !== 'string') {
369
+ const bytes = await readBinaryFromInput(filePathOrBuffer);
370
+ if (bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b)
371
+ fileType = 'zip';
372
+ }
373
+ else {
374
+ if (await isDirectory(filePathOrBuffer)) {
375
+ fileType = 'dir';
376
+ }
377
+ else {
378
+ const lowered = filePathOrBuffer.toLowerCase();
379
+ if (lowered.endsWith('.zip') || lowered.endsWith('.obz'))
380
+ fileType = 'zip';
362
381
  }
363
- const bytes = await readBinaryFromInput(input);
364
- return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
365
382
  }
366
383
  // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
367
- if (!(await isLikelyZip(filePathOrBuffer))) {
384
+ if (fileType === 'obf') {
368
385
  const asJson = await tryParseObfJson(filePathOrBuffer);
369
386
  if (!asJson)
370
387
  throw new Error('Invalid OBF content: not JSON and not ZIP');
371
388
  console.log('[OBF] Detected buffer/string as OBF JSON');
372
- const page = await this.processBoard(asJson, '[bufferOrString]', false);
389
+ const page = await this.processBoard(asJson, '[bufferOrString]');
373
390
  tree.addPage(page);
374
391
  // Set metadata from root board
375
392
  tree.metadata.format = 'obf';
@@ -385,18 +402,31 @@ class ObfProcessor extends BaseProcessor {
385
402
  tree.rootId = page.id;
386
403
  return tree;
387
404
  }
388
- try {
389
- this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
390
- }
391
- catch (err) {
392
- console.error('[OBF] Error loading ZIP:', err);
393
- throw err;
405
+ this.zipFile = {
406
+ readFile: async (name) => {
407
+ return await readBinaryFromInput(join(filePathOrBuffer, name));
408
+ },
409
+ listFiles: () => {
410
+ throw new Error('Not implemented for directory input');
411
+ },
412
+ writeFiles: () => {
413
+ throw new Error('Not implemented for directory input');
414
+ },
415
+ };
416
+ if (fileType === 'zip') {
417
+ try {
418
+ this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
419
+ }
420
+ catch (err) {
421
+ console.error('[OBF] Error loading ZIP:', err);
422
+ throw err;
423
+ }
394
424
  }
395
425
  // Store the ZIP file reference for image extraction
396
426
  this.imageCache.clear(); // Clear cache for new file
397
- console.log('[OBF] Detected zip archive, extracting .obf files');
427
+ console.log('[OBF] Detected zip archive or directory, extracting .obf files');
398
428
  // List manifest and OBF files
399
- const filesInZip = this.zipFile.listFiles();
429
+ const filesInZip = fileType === 'zip' ? this.zipFile.listFiles() : await listDir(filePathOrBuffer);
400
430
  const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
401
431
  let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
402
432
  // Attempt to read manifest
@@ -430,7 +460,7 @@ class ObfProcessor extends BaseProcessor {
430
460
  const content = await this.zipFile.readFile(entryName);
431
461
  const boardData = await tryParseObfJson(decodeText(content));
432
462
  if (boardData) {
433
- const page = await this.processBoard(boardData, entryName, true);
463
+ const page = await this.processBoard(boardData, entryName);
434
464
  tree.addPage(page);
435
465
  // Set metadata if not already set (use first board as reference)
436
466
  if (!tree.metadata.format) {
@@ -439,12 +469,16 @@ class ObfProcessor extends BaseProcessor {
439
469
  tree.metadata.description = boardData.description_html;
440
470
  tree.metadata.locale = boardData.locale;
441
471
  tree.metadata.id = boardData.id;
472
+ tree.metadata._obfPagePaths = { [page.id]: entryName };
442
473
  if (boardData.url)
443
474
  tree.metadata.url = boardData.url;
444
475
  if (boardData.locale)
445
476
  tree.metadata.languages = [boardData.locale];
446
477
  tree.rootId = page.id;
447
478
  }
479
+ else {
480
+ tree.metadata._obfPagePaths[page.id] = entryName;
481
+ }
448
482
  }
449
483
  else {
450
484
  console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
@@ -497,11 +531,18 @@ class ObfProcessor extends BaseProcessor {
497
531
  }
498
532
  return { rows: totalRows, columns: totalColumns, order, buttonPositions };
499
533
  }
500
- createObfBoardFromPage(page, fallbackName, metadata) {
534
+ createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
501
535
  const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
502
536
  const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
503
537
  ? metadata.name
504
538
  : page.name || fallbackName;
539
+ let images = Array.isArray(page.images) ? page.images : [];
540
+ if (!embedData) {
541
+ images = images.map((image) => {
542
+ delete image.data;
543
+ return image;
544
+ });
545
+ }
505
546
  return {
506
547
  format: OBF_FORMAT_VERSION,
507
548
  id: page.id,
@@ -538,7 +579,7 @@ class ObfProcessor extends BaseProcessor {
538
579
  hidden: button.visibility === 'Hidden' || false,
539
580
  };
540
581
  }),
541
- images: Array.isArray(page.images) ? page.images : [],
582
+ images,
542
583
  sounds: Array.isArray(page.sounds) ? page.sounds : [],
543
584
  };
544
585
  }
@@ -575,23 +616,22 @@ class ObfProcessor extends BaseProcessor {
575
616
  await this.saveFromTree(tree, outputPath);
576
617
  return await readBinaryFromInput(outputPath);
577
618
  }
578
- async saveFromTree(tree, outputPath) {
579
- const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
619
+ async saveFromTree(tree, outputPath, embedData = false) {
620
+ const { writeTextToPath, writeBinaryToPath, pathExists, mkDir, join } = this.options.fileAdapter;
580
621
  if (outputPath.endsWith('.obf')) {
581
622
  // Save as single OBF JSON file
582
623
  const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
583
624
  if (!rootPage) {
584
625
  throw new Error('No pages to save');
585
626
  }
586
- const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
627
+ const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata, embedData);
587
628
  await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
588
629
  }
589
630
  else {
590
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
591
631
  const files = Object.values(tree.pages).map((page) => {
592
- const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
632
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata, embedData);
593
633
  const obfContent = JSON.stringify(obfBoard, null, 2);
594
- const name = getPageFilename(page.id);
634
+ const name = this.getPageFilename(page.id, tree.metadata);
595
635
  return {
596
636
  name,
597
637
  data: new TextEncoder().encode(obfContent),
@@ -601,7 +641,10 @@ class ObfProcessor extends BaseProcessor {
601
641
  format: OBF_FORMAT_VERSION,
602
642
  root: tree.metadata.defaultHomePageId,
603
643
  paths: {
604
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
644
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
645
+ id,
646
+ this.getPageFilename(page.id, tree.metadata),
647
+ ])),
605
648
  images: {}, //TODO Add support for saving images as files
606
649
  sounds: {}, //TODO Add support for saving sounds as files
607
650
  },
@@ -610,10 +653,22 @@ class ObfProcessor extends BaseProcessor {
610
653
  name: 'manifest.json',
611
654
  data: new TextEncoder().encode(JSON.stringify(manifest)),
612
655
  });
613
- const fileExists = await pathExists(outputPath);
614
- this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
615
- const zipData = await this.zipFile.writeFiles(files);
616
- await writeBinaryToPath(outputPath, zipData);
656
+ if (outputPath.endsWith('.obz') || outputPath.endsWith('.zip')) {
657
+ console.log('[OBF] Saving to ZIP file:', outputPath);
658
+ const fileExists = await pathExists(outputPath);
659
+ this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
660
+ const zipData = await this.zipFile.writeFiles(files);
661
+ await writeBinaryToPath(outputPath, zipData);
662
+ }
663
+ else {
664
+ console.log('[OBF] Saving to directory:', outputPath);
665
+ if (!(await pathExists(outputPath)))
666
+ await mkDir(outputPath);
667
+ for (const file of files) {
668
+ const filePath = join(outputPath, file.name);
669
+ await writeBinaryToPath(filePath, file.data);
670
+ }
671
+ }
617
672
  }
618
673
  }
619
674
  /**
@@ -640,13 +695,12 @@ class ObfProcessor extends BaseProcessor {
640
695
  const AdmZip = (await import('adm-zip')).default;
641
696
  const originalZip = new AdmZip(originalPath);
642
697
  const outputZip = new AdmZip();
643
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
644
698
  // Track which .obf files we're modifying
645
699
  const modifiedObfFiles = new Set();
646
700
  // Generate new .obf files for pages in the tree
647
701
  const newObfFiles = new Map();
648
702
  for (const page of Object.values(tree.pages)) {
649
- const obfFilename = getPageFilename(page.id);
703
+ const obfFilename = this.getPageFilename(page.id, tree.metadata);
650
704
  modifiedObfFiles.add(obfFilename);
651
705
  const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
652
706
  const obfContent = JSON.stringify(obfBoard, null, 2);
@@ -659,7 +713,10 @@ class ObfProcessor extends BaseProcessor {
659
713
  format: OBF_FORMAT_VERSION,
660
714
  root: tree.metadata.defaultHomePageId,
661
715
  paths: {
662
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
716
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
717
+ id,
718
+ this.getPageFilename(page.id, tree.metadata),
719
+ ])),
663
720
  images: {},
664
721
  sounds: {},
665
722
  },
@@ -1,3 +1,2 @@
1
1
  export { MorphologyEngine } from './engine';
2
- export { Grid3VerbsParser } from './grid3VerbsParser';
3
2
  export { WordFormGenerator } from './wordFormGenerator';
@@ -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 || '',