@willwade/aac-processors 0.0.6 → 0.0.8

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 (35) hide show
  1. package/README.md +2 -40
  2. package/dist/analytics/history.d.ts +57 -0
  3. package/dist/analytics/history.js +72 -0
  4. package/dist/core/analyze.d.ts +10 -0
  5. package/dist/core/analyze.js +10 -0
  6. package/dist/core/stringCasing.d.ts +11 -0
  7. package/dist/core/stringCasing.js +11 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +5 -0
  10. package/dist/processors/dotProcessor.js +31 -12
  11. package/dist/processors/gridset/colorUtils.d.ts +69 -0
  12. package/dist/processors/gridset/colorUtils.js +331 -0
  13. package/dist/processors/gridset/helpers.d.ts +125 -0
  14. package/dist/processors/gridset/helpers.js +354 -0
  15. package/dist/processors/gridset/styleHelpers.d.ts +3 -4
  16. package/dist/processors/gridset/styleHelpers.js +10 -44
  17. package/dist/processors/index.d.ts +6 -4
  18. package/dist/processors/index.js +51 -3
  19. package/dist/processors/obfProcessor.js +10 -2
  20. package/dist/processors/snap/helpers.d.ts +85 -0
  21. package/dist/processors/snap/helpers.js +259 -2
  22. package/dist/processors/snapProcessor.js +27 -5
  23. package/dist/processors/touchchat/helpers.d.ts +12 -0
  24. package/dist/processors/touchchat/helpers.js +12 -2
  25. package/dist/utils/dotnetTicks.d.ts +13 -0
  26. package/dist/utils/dotnetTicks.js +21 -0
  27. package/package.json +8 -6
  28. package/docs/.keep +0 -1
  29. package/docs/ApplePanels.md +0 -309
  30. package/docs/Grid3-Styling-Guide.md +0 -287
  31. package/docs/Grid3-XML-Format.md +0 -1788
  32. package/docs/TobiiDynavox-Snap-Details.md +0 -394
  33. package/docs/asterics-Grid-fileformat-details.md +0 -443
  34. package/docs/obf_.obz Open Board File Formats.md +0 -432
  35. package/docs/touchchat.md +0 -520
package/README.md CHANGED
@@ -404,8 +404,8 @@ processor.saveFromTree(tree, "styled-board.spb");
404
404
  | **Grid3** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Style references |
405
405
  | **Asterics Grid** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Metadata-based |
406
406
  | **Apple Panels** | ✅ Yes | ✅ Size only | ❌ No | ✅ Display weight |
407
- | **Dot** | ❌No | ❌ Yes | ❌ No | ❌ Basic only |
408
- | **OPML** | ❌No | ❌ Yes | ❌ No | ❌ Basic only |
407
+ | **Dot** | ❌No | ❌ Yes | ❌ No | ❌ Basic only |
408
+ | **OPML** | ❌No | ❌ Yes | ❌ No | ❌ Basic only |
409
409
  | **Excel** | ✅ Yes | ✅ Size only | ❌ No | ✅ Display weight |
410
410
 
411
411
  #### Cross-Format Styling Conversion
@@ -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,16 +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
-
728
713
  ### Medium Priority
729
714
 
730
715
  - [ ] **Performance optimization** - Optimize memory usage for very large communication boards (1000+ buttons)
@@ -749,29 +734,6 @@ Inspired by the Python AACProcessors project
749
734
  - [ ] **CI/CD improvements** - Add automated releases and npm publishing
750
735
  - [ ] **Documentation improvements** - Add more real-world examples and tutorials
751
736
 
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
737
  ## More enhancements
776
738
 
777
739
  - Much more effort put into fixing the layout issues. Grid sizes are not reliably and X, Y positions too. Particularly in the Grid3
@@ -0,0 +1,57 @@
1
+ import { dotNetTicksToDate } from '../utils/dotnetTicks';
2
+ import { Grid3UserPath } from '../processors/gridset/helpers';
3
+ import { SnapUserInfo } from '../processors/snap/helpers';
4
+ export type HistorySource = 'Grid' | 'Snap';
5
+ export interface HistoryOccurrence {
6
+ timestamp: Date;
7
+ latitude?: number | null;
8
+ longitude?: number | null;
9
+ modeling?: boolean;
10
+ accessMethod?: number | null;
11
+ pageId?: string | null;
12
+ }
13
+ export interface HistoryPlatformExtras {
14
+ label?: string;
15
+ message?: string;
16
+ buttonId?: string;
17
+ contentXml?: string;
18
+ }
19
+ export interface HistoryEntry {
20
+ id: string;
21
+ source: HistorySource;
22
+ content: string;
23
+ occurrences: HistoryOccurrence[];
24
+ raw?: unknown;
25
+ platform?: HistoryPlatformExtras;
26
+ }
27
+ export { dotNetTicksToDate };
28
+ /**
29
+ * Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
30
+ */
31
+ export declare function readGrid3History(historyDbPath: string): HistoryEntry[];
32
+ /**
33
+ * Read Grid 3 history for a specific user/language combination.
34
+ */
35
+ export declare function readGrid3HistoryForUser(userName: string, langCode?: string): HistoryEntry[];
36
+ /**
37
+ * Read every available Grid 3 history database on the machine.
38
+ */
39
+ export declare function readAllGrid3History(): HistoryEntry[];
40
+ /**
41
+ * Read Snap button usage from a pageset database and tag entries with source.
42
+ */
43
+ export declare function readSnapUsage(pagesetPath: string): HistoryEntry[];
44
+ /**
45
+ * Read Snap usage for a specific user across all discovered pagesets.
46
+ */
47
+ export declare function readSnapUsageForUser(userId?: string, packageNamePattern?: string): HistoryEntry[];
48
+ export declare function listSnapUsers(): SnapUserInfo[];
49
+ /**
50
+ * List Grid 3 users on the current machine.
51
+ */
52
+ export declare function listGrid3Users(): Grid3UserPath[];
53
+ /**
54
+ * Convenience helper to gather all available history across Grid 3 and Snap.
55
+ * Returns an empty array if no history files are present.
56
+ */
57
+ export declare function collectUnifiedHistory(): HistoryEntry[];
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dotNetTicksToDate = void 0;
4
+ exports.readGrid3History = readGrid3History;
5
+ exports.readGrid3HistoryForUser = readGrid3HistoryForUser;
6
+ exports.readAllGrid3History = readAllGrid3History;
7
+ exports.readSnapUsage = readSnapUsage;
8
+ exports.readSnapUsageForUser = readSnapUsageForUser;
9
+ exports.listSnapUsers = listSnapUsers;
10
+ exports.listGrid3Users = listGrid3Users;
11
+ exports.collectUnifiedHistory = collectUnifiedHistory;
12
+ const dotnetTicks_1 = require("../utils/dotnetTicks");
13
+ Object.defineProperty(exports, "dotNetTicksToDate", { enumerable: true, get: function () { return dotnetTicks_1.dotNetTicksToDate; } });
14
+ const helpers_1 = require("../processors/gridset/helpers");
15
+ const helpers_2 = require("../processors/snap/helpers");
16
+ /**
17
+ * Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
18
+ */
19
+ function readGrid3History(historyDbPath) {
20
+ return (0, helpers_1.readGrid3History)(historyDbPath).map((e) => ({
21
+ ...e,
22
+ source: 'Grid',
23
+ }));
24
+ }
25
+ /**
26
+ * Read Grid 3 history for a specific user/language combination.
27
+ */
28
+ function readGrid3HistoryForUser(userName, langCode) {
29
+ return (0, helpers_1.readGrid3HistoryForUser)(userName, langCode).map((e) => ({
30
+ ...e,
31
+ source: 'Grid',
32
+ }));
33
+ }
34
+ /**
35
+ * Read every available Grid 3 history database on the machine.
36
+ */
37
+ function readAllGrid3History() {
38
+ return (0, helpers_1.readAllGrid3History)().map((e) => ({ ...e, source: 'Grid' }));
39
+ }
40
+ /**
41
+ * Read Snap button usage from a pageset database and tag entries with source.
42
+ */
43
+ function readSnapUsage(pagesetPath) {
44
+ return (0, helpers_2.readSnapUsage)(pagesetPath).map((e) => ({ ...e, source: 'Snap' }));
45
+ }
46
+ /**
47
+ * Read Snap usage for a specific user across all discovered pagesets.
48
+ */
49
+ function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
50
+ return (0, helpers_2.readSnapUsageForUser)(userId, packageNamePattern).map((e) => ({
51
+ ...e,
52
+ source: 'Snap',
53
+ }));
54
+ }
55
+ function listSnapUsers() {
56
+ return (0, helpers_2.findSnapUsers)();
57
+ }
58
+ /**
59
+ * List Grid 3 users on the current machine.
60
+ */
61
+ function listGrid3Users() {
62
+ return (0, helpers_1.findGrid3Users)();
63
+ }
64
+ /**
65
+ * Convenience helper to gather all available history across Grid 3 and Snap.
66
+ * Returns an empty array if no history files are present.
67
+ */
68
+ function collectUnifiedHistory() {
69
+ const gridHistory = readAllGrid3History();
70
+ const snapHistory = (0, helpers_2.findSnapUsers)().flatMap((u) => readSnapUsageForUser(u.userId));
71
+ return [...gridHistory, ...snapHistory];
72
+ }
@@ -1,6 +1,16 @@
1
1
  import { AACTree } from './treeStructure';
2
2
  import { BaseProcessor, ProcessorOptions } from './baseProcessor';
3
+ /**
4
+ * Resolve a processor instance by friendly format name or common extension.
5
+ * @param format Format key or extension (e.g., 'snap', 'obf', 'xlsx')
6
+ * @param options Optional processor configuration
7
+ */
3
8
  export declare function getProcessor(format: string, options?: ProcessorOptions): BaseProcessor;
9
+ /**
10
+ * Convenience helper to load a file into an AACTree using the inferred processor.
11
+ * @param file Path to the source file
12
+ * @param format Format key or extension (passed to getProcessor)
13
+ */
4
14
  export declare function analyze(file: string, format: string): {
5
15
  tree: AACTree;
6
16
  };
@@ -11,6 +11,11 @@ const snapProcessor_1 = require("../processors/snapProcessor");
11
11
  const dotProcessor_1 = require("../processors/dotProcessor");
12
12
  const excelProcessor_1 = require("../processors/excelProcessor");
13
13
  const applePanelsProcessor_1 = require("../processors/applePanelsProcessor");
14
+ /**
15
+ * Resolve a processor instance by friendly format name or common extension.
16
+ * @param format Format key or extension (e.g., 'snap', 'obf', 'xlsx')
17
+ * @param options Optional processor configuration
18
+ */
14
19
  function getProcessor(format, options) {
15
20
  const normalizedFormat = (format || '').toLowerCase();
16
21
  switch (normalizedFormat) {
@@ -42,6 +47,11 @@ function getProcessor(format, options) {
42
47
  throw new Error('Unknown format: ' + format);
43
48
  }
44
49
  }
50
+ /**
51
+ * Convenience helper to load a file into an AACTree using the inferred processor.
52
+ * @param file Path to the source file
53
+ * @param format Format key or extension (passed to getProcessor)
54
+ */
45
55
  function analyze(file, format) {
46
56
  const processor = getProcessor(format);
47
57
  const tree = processor.loadIntoTree(file);
@@ -20,6 +20,9 @@ export declare enum StringCasing {
20
20
  * @param text - The text to analyze for casing pattern
21
21
  * @returns StringCasing enum value representing the detected casing
22
22
  */
23
+ /**
24
+ * Infer the dominant casing style of a string (camel, pascal, snake, kebab, title, sentence, upper, lower).
25
+ */
23
26
  export declare function detectCasing(text: string): StringCasing;
24
27
  /**
25
28
  * Converts text to the specified casing
@@ -27,6 +30,11 @@ export declare function detectCasing(text: string): StringCasing;
27
30
  * @param targetCasing - The desired casing format
28
31
  * @returns The text converted to the target casing
29
32
  */
33
+ /**
34
+ * Convert a string into the requested casing style.
35
+ * @param text Input string
36
+ * @param targetCasing Desired casing variant
37
+ */
30
38
  export declare function convertCasing(text: string, targetCasing: StringCasing): string;
31
39
  /**
32
40
  * Utility function to check if text is primarily numeric or empty
@@ -34,4 +42,7 @@ export declare function convertCasing(text: string, targetCasing: StringCasing):
34
42
  * @param text - The text to check
35
43
  * @returns True if the text should be considered non-meaningful
36
44
  */
45
+ /**
46
+ * Check whether a string is empty or represents a numeric value.
47
+ */
37
48
  export declare function isNumericOrEmpty(text: string): boolean;
@@ -27,6 +27,9 @@ var StringCasing;
27
27
  * @param text - The text to analyze for casing pattern
28
28
  * @returns StringCasing enum value representing the detected casing
29
29
  */
30
+ /**
31
+ * Infer the dominant casing style of a string (camel, pascal, snake, kebab, title, sentence, upper, lower).
32
+ */
30
33
  function detectCasing(text) {
31
34
  if (!text || text.length === 0)
32
35
  return StringCasing.LOWER;
@@ -102,6 +105,11 @@ function detectCasing(text) {
102
105
  * @param targetCasing - The desired casing format
103
106
  * @returns The text converted to the target casing
104
107
  */
108
+ /**
109
+ * Convert a string into the requested casing style.
110
+ * @param text Input string
111
+ * @param targetCasing Desired casing variant
112
+ */
105
113
  function convertCasing(text, targetCasing) {
106
114
  if (!text || text.length === 0)
107
115
  return text;
@@ -164,6 +172,9 @@ function convertCasing(text, targetCasing) {
164
172
  * @param text - The text to check
165
173
  * @returns True if the text should be considered non-meaningful
166
174
  */
175
+ /**
176
+ * Check whether a string is empty or represents a numeric value.
177
+ */
167
178
  function isNumericOrEmpty(text) {
168
179
  const trimmed = text.trim();
169
180
  if (trimmed.length <= 1)
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './core/treeStructure';
2
2
  export * from './core/baseProcessor';
3
3
  export * from './core/stringCasing';
4
4
  export * from './processors';
5
+ export { collectUnifiedHistory, listGrid3Users as listHistoryGrid3Users, listSnapUsers as listHistorySnapUsers, } from './analytics/history';
5
6
  import { BaseProcessor } from './core/baseProcessor';
6
7
  /**
7
8
  * Factory function to get the appropriate processor for a file extension
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.listHistorySnapUsers = exports.listHistoryGrid3Users = exports.collectUnifiedHistory = void 0;
17
18
  exports.getProcessor = getProcessor;
18
19
  exports.getSupportedExtensions = getSupportedExtensions;
19
20
  exports.isExtensionSupported = isExtensionSupported;
@@ -22,6 +23,10 @@ __exportStar(require("./core/treeStructure"), exports);
22
23
  __exportStar(require("./core/baseProcessor"), exports);
23
24
  __exportStar(require("./core/stringCasing"), exports);
24
25
  __exportStar(require("./processors"), exports);
26
+ var history_1 = require("./analytics/history");
27
+ Object.defineProperty(exports, "collectUnifiedHistory", { enumerable: true, get: function () { return history_1.collectUnifiedHistory; } });
28
+ Object.defineProperty(exports, "listHistoryGrid3Users", { enumerable: true, get: function () { return history_1.listGrid3Users; } });
29
+ Object.defineProperty(exports, "listHistorySnapUsers", { enumerable: true, get: function () { return history_1.listSnapUsers; } });
25
30
  const dotProcessor_1 = require("./processors/dotProcessor");
26
31
  const excelProcessor_1 = require("./processors/excelProcessor");
27
32
  const opmlProcessor_1 = require("./processors/opmlProcessor");
@@ -17,17 +17,14 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
17
17
  const edges = [];
18
18
  // Extract all edge statements using regex to handle single-line DOT files
19
19
  const edgeRegex = /"?([^"\s]+)"?\s*->\s*"?([^"\s]+)"?(?:\s*\[label="([^"]+)"\])?/g;
20
- const nodeRegex = /"?([^"\s]+)"?\s*\[label="([^"]+)"\]/g;
21
- // Find all explicit node definitions
22
- let nodeMatch;
23
- while ((nodeMatch = nodeRegex.exec(content)) !== null) {
24
- const [, id, label] = nodeMatch;
25
- nodes.set(id, { id, label });
26
- }
27
- // Find all edge definitions
20
+ // We need to find nodes, but avoid matching the target of an edge which might look like a node definition
21
+ // e.g. A -> B [label="L"] -- "B [label="L"]" looks like a node def
22
+ // Strategy: Find all edges, record them, and then "mask" them in the content to avoid false positives for nodes
23
+ let maskedContent = content;
28
24
  let edgeMatch;
25
+ // Find all edge definitions
29
26
  while ((edgeMatch = edgeRegex.exec(content)) !== null) {
30
- const [, from, to, label] = edgeMatch;
27
+ const [fullMatch, from, to, label] = edgeMatch;
31
28
  edges.push({ from, to, label });
32
29
  // Add nodes if they don't exist (implicit definition)
33
30
  if (!nodes.has(from)) {
@@ -36,6 +33,23 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
36
33
  if (!nodes.has(to)) {
37
34
  nodes.set(to, { id: to, label: to });
38
35
  }
36
+ // Mask this edge in the content so we don't match it as a node
37
+ // We replace it with spaces to preserve indices if needed, but simple replacement is enough here
38
+ maskedContent = maskedContent.replace(fullMatch, ' '.repeat(fullMatch.length));
39
+ }
40
+ // Now find explicit node definitions in the masked content
41
+ // This regex matches: ID [label="LABEL"]
42
+ // We use a non-greedy match for the label content to handle escaped quotes if possible,
43
+ // but the previous regex `[^"]+` was too simple.
44
+ // Better regex for quoted string content: (?:[^"\\]|\\.)*
45
+ const nodeRegex = /"?([^"\s]+)"?\s*\[label="((?:[^"\\]|\\.)*)"\]/g;
46
+ let nodeMatch;
47
+ while ((nodeMatch = nodeRegex.exec(maskedContent)) !== null) {
48
+ const [, id, rawLabel] = nodeMatch;
49
+ // Unescape the label: replace \" with " and \\ with \
50
+ const label = rawLabel.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
51
+ // Only update if not already defined or if we want to override the implicit label
52
+ nodes.set(id, { id, label });
39
53
  }
40
54
  return { nodes: Array.from(nodes.values()), edges };
41
55
  }
@@ -82,7 +96,8 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
82
96
  let hasControl = false;
83
97
  for (let i = 0; i < head.length; i++) {
84
98
  const code = head.charCodeAt(i);
85
- if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31) || code >= 127) {
99
+ // Allow UTF-8 characters (code >= 127)
100
+ if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31)) {
86
101
  hasControl = true;
87
102
  break;
88
103
  }
@@ -150,10 +165,14 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
150
165
  }
151
166
  saveFromTree(tree, _outputPath) {
152
167
  let dotContent = 'digraph AACBoard {\n';
168
+ // Helper to escape DOT string
169
+ const escapeDotString = (str) => {
170
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
171
+ };
153
172
  // Add nodes
154
173
  for (const pageId in tree.pages) {
155
174
  const page = tree.pages[pageId];
156
- dotContent += ` "${page.id}" [label="${page.name}"]\n`;
175
+ dotContent += ` "${page.id}" [label="${escapeDotString(page.name)}"]\n`;
157
176
  }
158
177
  // Add edges from navigation buttons (semantic intent or legacy targetPageId)
159
178
  for (const pageId in tree.pages) {
@@ -166,7 +185,7 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
166
185
  .forEach((btn) => {
167
186
  const target = btn.semanticAction?.targetId || btn.targetPageId;
168
187
  if (target) {
169
- dotContent += ` "${page.id}" -> "${target}" [label="${btn.label}"]\n`;
188
+ dotContent += ` "${page.id}" -> "${target}" [label="${escapeDotString(btn.label)}"]\n`;
170
189
  }
171
190
  });
172
191
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Grid3 Color Utilities
3
+ *
4
+ * Comprehensive color handling for Grid3 format, including:
5
+ * - CSS color name lookup (147 named colors)
6
+ * - Color format conversion (hex, RGB, RGBA, named colors)
7
+ * - Color manipulation (darkening, normalization)
8
+ * - Grid3-specific color formatting (8-digit ARGB hex)
9
+ */
10
+ /**
11
+ * Get RGB values for a CSS color name
12
+ * @param name - CSS color name (case-insensitive)
13
+ * @returns RGB tuple [r, g, b] or undefined if not found
14
+ */
15
+ export declare function getNamedColor(name: string): [number, number, number] | undefined;
16
+ /**
17
+ * Convert RGBA values to hex format
18
+ * @param r - Red channel (0-255)
19
+ * @param g - Green channel (0-255)
20
+ * @param b - Blue channel (0-255)
21
+ * @param a - Alpha channel (0-1)
22
+ * @returns Hex color string in format #RRGGBBAA
23
+ */
24
+ export declare function rgbaToHex(r: number, g: number, b: number, a: number): string;
25
+ /**
26
+ * Convert a single color channel value to hex
27
+ * @param value - Channel value (0-255)
28
+ * @returns Two-digit hex string
29
+ */
30
+ export declare function channelToHex(value: number): string;
31
+ /**
32
+ * Clamp RGB channel value to valid range
33
+ * @param value - Channel value
34
+ * @returns Clamped value (0-255)
35
+ */
36
+ export declare function clampColorChannel(value: number): number;
37
+ /**
38
+ * Clamp alpha value to valid range
39
+ * @param value - Alpha value
40
+ * @returns Clamped value (0-1)
41
+ */
42
+ export declare function clampAlpha(value: number): number;
43
+ /**
44
+ * Convert any color format to hex
45
+ * Supports: hex (#RGB, #RRGGBB, #RRGGBBAA), RGB/RGBA, and CSS color names
46
+ * @param value - Color string in any supported format
47
+ * @returns Hex color string (#RRGGBBAA) or undefined if invalid
48
+ */
49
+ export declare function toHexColor(value: string): string | undefined;
50
+ /**
51
+ * Darken a hex color by a specified amount
52
+ * @param hex - Hex color string
53
+ * @param amount - Amount to darken (0-255)
54
+ * @returns Darkened hex color
55
+ */
56
+ export declare function darkenColor(hex: string, amount: number): string;
57
+ /**
58
+ * Normalize any color format to Grid3's 8-digit hex format
59
+ * @param input - Color string in any supported format
60
+ * @param fallback - Fallback color if input is invalid (default: white)
61
+ * @returns Normalized color in format #AARRGGBBFF
62
+ */
63
+ export declare function normalizeColor(input: string, fallback?: string): string;
64
+ /**
65
+ * Ensure a color has an alpha channel (Grid3 format requires 8-digit ARGB)
66
+ * @param color - Color string (hex format)
67
+ * @returns Color with alpha channel in format #AARRGGBBFF
68
+ */
69
+ export declare function ensureAlphaChannel(color: string | undefined): string;