@willwade/aac-processors 0.3.0 → 0.3.1

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.
@@ -44,16 +44,23 @@ export function decodeNsColor(buffer) {
44
44
  if (!buffer)
45
45
  return undefined;
46
46
  const s = bufferToLatinString(buffer);
47
- // Require decimal points to avoid matching bplist header bytes (e.g. "00").
48
- // Accept 3 or 4 components (RGB or RGBA).
49
- const rgbMatch = s.match(/(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+\.\d+)(?:\s+(\d+\.\d+))?/);
50
- if (rgbMatch) {
51
- const [, r, g, b] = rgbMatch;
52
- return rgbToHex(parseFloat(r), parseFloat(g), parseFloat(b));
47
+ // Match RGB[A] component strings like "1 0.949 0.561" or "0.5 0.5 0.5 1".
48
+ // Allow integer components (e.g. "1") but require at least one decimal to
49
+ // avoid matching bplist header bytes (e.g. "00 00 00").
50
+ const candidates = s.match(/\d+(?:\.\d+)?\s+\d+(?:\.\d+)?\s+\d+(?:\.\d+)?(?:\s+\d+(?:\.\d+)?)?/g);
51
+ if (candidates) {
52
+ // Find the first candidate that looks like RGB (has at least one decimal point).
53
+ const rgbStr = candidates.find((c) => c.includes('.'));
54
+ if (rgbStr) {
55
+ const parts = rgbStr.split(/\s+/).map(Number);
56
+ return rgbToHex(parts[0], parts[1], parts[2]);
57
+ }
53
58
  }
54
59
  return undefined;
55
60
  }
56
61
  const FONT_KEYWORDS = new Set([
62
+ 'bplist',
63
+ 'bplist00',
57
64
  'UIFont',
58
65
  'UIFontName',
59
66
  'UIFontPointSize',
@@ -35,6 +35,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
35
35
  const actionData = rawButton.ActionData || {};
36
36
  const label = rawButton.ButtonText ?? '';
37
37
  const ttsText = actionData.TTSText;
38
+ const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
38
39
  switch (buttonType) {
39
40
  case 'Jump': {
40
41
  const jumpTo = actionData.JumpTo;
@@ -45,7 +46,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
45
46
  intent: AACSemanticIntent.NAVIGATE_TO,
46
47
  targetId: target,
47
48
  platformData: {
48
- gotalkNow: { buttonType: 'Jump', jumpTo },
49
+ gotalkNow: { buttonType: 'Jump', jumpTo, jumpBook: actionData.JumpBook },
49
50
  },
50
51
  fallback: { type: 'NAVIGATE', targetPageId: target },
51
52
  },
@@ -53,25 +54,78 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
53
54
  targetPageId: target,
54
55
  };
55
56
  }
56
- case 'Audio': {
57
- // Audio buttons play a recorded .caf file named <pageId>-<buttonIndex>.caf
57
+ case 'Audio':
58
+ case 'Recorded': {
59
+ // Recorded audio: plays a .caf file named <pageId>-<buttonIndex>.caf.
60
+ // Recorded buttons may also have TrimStart/TrimEnd for audio trimming.
58
61
  const audioLocation = `${pageId}-${buttonIndex}.caf`;
59
62
  return {
60
63
  semanticAction: {
61
64
  category: AACSemanticCategory.MEDIA,
62
65
  intent: AACSemanticIntent.PLAY_SOUND,
63
66
  platformData: {
64
- gotalkNow: { buttonType: 'Audio', audioLocation },
67
+ gotalkNow: {
68
+ buttonType,
69
+ audioLocation,
70
+ trimStart: actionData.TrimStart,
71
+ trimEnd: actionData.TrimEnd,
72
+ },
65
73
  },
66
74
  fallback: { type: 'ACTION' },
67
75
  },
68
- message: label,
76
+ message: spoken,
77
+ };
78
+ }
79
+ case 'URL': {
80
+ return {
81
+ semanticAction: {
82
+ category: AACSemanticCategory.SYSTEM_CONTROL,
83
+ intent: AACSemanticIntent.WEB_NAVIGATE,
84
+ text: actionData.URL,
85
+ platformData: {
86
+ gotalkNow: {
87
+ buttonType: 'URL',
88
+ url: actionData.URL,
89
+ openInternally: actionData.OpenInternally,
90
+ },
91
+ },
92
+ fallback: { type: 'ACTION' },
93
+ },
94
+ message: spoken,
95
+ };
96
+ }
97
+ case 'GoVisual': {
98
+ return {
99
+ semanticAction: {
100
+ category: AACSemanticCategory.SYSTEM_CONTROL,
101
+ intent: AACSemanticIntent.PLATFORM_SPECIFIC,
102
+ platformData: {
103
+ gotalkNow: {
104
+ buttonType: 'GoVisual',
105
+ goVisual: actionData.GoVisual,
106
+ goVisualLink: actionData.GoVisualLink,
107
+ },
108
+ },
109
+ fallback: { type: 'ACTION' },
110
+ },
111
+ message: spoken,
69
112
  };
70
113
  }
71
- case 'Express':
72
- case 'Bookmark':
73
- case 'URL':
74
114
  case 'Video': {
115
+ return {
116
+ semanticAction: {
117
+ category: AACSemanticCategory.MEDIA,
118
+ intent: AACSemanticIntent.PLAY_VIDEO,
119
+ platformData: {
120
+ gotalkNow: { buttonType: 'Video' },
121
+ },
122
+ fallback: { type: 'ACTION' },
123
+ },
124
+ message: spoken,
125
+ };
126
+ }
127
+ case 'Express':
128
+ case 'Bookmark': {
75
129
  return {
76
130
  semanticAction: {
77
131
  category: AACSemanticCategory.SYSTEM_CONTROL,
@@ -81,20 +135,19 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
81
135
  },
82
136
  fallback: { type: 'ACTION' },
83
137
  },
84
- message: ttsText && ttsText.length > 0 ? ttsText : label,
138
+ message: spoken,
85
139
  };
86
140
  }
87
141
  case 'TTS':
88
142
  default: {
89
- // When TTSText is empty the visible ButtonText is spoken.
90
- const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
143
+ // Empty/placeholder cells (no ButtonType or no label) get a SPEAK with empty text.
91
144
  return {
92
145
  semanticAction: {
93
146
  category: AACSemanticCategory.COMMUNICATION,
94
147
  intent: AACSemanticIntent.SPEAK_TEXT,
95
148
  text: spoken,
96
149
  platformData: {
97
- gotalkNow: { buttonType: 'TTS' },
150
+ gotalkNow: { buttonType: buttonType === 'TTS' ? 'TTS' : 'Empty' },
98
151
  },
99
152
  fallback: { type: 'SPEAK', message: spoken },
100
153
  },
@@ -302,17 +355,39 @@ class GotalkNowProcessor extends BaseProcessor {
302
355
  if (first?.SourceImageName)
303
356
  button.symbolPath = first.SourceImageName;
304
357
  }
305
- // Visibility: GoTalk buttons can be disabled.
306
- if (rawButton.Enabled === false) {
358
+ // Visibility: GoTalk buttons can be disabled via either field.
359
+ if (rawButton.Enabled === false || rawButton.Disabled === true) {
307
360
  button.visibility = 'Disabled';
308
361
  }
309
- // Preserve raw style data opaquely for round-trip styling.
362
+ // Auditory cue (played when scanning/highlighting).
363
+ if (rawButton.CueType && rawButton.CueType !== 'None') {
364
+ const cueText = rawButton.CueData?.TTSText;
365
+ if (cueText && cueText.length > 0) {
366
+ button.audioDescription = cueText;
367
+ }
368
+ }
369
+ // Recorded audio reference.
370
+ const btnType = rawButton.ButtonType;
371
+ if (btnType === 'Audio' || btnType === 'Recorded') {
372
+ button.audioRecording = {
373
+ identifier: `${pageId}-${btnIndex}.caf`,
374
+ };
375
+ }
376
+ // AfterAction: motor-planning "speak and return" behaviour.
377
+ // AfterAction is an enum (0=stay, 2=..., 50=jump-back) and
378
+ // AfterActionData has the same shape as ActionData (e.g. {JumpTo: 0}).
379
+ const afterActionJumpTo = rawButton.AfterActionData?.JumpTo;
380
+ // Preserve raw style + extended data opaquely for round-trip styling.
310
381
  button.parameters = {
311
382
  ...(button.parameters || {}),
312
383
  __gotalkNow: {
313
384
  buttonType: rawButton.ButtonType,
314
385
  textShadow: rawButton.TextShadow,
315
386
  textCenter: rawButton.TextCenter,
387
+ cueType: rawButton.CueType,
388
+ afterAction: rawButton.AfterAction,
389
+ afterActionJumpTo: afterActionJumpTo !== undefined ? String(afterActionJumpTo) : undefined,
390
+ jumpBook: rawButton.ActionData?.JumpBook,
316
391
  },
317
392
  };
318
393
  page._loadButton(button);
@@ -2960,8 +2960,8 @@ class GridsetProcessor extends BaseProcessor {
2960
2960
  cellContent.CaptionAndImage = captionAndImage;
2961
2961
  }
2962
2962
  const cellData = {
2963
- '@_X': position.x + 1, // Grid3 uses 1-based X coordinates
2964
- '@_Y': position.y + yOffset + 1, // Grid3 uses 1-based Y coordinates with workspace offset
2963
+ '@_X': position.x, // Grid 3 coordinates are 0-based (matching loader)
2964
+ '@_Y': position.y + yOffset,
2965
2965
  '@_ColumnSpan': position.columnSpan,
2966
2966
  '@_RowSpan': position.rowSpan,
2967
2967
  Content: cellContent,
@@ -144,14 +144,21 @@ class ObfProcessor extends BaseProcessor {
144
144
  const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
145
145
  const images = boardData.images;
146
146
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
147
+ // Resolve load_board.path to a board ID (strip .obf suffix if present).
148
+ // The OBF spec says load_board.path is a file path, but internally we
149
+ // store the board ID as targetPageId.
150
+ const rawPath = btn.load_board?.path;
151
+ const boardRef = rawPath
152
+ ? rawPath.replace(/\.obf$/i, '').replace(/\.obz$/i, '')
153
+ : undefined;
147
154
  const semanticAction = btn.load_board
148
155
  ? {
149
156
  category: AACSemanticCategory.NAVIGATION,
150
157
  intent: AACSemanticIntent.NAVIGATE_TO,
151
- targetId: btn.load_board.path,
158
+ targetId: boardRef,
152
159
  fallback: {
153
160
  type: 'NAVIGATE',
154
- targetPageId: btn.load_board.path,
161
+ targetPageId: boardRef,
155
162
  },
156
163
  }
157
164
  : {
@@ -199,7 +206,7 @@ class ObfProcessor extends BaseProcessor {
199
206
  resolvedImageEntry: resolvedImage,
200
207
  parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
201
208
  semanticAction,
202
- targetPageId: btn.load_board?.path,
209
+ targetPageId: boardRef,
203
210
  semantic_id: btn.semantic_id, // Extract semantic_id if present
204
211
  });
205
212
  }));
@@ -618,7 +625,7 @@ class ObfProcessor extends BaseProcessor {
618
625
  vocalization: button.message || button.label,
619
626
  load_board: button.semanticAction?.intent === AACSemanticIntent.NAVIGATE_TO && button.targetPageId
620
627
  ? {
621
- path: button.targetPageId,
628
+ path: this.getPageFilename(button.targetPageId, metadata),
622
629
  }
623
630
  : undefined,
624
631
  background_color: button.style?.backgroundColor,
@@ -117,7 +117,14 @@ export interface AACSemanticAction {
117
117
  gotalkNow?: {
118
118
  buttonType: string;
119
119
  jumpTo?: number;
120
+ jumpBook?: string;
120
121
  audioLocation?: string;
122
+ trimStart?: number;
123
+ trimEnd?: number;
124
+ url?: string;
125
+ openInternally?: boolean;
126
+ goVisual?: string;
127
+ goVisualLink?: unknown;
121
128
  };
122
129
  };
123
130
  fallback?: {
@@ -48,16 +48,23 @@ function decodeNsColor(buffer) {
48
48
  if (!buffer)
49
49
  return undefined;
50
50
  const s = bufferToLatinString(buffer);
51
- // Require decimal points to avoid matching bplist header bytes (e.g. "00").
52
- // Accept 3 or 4 components (RGB or RGBA).
53
- const rgbMatch = s.match(/(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+\.\d+)(?:\s+(\d+\.\d+))?/);
54
- if (rgbMatch) {
55
- const [, r, g, b] = rgbMatch;
56
- return rgbToHex(parseFloat(r), parseFloat(g), parseFloat(b));
51
+ // Match RGB[A] component strings like "1 0.949 0.561" or "0.5 0.5 0.5 1".
52
+ // Allow integer components (e.g. "1") but require at least one decimal to
53
+ // avoid matching bplist header bytes (e.g. "00 00 00").
54
+ const candidates = s.match(/\d+(?:\.\d+)?\s+\d+(?:\.\d+)?\s+\d+(?:\.\d+)?(?:\s+\d+(?:\.\d+)?)?/g);
55
+ if (candidates) {
56
+ // Find the first candidate that looks like RGB (has at least one decimal point).
57
+ const rgbStr = candidates.find((c) => c.includes('.'));
58
+ if (rgbStr) {
59
+ const parts = rgbStr.split(/\s+/).map(Number);
60
+ return rgbToHex(parts[0], parts[1], parts[2]);
61
+ }
57
62
  }
58
63
  return undefined;
59
64
  }
60
65
  const FONT_KEYWORDS = new Set([
66
+ 'bplist',
67
+ 'bplist00',
61
68
  'UIFont',
62
69
  'UIFontName',
63
70
  'UIFontPointSize',
@@ -41,6 +41,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
41
41
  const actionData = rawButton.ActionData || {};
42
42
  const label = rawButton.ButtonText ?? '';
43
43
  const ttsText = actionData.TTSText;
44
+ const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
44
45
  switch (buttonType) {
45
46
  case 'Jump': {
46
47
  const jumpTo = actionData.JumpTo;
@@ -51,7 +52,7 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
51
52
  intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
52
53
  targetId: target,
53
54
  platformData: {
54
- gotalkNow: { buttonType: 'Jump', jumpTo },
55
+ gotalkNow: { buttonType: 'Jump', jumpTo, jumpBook: actionData.JumpBook },
55
56
  },
56
57
  fallback: { type: 'NAVIGATE', targetPageId: target },
57
58
  },
@@ -59,25 +60,78 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
59
60
  targetPageId: target,
60
61
  };
61
62
  }
62
- case 'Audio': {
63
- // Audio buttons play a recorded .caf file named <pageId>-<buttonIndex>.caf
63
+ case 'Audio':
64
+ case 'Recorded': {
65
+ // Recorded audio: plays a .caf file named <pageId>-<buttonIndex>.caf.
66
+ // Recorded buttons may also have TrimStart/TrimEnd for audio trimming.
64
67
  const audioLocation = `${pageId}-${buttonIndex}.caf`;
65
68
  return {
66
69
  semanticAction: {
67
70
  category: treeStructure_1.AACSemanticCategory.MEDIA,
68
71
  intent: treeStructure_1.AACSemanticIntent.PLAY_SOUND,
69
72
  platformData: {
70
- gotalkNow: { buttonType: 'Audio', audioLocation },
73
+ gotalkNow: {
74
+ buttonType,
75
+ audioLocation,
76
+ trimStart: actionData.TrimStart,
77
+ trimEnd: actionData.TrimEnd,
78
+ },
71
79
  },
72
80
  fallback: { type: 'ACTION' },
73
81
  },
74
- message: label,
82
+ message: spoken,
83
+ };
84
+ }
85
+ case 'URL': {
86
+ return {
87
+ semanticAction: {
88
+ category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
89
+ intent: treeStructure_1.AACSemanticIntent.WEB_NAVIGATE,
90
+ text: actionData.URL,
91
+ platformData: {
92
+ gotalkNow: {
93
+ buttonType: 'URL',
94
+ url: actionData.URL,
95
+ openInternally: actionData.OpenInternally,
96
+ },
97
+ },
98
+ fallback: { type: 'ACTION' },
99
+ },
100
+ message: spoken,
101
+ };
102
+ }
103
+ case 'GoVisual': {
104
+ return {
105
+ semanticAction: {
106
+ category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
107
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
108
+ platformData: {
109
+ gotalkNow: {
110
+ buttonType: 'GoVisual',
111
+ goVisual: actionData.GoVisual,
112
+ goVisualLink: actionData.GoVisualLink,
113
+ },
114
+ },
115
+ fallback: { type: 'ACTION' },
116
+ },
117
+ message: spoken,
75
118
  };
76
119
  }
77
- case 'Express':
78
- case 'Bookmark':
79
- case 'URL':
80
120
  case 'Video': {
121
+ return {
122
+ semanticAction: {
123
+ category: treeStructure_1.AACSemanticCategory.MEDIA,
124
+ intent: treeStructure_1.AACSemanticIntent.PLAY_VIDEO,
125
+ platformData: {
126
+ gotalkNow: { buttonType: 'Video' },
127
+ },
128
+ fallback: { type: 'ACTION' },
129
+ },
130
+ message: spoken,
131
+ };
132
+ }
133
+ case 'Express':
134
+ case 'Bookmark': {
81
135
  return {
82
136
  semanticAction: {
83
137
  category: treeStructure_1.AACSemanticCategory.SYSTEM_CONTROL,
@@ -87,20 +141,19 @@ function buildSemanticAction(rawButton, pageId, buttonIndex) {
87
141
  },
88
142
  fallback: { type: 'ACTION' },
89
143
  },
90
- message: ttsText && ttsText.length > 0 ? ttsText : label,
144
+ message: spoken,
91
145
  };
92
146
  }
93
147
  case 'TTS':
94
148
  default: {
95
- // When TTSText is empty the visible ButtonText is spoken.
96
- const spoken = ttsText && ttsText.length > 0 ? ttsText : label;
149
+ // Empty/placeholder cells (no ButtonType or no label) get a SPEAK with empty text.
97
150
  return {
98
151
  semanticAction: {
99
152
  category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
100
153
  intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
101
154
  text: spoken,
102
155
  platformData: {
103
- gotalkNow: { buttonType: 'TTS' },
156
+ gotalkNow: { buttonType: buttonType === 'TTS' ? 'TTS' : 'Empty' },
104
157
  },
105
158
  fallback: { type: 'SPEAK', message: spoken },
106
159
  },
@@ -308,17 +361,39 @@ class GotalkNowProcessor extends baseProcessor_1.BaseProcessor {
308
361
  if (first?.SourceImageName)
309
362
  button.symbolPath = first.SourceImageName;
310
363
  }
311
- // Visibility: GoTalk buttons can be disabled.
312
- if (rawButton.Enabled === false) {
364
+ // Visibility: GoTalk buttons can be disabled via either field.
365
+ if (rawButton.Enabled === false || rawButton.Disabled === true) {
313
366
  button.visibility = 'Disabled';
314
367
  }
315
- // Preserve raw style data opaquely for round-trip styling.
368
+ // Auditory cue (played when scanning/highlighting).
369
+ if (rawButton.CueType && rawButton.CueType !== 'None') {
370
+ const cueText = rawButton.CueData?.TTSText;
371
+ if (cueText && cueText.length > 0) {
372
+ button.audioDescription = cueText;
373
+ }
374
+ }
375
+ // Recorded audio reference.
376
+ const btnType = rawButton.ButtonType;
377
+ if (btnType === 'Audio' || btnType === 'Recorded') {
378
+ button.audioRecording = {
379
+ identifier: `${pageId}-${btnIndex}.caf`,
380
+ };
381
+ }
382
+ // AfterAction: motor-planning "speak and return" behaviour.
383
+ // AfterAction is an enum (0=stay, 2=..., 50=jump-back) and
384
+ // AfterActionData has the same shape as ActionData (e.g. {JumpTo: 0}).
385
+ const afterActionJumpTo = rawButton.AfterActionData?.JumpTo;
386
+ // Preserve raw style + extended data opaquely for round-trip styling.
316
387
  button.parameters = {
317
388
  ...(button.parameters || {}),
318
389
  __gotalkNow: {
319
390
  buttonType: rawButton.ButtonType,
320
391
  textShadow: rawButton.TextShadow,
321
392
  textCenter: rawButton.TextCenter,
393
+ cueType: rawButton.CueType,
394
+ afterAction: rawButton.AfterAction,
395
+ afterActionJumpTo: afterActionJumpTo !== undefined ? String(afterActionJumpTo) : undefined,
396
+ jumpBook: rawButton.ActionData?.JumpBook,
322
397
  },
323
398
  };
324
399
  page._loadButton(button);
@@ -2963,8 +2963,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
2963
2963
  cellContent.CaptionAndImage = captionAndImage;
2964
2964
  }
2965
2965
  const cellData = {
2966
- '@_X': position.x + 1, // Grid3 uses 1-based X coordinates
2967
- '@_Y': position.y + yOffset + 1, // Grid3 uses 1-based Y coordinates with workspace offset
2966
+ '@_X': position.x, // Grid 3 coordinates are 0-based (matching loader)
2967
+ '@_Y': position.y + yOffset,
2968
2968
  '@_ColumnSpan': position.columnSpan,
2969
2969
  '@_RowSpan': position.rowSpan,
2970
2970
  Content: cellContent,
@@ -147,14 +147,21 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
147
147
  const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
148
148
  const images = boardData.images;
149
149
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
150
+ // Resolve load_board.path to a board ID (strip .obf suffix if present).
151
+ // The OBF spec says load_board.path is a file path, but internally we
152
+ // store the board ID as targetPageId.
153
+ const rawPath = btn.load_board?.path;
154
+ const boardRef = rawPath
155
+ ? rawPath.replace(/\.obf$/i, '').replace(/\.obz$/i, '')
156
+ : undefined;
150
157
  const semanticAction = btn.load_board
151
158
  ? {
152
159
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
153
160
  intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
154
- targetId: btn.load_board.path,
161
+ targetId: boardRef,
155
162
  fallback: {
156
163
  type: 'NAVIGATE',
157
- targetPageId: btn.load_board.path,
164
+ targetPageId: boardRef,
158
165
  },
159
166
  }
160
167
  : {
@@ -202,7 +209,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
202
209
  resolvedImageEntry: resolvedImage,
203
210
  parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
204
211
  semanticAction,
205
- targetPageId: btn.load_board?.path,
212
+ targetPageId: boardRef,
206
213
  semantic_id: btn.semantic_id, // Extract semantic_id if present
207
214
  });
208
215
  }));
@@ -621,7 +628,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
621
628
  vocalization: button.message || button.label,
622
629
  load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
623
630
  ? {
624
- path: button.targetPageId,
631
+ path: this.getPageFilename(button.targetPageId, metadata),
625
632
  }
626
633
  : undefined,
627
634
  background_color: button.style?.backgroundColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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
  "browser": "dist/browser/index.browser.js",