@willwade/aac-processors 0.0.4 → 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.
@@ -56,7 +56,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
56
56
  r: text,
57
57
  },
58
58
  {
59
- r: { '__cdata': ' ' },
59
+ r: { __cdata: ' ' },
60
60
  },
61
61
  ],
62
62
  },
@@ -131,92 +131,85 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
131
131
  },
132
132
  };
133
133
  case 'SPEAK_TEXT':
134
- case 'SPEAK_IMMEDIATE':
135
- // For communication buttons, insert text into message bar (sentence building)
136
- // Grid3 requires explicit trailing space for automatic word spacing
137
- // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
138
- // Users can speak the complete sentence with a dedicated Speak button
139
- {
140
- let text = semanticAction.text || button.message || button.label || '';
141
- // Remove trailing space from message if present (we'll add it as separate segment)
142
- if (text.endsWith(' ')) {
143
- text = text.slice(0, -1);
144
- }
145
- return {
146
- Command: {
147
- '@_ID': 'Action.InsertText',
148
- Parameter: {
149
- '@_Key': 'text',
150
- p: {
151
- s: [
152
- {
153
- r: text,
154
- },
155
- {
156
- r: { '__cdata': ' ' },
157
- },
158
- ],
159
- },
134
+ case 'SPEAK_IMMEDIATE': {
135
+ // Users can speak the complete sentence with a dedicated Speak button // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace) // Grid3 requires explicit trailing space for automatic word spacing // For communication buttons, insert text into message bar (sentence building)
136
+ let text = semanticAction.text || button.message || button.label || '';
137
+ // Remove trailing space from message if present (we'll add it as separate segment)
138
+ if (text.endsWith(' ')) {
139
+ text = text.slice(0, -1);
140
+ }
141
+ return {
142
+ Command: {
143
+ '@_ID': 'Action.InsertText',
144
+ Parameter: {
145
+ '@_Key': 'text',
146
+ p: {
147
+ s: [
148
+ {
149
+ r: text,
150
+ },
151
+ {
152
+ r: { __cdata: ' ' },
153
+ },
154
+ ],
160
155
  },
161
156
  },
162
- };
157
+ },
158
+ };
159
+ }
160
+ case 'INSERT_TEXT': {
161
+ // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace) // Add trailing space for word buttons to enable sentence building
162
+ let text = semanticAction.text || button.message || button.label || '';
163
+ // Remove trailing space from message if present (we'll add it as separate segment)
164
+ if (text.endsWith(' ')) {
165
+ text = text.slice(0, -1);
163
166
  }
164
- case 'INSERT_TEXT':
165
- // Add trailing space for word buttons to enable sentence building
166
- // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
167
- {
168
- let text = semanticAction.text || button.message || button.label || '';
169
- // Remove trailing space from message if present (we'll add it as separate segment)
170
- if (text.endsWith(' ')) {
171
- text = text.slice(0, -1);
172
- }
173
- return {
174
- Command: {
175
- '@_ID': 'Action.InsertText',
176
- Parameter: {
177
- '@_Key': 'text',
178
- p: {
179
- s: [
180
- {
181
- r: text,
182
- },
183
- {
184
- r: { '__cdata': ' ' },
185
- },
186
- ],
187
- },
167
+ return {
168
+ Command: {
169
+ '@_ID': 'Action.InsertText',
170
+ Parameter: {
171
+ '@_Key': 'text',
172
+ p: {
173
+ s: [
174
+ {
175
+ r: text,
176
+ },
177
+ {
178
+ r: { __cdata: ' ' },
179
+ },
180
+ ],
188
181
  },
189
182
  },
190
- };
191
- }
192
- default:
193
- // Fallback to insert text with structured XML format
183
+ },
184
+ };
185
+ }
186
+ default: {
194
187
  // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
195
- {
196
- let text = semanticAction.text || button.message || button.label || '';
197
- // Remove trailing space from message if present (we'll add it as separate segment)
198
- if (text.endsWith(' ')) {
199
- text = text.slice(0, -1);
200
- }
201
- return {
202
- Command: {
203
- '@_ID': 'Action.InsertText',
204
- Parameter: {
205
- '@_Key': 'text',
206
- p: {
207
- s: [
208
- {
209
- r: text,
210
- },
211
- {
212
- r: { '__cdata': ' ' },
213
- },
214
- ],
215
- },
188
+ // Fallback to insert text with structured XML format
189
+ let text = semanticAction.text || button.message || button.label || '';
190
+ // Remove trailing space from message if present (we'll add it as separate segment)
191
+ if (text.endsWith(' ')) {
192
+ text = text.slice(0, -1);
193
+ }
194
+ return {
195
+ Command: {
196
+ '@_ID': 'Action.InsertText',
197
+ Parameter: {
198
+ '@_Key': 'text',
199
+ p: {
200
+ s: [
201
+ {
202
+ r: text,
203
+ },
204
+ {
205
+ r: { __cdata: ' ' },
206
+ },
207
+ ],
216
208
  },
217
209
  },
218
- };
219
- }
210
+ },
211
+ };
212
+ }
220
213
  }
221
214
  }
222
215
  // Helper function to convert Grid 3 style to AACStyle
@@ -288,10 +281,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
288
281
  if (entries) {
289
282
  const arr = Array.isArray(entries) ? entries : [entries];
290
283
  for (const ent of arr) {
291
- const staticFile = (ent['@_StaticFile'] ||
292
- ent.StaticFile ||
293
- ent.staticFile ||
294
- '').replace(/\\/g, '/');
284
+ const rawStaticFile = ent['@_StaticFile'] || ent.StaticFile || ent.staticFile;
285
+ const staticFile = typeof rawStaticFile === 'string' ? rawStaticFile.replace(/\\/g, '/') : '';
295
286
  if (!staticFile)
296
287
  continue;
297
288
  const df = ent.DynamicFiles || ent.dynamicFiles;
@@ -987,26 +978,22 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
987
978
  const buttonImages = new Map();
988
979
  // Helper function to add style and return its ID
989
980
  const addStyle = (style) => {
990
- if (!style || typeof style !== 'object')
991
- return '';
992
- const obj = style;
993
- if (Object.keys(obj).length === 0)
981
+ if (!style)
994
982
  return '';
995
- const styleKey = JSON.stringify(obj);
983
+ const normalizedStyle = { ...style };
984
+ const styleKey = JSON.stringify(normalizedStyle);
996
985
  const existing = uniqueStyles.get(styleKey);
997
986
  if (existing)
998
987
  return existing.id;
999
988
  const styleId = `Style${styleIdCounter++}`;
1000
- uniqueStyles.set(styleKey, { id: styleId, style: obj });
989
+ uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
1001
990
  return styleId;
1002
991
  };
1003
992
  // Collect styles from all pages and buttons
1004
993
  Object.values(tree.pages).forEach((page) => {
1005
- if (page.style)
1006
- addStyle(page.style);
994
+ addStyle(page.style);
1007
995
  page.buttons.forEach((button) => {
1008
- if (button.style)
1009
- addStyle(button.style);
996
+ addStyle(button.style);
1010
997
  });
1011
998
  });
1012
999
  // Get the home/start grid from tree.rootId, fallback to first page
@@ -1053,8 +1040,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1053
1040
  // When TileColour is present, BackColour is the surround (outer area)
1054
1041
  // For "None" surround, just use BackColour for the fill (no TileColour)
1055
1042
  BackColour: this.ensureAlphaChannel(style.backgroundColor),
1056
- BorderColour: this.ensureAlphaChannel(style.borderColor) || '#000000FF',
1057
- FontColour: this.ensureAlphaChannel(style.fontColor) || '#000000FF',
1043
+ BorderColour: this.ensureAlphaChannel(style.borderColor),
1044
+ FontColour: this.ensureAlphaChannel(style.fontColor),
1058
1045
  FontName: style.fontFamily || 'Arial',
1059
1046
  FontSize: style.fontSize?.toString() || '16',
1060
1047
  };
@@ -1081,7 +1068,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1081
1068
  // Collect grid file paths for FileMap.xml
1082
1069
  const gridFilePaths = [];
1083
1070
  // Create a grid for each page
1084
- Object.values(tree.pages).forEach((page, index) => {
1071
+ Object.values(tree.pages).forEach((page) => {
1085
1072
  const gridData = {
1086
1073
  Grid: {
1087
1074
  '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
@@ -1121,8 +1108,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1121
1108
  if (button.image) {
1122
1109
  // Try to determine file extension from image name or default to PNG
1123
1110
  let imageExt = 'png';
1124
- if (button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
1125
- imageExt = button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)[1].toLowerCase();
1111
+ const imageMatch = button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i);
1112
+ if (imageMatch) {
1113
+ imageExt = imageMatch[1].toLowerCase();
1126
1114
  }
1127
1115
  // Grid3 dynamically constructs image filenames by prepending cell coordinates
1128
1116
  // The XML should only contain the suffix: -0-text-0.{ext}
@@ -1131,7 +1119,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1131
1119
  // Extract image data from button parameters if available
1132
1120
  // (AstericsGridProcessor stores it there during loadIntoTree)
1133
1121
  let imageData = Buffer.alloc(0);
1134
- if (button.parameters && button.parameters.imageData && Buffer.isBuffer(button.parameters.imageData)) {
1122
+ if (button.parameters &&
1123
+ button.parameters.imageData &&
1124
+ Buffer.isBuffer(button.parameters.imageData)) {
1135
1125
  imageData = button.parameters.imageData;
1136
1126
  }
1137
1127
  // Store image data for later writing to ZIP
@@ -1182,7 +1172,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1182
1172
  }
1183
1173
  return cellData;
1184
1174
  }),
1185
- ]
1175
+ ],
1186
1176
  }
1187
1177
  : { Cell: [] },
1188
1178
  },
@@ -1202,7 +1192,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1202
1192
  zip.addFile(gridPath, Buffer.from(xmlContent, 'utf8'));
1203
1193
  });
1204
1194
  // Write image files to ZIP
1205
- buttonImages.forEach((imgData, buttonId) => {
1195
+ buttonImages.forEach((imgData) => {
1206
1196
  if (imgData.imageData && imgData.imageData.length > 0) {
1207
1197
  // Create image path in the grid's directory
1208
1198
  const imagePath = `Grids\\${imgData.pageName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
@@ -1221,7 +1211,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1221
1211
  const imageFiles = [];
1222
1212
  // Collect image filenames for buttons on this page
1223
1213
  // IMPORTANT: FileMap.xml requires full paths like "Grids\PageName\1-5-0-text-0.png"
1224
- buttonImages.forEach((imgData, buttonId) => {
1214
+ buttonImages.forEach((imgData) => {
1225
1215
  if (imgData.pageName === gridName && imgData.imageData.length > 0) {
1226
1216
  const imagePath = `Grids\\${gridName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
1227
1217
  imageFiles.push(imagePath);
@@ -1229,9 +1219,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1229
1219
  });
1230
1220
  return {
1231
1221
  '@_StaticFile': gridPath,
1232
- DynamicFiles: imageFiles.length > 0 ? {
1233
- File: imageFiles
1234
- } : {},
1222
+ DynamicFiles: imageFiles.length > 0
1223
+ ? {
1224
+ File: imageFiles,
1225
+ }
1226
+ : {},
1235
1227
  };
1236
1228
  }),
1237
1229
  },
@@ -11,5 +11,6 @@ export { getPageTokenImageMap, getAllowedImageEntries, openImage } from './grids
11
11
  export { getPageTokenImageMap as getGridsetPageTokenImageMap, getAllowedImageEntries as getGridsetAllowedImageEntries, openImage as openGridsetImage, } from './gridset/helpers';
12
12
  export { resolveGrid3CellImage } from './gridset/resolver';
13
13
  export { createWordlist, extractWordlists, updateWordlist, wordlistToXml, type WordList, type WordListItem, } from './gridset/wordlistHelpers';
14
+ export { DEFAULT_GRID3_STYLES, CATEGORY_STYLES, ensureAlphaChannel, createDefaultStylesXml, createCategoryStyle, type Grid3Style, } from './gridset/styleHelpers';
14
15
  export { getPageTokenImageMap as getSnapPageTokenImageMap, getAllowedImageEntries as getSnapAllowedImageEntries, openImage as openSnapImage, } from './snap/helpers';
15
16
  export { getPageTokenImageMap as getTouchChatPageTokenImageMap, getAllowedImageEntries as getTouchChatAllowedImageEntries, openImage as openTouchChatImage, } from './touchchat/helpers';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.openTouchChatImage = exports.getTouchChatAllowedImageEntries = exports.getTouchChatPageTokenImageMap = exports.openSnapImage = exports.getSnapAllowedImageEntries = exports.getSnapPageTokenImageMap = exports.wordlistToXml = exports.updateWordlist = exports.extractWordlists = exports.createWordlist = exports.resolveGrid3CellImage = exports.openGridsetImage = exports.getGridsetAllowedImageEntries = exports.getGridsetPageTokenImageMap = exports.openImage = exports.getAllowedImageEntries = exports.getPageTokenImageMap = exports.AstericsGridProcessor = exports.TouchChatProcessor = exports.SnapProcessor = exports.OpmlProcessor = exports.ObfProcessor = exports.GridsetProcessor = exports.ExcelProcessor = exports.DotProcessor = exports.ApplePanelsProcessor = void 0;
3
+ exports.openTouchChatImage = exports.getTouchChatAllowedImageEntries = exports.getTouchChatPageTokenImageMap = exports.openSnapImage = exports.getSnapAllowedImageEntries = exports.getSnapPageTokenImageMap = exports.createCategoryStyle = exports.createDefaultStylesXml = exports.ensureAlphaChannel = exports.CATEGORY_STYLES = exports.DEFAULT_GRID3_STYLES = exports.wordlistToXml = exports.updateWordlist = exports.extractWordlists = exports.createWordlist = exports.resolveGrid3CellImage = exports.openGridsetImage = exports.getGridsetAllowedImageEntries = exports.getGridsetPageTokenImageMap = exports.openImage = exports.getAllowedImageEntries = exports.getPageTokenImageMap = exports.AstericsGridProcessor = exports.TouchChatProcessor = exports.SnapProcessor = exports.OpmlProcessor = exports.ObfProcessor = exports.GridsetProcessor = exports.ExcelProcessor = exports.DotProcessor = exports.ApplePanelsProcessor = void 0;
4
4
  var applePanelsProcessor_1 = require("./applePanelsProcessor");
5
5
  Object.defineProperty(exports, "ApplePanelsProcessor", { enumerable: true, get: function () { return applePanelsProcessor_1.ApplePanelsProcessor; } });
6
6
  var dotProcessor_1 = require("./dotProcessor");
@@ -36,6 +36,13 @@ Object.defineProperty(exports, "createWordlist", { enumerable: true, get: functi
36
36
  Object.defineProperty(exports, "extractWordlists", { enumerable: true, get: function () { return wordlistHelpers_1.extractWordlists; } });
37
37
  Object.defineProperty(exports, "updateWordlist", { enumerable: true, get: function () { return wordlistHelpers_1.updateWordlist; } });
38
38
  Object.defineProperty(exports, "wordlistToXml", { enumerable: true, get: function () { return wordlistHelpers_1.wordlistToXml; } });
39
+ // Gridset (Grid 3) style helpers
40
+ var styleHelpers_1 = require("./gridset/styleHelpers");
41
+ Object.defineProperty(exports, "DEFAULT_GRID3_STYLES", { enumerable: true, get: function () { return styleHelpers_1.DEFAULT_GRID3_STYLES; } });
42
+ Object.defineProperty(exports, "CATEGORY_STYLES", { enumerable: true, get: function () { return styleHelpers_1.CATEGORY_STYLES; } });
43
+ Object.defineProperty(exports, "ensureAlphaChannel", { enumerable: true, get: function () { return styleHelpers_1.ensureAlphaChannel; } });
44
+ Object.defineProperty(exports, "createDefaultStylesXml", { enumerable: true, get: function () { return styleHelpers_1.createDefaultStylesXml; } });
45
+ Object.defineProperty(exports, "createCategoryStyle", { enumerable: true, get: function () { return styleHelpers_1.createCategoryStyle; } });
39
46
  // Snap helpers (stubs)
40
47
  var helpers_3 = require("./snap/helpers");
41
48
  Object.defineProperty(exports, "getSnapPageTokenImageMap", { enumerable: true, get: function () { return helpers_3.getPageTokenImageMap; } });
@@ -5,6 +5,8 @@ declare class ObfProcessor extends BaseProcessor {
5
5
  private processBoard;
6
6
  extractTexts(filePathOrBuffer: string | Buffer): string[];
7
7
  loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
8
+ private buildGridMetadata;
9
+ private createObfBoardFromPage;
8
10
  processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
9
11
  saveFromTree(tree: AACTree, outputPath: string): void;
10
12
  /**
@@ -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: ' ',