color-name-list 12.2.0 → 13.0.0

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 (47) hide show
  1. package/README.md +2 -2
  2. package/changes.svg +3 -3
  3. package/dist/colornames.bestof.csv +5 -4
  4. package/dist/colornames.bestof.esm.js +1 -1
  5. package/dist/colornames.bestof.esm.mjs +1 -1
  6. package/dist/colornames.bestof.html +1 -1
  7. package/dist/colornames.bestof.json +1 -1
  8. package/dist/colornames.bestof.min.json +1 -1
  9. package/dist/colornames.bestof.scss +1 -1
  10. package/dist/colornames.bestof.umd.js +1 -1
  11. package/dist/colornames.bestof.xml +17 -13
  12. package/dist/colornames.bestof.yaml +13 -10
  13. package/dist/colornames.csv +11 -38
  14. package/dist/colornames.esm.js +1 -1
  15. package/dist/colornames.esm.mjs +1 -1
  16. package/dist/colornames.html +1 -1
  17. package/dist/colornames.json +1 -1
  18. package/dist/colornames.min.json +1 -1
  19. package/dist/colornames.scss +1 -1
  20. package/dist/colornames.short.csv +3 -2
  21. package/dist/colornames.short.esm.js +1 -1
  22. package/dist/colornames.short.esm.mjs +1 -1
  23. package/dist/colornames.short.html +1 -1
  24. package/dist/colornames.short.json +1 -1
  25. package/dist/colornames.short.min.json +1 -1
  26. package/dist/colornames.short.scss +1 -1
  27. package/dist/colornames.short.umd.js +1 -1
  28. package/dist/colornames.short.xml +9 -5
  29. package/dist/colornames.short.yaml +7 -4
  30. package/dist/colornames.umd.js +1 -1
  31. package/dist/colornames.xml +17 -125
  32. package/dist/colornames.yaml +15 -96
  33. package/dist/history.json +1 -1
  34. package/package.json +1 -1
  35. package/scripts/build.js +33 -161
  36. package/scripts/lib.js +38 -6
  37. package/scripts/sortSrc.js +4 -5
  38. package/src/colornames.csv +13 -40
  39. package/tests/_utils/report.js +76 -0
  40. package/tests/csv-test-data.js +108 -0
  41. package/tests/duplicate-allowlist.json +28 -1
  42. package/tests/duplicate-plurals-allowlist.json +4 -1
  43. package/tests/duplicates.test.js +134 -40
  44. package/tests/formats.test.js +9 -8
  45. package/tests/imports.test.js +12 -14
  46. package/tests/sorting.test.js +10 -12
  47. package/tests/validations.test.js +219 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "color-name-list",
3
- "version": "12.2.0",
3
+ "version": "13.0.0",
4
4
  "description": "long list of color names",
5
5
  "main": "dist/colornames.json",
6
6
  "browser": "dist/colornames.umd.js",
package/scripts/build.js CHANGED
@@ -1,22 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { parseCSVString, findDuplicates, objArrToString } from './lib.js';
3
+ import { parseCSVString, objArrToString } from './lib.js';
4
4
  import { exec } from 'child_process';
5
5
 
6
- const args = process.argv;
7
- // treat --testonly / --testOnly the same
8
- const isTestRun = args.some((arg) => arg.toLowerCase() === '--testonly');
9
-
10
- // only hex colors with 6 values
11
- const hexColorValidation = /^#[0-9a-f]{6}$/;
12
- const errors = [];
13
-
14
- // spaces regex
15
- const spacesValidation = /^\s+|\s{2,}|\s$/;
16
-
17
- // quote regex
18
- const quoteValidation = /"|'|`/;
19
-
20
6
  // setting
21
7
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
22
8
  const baseFolder = __dirname + '/../';
@@ -29,7 +15,6 @@ const readmeFileName = 'README.md';
29
15
  const fileNameShortPostfix = '.short';
30
16
  const maxShortNameLength = 12;
31
17
 
32
- const sortBy = 'name';
33
18
  const csvKeys = ['name', 'hex'];
34
19
  const bestOfKey = 'good name';
35
20
 
@@ -40,63 +25,6 @@ const src = fs
40
25
 
41
26
  const colorsSrc = parseCSVString(src);
42
27
 
43
- // sort by sorting criteria
44
- colorsSrc.entries.sort((a, b) => {
45
- return a[sortBy].localeCompare(b[sortBy]);
46
- });
47
-
48
- csvKeys.forEach((key) => {
49
- // find duplicates
50
- const dupes = findDuplicates(colorsSrc.values[key]);
51
- dupes.forEach((dupe) => {
52
- log(key, dupe, `found a double ${key}`);
53
- });
54
- });
55
-
56
- // loop hex values for validations
57
- colorsSrc.values['hex'].forEach((hex) => {
58
- // validate HEX values
59
- if (!hexColorValidation.test(hex)) {
60
- log(
61
- 'hex',
62
- hex,
63
- `${hex} is not a valid hex value. (Or to short, we avoid using the hex shorthands, no capital letters)`
64
- );
65
- }
66
- });
67
-
68
- // loop names
69
- colorsSrc.values['name'].forEach((name) => {
70
- // check for spaces
71
- if (spacesValidation.test(name)) {
72
- log('name', name, `${name} found either a leading or trailing space (or both)`);
73
- }
74
- if (quoteValidation.test(name)) {
75
- log('name', name, `${name} found a quote character, should be an apostrophe ’`);
76
- }
77
- });
78
-
79
- // loop good name markers
80
- colorsSrc.values[bestOfKey].forEach((str) => {
81
- // check for spaces
82
- if (spacesValidation.test(str)) {
83
- // Use the actual CSV key so we can resolve the offending entries (names)
84
- log(bestOfKey, str, `${str} found either a leading or trailing space (or both)`);
85
- }
86
-
87
- if (!(str == 'x' || str == '')) {
88
- // Use the actual CSV key so we can resolve the offending entries (names)
89
- log(bestOfKey, str, `${str} must be a lowercase "x" character or empty`);
90
- }
91
- });
92
-
93
- showLog();
94
- // In test mode we still perform the build so tests can import dist artifacts,
95
- // but we avoid mutating repository files like README.md or generating the SVG.
96
- if (isTestRun) {
97
- console.log('Test mode: skipping README & SVG generation.');
98
- }
99
-
100
28
  // creates JS related files
101
29
  const JSONExportString = JSON.stringify(
102
30
  [...colorsSrc.entries].map(
@@ -339,91 +267,37 @@ for (const outputFormat in outputFormats) {
339
267
  }
340
268
  }
341
269
 
342
- if (!isTestRun) {
343
- // updates the color count in readme file
344
- const readme = fs
345
- .readFileSync(path.normalize(`${baseFolder}${readmeFileName}`), 'utf8')
346
- .toString();
347
- fs.writeFileSync(
348
- path.normalize(`${baseFolder}${readmeFileName}`),
349
- readme
350
- .replace(
351
- // update color count in text
352
- /__\d+__/g,
353
- `__${colorsSrc.entries.length}__`
354
- )
355
- .replace(
356
- // update color count in badge
357
- /\d+-colors-orange/,
358
- `${colorsSrc.entries.length}-colors-orange`
359
- )
360
- .replace(
361
- // update color count in percentage
362
- /__\d+(\.\d+)?%__/,
363
- `__${((colorsSrc.entries.length / (256 * 256 * 256)) * 100).toFixed(2)}%__`
364
- )
365
- .replace(
366
- // update file size
367
- /\d+(\.\d+)? MB\)__/, // no global to only hit first occurrence
368
- `${(
369
- fs.statSync(path.normalize(`${baseFolder}${folderDist}${fileNameSrc}.json`)).size /
370
- 1024 /
371
- 1024
372
- ).toFixed(2)} MB)__`
373
- ),
374
- 'utf8'
375
- );
376
- }
377
-
378
- /**
379
- * outputs the collected logs
380
- */
381
- function showLog() {
382
- let errorLevel = 0;
383
- let totalErrors = 0;
384
- errors.forEach((error, i) => {
385
- totalErrors = i + 1;
386
- errorLevel = error.errorLevel || errorLevel;
387
- console.log(`${error.errorLevel ? '⛔' : '⚠'} ${error.message}`);
388
- if (Array.isArray(error.entries) && error.entries.length) {
389
- // Print a concise list of offending names for quick scanning
390
- const nameList = error.entries
391
- .map((e) => (e && e.name ? `${e.name}${e.hex ? ` (${e.hex})` : ''}` : ''))
392
- .filter(Boolean)
393
- .join(', ');
394
- if (nameList) {
395
- console.log(`Offending name(s): ${nameList}`);
396
- }
397
- }
398
- // Keep the JSON dump for full context
399
- console.log(JSON.stringify(error.entries));
400
- console.log('*-------------------------*');
401
- });
402
- if (errorLevel) {
403
- throw new Error(`⚠ failed because of the ${totalErrors} error${totalErrors > 1 ? 's' : ''} above ⚠`);
404
- }
405
- return totalErrors;
406
- }
407
-
408
- /**
409
- * logs errors and warning
410
- * @param {string} key key to look for in input
411
- * @param {string} value value to look for
412
- * @param {string} message error message
413
- * @param {Number} errorLevel if any error is set to 1, the program will exit
414
- */
415
- function log(key, value, message, errorLevel = 1) {
416
- const error = {};
417
- // looks for the original item that caused the error
418
- error.entries = colorsSrc.entries.filter((entry) => {
419
- return entry[key] === value;
420
- });
421
-
422
- error.message = message;
423
- error.errorLevel = errorLevel;
424
-
425
- errors.push(error);
426
- }
270
+ // updates the color count in readme file
271
+ const readme = fs.readFileSync(path.normalize(`${baseFolder}${readmeFileName}`), 'utf8').toString();
272
+ fs.writeFileSync(
273
+ path.normalize(`${baseFolder}${readmeFileName}`),
274
+ readme
275
+ .replace(
276
+ // update color count in text
277
+ /__\d+__/g,
278
+ `__${colorsSrc.entries.length}__`
279
+ )
280
+ .replace(
281
+ // update color count in badge
282
+ /\d+-colors-orange/,
283
+ `${colorsSrc.entries.length}-colors-orange`
284
+ )
285
+ .replace(
286
+ // update color count in percentage
287
+ /__\d+(\.\d+)?%__/,
288
+ `__${((colorsSrc.entries.length / (256 * 256 * 256)) * 100).toFixed(2)}%__`
289
+ )
290
+ .replace(
291
+ // update file size
292
+ /\d+(\.\d+)? MB\)__/, // no global to only hit first occurrence
293
+ `${(
294
+ fs.statSync(path.normalize(`${baseFolder}${folderDist}${fileNameSrc}.json`)).size /
295
+ 1024 /
296
+ 1024
297
+ ).toFixed(2)} MB)__`
298
+ ),
299
+ 'utf8'
300
+ );
427
301
 
428
302
  // gets SVG template
429
303
  const svgTpl = fs.readFileSync(path.normalize(__dirname + '/changes.svg.tpl'), 'utf8').toString();
@@ -497,6 +371,4 @@ function diffSVG() {
497
371
  );
498
372
  }
499
373
 
500
- if (!isTestRun) {
501
- diffSVG();
502
- }
374
+ diffSVG();
package/scripts/lib.js CHANGED
@@ -131,19 +131,50 @@ export const normalizeNameForDuplicates = (name) => {
131
131
  * @returns {Array<{norm:string, entries:Array<{name:string, lineNumber?:number}>}>}
132
132
  */
133
133
  export const findNearDuplicateNameConflicts = (items, options = {}) => {
134
- const { allowlist = [], foldPlurals = false, pluralAllowlist = [] } = options;
135
-
136
- // Normalize allowlist entries so callers can provide raw names or already-normalized keys.
134
+ const {
135
+ allowlist = [],
136
+ foldPlurals = false,
137
+ pluralAllowlist = [],
138
+ foldStopwords = false,
139
+ stopwords = [],
140
+ } = options;
141
+
142
+ // Precompute stopword set (normalized to lowercase ASCII) when folding is enabled
143
+ const stopSet = foldStopwords
144
+ ? new Set(
145
+ (Array.isArray(stopwords) ? stopwords : [])
146
+ .filter((w) => typeof w === 'string' && w.trim().length)
147
+ .map((w) =>
148
+ String(w)
149
+ .toLowerCase()
150
+ .normalize('NFD')
151
+ .replace(/[\u0300-\u036f]/g, '')
152
+ )
153
+ )
154
+ : null;
155
+
156
+ // Helper: normalize name using current options (stopword folding if enabled)
157
+ const normalizeForOptions = (name) => {
158
+ const base = String(name)
159
+ .toLowerCase()
160
+ .normalize('NFD')
161
+ .replace(/[\u0300-\u036f]/g, '');
162
+ const tokens = base.match(/[a-z0-9]+/g) || [];
163
+ const filtered = foldStopwords && stopSet && stopSet.size ? tokens.filter((t) => !stopSet.has(t)) : tokens;
164
+ return filtered.length ? filtered.join('') : normalizeNameForDuplicates(name);
165
+ };
166
+
167
+ // Normalize allowlist entries using the same normalization function.
137
168
  const allowSet = new Set(
138
169
  (Array.isArray(allowlist) ? allowlist : [])
139
170
  .filter((v) => typeof v === 'string' && v.trim().length)
140
- .map((v) => normalizeNameForDuplicates(String(v)))
171
+ .map((v) => normalizeForOptions(String(v)))
141
172
  );
142
173
 
143
174
  const byNorm = new Map();
144
175
  for (const item of items) {
145
176
  if (!item || typeof item.name !== 'string') continue;
146
- const norm = normalizeNameForDuplicates(item.name);
177
+ const norm = normalizeForOptions(item.name);
147
178
  if (!byNorm.has(norm)) byNorm.set(norm, []);
148
179
  byNorm.get(norm).push({ name: item.name, lineNumber: item.lineNumber });
149
180
  }
@@ -153,7 +184,8 @@ export const findNearDuplicateNameConflicts = (items, options = {}) => {
153
184
  const pluralAllowSet = new Set(
154
185
  (Array.isArray(pluralAllowlist) ? pluralAllowlist : [])
155
186
  .filter((v) => typeof v === 'string' && v.trim().length)
156
- .map((v) => normalizeNameForDuplicates(String(v)))
187
+ // Use the same normalization as used for keys (respects stopword folding)
188
+ .map((v) => normalizeForOptions(String(v)))
157
189
  );
158
190
  // We iterate over a snapshot of keys because we'll mutate the map.
159
191
  for (const key of Array.from(byNorm.keys())) {
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Script to sort the colornames.csv file alphabetically by name
5
- * This helps maintain order when new colors are added to the list
6
- */
4
+ * Script to sort the colornames.csv file alphabetically by name
5
+ * This helps maintain order when new colors are added to the list
6
+ */
7
7
 
8
8
  import fs from 'fs';
9
9
  import path from 'path';
@@ -28,7 +28,7 @@ const readAndSortCSV = () => {
28
28
  while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
29
29
 
30
30
  // Trim trailing whitespace on each line
31
- lines = lines.map(l => l.replace(/\s+$/,''));
31
+ lines = lines.map((l) => l.replace(/\s+$/, ''));
32
32
 
33
33
  // The header should be kept as the first line
34
34
  const header = lines[0];
@@ -52,7 +52,6 @@ const readAndSortCSV = () => {
52
52
 
53
53
  console.log(`✅ Successfully sorted ${sortedColorLines.length} colors alphabetically by name`);
54
54
  console.log(`📝 File saved: ${csvPath}`);
55
-
56
55
  } catch (error) {
57
56
  console.error('❌ Error sorting the CSV file:', error);
58
57
  process.exit(1);
@@ -200,7 +200,6 @@ After Dinner Mint,#e3f5e5,
200
200
  After Eight,#3d2e24,
201
201
  After Eight Filling,#d6eae8,
202
202
  After Midnight,#38393f,x
203
- After Rain,#c1dbea,
204
203
  After Shock,#fec65f,
205
204
  After the Rain,#8bc4d1,
206
205
  After the Storm,#33616a,x
@@ -930,7 +929,6 @@ Apple Crisp,#e19c55,
930
929
  Apple Crunch,#fee5c9,
931
930
  Apple Cucumber,#dbdbbc,
932
931
  Apple Custard,#fddfae,
933
- Apple Day,#7e976d,
934
932
  Apple Flower,#edf4eb,
935
933
  Apple Fritter,#cc9350,
936
934
  Apple Green,#76cd26,
@@ -957,7 +955,7 @@ Apple Slice,#f1f0bf,
957
955
  Apple Turnover,#e8c194,
958
956
  Apple Valley,#ea8386,
959
957
  Apple Wine,#b59f62,
960
- Apple-A-Day,#903f45,
958
+ Apple-a-Day,#903f45,
961
959
  Applegate,#8ac479,
962
960
  Applegate Park,#aead93,
963
961
  Applemint,#cdeacd,
@@ -1402,7 +1400,7 @@ Asurmen Blue Wash,#273e51,
1402
1400
  Aswad Black,#17181c,
1403
1401
  At Ease,#e7eee1,
1404
1402
  At Ease Soldier,#9e9985,
1405
- At The Beach,#e7d9b9,
1403
+ At the Beach,#e7d9b9,
1406
1404
  Atelier,#a3abb8,
1407
1405
  Ateneo Blue,#003a6c,
1408
1406
  Athena,#dcd7cc,
@@ -2096,7 +2094,6 @@ Baywater Blue,#c9d8e4,
2096
2094
  Bazaar,#8f7777,
2097
2095
  Bazooka Pink,#ffa6c9,
2098
2096
  BBQ,#a35046,
2099
- Be Daring,#ffc943,
2100
2097
  Be Mine,#f4e3e7,
2101
2098
  Be My Valentine,#ec9dc3,
2102
2099
  Be Spontaneous,#a5cb66,
@@ -2933,7 +2930,7 @@ Blue Brocade,#70b8d0,
2933
2930
  Blue Bubble,#a6d7eb,
2934
2931
  Blue Burst,#309cd0,x
2935
2932
  Blue Buzz,#a1a2bd,
2936
- Blue By You,#a0b7ba,
2933
+ Blue by You,#a0b7ba,
2937
2934
  Blue Calico,#a5cde1,
2938
2935
  Blue Calypso,#55a7b6,
2939
2936
  Blue Cardinal Flower,#2f36ba,
@@ -3199,7 +3196,7 @@ Blue Team Spirit,#5885a2,
3199
3196
  Blue Thistle,#adc0d6,
3200
3197
  Blue Tint,#9fd9d7,
3201
3198
  Blue Titmouse,#4466ff,
3202
- Blue To You,#babfc5,
3199
+ Blue to You,#babfc5,
3203
3200
  Blue Tone Ink,#2b4057,
3204
3201
  Blue Topaz,#65aece,
3205
3202
  Blue Torus,#042993,
@@ -3777,7 +3774,7 @@ Broad Bean,#94975d,
3777
3774
  Broad Daylight,#bbddff,x
3778
3775
  Broadleaf Forest,#014421,
3779
3776
  Broadwater Blue,#034a71,
3780
- Broadway,#434442,
3777
+ Broadway,#145775,
3781
3778
  Broadway Lights,#fee07c,
3782
3779
  Brocade,#8c87c5,
3783
3780
  Brocade Violet,#7b4d6b,
@@ -3823,7 +3820,6 @@ Broomstick,#74462d,
3823
3820
  Brother Blue,#b0b7c6,
3824
3821
  Brown,#653700,x
3825
3822
  Brown Alpaca,#b86d29,x
3826
- Brown Bag,#deac6e,
3827
3823
  Brown Bear,#4a3f37,
3828
3824
  Brown Beauty,#4a3832,
3829
3825
  Brown Beige,#cc8833,
@@ -3872,7 +3868,7 @@ Brown Tumbleweed,#37290e,
3872
3868
  Brown Velvet,#704e40,
3873
3869
  Brown Wood,#b4674d,
3874
3870
  Brown Yellow,#dd9966,
3875
- Brown-Bag-It,#ddbda3,
3871
+ Brown-Bag It,#ddbda3,
3876
3872
  Brown-Noser,#79512c,
3877
3873
  Browned Off,#bb4433,
3878
3874
  Brownie,#964b00,x
@@ -4195,8 +4191,7 @@ Buzz-In,#ffd756,
4195
4191
  Buzzard,#5f563f,
4196
4192
  Buzzards Bay,#017a79,
4197
4193
  By Gum,#816a38,
4198
- By the Bayou,#007b90,
4199
- By The Sea,#8d999e,
4194
+ By the Sea,#8d999e,
4200
4195
  Byakuroku Green,#a5ba93,
4201
4196
  Bygone,#918e8a,
4202
4197
  Bypass,#b6c4d2,
@@ -5832,10 +5827,8 @@ Cloud Abyss,#dfe7eb,
5832
5827
  Cloud Blue,#a2b6b9,
5833
5828
  Cloud Break,#f6f1fe,x
5834
5829
  Cloud Cover,#adb5bc,
5835
- Cloud Cream,#ded1b3,
5836
5830
  Cloud Dancer,#f0eee9,x
5837
5831
  Cloud Grey,#b8a9af,
5838
- Cloud Nine,#e9e0db,
5839
5832
  Cloud Number Nine,#f9cec6,
5840
5833
  Cloud of Cream,#f1e2c4,x
5841
5834
  Cloud Over London,#c2bcb1,
@@ -6942,7 +6935,6 @@ Curaçao Blue,#008894,
6942
6935
  Curated Lilac,#a6a6b6,
6943
6936
  Curated White,#eae1ce,
6944
6937
  Curd,#f8e1ba,
6945
- Curds & Whey,#b59c76,
6946
6938
  Curds and Whey,#bca483,
6947
6939
  Cure All,#aa6988,
6948
6940
  Cured Eggplant,#380835,
@@ -6974,8 +6966,7 @@ Custard Powder,#f8dcaa,
6974
6966
  Custard Puff,#fceeae,
6975
6967
  Customs Green,#003839,
6976
6968
  Cut Heather,#9e909e,
6977
- Cut of Mustard,#bc914d,
6978
- Cut the Mustard,#ba7f38,
6969
+ Cut the Mustard,#ba7f38,x
6979
6970
  Cut Velvet,#b391c8,
6980
6971
  Cute Crab,#dd4444,x
6981
6972
  Cute Little Pink,#f4e2e1,
@@ -7747,7 +7738,7 @@ Devil Blue,#277594,
7747
7738
  Devil’s Advocate,#ff3344,x
7748
7739
  Devil’s Butterfly,#bb4422,
7749
7740
  Devil’s Flower Mantis,#8f9805,
7750
- Devil’s Grass,#44aa55,
7741
+ Devil’s Grass,#44aa55,x
7751
7742
  Devil’s Lip,#662a2c,
7752
7743
  Devil’s Plum,#423450,
7753
7744
  Deviled Eggs,#fecd82,x
@@ -8036,7 +8027,6 @@ Downing Sand,#cbbca5,
8036
8027
  Downing Slate,#777f86,
8037
8028
  Downing Stone,#a6a397,
8038
8029
  Downing Straw,#caab7d,
8039
- Downing to Earth,#635a4f,
8040
8030
  Download Progress,#58d332,
8041
8031
  Downpour,#43718b,
8042
8032
  Downriver,#092256,
@@ -12737,7 +12727,6 @@ Honey Bunny,#dbb881,x
12737
12727
  Honey Butter,#f5d29b,
12738
12728
  Honey Carrot Cake,#ff9955,
12739
12729
  Honey Chili,#883344,
12740
- Honey Cream,#fae8ca,
12741
12730
  Honey Crisp,#e9c160,x
12742
12731
  Honey Crusted Chicken,#ffbb55,
12743
12732
  Honey Do,#ededc7,x
@@ -13161,8 +13150,6 @@ Ilvaite Black,#330011,
13161
13150
  Imagery,#7a6e70,
13162
13151
  Imaginary Mauve,#89687d,
13163
13152
  Imagination,#dfe0ee,
13164
- Imagine,#af9468,
13165
- Imagine That,#947c98,
13166
13153
  Imam Ali Gold,#fae199,
13167
13154
  Imayou Pink,#d0576b,
13168
13155
  Immaculate Iguana,#aacc00,
@@ -13202,7 +13189,7 @@ Impulse Blue,#006699,
13202
13189
  Impulsive Purple,#624977,
13203
13190
  Impure White,#f5e7e3,
13204
13191
  Imrik Blue,#67aed0,
13205
- In A Pickle,#978c59,x
13192
+ In a Pickle,#978c59,x
13206
13193
  In Caffeine We Trust,#693c2a,
13207
13194
  In for a Penny,#ee8877,x
13208
13195
  In Good Taste,#b6d4a0,x
@@ -15889,7 +15876,6 @@ Mahogany Spice,#5b4646,
15889
15876
  Mahonia Berry Blue,#62788e,
15890
15877
  Mai Tai,#a56531,x
15891
15878
  Maiden Hair,#f5e9ca,
15892
- Maiden Mist,#b9c0c0,
15893
15879
  Maiden of the Mist,#efdceb,
15894
15880
  Maiden Pink,#ff2feb,
15895
15881
  Maiden Voyage,#8ac7d4,
@@ -17008,7 +16994,6 @@ Mission Wildflower,#9e5566,
17008
16994
  Mississippi Mud,#99886f,
17009
16995
  Mississippi River,#3b638c,x
17010
16996
  Missouri Mud,#a6a19b,
17011
- Mist Green,#aacebc,
17012
16997
  Mist Grey,#c4c4bc,
17013
16998
  Mist of Green,#e3f1eb,
17014
16999
  Mist Spirit,#e4ebe7,
@@ -17526,7 +17511,6 @@ Mousy Indigo,#5c544e,
17526
17511
  Moutarde de Bénichon,#bf9005,x
17527
17512
  Move Mint,#4effcd,
17528
17513
  Mover & Shaker,#9cce9e,
17529
- Mover and Shaker,#855d44,
17530
17514
  Movie Magic,#b2bfd5,
17531
17515
  Movie Star,#c52033,
17532
17516
  Mow the Lawn,#a9b49a,
@@ -18656,7 +18640,6 @@ Olive Grey,#afa78d,
18656
18640
  Olive Grove,#716a4d,
18657
18641
  Olive Haze,#888064,
18658
18642
  Olive Hint,#c9bd88,
18659
- Olive It,#aeab9a,
18660
18643
  Olive Leaf,#4e4b35,x
18661
18644
  Olive Leaf Tea,#78866b,
18662
18645
  Olive Martini,#ced2ab,
@@ -18970,7 +18953,6 @@ OU Crimson Red,#990000,
18970
18953
  Oubliette,#4f4944,
18971
18954
  Ouni Red,#ee7948,
18972
18955
  Our Little Secret,#a84b7a,x
18973
- Out of Blue,#c0f7db,
18974
18956
  Out of Fashion,#f26d8f,
18975
18957
  Out of Plumb,#9c909c,
18976
18958
  Out of the Blue,#1199ee,x
@@ -19732,7 +19714,6 @@ Pearl Yellow,#f1e3bc,
19732
19714
  Pearled Couscous,#f2e9d5,
19733
19715
  Pearled Ivory,#f0dfcc,
19734
19716
  Pearls & Lace,#dcd0cb,
19735
- Pearls and Lace,#eee7dc,
19736
19717
  Pearly,#f4e3df,x
19737
19718
  Pearly Pink,#ee99cc,x
19738
19719
  Pearly Purple,#b768a2,
@@ -21099,7 +21080,6 @@ Pretty Pale,#e3c6d6,
21099
21080
  Pretty Parasol,#ac5d3e,
21100
21081
  Pretty Pastry,#dfcdb2,x
21101
21082
  Pretty Petunia,#d6b7e2,
21102
- Pretty Pink,#ebb3b2,
21103
21083
  Pretty Pink Piggy,#eeaadd,
21104
21084
  Pretty Please,#ffccc8,
21105
21085
  Pretty Posie,#bcbde4,
@@ -22057,6 +22037,7 @@ Red Salsa,#fd3a4a,
22057
22037
  Red Sandstorm,#e5cac0,
22058
22038
  Red Sauce Parlor,#cc3b22,
22059
22039
  Red Savina Pepper,#ee0128,
22040
+ Red Seal,#c92d21,x
22060
22041
  Red Sentinel,#b9090f,
22061
22042
  Red Shade Wash,#862808,
22062
22043
  Red Shimmer,#fee0da,
@@ -24494,7 +24475,6 @@ Sinatra,#4675b7,
24494
24475
  Sinbad,#a6d5d0,
24495
24476
  Sinful,#645059,
24496
24477
  Singapore Orchid,#a020f0,
24497
- Singing Blue,#0074a4,
24498
24478
  Singing in the Rain,#8e9c98,
24499
24479
  Singing the Blues,#2b4d68,
24500
24480
  Single Origin,#713e39,x
@@ -25112,6 +25092,7 @@ Sour Lemon,#ffeea5,x
25112
25092
  Sour Lime,#acc326,x
25113
25093
  Sour Patch Peach,#f4d9c5,
25114
25094
  Sour Tarts,#fee5c8,
25095
+ Sour Veil,#ffffbf,x
25115
25096
  Sour Yellow,#eeff04,x
25116
25097
  Source Blue,#cdeae5,
25117
25098
  Source Green,#84b6a2,
@@ -26392,12 +26373,11 @@ Swedish Blue,#007cc0,
26392
26373
  Swedish Clover,#7b8867,
26393
26374
  Swedish Green,#184d43,
26394
26375
  Swedish Yellow,#fce081,
26395
- Sweet & Sour,#c9aa37,
26376
+ Sweet & Sour,#c4bf0b,
26396
26377
  Sweet 60,#f29eab,
26397
26378
  Sweet Almond,#cc9977,
26398
26379
  Sweet Alyssum,#e7c2de,
26399
26380
  Sweet and Sassy,#e1c9d1,x
26400
- Sweet and Sour,#c4bf0b,
26401
26381
  Sweet Angel,#f5c8bb,
26402
26382
  Sweet Angelica,#e8d08e,
26403
26383
  Sweet Annie,#9c946e,
@@ -26439,7 +26419,6 @@ Sweet Georgia Brown,#8b715a,
26439
26419
  Sweet Grape,#4d3d52,
26440
26420
  Sweet Grass,#b2b68a,
26441
26421
  Sweet Harbor,#d9dde7,
26442
- Sweet Honey,#d4a55c,
26443
26422
  Sweet Illusion,#e0e8ec,
26444
26423
  Sweet Jasmine,#f9f4d4,
26445
26424
  Sweet Juliet,#b8bfd2,
@@ -27003,13 +26982,10 @@ Thatched Cottage,#d6c7a6,
27003
26982
  Thatched Roof,#efe0c6,
27004
26983
  Thawed Out,#e1eeec,
27005
26984
  The Art of Seduction,#dd0088,
27006
- The Blarney Stone,#ab9f89,
27007
26985
  The Bluff,#ffc8c2,
27008
26986
  The Boulevard,#d0a492,
27009
- The Broadway,#145775,
27010
26987
  The Cottage,#837663,
27011
26988
  The Count’s Black,#102030,x
27012
- The Devil’s Grass,#666420,x
27013
26989
  The Ego Has Landed,#a75455,
27014
26990
  The End,#2a2a2a,x
27015
26991
  The End Is Beer,#eed508,
@@ -27024,7 +27000,6 @@ The Killing Joke,#b0bf1a,
27024
27000
  The Legend of Green,#558844,x
27025
27001
  The New Black,#ff8400,
27026
27002
  The Rainbow Fish,#4466ee,
27027
- The Real Teal,#007883,
27028
27003
  The Sickener,#db7093,
27029
27004
  The Speed of Light,#f6f4ef,
27030
27005
  The Vast of Night,#110066,x
@@ -29157,7 +29132,6 @@ Whisper of Smoke,#cbcecf,x
29157
29132
  Whisper of White,#eadbca,x
29158
29133
  Whisper Pink,#d4c5b4,
29159
29134
  Whisper Ridge,#c9c3b5,
29160
- Whisper White,#eae2d3,x
29161
29135
  Whisper Yellow,#ffe5b9,
29162
29136
  Whispered Secret,#3f4855,
29163
29137
  Whispering Blue,#c9dcdc,
@@ -29872,7 +29846,6 @@ Yearning,#061088,
29872
29846
  Yearning Desire,#ca135e,x
29873
29847
  Yeast,#fae1ac,
29874
29848
  Yell for Yellow,#fffe00,x
29875
- Yell Yellow,#ffffbf,x
29876
29849
  Yellow,#ffff00,x
29877
29850
  Yellow Acorn,#b68d4c,
29878
29851
  Yellow Avarice,#f5f5d9,
@@ -0,0 +1,76 @@
1
+ // Small utility to build consistent, helpful failure messages in tests.
2
+ // Keeps output informative while avoiding repeated boilerplate in each test file.
3
+
4
+ /**
5
+ * Simple pluralization helper
6
+ * @param {number} n
7
+ * @param {string} singular
8
+ * @param {string} [plural]
9
+ * @returns {string}
10
+ */
11
+ export function pluralize(n, singular, plural = `${singular}s`) {
12
+ return n === 1 ? singular : plural;
13
+ }
14
+
15
+ /**
16
+ * Join values as a readable, comma-separated list.
17
+ * Ensures uniqueness and stable order.
18
+ * @param {Array<string|number>} items
19
+ * @returns {string}
20
+ */
21
+ export function list(items) {
22
+ if (!items || !items.length) return '';
23
+ const uniq = [...new Set(items.map(String))];
24
+ return uniq.join(', ');
25
+ }
26
+
27
+ /**
28
+ * Build a consistent failure message for test assertions.
29
+ *
30
+ * @param {Object} opts
31
+ * @param {string} opts.title - Heading/title of the error (without counts)
32
+ * @param {Array<string>} [opts.offenders] - Values to summarize in a one-line list.
33
+ * @param {string} [opts.offenderLabel] - Label for offenders list (e.g. "name", "hex code").
34
+ * @param {Array<string>} [opts.details] - Additional bullet/section lines to include.
35
+ * @param {Array<string>} [opts.tips] - How-to-fix tips appended at the end.
36
+ * @param {number} [opts.count] - Optional explicit count (defaults to offenders.length).
37
+ * @param {string} [opts.icon] - Optional icon/emphasis prefix, default ⛔.
38
+ * @returns {string}
39
+ */
40
+ export function buildFailureMessage({
41
+ title,
42
+ offenders = [],
43
+ offenderLabel = 'item',
44
+ details = [],
45
+ tips = [],
46
+ count,
47
+ icon = '⛔',
48
+ } = {}) {
49
+ const msgLines = [];
50
+ const n = typeof count === 'number' ? count : offenders.length;
51
+
52
+ msgLines.push(
53
+ `${icon} ${title.replace('{n}', String(n)).replace('{items}', pluralize(n, offenderLabel))}`
54
+ );
55
+ msgLines.push('');
56
+
57
+ if (offenders.length) {
58
+ msgLines.push(`Offending ${pluralize(offenders.length, offenderLabel)}: ${list(offenders)}`);
59
+ msgLines.push('*-------------------------*');
60
+ msgLines.push('');
61
+ }
62
+
63
+ if (details.length) {
64
+ for (const line of details) msgLines.push(line);
65
+ msgLines.push('');
66
+ }
67
+
68
+ if (tips.length) {
69
+ msgLines.push('Tip:');
70
+ for (const t of tips) msgLines.push(` - ${t}`);
71
+ msgLines.push('');
72
+ }
73
+
74
+ msgLines.push('*-------------------------*');
75
+ return msgLines.join('\n');
76
+ }