@willwade/aac-processors 0.1.19 → 0.1.21

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 (94) hide show
  1. package/dist/browser/core/baseProcessor.js +4 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +24 -31
  3. package/dist/browser/processors/astericsGridProcessor.js +10 -3
  4. package/dist/browser/processors/dotProcessor.js +5 -2
  5. package/dist/browser/processors/gridset/colorUtils.js +354 -0
  6. package/dist/browser/processors/gridset/helpers.js +49 -45
  7. package/dist/browser/processors/gridset/index.js +61 -0
  8. package/dist/browser/processors/gridset/styleHelpers.js +205 -0
  9. package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
  10. package/dist/browser/processors/gridset/symbolSearch.js +248 -0
  11. package/dist/browser/processors/gridset/symbols.js +35 -68
  12. package/dist/browser/processors/gridsetProcessor.js +32 -41
  13. package/dist/browser/processors/obfProcessor.js +53 -45
  14. package/dist/browser/processors/opmlProcessor.js +5 -2
  15. package/dist/browser/processors/snap/helpers.js +49 -45
  16. package/dist/browser/processors/snapProcessor.js +67 -31
  17. package/dist/browser/processors/touchchatProcessor.js +54 -45
  18. package/dist/browser/utilities/analytics/reference/index.js +27 -19
  19. package/dist/browser/utils/io.js +67 -14
  20. package/dist/browser/utils/sqlite.js +6 -8
  21. package/dist/browser/utils/zip.js +45 -43
  22. package/dist/browser/validation/baseValidator.js +5 -0
  23. package/dist/browser/validation/gridsetValidator.js +12 -20
  24. package/dist/browser/validation/obfValidator.js +5 -4
  25. package/dist/browser/validation/snapValidator.js +9 -5
  26. package/dist/browser/validation/touchChatValidator.js +21 -11
  27. package/dist/cli/index.js +10 -15
  28. package/dist/core/baseProcessor.d.ts +7 -7
  29. package/dist/core/baseProcessor.js +4 -0
  30. package/dist/processors/applePanelsProcessor.js +29 -36
  31. package/dist/processors/astericsGridProcessor.js +20 -13
  32. package/dist/processors/dotProcessor.js +10 -7
  33. package/dist/processors/excelProcessor.js +9 -12
  34. package/dist/processors/gridset/helpers.d.ts +9 -11
  35. package/dist/processors/gridset/helpers.js +49 -71
  36. package/dist/processors/gridset/imageDebug.d.ts +3 -5
  37. package/dist/processors/gridset/imageDebug.js +4 -4
  38. package/dist/processors/gridset/password.d.ts +1 -1
  39. package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
  40. package/dist/processors/gridset/symbolExtractor.js +15 -38
  41. package/dist/processors/gridset/symbolSearch.d.ts +3 -2
  42. package/dist/processors/gridset/symbolSearch.js +12 -34
  43. package/dist/processors/gridset/symbols.d.ts +8 -6
  44. package/dist/processors/gridset/symbols.js +34 -67
  45. package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
  46. package/dist/processors/gridset/wordlistHelpers.js +15 -74
  47. package/dist/processors/gridsetProcessor.js +36 -68
  48. package/dist/processors/obfProcessor.js +58 -73
  49. package/dist/processors/obfsetProcessor.js +2 -2
  50. package/dist/processors/opmlProcessor.js +10 -7
  51. package/dist/processors/snap/helpers.d.ts +8 -8
  52. package/dist/processors/snap/helpers.js +50 -72
  53. package/dist/processors/snapProcessor.js +66 -30
  54. package/dist/processors/touchchatProcessor.js +54 -45
  55. package/dist/utilities/analytics/index.d.ts +3 -2
  56. package/dist/utilities/analytics/index.js +8 -10
  57. package/dist/utilities/analytics/reference/index.d.ts +5 -3
  58. package/dist/utilities/analytics/reference/index.js +26 -18
  59. package/dist/utilities/symbolTools.d.ts +4 -2
  60. package/dist/utilities/symbolTools.js +16 -15
  61. package/dist/utils/io.d.ts +24 -6
  62. package/dist/utils/io.js +64 -14
  63. package/dist/utils/sqlite.d.ts +2 -0
  64. package/dist/utils/sqlite.js +6 -8
  65. package/dist/utils/zip.d.ts +7 -3
  66. package/dist/utils/zip.js +45 -43
  67. package/dist/validation/applePanelsValidator.d.ts +2 -1
  68. package/dist/validation/applePanelsValidator.js +9 -11
  69. package/dist/validation/astericsValidator.d.ts +2 -1
  70. package/dist/validation/astericsValidator.js +5 -4
  71. package/dist/validation/baseValidator.d.ts +2 -2
  72. package/dist/validation/baseValidator.js +5 -0
  73. package/dist/validation/dotValidator.d.ts +2 -1
  74. package/dist/validation/dotValidator.js +5 -4
  75. package/dist/validation/excelValidator.d.ts +2 -1
  76. package/dist/validation/excelValidator.js +5 -4
  77. package/dist/validation/gridsetValidator.d.ts +2 -1
  78. package/dist/validation/gridsetValidator.js +11 -22
  79. package/dist/validation/index.d.ts +2 -2
  80. package/dist/validation/index.js +5 -4
  81. package/dist/validation/obfValidator.d.ts +2 -1
  82. package/dist/validation/obfValidator.js +5 -4
  83. package/dist/validation/obfsetValidator.d.ts +2 -1
  84. package/dist/validation/obfsetValidator.js +5 -4
  85. package/dist/validation/opmlValidator.d.ts +2 -1
  86. package/dist/validation/opmlValidator.js +5 -4
  87. package/dist/validation/snapValidator.d.ts +2 -1
  88. package/dist/validation/snapValidator.js +9 -5
  89. package/dist/validation/touchChatValidator.d.ts +4 -6
  90. package/dist/validation/touchChatValidator.js +21 -11
  91. package/dist/validation/validationTypes.d.ts +8 -1
  92. package/package.json +1 -1
  93. package/dist/core/fileProcessor.d.ts +0 -7
  94. package/dist/core/fileProcessor.js +0 -52
@@ -41,6 +41,8 @@
41
41
  */
42
42
  import { AACSemanticCategory } from './treeStructure';
43
43
  import { detectCasing, isNumericOrEmpty } from './stringCasing';
44
+ import { defaultFileAdapter } from '../utils/io';
45
+ import { getZipAdapter } from '../utils/zip';
44
46
  class BaseProcessor {
45
47
  constructor(options = {}) {
46
48
  // Default configuration: exclude navigation/system buttons
@@ -48,6 +50,8 @@ class BaseProcessor {
48
50
  excludeNavigationButtons: true,
49
51
  excludeSystemButtons: true,
50
52
  preserveAllButtons: false,
53
+ fileAdapter: defaultFileAdapter,
54
+ zipAdapter: getZipAdapter,
51
55
  ...options,
52
56
  };
53
57
  }
@@ -1,9 +1,8 @@
1
1
  import { BaseProcessor, } from '../core/baseProcessor';
2
2
  import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
3
- // Removed unused import: FileProcessor
4
3
  import plist from 'plist';
5
4
  import { ValidationFailureError, buildValidationResultFromMessage, } from '../validation/validationTypes';
6
- import { getBasename, getFs, getPath, readBinaryFromInput, readTextFromInput, writeTextToPath, } from '../utils/io';
5
+ import { getBasename } from '../utils/io';
7
6
  function isNormalizedPanel(panel) {
8
7
  return typeof panel.id === 'string';
9
8
  }
@@ -86,17 +85,16 @@ class ApplePanelsProcessor extends BaseProcessor {
86
85
  return texts;
87
86
  }
88
87
  async loadIntoTree(filePathOrBuffer) {
88
+ const { readBinaryFromInput, readTextFromInput, pathExists, getFileSize, join } = this.options.fileAdapter;
89
89
  await Promise.resolve();
90
90
  const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.plist';
91
91
  let buffer;
92
92
  try {
93
93
  if (typeof filePathOrBuffer === 'string') {
94
- const fs = getFs();
95
- const path = getPath();
96
94
  if (filePathOrBuffer.endsWith('.ascconfig')) {
97
- const panelDefsPath = path.join(filePathOrBuffer, 'Contents', 'Resources', 'PanelDefinitions.plist');
98
- if (fs.existsSync(panelDefsPath)) {
99
- buffer = fs.readFileSync(panelDefsPath);
95
+ const panelDefsPath = join(filePathOrBuffer, 'Contents', 'Resources', 'PanelDefinitions.plist');
96
+ if (pathExists(panelDefsPath)) {
97
+ buffer = readBinaryFromInput(panelDefsPath);
100
98
  }
101
99
  else {
102
100
  const validation = buildValidationResultFromMessage({
@@ -111,7 +109,7 @@ class ApplePanelsProcessor extends BaseProcessor {
111
109
  }
112
110
  }
113
111
  else {
114
- buffer = fs.readFileSync(filePathOrBuffer);
112
+ buffer = readBinaryFromInput(filePathOrBuffer);
115
113
  }
116
114
  }
117
115
  else {
@@ -247,8 +245,7 @@ class ApplePanelsProcessor extends BaseProcessor {
247
245
  filename,
248
246
  filesize: typeof filePathOrBuffer === 'string'
249
247
  ? (() => {
250
- const fs = getFs();
251
- return fs.existsSync(filePathOrBuffer) ? fs.statSync(filePathOrBuffer).size : 0;
248
+ return pathExists(filePathOrBuffer) ? getFileSize(filePathOrBuffer) : 0;
252
249
  })()
253
250
  : readBinaryFromInput(filePathOrBuffer).byteLength,
254
251
  format: 'applepanels',
@@ -260,6 +257,7 @@ class ApplePanelsProcessor extends BaseProcessor {
260
257
  }
261
258
  }
262
259
  async processTexts(filePathOrBuffer, translations, outputPath) {
260
+ const { readBinaryFromInput, join } = this.options.fileAdapter;
263
261
  // Load the tree, apply translations, and save to new file
264
262
  const tree = await this.loadIntoTree(filePathOrBuffer);
265
263
  // Apply translations to all text content
@@ -311,12 +309,12 @@ class ApplePanelsProcessor extends BaseProcessor {
311
309
  if (outputPath.endsWith('.plist')) {
312
310
  return readBinaryFromInput(outputPath);
313
311
  }
314
- const path = getPath();
315
312
  const configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
316
- const panelDefsPath = path.join(configPath, 'Contents', 'Resources', 'PanelDefinitions.plist');
313
+ const panelDefsPath = join(configPath, 'Contents', 'Resources', 'PanelDefinitions.plist');
317
314
  return readBinaryFromInput(panelDefsPath);
318
315
  }
319
316
  async saveFromTree(tree, outputPath) {
317
+ const { writeTextToPath, pathExists, mkDir, join, dirname } = this.options.fileAdapter;
320
318
  await Promise.resolve();
321
319
  // Support two output modes:
322
320
  // 1) Single-file .plist (PanelDefinitions.plist content written directly)
@@ -327,17 +325,15 @@ class ApplePanelsProcessor extends BaseProcessor {
327
325
  let contentsPath = '';
328
326
  let resourcesPath = '';
329
327
  if (!isSinglePlist) {
330
- const fs = getFs();
331
- const path = getPath();
332
328
  configPath = outputPath.endsWith('.ascconfig') ? outputPath : `${outputPath}.ascconfig`;
333
- contentsPath = path.join(configPath, 'Contents');
334
- resourcesPath = path.join(contentsPath, 'Resources');
335
- if (!fs.existsSync(configPath))
336
- fs.mkdirSync(configPath, { recursive: true });
337
- if (!fs.existsSync(contentsPath))
338
- fs.mkdirSync(contentsPath, { recursive: true });
339
- if (!fs.existsSync(resourcesPath))
340
- fs.mkdirSync(resourcesPath, { recursive: true });
329
+ contentsPath = join(configPath, 'Contents');
330
+ resourcesPath = join(contentsPath, 'Resources');
331
+ if (!pathExists(configPath))
332
+ mkDir(configPath, { recursive: true });
333
+ if (!pathExists(contentsPath))
334
+ mkDir(contentsPath, { recursive: true });
335
+ if (!pathExists(resourcesPath))
336
+ mkDir(resourcesPath, { recursive: true });
341
337
  // Create Info.plist (bundle mode only)
342
338
  const infoPlist = {
343
339
  ASCConfigurationDisplayName: tree.metadata?.name || 'AAC Processors Export',
@@ -353,10 +349,10 @@ class ApplePanelsProcessor extends BaseProcessor {
353
349
  `Generated by AAC Processors${tree.metadata?.author ? ` - Author: ${tree.metadata.author}` : ''}`,
354
350
  };
355
351
  const infoPlistContent = plist.build(infoPlist);
356
- writeTextToPath(path.join(contentsPath, 'Info.plist'), infoPlistContent);
352
+ writeTextToPath(join(contentsPath, 'Info.plist'), infoPlistContent);
357
353
  // Create AssetIndex.plist (empty)
358
354
  const assetIndexContent = plist.build({});
359
- writeTextToPath(path.join(resourcesPath, 'AssetIndex.plist'), assetIndexContent);
355
+ writeTextToPath(join(resourcesPath, 'AssetIndex.plist'), assetIndexContent);
360
356
  }
361
357
  // Build PanelDefinitions content from tree
362
358
  const panelsDict = {};
@@ -482,17 +478,14 @@ class ApplePanelsProcessor extends BaseProcessor {
482
478
  const panelDefsContent = plist.build(panelDefinitions);
483
479
  if (isSinglePlist) {
484
480
  // Write single PanelDefinitions.plist file directly
485
- const fs = getFs();
486
- const path = getPath();
487
- const dir = path.dirname(outputPath);
488
- if (!fs.existsSync(dir))
489
- fs.mkdirSync(dir, { recursive: true });
481
+ const dir = dirname(outputPath);
482
+ if (!pathExists(dir))
483
+ mkDir(dir, { recursive: true });
490
484
  writeTextToPath(outputPath, panelDefsContent);
491
485
  }
492
486
  else {
493
487
  // Write into bundle structure
494
- const path = getPath();
495
- writeTextToPath(path.join(resourcesPath, 'PanelDefinitions.plist'), panelDefsContent);
488
+ writeTextToPath(join(resourcesPath, 'PanelDefinitions.plist'), panelDefsContent);
496
489
  }
497
490
  }
498
491
  createApplePanelsAction(button) {
@@ -1,7 +1,7 @@
1
1
  import { BaseProcessor, } from '../core/baseProcessor';
2
2
  import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
3
3
  import { ValidationFailureError, buildValidationResultFromMessage, } from '../validation/validationTypes';
4
- import { getBasename, getFs, readBinaryFromInput, readTextFromInput, writeTextToPath, encodeBase64, } from '../utils/io';
4
+ import { getBasename, encodeBase64 } from '../utils/io';
5
5
  const DEFAULT_COLOR_SCHEME_DEFINITIONS = [
6
6
  {
7
7
  name: 'CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT',
@@ -571,6 +571,7 @@ class AstericsGridProcessor extends BaseProcessor {
571
571
  return texts;
572
572
  }
573
573
  extractRawTexts(filePathOrBuffer) {
574
+ const { readTextFromInput } = this.options.fileAdapter;
574
575
  let content = readTextFromInput(filePathOrBuffer);
575
576
  // Remove BOM if present
576
577
  if (content.charCodeAt(0) === 0xfeff) {
@@ -651,6 +652,7 @@ class AstericsGridProcessor extends BaseProcessor {
651
652
  }
652
653
  }
653
654
  async loadIntoTree(filePathOrBuffer) {
655
+ const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
654
656
  await Promise.resolve();
655
657
  const tree = new AACTree();
656
658
  const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.grd';
@@ -1049,6 +1051,7 @@ class AstericsGridProcessor extends BaseProcessor {
1049
1051
  });
1050
1052
  }
1051
1053
  async processTexts(filePathOrBuffer, translations, outputPath) {
1054
+ const { readTextFromInput, readBinaryFromInput, writeTextToPath } = this.options.fileAdapter;
1052
1055
  await Promise.resolve();
1053
1056
  let content = readTextFromInput(filePathOrBuffer);
1054
1057
  // Remove BOM if present
@@ -1172,6 +1175,7 @@ class AstericsGridProcessor extends BaseProcessor {
1172
1175
  }
1173
1176
  }
1174
1177
  async saveFromTree(tree, outputPath) {
1178
+ const { writeTextToPath } = this.options.fileAdapter;
1175
1179
  await Promise.resolve();
1176
1180
  // Use default Asterics Grid styling instead of taking from first page
1177
1181
  // This prevents issues where the first page has unusual colors (like purple)
@@ -1381,6 +1385,7 @@ class AstericsGridProcessor extends BaseProcessor {
1381
1385
  * Add audio recording to a specific grid element
1382
1386
  */
1383
1387
  addAudioToElement(filePath, elementId, audioData, metadata) {
1388
+ const { readTextFromInput, writeTextToPath } = this.options.fileAdapter;
1384
1389
  let content = readTextFromInput(filePath);
1385
1390
  // Remove BOM if present
1386
1391
  if (content.charCodeAt(0) === 0xfeff) {
@@ -1430,9 +1435,9 @@ class AstericsGridProcessor extends BaseProcessor {
1430
1435
  * Create a copy of the grid file with audio recordings added
1431
1436
  */
1432
1437
  createAudioEnhancedGridFile(sourceFilePath, targetFilePath, audioMappings) {
1438
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
1433
1439
  // Copy the source file to target
1434
- const fs = getFs();
1435
- fs.copyFileSync(sourceFilePath, targetFilePath);
1440
+ writeBinaryToPath(targetFilePath, readBinaryFromInput(sourceFilePath));
1436
1441
  // Add audio recordings to the copy
1437
1442
  audioMappings.forEach((audioInfo, elementId) => {
1438
1443
  try {
@@ -1448,6 +1453,7 @@ class AstericsGridProcessor extends BaseProcessor {
1448
1453
  * Extract all element IDs from the grid file for audio mapping
1449
1454
  */
1450
1455
  getElementIds(filePathOrBuffer) {
1456
+ const { readTextFromInput } = this.options.fileAdapter;
1451
1457
  let content = readTextFromInput(filePathOrBuffer);
1452
1458
  // Remove BOM if present
1453
1459
  if (content.charCodeAt(0) === 0xfeff) {
@@ -1471,6 +1477,7 @@ class AstericsGridProcessor extends BaseProcessor {
1471
1477
  * Check if an element has audio recording
1472
1478
  */
1473
1479
  hasAudioRecording(filePathOrBuffer, elementId) {
1480
+ const { readTextFromInput } = this.options.fileAdapter;
1474
1481
  let content = readTextFromInput(filePathOrBuffer);
1475
1482
  // Remove BOM if present
1476
1483
  if (content.charCodeAt(0) === 0xfeff) {
@@ -1,8 +1,7 @@
1
1
  import { BaseProcessor, } from '../core/baseProcessor';
2
2
  import { AACTree, AACPage, AACButton, AACSemanticIntent } from '../core/treeStructure';
3
- // Removed unused import: FileProcessor
4
3
  import { ValidationFailureError, buildValidationResultFromMessage, } from '../validation/validationTypes';
5
- import { getBasename, readBinaryFromInput, readTextFromInput, writeBinaryToPath, writeTextToPath, encodeText, } from '../utils/io';
4
+ import { getBasename, encodeText } from '../utils/io';
6
5
  class DotProcessor extends BaseProcessor {
7
6
  constructor(options) {
8
7
  super(options);
@@ -49,6 +48,7 @@ class DotProcessor extends BaseProcessor {
49
48
  return { nodes: Array.from(nodes.values()), edges };
50
49
  }
51
50
  async extractTexts(filePathOrBuffer) {
51
+ const { readTextFromInput } = this.options.fileAdapter;
52
52
  await Promise.resolve();
53
53
  const content = readTextFromInput(filePathOrBuffer);
54
54
  const { nodes, edges } = this.parseDotFile(content);
@@ -66,6 +66,7 @@ class DotProcessor extends BaseProcessor {
66
66
  return texts;
67
67
  }
68
68
  async loadIntoTree(filePathOrBuffer) {
69
+ const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
69
70
  await Promise.resolve();
70
71
  const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.dot';
71
72
  const buffer = readBinaryFromInput(filePathOrBuffer);
@@ -155,6 +156,7 @@ class DotProcessor extends BaseProcessor {
155
156
  }
156
157
  }
157
158
  async processTexts(filePathOrBuffer, translations, outputPath) {
159
+ const { readTextFromInput, writeBinaryToPath } = this.options.fileAdapter;
158
160
  await Promise.resolve();
159
161
  const content = readTextFromInput(filePathOrBuffer);
160
162
  let translatedContent = content;
@@ -171,6 +173,7 @@ class DotProcessor extends BaseProcessor {
171
173
  return resultBuffer;
172
174
  }
173
175
  async saveFromTree(tree, _outputPath) {
176
+ const { writeTextToPath } = this.options.fileAdapter;
174
177
  await Promise.resolve();
175
178
  let dotContent = `digraph "${tree.metadata?.name || 'AACBoard'}" {\n`;
176
179
  // Helper to escape DOT string
@@ -0,0 +1,354 @@
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
+ * CSS color names to RGB values
12
+ * Supports 147 standard CSS color names
13
+ */
14
+ const CSS_COLORS = {
15
+ aliceblue: [240, 248, 255],
16
+ antiquewhite: [250, 235, 215],
17
+ aqua: [0, 255, 255],
18
+ aquamarine: [127, 255, 212],
19
+ azure: [240, 255, 255],
20
+ beige: [245, 245, 220],
21
+ bisque: [255, 228, 196],
22
+ black: [0, 0, 0],
23
+ blanchedalmond: [255, 235, 205],
24
+ blue: [0, 0, 255],
25
+ blueviolet: [138, 43, 226],
26
+ brown: [165, 42, 42],
27
+ burlywood: [222, 184, 135],
28
+ cadetblue: [95, 158, 160],
29
+ chartreuse: [127, 255, 0],
30
+ chocolate: [210, 105, 30],
31
+ coral: [255, 127, 80],
32
+ cornflowerblue: [100, 149, 237],
33
+ cornsilk: [255, 248, 220],
34
+ crimson: [220, 20, 60],
35
+ cyan: [0, 255, 255],
36
+ darkblue: [0, 0, 139],
37
+ darkcyan: [0, 139, 139],
38
+ darkgoldenrod: [184, 134, 11],
39
+ darkgray: [169, 169, 169],
40
+ darkgreen: [0, 100, 0],
41
+ darkgrey: [169, 169, 169],
42
+ darkkhaki: [189, 183, 107],
43
+ darkmagenta: [139, 0, 139],
44
+ darkolivegreen: [85, 107, 47],
45
+ darkorange: [255, 140, 0],
46
+ darkorchid: [153, 50, 204],
47
+ darkred: [139, 0, 0],
48
+ darksalmon: [233, 150, 122],
49
+ darkseagreen: [143, 188, 143],
50
+ darkslateblue: [72, 61, 139],
51
+ darkslategray: [47, 79, 79],
52
+ darkslategrey: [47, 79, 79],
53
+ darkturquoise: [0, 206, 209],
54
+ darkviolet: [148, 0, 211],
55
+ deeppink: [255, 20, 147],
56
+ deepskyblue: [0, 191, 255],
57
+ dimgray: [105, 105, 105],
58
+ dimgrey: [105, 105, 105],
59
+ dodgerblue: [30, 144, 255],
60
+ firebrick: [178, 34, 34],
61
+ floralwhite: [255, 250, 240],
62
+ forestgreen: [34, 139, 34],
63
+ fuchsia: [255, 0, 255],
64
+ gainsboro: [220, 220, 220],
65
+ ghostwhite: [248, 248, 255],
66
+ gold: [255, 215, 0],
67
+ goldenrod: [218, 165, 32],
68
+ gray: [128, 128, 128],
69
+ grey: [128, 128, 128],
70
+ green: [0, 128, 0],
71
+ greenyellow: [173, 255, 47],
72
+ honeydew: [240, 255, 240],
73
+ hotpink: [255, 105, 180],
74
+ indianred: [205, 92, 92],
75
+ indigo: [75, 0, 130],
76
+ ivory: [255, 255, 240],
77
+ khaki: [240, 230, 140],
78
+ lavender: [230, 230, 250],
79
+ lavenderblush: [255, 240, 245],
80
+ lawngreen: [124, 252, 0],
81
+ lemonchiffon: [255, 250, 205],
82
+ lightblue: [173, 216, 230],
83
+ lightcoral: [240, 128, 128],
84
+ lightcyan: [224, 255, 255],
85
+ lightgoldenrodyellow: [250, 250, 210],
86
+ lightgray: [211, 211, 211],
87
+ lightgreen: [144, 238, 144],
88
+ lightgrey: [211, 211, 211],
89
+ lightpink: [255, 182, 193],
90
+ lightsalmon: [255, 160, 122],
91
+ lightseagreen: [32, 178, 170],
92
+ lightskyblue: [135, 206, 250],
93
+ lightslategray: [119, 136, 153],
94
+ lightslategrey: [119, 136, 153],
95
+ lightsteelblue: [176, 196, 222],
96
+ lightyellow: [255, 255, 224],
97
+ lime: [0, 255, 0],
98
+ limegreen: [50, 205, 50],
99
+ linen: [250, 240, 230],
100
+ magenta: [255, 0, 255],
101
+ maroon: [128, 0, 0],
102
+ mediumaquamarine: [102, 205, 170],
103
+ mediumblue: [0, 0, 205],
104
+ mediumorchid: [186, 85, 211],
105
+ mediumpurple: [147, 112, 219],
106
+ mediumseagreen: [60, 179, 113],
107
+ mediumslateblue: [123, 104, 238],
108
+ mediumspringgreen: [0, 250, 154],
109
+ mediumturquoise: [72, 209, 204],
110
+ mediumvioletred: [199, 21, 133],
111
+ midnightblue: [25, 25, 112],
112
+ mintcream: [245, 255, 250],
113
+ mistyrose: [255, 228, 225],
114
+ moccasin: [255, 228, 181],
115
+ navajowhite: [255, 222, 173],
116
+ navy: [0, 0, 128],
117
+ oldlace: [253, 245, 230],
118
+ olive: [128, 128, 0],
119
+ olivedrab: [107, 142, 35],
120
+ orange: [255, 165, 0],
121
+ orangered: [255, 69, 0],
122
+ orchid: [218, 112, 214],
123
+ palegoldenrod: [238, 232, 170],
124
+ palegreen: [152, 251, 152],
125
+ paleturquoise: [175, 238, 238],
126
+ palevioletred: [219, 112, 147],
127
+ papayawhip: [255, 239, 213],
128
+ peachpuff: [255, 218, 185],
129
+ peru: [205, 133, 63],
130
+ pink: [255, 192, 203],
131
+ plum: [221, 160, 221],
132
+ powderblue: [176, 224, 230],
133
+ purple: [128, 0, 128],
134
+ rebeccapurple: [102, 51, 153],
135
+ red: [255, 0, 0],
136
+ rosybrown: [188, 143, 143],
137
+ royalblue: [65, 105, 225],
138
+ saddlebrown: [139, 69, 19],
139
+ salmon: [250, 128, 114],
140
+ sandybrown: [244, 164, 96],
141
+ seagreen: [46, 139, 87],
142
+ seashell: [255, 245, 238],
143
+ sienna: [160, 82, 45],
144
+ silver: [192, 192, 192],
145
+ skyblue: [135, 206, 235],
146
+ slateblue: [106, 90, 205],
147
+ slategray: [112, 128, 144],
148
+ slategrey: [112, 128, 144],
149
+ snow: [255, 250, 250],
150
+ springgreen: [0, 255, 127],
151
+ steelblue: [70, 130, 180],
152
+ tan: [210, 180, 140],
153
+ teal: [0, 128, 128],
154
+ thistle: [216, 191, 216],
155
+ tomato: [255, 99, 71],
156
+ turquoise: [64, 224, 208],
157
+ violet: [238, 130, 238],
158
+ wheat: [245, 222, 179],
159
+ white: [255, 255, 255],
160
+ whitesmoke: [245, 245, 245],
161
+ yellow: [255, 255, 0],
162
+ yellowgreen: [154, 205, 50],
163
+ };
164
+ /**
165
+ * Get RGB values for a CSS color name
166
+ * @param name - CSS color name (case-insensitive)
167
+ * @returns RGB tuple [r, g, b] or undefined if not found
168
+ */
169
+ export function getNamedColor(name) {
170
+ const color = CSS_COLORS[name.toLowerCase()];
171
+ return color;
172
+ }
173
+ /**
174
+ * Convert RGBA values to hex format
175
+ * @param r - Red channel (0-255)
176
+ * @param g - Green channel (0-255)
177
+ * @param b - Blue channel (0-255)
178
+ * @param a - Alpha channel (0-1)
179
+ * @returns Hex color string in format #RRGGBBAA
180
+ */
181
+ export function rgbaToHex(r, g, b, a) {
182
+ const red = channelToHex(r);
183
+ const green = channelToHex(g);
184
+ const blue = channelToHex(b);
185
+ const alpha = channelToHex(Math.round(a * 255));
186
+ return `#${red}${green}${blue}${alpha}`;
187
+ }
188
+ /**
189
+ * Convert a single color channel value to hex
190
+ * @param value - Channel value (0-255)
191
+ * @returns Two-digit hex string
192
+ */
193
+ export function channelToHex(value) {
194
+ const clamped = Math.max(0, Math.min(255, Math.round(value)));
195
+ return clamped.toString(16).padStart(2, '0').toUpperCase();
196
+ }
197
+ /**
198
+ * Clamp RGB channel value to valid range
199
+ * @param value - Channel value
200
+ * @returns Clamped value (0-255)
201
+ */
202
+ export function clampColorChannel(value) {
203
+ if (Number.isNaN(value)) {
204
+ return 0;
205
+ }
206
+ return Math.max(0, Math.min(255, value));
207
+ }
208
+ /**
209
+ * Clamp alpha value to valid range
210
+ * @param value - Alpha value
211
+ * @returns Clamped value (0-1)
212
+ */
213
+ export function clampAlpha(value) {
214
+ if (Number.isNaN(value)) {
215
+ return 1;
216
+ }
217
+ return Math.max(0, Math.min(1, value));
218
+ }
219
+ /**
220
+ * Convert any color format to hex
221
+ * Supports: hex (#RGB, #RRGGBB, #RRGGBBAA), RGB/RGBA, and CSS color names
222
+ * @param value - Color string in any supported format
223
+ * @returns Hex color string (#RRGGBBAA) or undefined if invalid
224
+ */
225
+ export function toHexColor(value) {
226
+ // Try hex format
227
+ const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
228
+ if (hexMatch) {
229
+ const hex = hexMatch[1];
230
+ if (hex.length === 3 || hex.length === 4) {
231
+ return `#${hex
232
+ .split('')
233
+ .map((char) => char + char)
234
+ .join('')}`;
235
+ }
236
+ return `#${hex}`;
237
+ }
238
+ // Try RGB/RGBA format
239
+ const rgbMatch = value.match(/^rgba?\((.+)\)$/i);
240
+ if (rgbMatch) {
241
+ const parts = rgbMatch[1]
242
+ .split(',')
243
+ .map((part) => part.trim())
244
+ .filter(Boolean);
245
+ if (parts.length === 3 || parts.length === 4) {
246
+ const [r, g, b, a] = parts;
247
+ const red = clampColorChannel(parseFloat(r));
248
+ const green = clampColorChannel(parseFloat(g));
249
+ const blue = clampColorChannel(parseFloat(b));
250
+ const alpha = parts.length === 4 ? clampAlpha(parseFloat(a)) : 1;
251
+ return rgbaToHex(red, green, blue, alpha);
252
+ }
253
+ }
254
+ // Try CSS color name
255
+ const rgb = getNamedColor(value);
256
+ if (rgb) {
257
+ return rgbaToHex(rgb[0], rgb[1], rgb[2], 1);
258
+ }
259
+ return undefined;
260
+ }
261
+ /**
262
+ * Darken a hex color by a specified amount
263
+ * @param hex - Hex color string
264
+ * @param amount - Amount to darken (0-255)
265
+ * @returns Darkened hex color
266
+ */
267
+ export function darkenColor(hex, amount) {
268
+ const normalized = ensureAlphaChannel(hex).substring(1); // strip #
269
+ const rgb = normalized.substring(0, 6);
270
+ const alpha = normalized.substring(6) || 'FF';
271
+ const r = parseInt(rgb.substring(0, 2), 16);
272
+ const g = parseInt(rgb.substring(2, 4), 16);
273
+ const b = parseInt(rgb.substring(4, 6), 16);
274
+ const clamp = (val) => Math.max(0, Math.min(255, val));
275
+ const newR = clamp(r - amount);
276
+ const newG = clamp(g - amount);
277
+ const newB = clamp(b - amount);
278
+ return `#${channelToHex(newR)}${channelToHex(newG)}${channelToHex(newB)}${alpha.toUpperCase()}`;
279
+ }
280
+ /**
281
+ * Lighten a hex color by a specified amount
282
+ * @param hex - Hex color string
283
+ * @param amount - Amount to lighten (0-255)
284
+ * @returns Lightened hex color
285
+ */
286
+ export function lightenColor(hex, amount) {
287
+ const normalized = ensureAlphaChannel(hex).substring(1); // strip #
288
+ const rgb = normalized.substring(0, 6);
289
+ const alpha = normalized.substring(6) || 'FF';
290
+ const r = parseInt(rgb.substring(0, 2), 16);
291
+ const g = parseInt(rgb.substring(2, 4), 16);
292
+ const b = parseInt(rgb.substring(4, 6), 16);
293
+ const clamp = (val) => Math.max(0, Math.min(255, val));
294
+ const newR = clamp(r + amount);
295
+ const newG = clamp(g + amount);
296
+ const newB = clamp(b + amount);
297
+ return `#${channelToHex(newR)}${channelToHex(newG)}${channelToHex(newB)}${alpha.toUpperCase()}`;
298
+ }
299
+ /**
300
+ * Convert hex color to RGBA object
301
+ * @param hex - Hex color string (#RRGGBB or #RRGGBBAA)
302
+ * @returns RGBA object with r, g, b, a properties (0-1 for alpha)
303
+ */
304
+ export function hexToRgba(hex) {
305
+ const normalized = ensureAlphaChannel(hex).substring(1); // strip #
306
+ const rgb = normalized.substring(0, 6);
307
+ const alphaHex = normalized.substring(6) || 'FF';
308
+ const r = parseInt(rgb.substring(0, 2), 16);
309
+ const g = parseInt(rgb.substring(2, 4), 16);
310
+ const b = parseInt(rgb.substring(4, 6), 16);
311
+ const a = parseInt(alphaHex, 16) / 255;
312
+ return { r, g, b, a };
313
+ }
314
+ /**
315
+ * Normalize any color format to Grid3's 8-digit hex format
316
+ * @param input - Color string in any supported format
317
+ * @param fallback - Fallback color if input is invalid (default: white)
318
+ * @returns Normalized color in format #AARRGGBBFF
319
+ */
320
+ export function normalizeColor(input, fallback = '#FFFFFFFF') {
321
+ const trimmed = input.trim();
322
+ if (!trimmed) {
323
+ return fallback;
324
+ }
325
+ const hex = toHexColor(trimmed);
326
+ if (hex) {
327
+ return ensureAlphaChannel(hex).toUpperCase();
328
+ }
329
+ return fallback;
330
+ }
331
+ /**
332
+ * Ensure a color has an alpha channel (Grid3 format requires 8-digit ARGB)
333
+ * @param color - Color string (hex format)
334
+ * @returns Color with alpha channel in format #AARRGGBBFF
335
+ */
336
+ export function ensureAlphaChannel(color) {
337
+ if (!color)
338
+ return '#FFFFFFFF';
339
+ // If already 8 digits (with alpha), return as is
340
+ if (color.match(/^#[0-9A-Fa-f]{8}$/))
341
+ return color;
342
+ // If 6 digits (no alpha), add FF for fully opaque
343
+ if (color.match(/^#[0-9A-Fa-f]{6}$/))
344
+ return color + 'FF';
345
+ // If 3 digits (shorthand), expand to 8
346
+ if (color.match(/^#[0-9A-Fa-f]{3}$/)) {
347
+ const r = color[1];
348
+ const g = color[2];
349
+ const b = color[3];
350
+ return `#${r}${r}${g}${g}${b}${b}FF`;
351
+ }
352
+ // Invalid or unknown format, return white
353
+ return '#FFFFFFFF';
354
+ }