@willwade/aac-processors 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -658,13 +658,8 @@ npm test
658
658
 
659
659
  - The repository keeps `package.json` at `0.0.0-development`; release tags control the published version.
660
660
  - Create a GitHub release with a semantic tag (e.g. `v2.1.0`). Publishing only runs for non-prerelease tags.
661
- - Add an `NPM_TOKEN` repository secret with publish rights. The release workflow uses it to authenticate and calls `npm publish`.
662
661
  - The workflow (`.github/workflows/publish.yml`) automatically installs dependencies, rewrites the package version from the tag, and runs the standard publish pipeline.
663
662
 
664
- **Private distribution options**
665
- - Unscoped packages on npm must be public. To keep this package private, re-scope it (e.g. `@your-org/aac-processors`) and configure `publishConfig.access: "restricted"`—this requires an npm org with paid seats.
666
- - Alternatively, publish to GitHub Packages by adjusting the workflow’s registry URL and using a `GITHUB_TOKEN` with the `packages: write` permission.
667
-
668
663
  ---
669
664
 
670
665
  ## 📄 License
@@ -715,15 +710,6 @@ Inspired by the Python AACProcessors project
715
710
  - [ ] **Add Symbol Tools coverage** (currently 0%) - Implement tests for PCS and ARASAAC symbol lookups to reach >70% coverage
716
711
  - [ ] **Fix property-based test failures** - Resolve TypeScript interface compatibility issues in edge case generators
717
712
 
718
- ### Recently Completed ✅
719
-
720
- - [x] **Core utilities test coverage** - Complete implementation for analyze.ts and fileProcessor.ts (0% → 100% coverage, 45 comprehensive tests, src/core/ directory 30% → 83% coverage)
721
- - [x] **CLI test infrastructure** - Fixed DotProcessor parsing and test expectations (0 → 25 passing tests, 100% CLI functionality)
722
- - [x] **Critical linting fixes** - Resolved unsafe argument types and CLI type safety issues (177 → 123 errors, 32% improvement)
723
- - [x] **Audio test syntax fixes** - Fixed non-null assertion errors in audio tests (21 → 5 failing tests, 76% improvement)
724
- - [x] **Comprehensive styling support** - Complete implementation across all AAC formats with cross-format preservation (Added: AACStyle interface, enhanced all processors, 7 new test cases, complete documentation)
725
- - [x] **TouchChatProcessor save/load functionality** - Fixed button persistence and ID mapping (coverage improved from 57.62% to 86.44%)
726
- - [x] **Build integration** - Ensure `npm run build` is executed before CLI tests (Fixed: All test scripts now automatically build before running)
727
713
 
728
714
  ### Medium Priority
729
715
 
@@ -749,29 +735,6 @@ Inspired by the Python AACProcessors project
749
735
  - [ ] **CI/CD improvements** - Add automated releases and npm publishing
750
736
  - [ ] **Documentation improvements** - Add more real-world examples and tutorials
751
737
 
752
- ### Known Issues
753
-
754
- - ⚠️ **Audio Persistence**: 5 functional tests failing due to audio recording not persisting through SnapProcessor save/load cycle
755
- - ⚠️ **Grid3 Layout**: Grid sizes not reliable and X,Y positions incorrect, particularly affecting Grid3 processor functionality
756
- - ⚠️ **Database Constraints**: UNIQUE constraint violations with real-world data files (Page.Id and buttons.id conflicts)
757
- - ⚠️ **Linting**: 123 ESLint issues remaining (mostly in test files, reduced from 177)
758
- - ⚠️ **SnapProcessor**: Lowest coverage at 48.32%, missing comprehensive audio handling tests
759
- - ⚠️ **Memory usage**: Large files (>50MB) may cause memory pressure
760
- - ⚠️ **Concurrent access**: Some processors not fully thread-safe for simultaneous writes
761
-
762
- ### 🧪 Current Test Status & Immediate Follow-Up
763
-
764
- As of the latest run (`npm test`), **47 suites pass / 6 fail (10 individual tests)**. Remaining blockers are:
765
-
766
- 1. **Edge-case loaders** – corrupted JSON/XML fixtures still expect explicit exceptions. Decide whether to restore the old throwing behaviour (Asterics/OPML/DOT) or update the tests to accept the softer error reporting.
767
- 2. **Gridset exports & styling** – round-trip continues to lose a button and the styling suite cannot find `style.xml`. GridsetProcessor needs to preserve button arrays and emit the styling assets Grid 3 expects.
768
- 3. **DOT navigation semantics** – the deterministic DOT test still falls back to `SPEAK`. Improve semantic reconstruction when loading navigation edges so navigation buttons survive round-trips.
769
- 4. **Advanced workflow scenario** – the multi-format pipeline still loses Spanish translations (likely during the Gridset ⇄ Snap steps); trace text propagation and patch the conversion chain.
770
- 5. **Styling suite** – Grid 3 export still lacks `style.xml`; ensure the styling assets are generated alongside the gridset payload.
771
- 6. **Memory comparison suite** – memory delta expectations are still unmet (TouchChat GC + DOT vs others). Either recalibrate the harness or tune the processors before re-enabling the assertions.
772
-
773
- Clearing these items will put the test matrix back in the green and unblock the release.
774
-
775
738
  ## More enhancements
776
739
 
777
740
  - Much more effort put into fixing the layout issues. Grid sizes are not reliably and X, Y positions too. Particularly in the Grid3
package/dist/cli/index.js CHANGED
@@ -142,7 +142,7 @@ commander_1.program
142
142
  .option('--no-exclude-navigation', "Don't exclude navigation buttons (Home, Back)")
143
143
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
144
144
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
145
- .action((input, output, options) => {
145
+ .action(async (input, output, options) => {
146
146
  try {
147
147
  if (!options.format) {
148
148
  console.error('Error: --format option is required for convert command');
@@ -157,7 +157,7 @@ commander_1.program
157
157
  const tree = inputProcessor.loadIntoTree(input);
158
158
  // Save using output format with same filtering options
159
159
  const outputProcessor = (0, analyze_1.getProcessor)(options.format, filteringOptions);
160
- outputProcessor.saveFromTree(tree, output);
160
+ await outputProcessor.saveFromTree(tree, output);
161
161
  // Show filtering summary
162
162
  let filteringSummary = '';
163
163
  if (filteringOptions.preserveAllButtons) {
@@ -163,7 +163,11 @@ export declare class AACPage implements IAACPage {
163
163
  buttons: AACButton[];
164
164
  parentId: string | null;
165
165
  style?: AACStyle;
166
- constructor({ id, name, grid, buttons, parentId, style, }: {
166
+ locale?: string;
167
+ descriptionHtml?: string;
168
+ images?: any[];
169
+ sounds?: any[];
170
+ constructor({ id, name, grid, buttons, parentId, style, locale, descriptionHtml, images, sounds, }: {
167
171
  id: string;
168
172
  name?: string;
169
173
  grid?: Array<Array<AACButton | null>> | {
@@ -173,6 +177,10 @@ export declare class AACPage implements IAACPage {
173
177
  buttons?: AACButton[];
174
178
  parentId?: string | null;
175
179
  style?: AACStyle;
180
+ locale?: string;
181
+ descriptionHtml?: string;
182
+ images?: any[];
183
+ sounds?: any[];
176
184
  });
177
185
  addButton(button: AACButton): void;
178
186
  }
@@ -152,7 +152,7 @@ class AACButton {
152
152
  }
153
153
  exports.AACButton = AACButton;
154
154
  class AACPage {
155
- constructor({ id, name = '', grid = [], buttons = [], parentId = null, style, }) {
155
+ constructor({ id, name = '', grid = [], buttons = [], parentId = null, style, locale, descriptionHtml, images, sounds, }) {
156
156
  this.id = id;
157
157
  this.name = name;
158
158
  if (Array.isArray(grid)) {
@@ -169,6 +169,10 @@ class AACPage {
169
169
  this.buttons = buttons;
170
170
  this.parentId = parentId;
171
171
  this.style = style;
172
+ this.locale = locale;
173
+ this.descriptionHtml = descriptionHtml;
174
+ this.images = images;
175
+ this.sounds = sounds;
172
176
  }
173
177
  addButton(button) {
174
178
  this.buttons.push(button);
@@ -10,6 +10,45 @@ const treeStructure_1 = require("../core/treeStructure");
10
10
  const plist_1 = __importDefault(require("plist"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ function isNormalizedPanel(panel) {
14
+ return typeof panel.id === 'string';
15
+ }
16
+ function normalizePanel(panel, fallbackId) {
17
+ const rawId = panel.ID || fallbackId;
18
+ const buttons = Array.isArray(panel.PanelObjects)
19
+ ? panel.PanelObjects.filter((obj) => obj.PanelObjectType === 'Button')
20
+ : [];
21
+ const normalizedButtons = buttons.map((btn) => {
22
+ const firstAction = Array.isArray(btn.Actions) && btn.Actions.length > 0 ? btn.Actions[0] : undefined;
23
+ const isCharSequence = firstAction &&
24
+ (firstAction.ActionType === 'ActionPressKeyCharSequence' ||
25
+ firstAction.ActionType === 'ActionSendKeys');
26
+ const charString = isCharSequence ? firstAction?.ActionParam?.CharString : undefined;
27
+ const targetPanel = firstAction && firstAction.ActionType === 'ActionOpenPanel'
28
+ ? firstAction.ActionParam?.PanelID?.replace(/^USER\./, '')
29
+ : undefined;
30
+ return {
31
+ label: btn.DisplayText || 'Button',
32
+ message: charString || btn.DisplayText || 'Button',
33
+ DisplayColor: btn.DisplayColor,
34
+ DisplayImageWeight: btn.DisplayImageWeight,
35
+ FontSize: btn.FontSize,
36
+ Rect: btn.Rect,
37
+ targetPanel,
38
+ };
39
+ });
40
+ return {
41
+ id: rawId.replace(/^USER\./, ''),
42
+ name: panel.Name || 'Panel',
43
+ buttons: normalizedButtons,
44
+ };
45
+ }
46
+ function normalizeActionParameters(input) {
47
+ if (typeof input === 'object' && input !== null) {
48
+ return { ...input };
49
+ }
50
+ return {};
51
+ }
13
52
  class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
14
53
  constructor(options) {
15
54
  super(options);
@@ -81,41 +120,21 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
81
120
  // Handle both old format (panels array) and new Apple Panels format (Panels dict)
82
121
  let panelsData = [];
83
122
  if (Array.isArray(parsedData.panels)) {
84
- // Old format
85
- panelsData = parsedData.panels;
123
+ panelsData = parsedData.panels.map((panel, index) => {
124
+ if (isNormalizedPanel(panel)) {
125
+ return panel;
126
+ }
127
+ const panelData = panel || {
128
+ PanelObjects: [],
129
+ };
130
+ return normalizePanel(panelData, `panel_${index}`);
131
+ });
86
132
  }
87
133
  else if (parsedData.Panels) {
88
- // Apple Panels format: convert Panels dict to array
89
134
  const panelsDict = parsedData.Panels;
90
135
  panelsData = Object.keys(panelsDict).map((panelId) => {
91
- const panel = panelsDict[panelId];
92
- return {
93
- id: (panel.ID || panelId).replace(/^USER\./, ''), // Strip USER. prefix to maintain original IDs
94
- name: panel.Name || 'Panel',
95
- buttons: (panel.PanelObjects || [])
96
- .filter((obj) => obj.PanelObjectType === 'Button')
97
- .map((btn) => {
98
- const firstAction = Array.isArray(btn.Actions) && btn.Actions.length > 0 ? btn.Actions[0] : undefined;
99
- const isCharSequence = firstAction &&
100
- (firstAction.ActionType === 'ActionPressKeyCharSequence' ||
101
- firstAction.ActionType === 'ActionSendKeys');
102
- const charString = isCharSequence
103
- ? (firstAction.ActionParam?.CharString ?? undefined)
104
- : undefined;
105
- const targetPanel = firstAction && firstAction.ActionType === 'ActionOpenPanel'
106
- ? firstAction.ActionParam?.PanelID?.replace(/^USER\./, '')
107
- : undefined;
108
- return {
109
- label: btn.DisplayText || 'Button',
110
- message: charString || btn.DisplayText || 'Button',
111
- DisplayColor: btn.DisplayColor,
112
- DisplayImageWeight: btn.DisplayImageWeight,
113
- FontSize: btn.FontSize,
114
- Rect: btn.Rect,
115
- targetPanel,
116
- };
117
- }),
118
- };
136
+ const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
137
+ return normalizePanel(rawPanel, panelId);
119
138
  });
120
139
  }
121
140
  const data = { panels: panelsData };
@@ -219,15 +238,24 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
219
238
  Object.values(tree.pages).forEach((page) => {
220
239
  // Translate page names
221
240
  if (page.name && translations.has(page.name)) {
222
- page.name = translations.get(page.name);
241
+ const translatedName = translations.get(page.name);
242
+ if (translatedName !== undefined) {
243
+ page.name = translatedName;
244
+ }
223
245
  }
224
246
  // Translate button labels and messages
225
247
  page.buttons.forEach((button) => {
226
248
  if (button.label && translations.has(button.label)) {
227
- button.label = translations.get(button.label);
249
+ const translatedLabel = translations.get(button.label);
250
+ if (translatedLabel !== undefined) {
251
+ button.label = translatedLabel;
252
+ }
228
253
  }
229
254
  if (button.message && translations.has(button.message)) {
230
- button.message = translations.get(button.message);
255
+ const translatedMessage = translations.get(button.message);
256
+ if (translatedMessage !== undefined) {
257
+ button.message = translatedMessage;
258
+ }
231
259
  }
232
260
  if (button.semanticAction) {
233
261
  const intentStr = String(button.semanticAction.intent);
@@ -304,10 +332,8 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
304
332
  const panelId = `USER.${page.id}`;
305
333
  // Detect actual grid dimensions from the source data
306
334
  let gridCols = 4; // Default fallback
307
- let gridRows = Math.ceil(page.buttons.length / gridCols);
308
335
  if (page.grid && page.grid.length > 0) {
309
336
  // Use actual grid dimensions from source
310
- gridRows = page.grid.length;
311
337
  gridCols = page.grid[0] ? page.grid[0].length : 4;
312
338
  // Find the actual used area to avoid empty space
313
339
  let maxUsedX = 0, maxUsedY = 0;
@@ -322,7 +348,6 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
322
348
  // Use the actual used dimensions if they're reasonable
323
349
  if (maxUsedX > 0 && maxUsedY > 0) {
324
350
  gridCols = maxUsedX + 1;
325
- gridRows = maxUsedY + 1;
326
351
  }
327
352
  }
328
353
  else {
@@ -340,7 +365,6 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
340
365
  else {
341
366
  gridCols = 8; // 8 columns for very large sets
342
367
  }
343
- gridRows = Math.ceil(buttonCount / gridCols);
344
368
  }
345
369
  const panelObjects = page.buttons.map((button, buttonIndex) => {
346
370
  // Find button position in grid layout and convert to Rect format
@@ -379,7 +403,8 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
379
403
  FontSize: button.style?.fontSize || 12,
380
404
  ID: `Button.${button.id}`,
381
405
  PanelObjectType: 'Button',
382
- Rect: rect,
406
+ Rect: rect ?? '{{0, 0}, {100, 25}}',
407
+ Actions: [],
383
408
  };
384
409
  if (button.style?.backgroundColor) {
385
410
  buttonObj.DisplayColor = button.style.backgroundColor;
@@ -414,8 +439,9 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
414
439
  UsesPinnedResizing: false,
415
440
  };
416
441
  });
442
+ const panelsValue = Object.fromEntries(Object.entries(panelsDict).map(([key, value]) => [key, value]));
417
443
  const panelDefinitions = {
418
- Panels: panelsDict,
444
+ Panels: panelsValue,
419
445
  ToolbarOrdering: {
420
446
  ToolbarIdentifiersAfterBasePanel: [],
421
447
  ToolbarIdentifiersPriorToBasePanel: [],
@@ -439,7 +465,7 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
439
465
  if (button.semanticAction?.platformData?.applePanels) {
440
466
  const applePanelsData = button.semanticAction.platformData.applePanels;
441
467
  return {
442
- ActionParam: applePanelsData.parameters,
468
+ ActionParam: normalizeActionParameters(applePanelsData.parameters),
443
469
  ActionRecordedOffset: 0.0,
444
470
  ActionType: applePanelsData.actionType,
445
471
  ID: `Action.${button.id}`,