@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
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
133
|
-
|
|
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 &&
|
|
161
|
-
resolvedImage =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 (
|
|
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]'
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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
|
|
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]) => [
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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]) => [
|
|
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
|
},
|
|
@@ -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, {
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
p: {
|
|
207
|
+
s: {
|
|
208
|
+
r: item.text,
|
|
209
|
+
},
|
|
208
210
|
},
|
|
209
211
|
},
|
|
210
212
|
Image: item.image || '',
|