@willwade/aac-processors 0.0.5 → 0.0.6

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.
@@ -9,12 +9,15 @@ const treeStructure_1 = require("../core/treeStructure");
9
9
  // Removed unused import: FileProcessor
10
10
  const adm_zip_1 = __importDefault(require("adm-zip"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
+ // Removed unused import: path
13
+ const OBF_FORMAT_VERSION = 'open-board-0.1';
12
14
  class ObfProcessor extends baseProcessor_1.BaseProcessor {
13
15
  constructor(options) {
14
16
  super(options);
15
17
  }
16
18
  processBoard(boardData, _boardPath) {
17
- const buttons = (boardData.buttons || []).map((btn) => {
19
+ const sourceButtons = boardData.buttons || [];
20
+ const buttons = sourceButtons.map((btn) => {
18
21
  const semanticAction = btn.load_board
19
22
  ? {
20
23
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
@@ -46,33 +49,62 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
46
49
  targetPageId: btn.load_board?.path,
47
50
  });
48
51
  });
52
+ const buttonMap = new Map(buttons.map((btn) => [btn.id, btn]));
49
53
  const page = new treeStructure_1.AACPage({
50
54
  id: String(boardData?.id || ''),
51
55
  name: String(boardData?.name || ''),
52
56
  grid: [],
53
57
  buttons,
54
58
  parentId: null,
59
+ locale: boardData.locale,
60
+ descriptionHtml: boardData.description_html,
61
+ images: boardData.images,
62
+ sounds: boardData.sounds,
55
63
  });
56
64
  // Process grid layout if available
57
65
  if (boardData.grid) {
58
- const rows = boardData.grid.rows;
59
- const cols = boardData.grid.columns;
60
- const grid = Array(rows)
61
- .fill(null)
62
- .map(() => Array(cols).fill(null));
63
- for (const btn of boardData.buttons) {
64
- if (typeof btn.box_id === 'number') {
65
- const row = Math.floor(btn.box_id / cols);
66
- const col = btn.box_id % cols;
67
- if (row < rows && col < cols) {
68
- const aacBtn = buttons.find((b) => b.id === btn.id);
69
- if (aacBtn) {
70
- grid[row][col] = aacBtn;
66
+ const rows = typeof boardData.grid.rows === 'number'
67
+ ? boardData.grid.rows
68
+ : boardData.grid.order?.length || 0;
69
+ const cols = typeof boardData.grid.columns === 'number'
70
+ ? boardData.grid.columns
71
+ : boardData.grid.order
72
+ ? boardData.grid.order.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0)
73
+ : 0;
74
+ if (rows > 0 && cols > 0) {
75
+ const grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
76
+ if (Array.isArray(boardData.grid.order) && boardData.grid.order.length) {
77
+ boardData.grid.order.forEach((orderRow, rowIndex) => {
78
+ if (!Array.isArray(orderRow))
79
+ return;
80
+ orderRow.forEach((cellId, colIndex) => {
81
+ if (cellId === null || cellId === undefined)
82
+ return;
83
+ if (rowIndex >= rows || colIndex >= cols)
84
+ return;
85
+ const aacBtn = buttonMap.get(String(cellId));
86
+ if (aacBtn) {
87
+ grid[rowIndex][colIndex] = aacBtn;
88
+ }
89
+ });
90
+ });
91
+ }
92
+ else {
93
+ for (const btn of sourceButtons) {
94
+ if (typeof btn.box_id === 'number') {
95
+ const row = Math.floor(btn.box_id / cols);
96
+ const col = btn.box_id % cols;
97
+ if (row < rows && col < cols) {
98
+ const aacBtn = buttonMap.get(String(btn.id));
99
+ if (aacBtn) {
100
+ grid[row][col] = aacBtn;
101
+ }
102
+ }
71
103
  }
72
104
  }
73
105
  }
106
+ page.grid = grid;
74
107
  }
75
- page.grid = grid;
76
108
  }
77
109
  return page;
78
110
  }
@@ -123,9 +155,8 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
123
155
  }
124
156
  // If input is a string path and ends with .obf, treat as JSON
125
157
  if (typeof filePathOrBuffer === 'string' && filePathOrBuffer.endsWith('.obf')) {
126
- const fs = require('fs');
127
158
  try {
128
- const content = fs.readFileSync(filePathOrBuffer, 'utf8');
159
+ const content = fs_1.default.readFileSync(filePathOrBuffer, 'utf8');
129
160
  const boardData = tryParseObfJson(content);
130
161
  if (boardData) {
131
162
  console.log('[OBF] Detected .obf file, parsed as JSON');
@@ -186,6 +217,73 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
186
217
  });
187
218
  return tree;
188
219
  }
220
+ buildGridMetadata(page) {
221
+ const buttonPositions = new Map();
222
+ const totalRows = Array.isArray(page.grid) ? page.grid.length : 0;
223
+ const totalColumns = totalRows > 0
224
+ ? page.grid.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0)
225
+ : 0;
226
+ if (totalRows === 0 || totalColumns === 0) {
227
+ if (!page.buttons.length) {
228
+ return { rows: 0, columns: 0, order: [], buttonPositions };
229
+ }
230
+ const fallbackRow = page.buttons.map((button, index) => {
231
+ const id = String(button.id ?? '');
232
+ buttonPositions.set(id, index);
233
+ return id;
234
+ });
235
+ return { rows: 1, columns: fallbackRow.length, order: [fallbackRow], buttonPositions };
236
+ }
237
+ const order = [];
238
+ for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
239
+ const sourceRow = page.grid[rowIndex] || [];
240
+ const orderRow = [];
241
+ for (let colIndex = 0; colIndex < totalColumns; colIndex++) {
242
+ const cell = sourceRow[colIndex] || null;
243
+ if (cell) {
244
+ const id = String(cell.id ?? '');
245
+ orderRow.push(id);
246
+ buttonPositions.set(id, rowIndex * totalColumns + colIndex);
247
+ }
248
+ else {
249
+ orderRow.push(null);
250
+ }
251
+ }
252
+ order.push(orderRow);
253
+ }
254
+ return { rows: totalRows, columns: totalColumns, order, buttonPositions };
255
+ }
256
+ createObfBoardFromPage(page, fallbackName) {
257
+ const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
258
+ const boardName = page.name || fallbackName;
259
+ return {
260
+ format: OBF_FORMAT_VERSION,
261
+ id: page.id,
262
+ locale: page.locale || 'en',
263
+ name: boardName,
264
+ description_html: page.descriptionHtml || boardName,
265
+ grid: {
266
+ rows,
267
+ columns,
268
+ order,
269
+ },
270
+ buttons: page.buttons.map((button) => ({
271
+ id: button.id,
272
+ label: button.label,
273
+ vocalization: button.message || button.label,
274
+ load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
275
+ ? {
276
+ path: button.targetPageId,
277
+ }
278
+ : undefined,
279
+ background_color: button.style?.backgroundColor,
280
+ border_color: button.style?.borderColor,
281
+ box_id: buttonPositions.get(String(button.id ?? '')),
282
+ })),
283
+ images: Array.isArray(page.images) ? page.images : [],
284
+ sounds: Array.isArray(page.sounds) ? page.sounds : [],
285
+ };
286
+ }
189
287
  processTexts(filePathOrBuffer, translations, outputPath) {
190
288
  // Load the tree, apply translations, and save to new file
191
289
  const tree = this.loadIntoTree(filePathOrBuffer);
@@ -193,15 +291,24 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
193
291
  Object.values(tree.pages).forEach((page) => {
194
292
  // Translate page names
195
293
  if (page.name && translations.has(page.name)) {
196
- page.name = translations.get(page.name);
294
+ const translatedName = translations.get(page.name);
295
+ if (translatedName !== undefined) {
296
+ page.name = translatedName;
297
+ }
197
298
  }
198
299
  // Translate button labels and messages
199
300
  page.buttons.forEach((button) => {
200
301
  if (button.label && translations.has(button.label)) {
201
- button.label = translations.get(button.label);
302
+ const translatedLabel = translations.get(button.label);
303
+ if (translatedLabel !== undefined) {
304
+ button.label = translatedLabel;
305
+ }
202
306
  }
203
307
  if (button.message && translations.has(button.message)) {
204
- button.message = translations.get(button.message);
308
+ const translatedMessage = translations.get(button.message);
309
+ if (translatedMessage !== undefined) {
310
+ button.message = translatedMessage;
311
+ }
205
312
  }
206
313
  });
207
314
  });
@@ -216,44 +323,14 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
216
323
  if (!rootPage) {
217
324
  throw new Error('No pages to save');
218
325
  }
219
- const obfBoard = {
220
- id: rootPage.id,
221
- name: rootPage.name || 'Exported Board',
222
- buttons: rootPage.buttons.map((button) => ({
223
- id: button.id,
224
- label: button.label,
225
- vocalization: button.message || button.label,
226
- load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
227
- ? {
228
- path: button.targetPageId,
229
- }
230
- : undefined,
231
- background_color: button.style?.backgroundColor,
232
- border_color: button.style?.borderColor,
233
- })),
234
- };
326
+ const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board');
235
327
  fs_1.default.writeFileSync(outputPath, JSON.stringify(obfBoard, null, 2));
236
328
  }
237
329
  else {
238
330
  // Save as OBZ (zip with multiple OBF files)
239
331
  const zip = new adm_zip_1.default();
240
332
  Object.values(tree.pages).forEach((page) => {
241
- const obfBoard = {
242
- id: page.id,
243
- name: page.name || 'Board',
244
- buttons: page.buttons.map((button) => ({
245
- id: button.id,
246
- label: button.label,
247
- vocalization: button.message || button.label,
248
- load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
249
- ? {
250
- path: button.targetPageId,
251
- }
252
- : undefined,
253
- background_color: button.style?.backgroundColor,
254
- border_color: button.style?.borderColor,
255
- })),
256
- };
333
+ const obfBoard = this.createObfBoardFromPage(page, 'Board');
257
334
  const obfContent = JSON.stringify(obfBoard, null, 2);
258
335
  zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
259
336
  });
@@ -173,7 +173,18 @@ class OpmlProcessor extends baseProcessor_1.BaseProcessor {
173
173
  .filter((b) => b.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO &&
174
174
  !!b.targetPageId &&
175
175
  !!tree.pages[b.targetPageId])
176
- .map((b) => buildOutline(tree.pages[b.targetPageId], new Set(visited))); // Pass copy of visited set
176
+ .map((b) => {
177
+ const targetId = b.targetPageId;
178
+ if (!targetId) {
179
+ return null;
180
+ }
181
+ const targetPage = tree.pages[targetId];
182
+ if (!targetPage) {
183
+ return null;
184
+ }
185
+ return buildOutline(targetPage, new Set(visited));
186
+ })
187
+ .filter((childOutline) => childOutline !== null);
177
188
  if (childOutlines.length)
178
189
  outline.outline = childOutlines;
179
190
  return outline;
@@ -206,8 +217,7 @@ class OpmlProcessor extends baseProcessor_1.BaseProcessor {
206
217
  },
207
218
  };
208
219
  // Convert to XML
209
- const { XMLBuilder } = require('fast-xml-parser');
210
- const builder = new XMLBuilder({
220
+ const builder = new fast_xml_parser_1.XMLBuilder({
211
221
  ignoreAttributes: false,
212
222
  format: true,
213
223
  indentBy: ' ',
@@ -185,7 +185,6 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
185
185
  }
186
186
  // Create semantic action for Snap button
187
187
  let semanticAction;
188
- let legacyAction = null;
189
188
  if (targetPageUniqueId) {
190
189
  semanticAction = {
191
190
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
@@ -202,10 +201,6 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
202
201
  targetPageId: targetPageUniqueId,
203
202
  },
204
203
  };
205
- legacyAction = {
206
- type: 'NAVIGATE',
207
- targetPageId: targetPageUniqueId,
208
- };
209
204
  }
210
205
  else {
211
206
  semanticAction = {
@@ -283,15 +278,16 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
283
278
  return tree;
284
279
  }
285
280
  catch (error) {
281
+ const fileIdentifier = typeof filePathOrBuffer === 'string' ? filePathOrBuffer : '[buffer input]';
286
282
  // Provide more specific error messages
287
283
  if (error.code === 'SQLITE_NOTADB') {
288
284
  throw new Error(`Invalid SQLite database file: ${typeof filePathOrBuffer === 'string' ? filePathOrBuffer : 'buffer'}`);
289
285
  }
290
286
  else if (error.code === 'ENOENT') {
291
- throw new Error(`File not found: ${filePathOrBuffer}`);
287
+ throw new Error(`File not found: ${fileIdentifier}`);
292
288
  }
293
289
  else if (error.code === 'EACCES') {
294
- throw new Error(`Permission denied accessing file: ${filePathOrBuffer}`);
290
+ throw new Error(`Permission denied accessing file: ${fileIdentifier}`);
295
291
  }
296
292
  else {
297
293
  throw new Error(`Failed to load Snap file: ${error.message}`);
@@ -320,15 +316,24 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
320
316
  Object.values(tree.pages).forEach((page) => {
321
317
  // Translate page names
322
318
  if (page.name && translations.has(page.name)) {
323
- page.name = translations.get(page.name);
319
+ const translatedName = translations.get(page.name);
320
+ if (translatedName !== undefined) {
321
+ page.name = translatedName;
322
+ }
324
323
  }
325
324
  // Translate button labels and messages
326
325
  page.buttons.forEach((button) => {
327
326
  if (button.label && translations.has(button.label)) {
328
- button.label = translations.get(button.label);
327
+ const translatedLabel = translations.get(button.label);
328
+ if (translatedLabel !== undefined) {
329
+ button.label = translatedLabel;
330
+ }
329
331
  }
330
332
  if (button.message && translations.has(button.message)) {
331
- button.message = translations.get(button.message);
333
+ const translatedMessage = translations.get(button.message);
334
+ if (translatedMessage !== undefined) {
335
+ button.message = translatedMessage;
336
+ }
332
337
  }
333
338
  });
334
339
  });
@@ -419,6 +424,9 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
419
424
  // Second pass: create buttons with proper page references
420
425
  Object.values(tree.pages).forEach((page) => {
421
426
  const numericPageId = pageIdMap.get(page.id);
427
+ if (numericPageId === undefined) {
428
+ return;
429
+ }
422
430
  page.buttons.forEach((button, index) => {
423
431
  // Find button position in grid layout
424
432
  let gridPosition = `${index % 4},${Math.floor(index / 4)}`; // Default fallback
@@ -12,6 +12,9 @@ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const os_1 = __importDefault(require("os"));
15
+ const toNumberOrUndefined = (value) => typeof value === 'number' ? value : undefined;
16
+ const toStringOrUndefined = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
17
+ const toBooleanOrUndefined = (value) => typeof value === 'number' ? value !== 0 : undefined;
15
18
  function intToHex(colorInt) {
16
19
  if (colorInt === null || typeof colorInt === 'undefined') {
17
20
  return undefined;
@@ -84,7 +87,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
84
87
  const buttonStyles = new Map();
85
88
  const pageStyles = new Map();
86
89
  try {
87
- const buttonStyleRows = db.prepare('SELECT * FROM button_styles').all();
90
+ const buttonStyleRows = db
91
+ .prepare('SELECT * FROM button_styles')
92
+ .all();
88
93
  buttonStyleRows.forEach((style) => {
89
94
  buttonStyles.set(style.id, style);
90
95
  });
@@ -162,15 +167,15 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
162
167
  style: {
163
168
  backgroundColor: intToHex(style?.body_color),
164
169
  borderColor: intToHex(style?.border_color),
165
- borderWidth: style?.border_width,
170
+ borderWidth: toNumberOrUndefined(style?.border_width),
166
171
  fontColor: intToHex(style?.font_color),
167
- fontSize: style?.font_height,
168
- fontFamily: style?.font_name,
169
- fontWeight: style?.font_bold ? 'bold' : 'normal',
170
- fontStyle: style?.font_italic ? 'italic' : 'normal',
171
- textUnderline: style?.font_underline,
172
- transparent: style?.transparent,
173
- labelOnTop: style?.label_on_top,
172
+ fontSize: toNumberOrUndefined(style?.font_height),
173
+ fontFamily: toStringOrUndefined(style?.font_name),
174
+ fontWeight: style?.font_bold ? 'bold' : undefined,
175
+ fontStyle: style?.font_italic ? 'italic' : undefined,
176
+ textUnderline: toBooleanOrUndefined(style?.font_underline),
177
+ transparent: toBooleanOrUndefined(style?.transparent),
178
+ labelOnTop: toBooleanOrUndefined(style?.label_on_top),
174
179
  },
175
180
  });
176
181
  buttonBoxes.get(cell.box_id)?.push({
@@ -276,15 +281,15 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
276
281
  style: {
277
282
  backgroundColor: intToHex(style?.body_color),
278
283
  borderColor: intToHex(style?.border_color),
279
- borderWidth: style?.border_width,
284
+ borderWidth: toNumberOrUndefined(style?.border_width),
280
285
  fontColor: intToHex(style?.font_color),
281
- fontSize: style?.font_height,
282
- fontFamily: style?.font_name,
283
- fontWeight: style?.font_bold ? 'bold' : 'normal',
284
- fontStyle: style?.font_italic ? 'italic' : 'normal',
285
- textUnderline: style?.font_underline,
286
- transparent: style?.transparent,
287
- labelOnTop: style?.label_on_top,
286
+ fontSize: toNumberOrUndefined(style?.font_height),
287
+ fontFamily: toStringOrUndefined(style?.font_name),
288
+ fontWeight: style?.font_bold ? 'bold' : undefined,
289
+ fontStyle: style?.font_italic ? 'italic' : undefined,
290
+ textUnderline: toBooleanOrUndefined(style?.font_underline),
291
+ transparent: toBooleanOrUndefined(style?.transparent),
292
+ labelOnTop: toBooleanOrUndefined(style?.label_on_top),
288
293
  },
289
294
  });
290
295
  // Find the page that references this resource
@@ -380,15 +385,24 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
380
385
  Object.values(tree.pages).forEach((page) => {
381
386
  // Translate page names
382
387
  if (page.name && translations.has(page.name)) {
383
- page.name = translations.get(page.name);
388
+ const translatedName = translations.get(page.name);
389
+ if (translatedName !== undefined) {
390
+ page.name = translatedName;
391
+ }
384
392
  }
385
393
  // Translate button labels and messages
386
394
  page.buttons.forEach((button) => {
387
395
  if (button.label && translations.has(button.label)) {
388
- button.label = translations.get(button.label);
396
+ const translatedLabel = translations.get(button.label);
397
+ if (translatedLabel !== undefined) {
398
+ button.label = translatedLabel;
399
+ }
389
400
  }
390
401
  if (button.message && translations.has(button.message)) {
391
- button.message = translations.get(button.message);
402
+ const translatedMessage = translations.get(button.message);
403
+ if (translatedMessage !== undefined) {
404
+ button.message = translatedMessage;
405
+ }
392
406
  }
393
407
  });
394
408
  });
@@ -560,7 +574,10 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
560
574
  insertPageStyle.run(pageStyleId, hexToInt(page.style.backgroundColor), page.style.backgroundColor ? 1 : 0);
561
575
  }
562
576
  else {
563
- pageStyleId = pageStyleMap.get(styleKey);
577
+ const existingPageStyleId = pageStyleMap.get(styleKey);
578
+ if (typeof existingPageStyleId === 'number') {
579
+ pageStyleId = existingPageStyleId;
580
+ }
564
581
  }
565
582
  }
566
583
  // Insert resource for page name
@@ -577,6 +594,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
577
594
  // Second pass: create buttons and their relationships
578
595
  Object.values(tree.pages).forEach((page) => {
579
596
  const numericPageId = pageIdMap.get(page.id);
597
+ if (numericPageId === undefined) {
598
+ return;
599
+ }
580
600
  if (page.buttons.length > 0) {
581
601
  // Calculate grid dimensions from page.grid or use fallback
582
602
  let gridWidth = 4; // Default fallback
@@ -631,7 +651,10 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
631
651
  insertButtonStyle.run(buttonStyleId, button.style.labelOnTop ? 1 : 0, button.style.transparent ? 1 : 0, hexToInt(button.style.fontColor), hexToInt(button.style.backgroundColor), hexToInt(button.style.borderColor), button.style.borderWidth, button.style.fontFamily, button.style.fontWeight === 'bold' ? 1 : 0, button.style.textUnderline ? 1 : 0, button.style.fontStyle === 'italic' ? 1 : 0, button.style.fontSize);
632
652
  }
633
653
  else {
634
- buttonStyleId = buttonStyleMap.get(styleKey);
654
+ const existingButtonStyleId = buttonStyleMap.get(styleKey);
655
+ if (typeof existingButtonStyleId === 'number') {
656
+ buttonStyleId = existingButtonStyleId;
657
+ }
635
658
  }
636
659
  }
637
660
  if (!insertedButtonIds.has(numericButtonId)) {
@@ -33,6 +33,10 @@ export interface AACPage {
33
33
  buttons: AACButton[];
34
34
  parentId: string | null;
35
35
  style?: AACStyle;
36
+ locale?: string;
37
+ descriptionHtml?: string;
38
+ images?: any[];
39
+ sounds?: any[];
36
40
  }
37
41
  export interface AACTree {
38
42
  pages: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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
  "types": "dist/index.d.ts",