@willwade/aac-processors 0.0.8 โ†’ 0.0.10

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.
Files changed (58) hide show
  1. package/README.md +116 -11
  2. package/dist/cli/index.js +87 -0
  3. package/dist/core/analyze.js +1 -0
  4. package/dist/core/baseProcessor.d.ts +3 -0
  5. package/dist/core/fileProcessor.js +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +3 -0
  8. package/dist/optional/symbolTools.js +4 -2
  9. package/dist/processors/gridset/helpers.d.ts +3 -1
  10. package/dist/processors/gridset/helpers.js +24 -5
  11. package/dist/processors/gridset/password.d.ts +11 -0
  12. package/dist/processors/gridset/password.js +37 -0
  13. package/dist/processors/gridset/resolver.d.ts +1 -1
  14. package/dist/processors/gridset/resolver.js +8 -4
  15. package/dist/processors/gridset/wordlistHelpers.d.ts +2 -2
  16. package/dist/processors/gridset/wordlistHelpers.js +7 -4
  17. package/dist/processors/gridsetProcessor.d.ts +15 -1
  18. package/dist/processors/gridsetProcessor.js +64 -22
  19. package/dist/processors/index.d.ts +1 -0
  20. package/dist/processors/index.js +5 -2
  21. package/dist/processors/obfProcessor.d.ts +7 -0
  22. package/dist/processors/obfProcessor.js +9 -0
  23. package/dist/processors/snap/helpers.d.ts +2 -0
  24. package/dist/processors/snap/helpers.js +2 -0
  25. package/dist/processors/snapProcessor.d.ts +7 -0
  26. package/dist/processors/snapProcessor.js +9 -0
  27. package/dist/processors/touchchatProcessor.d.ts +7 -0
  28. package/dist/processors/touchchatProcessor.js +9 -0
  29. package/dist/utilities/screenshotConverter.d.ts +69 -0
  30. package/dist/utilities/screenshotConverter.js +453 -0
  31. package/dist/validation/baseValidator.d.ts +80 -0
  32. package/dist/validation/baseValidator.js +160 -0
  33. package/dist/validation/gridsetValidator.d.ts +36 -0
  34. package/dist/validation/gridsetValidator.js +288 -0
  35. package/dist/validation/index.d.ts +13 -0
  36. package/dist/validation/index.js +69 -0
  37. package/dist/validation/obfValidator.d.ts +44 -0
  38. package/dist/validation/obfValidator.js +530 -0
  39. package/dist/validation/snapValidator.d.ts +33 -0
  40. package/dist/validation/snapValidator.js +237 -0
  41. package/dist/validation/touchChatValidator.d.ts +33 -0
  42. package/dist/validation/touchChatValidator.js +229 -0
  43. package/dist/validation/validationTypes.d.ts +64 -0
  44. package/dist/validation/validationTypes.js +15 -0
  45. package/examples/README.md +7 -0
  46. package/examples/demo.js +143 -0
  47. package/examples/obf/aboutme.json +376 -0
  48. package/examples/obf/array.json +6 -0
  49. package/examples/obf/hash.json +4 -0
  50. package/examples/obf/links.obz +0 -0
  51. package/examples/obf/simple.obf +53 -0
  52. package/examples/package-lock.json +1326 -0
  53. package/examples/package.json +10 -0
  54. package/examples/styling-example.ts +316 -0
  55. package/examples/translate.js +39 -0
  56. package/examples/translate_demo.js +254 -0
  57. package/examples/typescript-demo.ts +251 -0
  58. package/package.json +3 -1
package/README.md CHANGED
@@ -27,6 +27,7 @@ A comprehensive **TypeScript library** for processing AAC (Augmentative and Alte
27
27
  - ๐ŸŒ **Translation workflows** - Built-in i18n support with `processTexts()`
28
28
  - ๐ŸŽจ **Comprehensive styling support** - Preserve visual appearance across formats
29
29
  - ๐Ÿงช **Property-based testing** - Robust validation with 140+ tests
30
+ - โœ… **Format validation** - Spec-based validation for all supported formats
30
31
  - โšก **Performance optimized** - Memory-efficient processing of large files
31
32
  - ๐Ÿ›ก๏ธ **Error recovery** - Graceful handling of corrupted data
32
33
  - ๐Ÿ”’ **Thread-safe** - Concurrent processing support
@@ -58,6 +59,37 @@ npm run build
58
59
 
59
60
  ---
60
61
 
62
+ ## Using with Electron
63
+
64
+ `better-sqlite3` is a native module and must be rebuilt against Electron's Node.js runtime. If you see a `NODE_MODULE_VERSION` mismatch error, rebuild after installing dependencies:
65
+
66
+ ```bash
67
+ npm install
68
+ npx electron-rebuild
69
+ ```
70
+
71
+ Or add a postinstall hook so the rebuild happens automatically:
72
+
73
+ ```json
74
+ {
75
+ "scripts": {
76
+ "postinstall": "electron-builder install-app-deps"
77
+ }
78
+ }
79
+ ```
80
+
81
+ This step is only required for Electron apps; regular Node.js consumers do not need it.
82
+
83
+ ---
84
+
85
+ ## Windows Data Paths
86
+
87
+ - **Grid 3 history**: `C:\Users\Public\Documents\Smartbox\Grid 3\Users\{username}\{langCode}\Phrases\history.sqlite`
88
+ - **Grid 3 vocabularies**: `C:\Users\Public\Documents\Smartbox\Grid 3\Users\{username}\Grid Sets\`
89
+ - **Snap vocabularies**: `C:\Users\{username}\AppData\Roaming\Tobii Dynavox\Snap Scene\Users\{userId}\` (`.sps`/`.spb`)
90
+
91
+ ---
92
+
61
93
  ## ๐Ÿ”ง Quick Start
62
94
 
63
95
  ### Basic Usage (TypeScript/ES6)
@@ -160,6 +192,78 @@ const translatedBuffer = processor.processTexts(
160
192
  console.log("Translation complete!");
161
193
  ```
162
194
 
195
+ ### Format Validation
196
+
197
+ Validate AAC files against format specifications to ensure data integrity:
198
+
199
+ ```typescript
200
+ import { ObfProcessor, GridsetProcessor } from "aac-processors";
201
+
202
+ // Validate OBF/OBZ files
203
+ const obfProcessor = new ObfProcessor();
204
+ const result = await obfProcessor.validate("board.obf");
205
+
206
+ console.log(`Valid: ${result.valid}`);
207
+ console.log(`Errors: ${result.errors}`);
208
+ console.log(`Warnings: ${result.warnings}`);
209
+
210
+ // Detailed validation results
211
+ if (!result.valid) {
212
+ result.results
213
+ .filter((check) => !check.valid)
214
+ .forEach((check) => {
215
+ console.log(`โœ— ${check.description}: ${check.error}`);
216
+ });
217
+ }
218
+
219
+ // Validate Gridset files (with optional password for encrypted files)
220
+ const gridsetProcessor = new GridsetProcessor({
221
+ gridsetPassword: "optional-password",
222
+ });
223
+ const gridsetResult = await gridsetProcessor.validate("vocab.gridsetx");
224
+ ```
225
+
226
+ #### Using the CLI
227
+
228
+ ```bash
229
+ # Validate a file
230
+ aacprocessors validate board.obf
231
+
232
+ # JSON output
233
+ aacprocessors validate board.obf --json
234
+
235
+ # Quiet mode (just valid/invalid)
236
+ aacprocessors validate board.gridset --quiet
237
+
238
+ # Validate encrypted Gridset file
239
+ aacprocessors validate board.gridsetx --gridset-password <password>
240
+ ```
241
+
242
+ #### What Gets Validated?
243
+
244
+ - **OBF/OBZ**: Spec compliance (Open Board Format)
245
+ - Required fields (format, id, locale, buttons, grid, images, sounds)
246
+ - Grid structure (rows, columns, order)
247
+ - Button references (image_id, sound_id, load_board paths)
248
+ - Color formats (RGB/RGBA)
249
+ - Cross-reference validation
250
+
251
+ - **Gridset**: XML structure
252
+ - Required elements (gridset, pages, cells)
253
+ - FixedCellSize configuration
254
+ - Page and cell attributes
255
+ - Image references
256
+
257
+ - **Snap**: Package structure
258
+ - ZIP package validity
259
+ - Settings file format
260
+ - Page/button configurations
261
+
262
+ - **TouchChat**: XML structure
263
+ - PageSet hierarchy
264
+ - Button definitions
265
+ - Navigation links
266
+
163
267
  ### Cross-Format Conversion
164
268
 
165
269
  Convert between any supported AAC formats:
@@ -475,6 +579,7 @@ npx aac-processors analyze examples/example.gridset --pretty
475
579
  - `--pretty` - Human-readable output (analyze command)
476
580
  - `--verbose` - Detailed output (extract command)
477
581
  - `--quiet` - Minimal output (extract command)
582
+ - `--gridset-password <password>` - Password for encrypted Grid 3 archives (`.gridsetx`)
478
583
 
479
584
  **Button Filtering Options:**
480
585
 
@@ -563,17 +668,17 @@ interface AACButton {
563
668
 
564
669
  ### Supported Processors
565
670
 
566
- | Processor | File Extensions | Description |
567
- | ----------------------- | --------------- | ----------------------------- |
568
- | `DotProcessor` | `.dot` | Graphviz DOT format |
569
- | `OpmlProcessor` | `.opml` | OPML hierarchical format |
570
- | `ObfProcessor` | `.obf`, `.obz` | Open Board Format (JSON/ZIP) |
571
- | `SnapProcessor` | `.sps`, `.spb` | Tobii Dynavox Snap format |
572
- | `GridsetProcessor` | `.gridset` | Smartbox Grid 3 format |
573
- | `TouchChatProcessor` | `.ce` | PRC-Saltillo TouchChat format |
574
- | `ApplePanelsProcessor` | `.plist` | iOS Apple Panels format |
575
- | `AstericsGridProcessor` | `.grd` | Asterics Grid native format |
576
- | `ExcelProcessor` | `.xlsx` | Microsoft Excel format |
671
+ | Processor | File Extensions | Description |
672
+ | ----------------------- | ----------------------- | ----------------------------- |
673
+ | `DotProcessor` | `.dot` | Graphviz DOT format |
674
+ | `OpmlProcessor` | `.opml` | OPML hierarchical format |
675
+ | `ObfProcessor` | `.obf`, `.obz` | Open Board Format (JSON/ZIP) |
676
+ | `SnapProcessor` | `.sps`, `.spb` | Tobii Dynavox Snap format |
677
+ | `GridsetProcessor` | `.gridset`, `.gridsetx` | Smartbox Grid 3 format |
678
+ | `TouchChatProcessor` | `.ce` | PRC-Saltillo TouchChat format |
679
+ | `ApplePanelsProcessor` | `.plist` | iOS Apple Panels format |
680
+ | `AstericsGridProcessor` | `.grd` | Asterics Grid native format |
681
+ | `ExcelProcessor` | `.xlsx` | Microsoft Excel format |
577
682
 
578
683
  ---
579
684
 
package/dist/cli/index.js CHANGED
@@ -23,6 +23,9 @@ function detectFormat(filePath) {
23
23
  // Helper function to parse filtering options from CLI arguments
24
24
  function parseFilteringOptions(options) {
25
25
  const processorOptions = {};
26
+ if (options.gridsetPassword) {
27
+ processorOptions.gridsetPassword = options.gridsetPassword;
28
+ }
26
29
  // Handle preserve all buttons flag
27
30
  if (options.preserveAllButtons) {
28
31
  processorOptions.preserveAllButtons = true;
@@ -63,6 +66,7 @@ commander_1.program
63
66
  .option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
64
67
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
65
68
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
69
+ .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
66
70
  .action((file, options) => {
67
71
  try {
68
72
  // Parse filtering options
@@ -97,6 +101,7 @@ commander_1.program
97
101
  .option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
98
102
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
99
103
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
104
+ .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
100
105
  .action((file, options) => {
101
106
  try {
102
107
  // Parse filtering options
@@ -142,6 +147,7 @@ commander_1.program
142
147
  .option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
143
148
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
144
149
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
150
+ .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
145
151
  .action(async (input, output, options) => {
146
152
  try {
147
153
  if (!options.format) {
@@ -182,6 +188,87 @@ commander_1.program
182
188
  process.exit(1);
183
189
  }
184
190
  });
191
+ commander_1.program
192
+ .command('validate <file>')
193
+ .description('Validate an AAC file format')
194
+ .option('--format <format>', 'Format type (auto-detected if not specified)')
195
+ .option('--json', 'Output results as JSON')
196
+ .option('--quiet', 'Only output validation result (valid/invalid)')
197
+ .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
198
+ .action(async (file, options) => {
199
+ try {
200
+ // Auto-detect format if not specified
201
+ const format = options.format || detectFormat(file);
202
+ // Get processor with gridset password if provided
203
+ const processorOptions = {};
204
+ if (options.gridsetPassword) {
205
+ processorOptions.gridsetPassword = options.gridsetPassword;
206
+ }
207
+ const processor = (0, analyze_1.getProcessor)(format, processorOptions);
208
+ // Check if processor supports validation
209
+ if (!processor.validate) {
210
+ console.error(`Error: Validation not supported for format '${format}'`);
211
+ process.exit(1);
212
+ }
213
+ // Run validation
214
+ const result = await processor.validate(file);
215
+ // Output results
216
+ if (options.quiet) {
217
+ console.log(result.valid ? 'valid' : 'invalid');
218
+ }
219
+ else if (options.json) {
220
+ console.log(JSON.stringify(result, null, 2));
221
+ }
222
+ else {
223
+ // Pretty print validation results
224
+ console.log(`\nValidation Results for: ${result.filename}`);
225
+ console.log(`Format: ${result.format}`);
226
+ console.log(`File size: ${result.filesize} bytes`);
227
+ console.log(`Status: ${result.valid ? 'โœ“ VALID' : 'โœ— INVALID'}`);
228
+ console.log(`Errors: ${result.errors}`);
229
+ console.log(`Warnings: ${result.warnings}\n`);
230
+ if (result.errors > 0 || result.warnings > 0) {
231
+ if (result.errors > 0) {
232
+ console.log('Errors:');
233
+ result.results
234
+ .filter((r) => !r.valid)
235
+ .forEach((check) => {
236
+ console.log(` โœ— ${check.description}`);
237
+ if (check.error) {
238
+ console.log(` ${check.error}`);
239
+ }
240
+ });
241
+ }
242
+ if (result.warnings > 0) {
243
+ console.log('\nWarnings:');
244
+ result.results.forEach((check) => {
245
+ if (check.warnings && check.warnings.length > 0) {
246
+ console.log(` โš  ${check.description}`);
247
+ check.warnings.forEach((warning) => {
248
+ console.log(` ${warning}`);
249
+ });
250
+ }
251
+ });
252
+ }
253
+ }
254
+ // Show sub-results if available
255
+ if (result.sub_results && result.sub_results.length > 0) {
256
+ console.log('\nSub-results:');
257
+ result.sub_results.forEach((sub, idx) => {
258
+ console.log(` [${idx + 1}] ${sub.filename}`);
259
+ console.log(` Status: ${sub.valid ? 'โœ“' : 'โœ—'} (${sub.errors} errors, ${sub.warnings} warnings)`);
260
+ });
261
+ }
262
+ console.log('');
263
+ }
264
+ // Exit with appropriate code
265
+ process.exit(result.valid ? 0 : 1);
266
+ }
267
+ catch (error) {
268
+ console.error('Error validating file:', error instanceof Error ? error.message : String(error));
269
+ process.exit(1);
270
+ }
271
+ });
185
272
  // Show help if no command provided
186
273
  if (process.argv.length <= 2) {
187
274
  commander_1.program.help();
@@ -27,6 +27,7 @@ function getProcessor(format, options) {
27
27
  case 'ce': // TouchChat file extension
28
28
  return new touchchatProcessor_1.TouchChatProcessor(options);
29
29
  case 'gridset':
30
+ case 'gridsetx':
30
31
  return new gridsetProcessor_1.GridsetProcessor(options); // Grid3 format
31
32
  case 'grd': // Asterics Grid file extension
32
33
  return new astericsGridProcessor_1.AstericsGridProcessor(options);
@@ -1,8 +1,10 @@
1
1
  import { AACTree, AACButton } from './treeStructure';
2
2
  import { StringCasing } from './stringCasing';
3
+ import { ValidationResult } from '../validation/validationTypes';
3
4
  export interface ProcessorOptions {
4
5
  excludeNavigationButtons?: boolean;
5
6
  excludeSystemButtons?: boolean;
7
+ gridsetPassword?: string;
6
8
  customButtonFilter?: (button: AACButton) => boolean;
7
9
  preserveAllButtons?: boolean;
8
10
  }
@@ -44,6 +46,7 @@ declare abstract class BaseProcessor {
44
46
  abstract loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
45
47
  abstract processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
46
48
  abstract saveFromTree(tree: AACTree, outputPath: string): void | Promise<void>;
49
+ validate?(filePath: string): Promise<ValidationResult>;
47
50
  /**
48
51
  * Extract strings with metadata for external platform integration
49
52
  * @param filePath - Path to the AAC file
@@ -20,6 +20,7 @@ class FileProcessor {
20
20
  const ext = path_1.default.extname(filePathOrBuffer).toLowerCase();
21
21
  switch (ext) {
22
22
  case '.gridset':
23
+ case '.gridsetx':
23
24
  return 'gridset';
24
25
  case '.obf':
25
26
  case '.obz':
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './core/baseProcessor';
3
3
  export * from './core/stringCasing';
4
4
  export * from './processors';
5
5
  export { collectUnifiedHistory, listGrid3Users as listHistoryGrid3Users, listSnapUsers as listHistorySnapUsers, } from './analytics/history';
6
+ export * from './validation';
6
7
  import { BaseProcessor } from './core/baseProcessor';
7
8
  /**
8
9
  * Factory function to get the appropriate processor for a file extension
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ var history_1 = require("./analytics/history");
27
27
  Object.defineProperty(exports, "collectUnifiedHistory", { enumerable: true, get: function () { return history_1.collectUnifiedHistory; } });
28
28
  Object.defineProperty(exports, "listHistoryGrid3Users", { enumerable: true, get: function () { return history_1.listGrid3Users; } });
29
29
  Object.defineProperty(exports, "listHistorySnapUsers", { enumerable: true, get: function () { return history_1.listSnapUsers; } });
30
+ __exportStar(require("./validation"), exports);
30
31
  const dotProcessor_1 = require("./processors/dotProcessor");
31
32
  const excelProcessor_1 = require("./processors/excelProcessor");
32
33
  const opmlProcessor_1 = require("./processors/opmlProcessor");
@@ -58,6 +59,7 @@ function getProcessor(filePathOrExtension) {
58
59
  case '.obz':
59
60
  return new obfProcessor_1.ObfProcessor();
60
61
  case '.gridset':
62
+ case '.gridsetx':
61
63
  return new gridsetProcessor_1.GridsetProcessor();
62
64
  case '.spb':
63
65
  case '.sps':
@@ -84,6 +86,7 @@ function getSupportedExtensions() {
84
86
  '.obf',
85
87
  '.obz',
86
88
  '.gridset',
89
+ '.gridsetx',
87
90
  '.spb',
88
91
  '.sps',
89
92
  '.ce',
@@ -7,6 +7,7 @@ exports.TouchChatSymbolResolver = exports.TouchChatSymbolExtractor = exports.Gri
7
7
  exports.resolveSymbol = resolveSymbol;
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
+ const password_1 = require("../processors/gridset/password");
10
11
  // --- Base Classes ---
11
12
  class SymbolExtractor {
12
13
  }
@@ -76,8 +77,9 @@ class Grid3SymbolExtractor extends SymbolExtractor {
76
77
  const zip = new AdmZip(filePath);
77
78
  const parser = new XMLParser();
78
79
  const refs = new Set();
79
- zip.getEntries().forEach((entry) => {
80
- if (entry.entryName.endsWith('.gridset')) {
80
+ const entries = (0, password_1.getZipEntriesWithPassword)(zip, (0, password_1.resolveGridsetPasswordFromEnv)());
81
+ entries.forEach((entry) => {
82
+ if (entry.entryName.endsWith('.gridset') || entry.entryName.endsWith('.gridsetx')) {
81
83
  const xmlBuffer = entry.getData();
82
84
  // Parse to validate XML structure (future: extract refs)
83
85
  parser.parse(xmlBuffer.toString('utf8'));
@@ -15,7 +15,7 @@ export declare function getAllowedImageEntries(tree: AACTree): Set<string>;
15
15
  * @param entryPath Entry name inside the zip
16
16
  * @returns Image data buffer or null if not found
17
17
  */
18
- export declare function openImage(gridsetBuffer: Buffer, entryPath: string): Buffer | null;
18
+ export declare function openImage(gridsetBuffer: Buffer, entryPath: string, password?: string | undefined): Buffer | null;
19
19
  /**
20
20
  * Generate a random GUID for Grid3 elements
21
21
  * Grid3 uses GUIDs for grid identification
@@ -79,6 +79,8 @@ export declare function getCommonDocumentsPath(): string;
79
79
  * Find all Grid3 user data paths
80
80
  * Searches for users and language codes in the Grid3 directory structure
81
81
  * C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\{langCode}\Phrases\history.sqlite
82
+ * Grid set/vocabulary archives live alongside users at:
83
+ * C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\Grid Sets\
82
84
  * @returns Array of Grid3 user path information
83
85
  */
84
86
  export declare function findGrid3UserPaths(): Grid3UserPath[];
@@ -49,6 +49,7 @@ const path = __importStar(require("path"));
49
49
  const child_process_1 = require("child_process");
50
50
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
51
51
  const dotnetTicks_1 = require("../../utils/dotnetTicks");
52
+ const password_1 = require("./password");
52
53
  function normalizeZipPath(p) {
53
54
  const unified = p.replace(/\\/g, '/');
54
55
  try {
@@ -94,10 +95,11 @@ function getAllowedImageEntries(tree) {
94
95
  * @param entryPath Entry name inside the zip
95
96
  * @returns Image data buffer or null if not found
96
97
  */
97
- function openImage(gridsetBuffer, entryPath) {
98
+ function openImage(gridsetBuffer, entryPath, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
98
99
  const zip = new adm_zip_1.default(gridsetBuffer);
100
+ const entries = (0, password_1.getZipEntriesWithPassword)(zip, password);
99
101
  const want = normalizeZipPath(entryPath);
100
- const entry = zip.getEntries().find((e) => normalizeZipPath(e.entryName) === want);
102
+ const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
101
103
  if (!entry)
102
104
  return null;
103
105
  return entry.getData();
@@ -197,6 +199,8 @@ function getCommonDocumentsPath() {
197
199
  * Find all Grid3 user data paths
198
200
  * Searches for users and language codes in the Grid3 directory structure
199
201
  * C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\{langCode}\Phrases\history.sqlite
202
+ * Grid set/vocabulary archives live alongside users at:
203
+ * C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\Grid Sets\
200
204
  * @returns Array of Grid3 user path information
201
205
  */
202
206
  function findGrid3UserPaths() {
@@ -290,7 +294,7 @@ function findGrid3Vocabularies(userName) {
290
294
  if (!entry.isFile())
291
295
  continue;
292
296
  const ext = path.extname(entry.name).toLowerCase();
293
- if (ext === '.gridset' || ext === '.grd' || ext === '.grdl') {
297
+ if (ext === '.gridset' || ext === '.gridsetx' || ext === '.grd' || ext === '.grdl') {
294
298
  results.push({
295
299
  userName: userDir.name,
296
300
  gridsetPath: path.win32.join(gridSetsDir, entry.name),
@@ -307,6 +311,8 @@ function findGrid3Vocabularies(userName) {
307
311
  * @returns Path to history.sqlite or null if not found
308
312
  */
309
313
  function findGrid3UserHistory(userName, langCode) {
314
+ if (!userName)
315
+ return null;
310
316
  const normalizedUser = userName.toLowerCase();
311
317
  const normalizedLang = langCode?.toLowerCase();
312
318
  const match = findGrid3UserPaths().find((u) => u.userName.toLowerCase() === normalizedUser &&
@@ -363,13 +369,26 @@ function readGrid3History(historyDbPath) {
363
369
  const events = new Map();
364
370
  for (const row of rows) {
365
371
  const phraseId = row.PhraseId;
366
- const contentText = parseGrid3ContentXml(String(row.ContentXml ?? row.TextValue ?? ''));
372
+ const rawContentSource = [row.ContentXml, row.TextValue].find((candidate) => {
373
+ if (candidate === null || candidate === undefined)
374
+ return false;
375
+ const asString = String(candidate);
376
+ return asString.trim().length > 0;
377
+ });
378
+ if (rawContentSource === undefined) {
379
+ continue; // Skip history rows with no usable text content
380
+ }
381
+ const rawContentText = String(rawContentSource);
382
+ const contentText = parseGrid3ContentXml(rawContentText);
383
+ const rawXml = typeof row.ContentXml === 'string' && row.ContentXml.trim().length > 0
384
+ ? row.ContentXml
385
+ : undefined;
367
386
  const entry = events.get(phraseId) ??
368
387
  {
369
388
  id: `grid:${phraseId}`,
370
389
  content: contentText,
371
390
  occurrences: [],
372
- rawXml: row.ContentXml,
391
+ rawXml,
373
392
  };
374
393
  entry.occurrences.push({
375
394
  timestamp: (0, dotnetTicks_1.dotNetTicksToDate)(BigInt(row.TickValue ?? 0)),
@@ -0,0 +1,11 @@
1
+ import { ProcessorOptions } from '../../core/baseProcessor';
2
+ import AdmZip from 'adm-zip';
3
+ /**
4
+ * Resolve the password to use for Grid3 archives.
5
+ * Preference order:
6
+ * 1. Explicit processor option
7
+ * 2. GRIDSET_PASSWORD env var
8
+ */
9
+ export declare function resolveGridsetPassword(options?: ProcessorOptions, source?: string | Buffer): string | undefined;
10
+ export declare function resolveGridsetPasswordFromEnv(): string | undefined;
11
+ export declare function getZipEntriesWithPassword(zip: AdmZip, password?: string): AdmZip.IZipEntry[];
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveGridsetPassword = resolveGridsetPassword;
7
+ exports.resolveGridsetPasswordFromEnv = resolveGridsetPasswordFromEnv;
8
+ exports.getZipEntriesWithPassword = getZipEntriesWithPassword;
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * Resolve the password to use for Grid3 archives.
12
+ * Preference order:
13
+ * 1. Explicit processor option
14
+ * 2. GRIDSET_PASSWORD env var
15
+ */
16
+ function resolveGridsetPassword(options, source) {
17
+ if (options?.gridsetPassword)
18
+ return options.gridsetPassword;
19
+ if (process.env.GRIDSET_PASSWORD)
20
+ return process.env.GRIDSET_PASSWORD;
21
+ if (typeof source === 'string') {
22
+ const ext = path_1.default.extname(source).toLowerCase();
23
+ if (ext === '.gridsetx')
24
+ return process.env.GRIDSET_PASSWORD;
25
+ }
26
+ return undefined;
27
+ }
28
+ function resolveGridsetPasswordFromEnv() {
29
+ return process.env.GRIDSET_PASSWORD;
30
+ }
31
+ // Wrapper to set the password before reading entries (typed getEntries lacks the optional arg)
32
+ function getZipEntriesWithPassword(zip, password) {
33
+ if (password) {
34
+ return zip.getEntries(password);
35
+ }
36
+ return zip.getEntries();
37
+ }
@@ -5,4 +5,4 @@ export declare function resolveGrid3CellImage(zip: any, args: {
5
5
  y?: number;
6
6
  dynamicFiles?: string[];
7
7
  builtinHandler?: (name: string) => string | null;
8
- }): string | null;
8
+ }, zipEntries?: any[]): string | null;
@@ -10,9 +10,13 @@ function normalizeZipPathLocal(p) {
10
10
  return unified;
11
11
  }
12
12
  }
13
- function listZipEntries(zip) {
13
+ function listZipEntries(zip, zipEntries) {
14
14
  try {
15
- const raw = typeof zip?.getEntries === 'function' ? zip.getEntries() : [];
15
+ const raw = Array.isArray(zipEntries) && zipEntries.length > 0
16
+ ? zipEntries
17
+ : typeof zip?.getEntries === 'function'
18
+ ? zip.getEntries()
19
+ : [];
16
20
  let entries = [];
17
21
  if (Array.isArray(raw))
18
22
  entries = raw;
@@ -33,12 +37,12 @@ function joinBaseDir(baseDir, leaf) {
33
37
  const base = normalizeZipPathLocal(baseDir).replace(/\/?$/, '/');
34
38
  return normalizeZipPathLocal(base + leaf.replace(/^\//, ''));
35
39
  }
36
- function resolveGrid3CellImage(zip, args) {
40
+ function resolveGrid3CellImage(zip, args, zipEntries) {
37
41
  const { baseDir, dynamicFiles } = args;
38
42
  const imageName = args.imageName?.trim();
39
43
  const x = args.x;
40
44
  const y = args.y;
41
- const entries = new Set(listZipEntries(zip));
45
+ const entries = new Set(listZipEntries(zip, zipEntries));
42
46
  const has = (p) => entries.has(normalizeZipPathLocal(p));
43
47
  // Built-in resource like [grid3x]...
44
48
  if (imageName && imageName.startsWith('[')) {
@@ -64,7 +64,7 @@ export declare function wordlistToXml(wordlist: WordList): string;
64
64
  * console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
65
65
  * });
66
66
  */
67
- export declare function extractWordlists(gridsetBuffer: Buffer): Map<string, WordList>;
67
+ export declare function extractWordlists(gridsetBuffer: Buffer, password?: string | undefined): Map<string, WordList>;
68
68
  /**
69
69
  * Updates or adds a wordlist to a specific grid in a gridset
70
70
  *
@@ -79,4 +79,4 @@ export declare function extractWordlists(gridsetBuffer: Buffer): Map<string, Wor
79
79
  * const updatedGridset = updateWordlist(gridsetBuffer, 'Greetings', newWordlist);
80
80
  * fs.writeFileSync('updated-gridset.gridset', updatedGridset);
81
81
  */
82
- export declare function updateWordlist(gridsetBuffer: Buffer, gridName: string, wordlist: WordList): Buffer;
82
+ export declare function updateWordlist(gridsetBuffer: Buffer, gridName: string, wordlist: WordList, password?: string | undefined): Buffer;
@@ -19,6 +19,7 @@ exports.extractWordlists = extractWordlists;
19
19
  exports.updateWordlist = updateWordlist;
20
20
  const adm_zip_1 = __importDefault(require("adm-zip"));
21
21
  const fast_xml_parser_1 = require("fast-xml-parser");
22
+ const password_1 = require("./password");
22
23
  /**
23
24
  * Creates a WordList object from an array of words/phrases or a dictionary
24
25
  *
@@ -104,7 +105,7 @@ function wordlistToXml(wordlist) {
104
105
  * console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
105
106
  * });
106
107
  */
107
- function extractWordlists(gridsetBuffer) {
108
+ function extractWordlists(gridsetBuffer, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
108
109
  const wordlists = new Map();
109
110
  const parser = new fast_xml_parser_1.XMLParser();
110
111
  let zip;
@@ -114,8 +115,9 @@ function extractWordlists(gridsetBuffer) {
114
115
  catch (error) {
115
116
  throw new Error(`Invalid gridset buffer: ${error.message}`);
116
117
  }
118
+ const entries = (0, password_1.getZipEntriesWithPassword)(zip, password);
117
119
  // Process each grid file
118
- zip.getEntries().forEach((entry) => {
120
+ entries.forEach((entry) => {
119
121
  if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
120
122
  try {
121
123
  const xmlContent = entry.getData().toString('utf8');
@@ -169,7 +171,7 @@ function extractWordlists(gridsetBuffer) {
169
171
  * const updatedGridset = updateWordlist(gridsetBuffer, 'Greetings', newWordlist);
170
172
  * fs.writeFileSync('updated-gridset.gridset', updatedGridset);
171
173
  */
172
- function updateWordlist(gridsetBuffer, gridName, wordlist) {
174
+ function updateWordlist(gridsetBuffer, gridName, wordlist, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
173
175
  const parser = new fast_xml_parser_1.XMLParser();
174
176
  const builder = new fast_xml_parser_1.XMLBuilder({
175
177
  ignoreAttributes: false,
@@ -184,9 +186,10 @@ function updateWordlist(gridsetBuffer, gridName, wordlist) {
184
186
  catch (error) {
185
187
  throw new Error(`Invalid gridset buffer: ${error.message}`);
186
188
  }
189
+ const entries = (0, password_1.getZipEntriesWithPassword)(zip, password);
187
190
  let found = false;
188
191
  // Find and update the grid
189
- zip.getEntries().forEach((entry) => {
192
+ entries.forEach((entry) => {
190
193
  if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
191
194
  const match = entry.entryName.match(/^Grids\/([^/]+)\//);
192
195
  const currentGridName = match ? match[1] : null;
@@ -1,14 +1,22 @@
1
1
  import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
2
2
  import { AACTree } from '../core/treeStructure';
3
+ import { ValidationResult } from '../validation/validationTypes';
3
4
  declare class GridsetProcessor extends BaseProcessor {
4
5
  constructor(options?: ProcessorOptions);
6
+ /**
7
+ * Decrypt and inflate a Grid3 encrypted payload (DesktopContentEncrypter).
8
+ * Uses AES-256-CBC with key/IV derived from the password padded with spaces
9
+ * and then Deflate decompression.
10
+ */
11
+ private decryptGridsetEntry;
12
+ private getGridsetPassword;
5
13
  private ensureAlphaChannel;
6
14
  private generateCommandsFromSemanticAction;
7
15
  private convertGrid3StyleToAACStyle;
8
16
  private getStyleById;
9
17
  private textOf;
10
18
  extractTexts(filePathOrBuffer: string | Buffer): string[];
11
- loadIntoTree(filePathOrBuffer: Buffer): AACTree;
19
+ loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
12
20
  processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
13
21
  saveFromTree(tree: AACTree, outputPath: string): void;
14
22
  private calculateColumnDefinitions;
@@ -24,5 +32,11 @@ declare class GridsetProcessor extends BaseProcessor {
24
32
  * Uses the generic implementation from BaseProcessor
25
33
  */
26
34
  generateTranslatedDownload(filePath: string, translatedStrings: TranslatedString[], sourceStrings: SourceString[]): Promise<string>;
35
+ /**
36
+ * Validate Gridset file format
37
+ * @param filePath - Path to the file to validate
38
+ * @returns Promise with validation result
39
+ */
40
+ validate(filePath: string): Promise<ValidationResult>;
27
41
  }
28
42
  export { GridsetProcessor };