color-name-list 12.1.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 (48) hide show
  1. package/.vscode/tasks.json +10 -0
  2. package/README.md +2 -2
  3. package/changes.svg +3 -3
  4. package/dist/colornames.bestof.csv +22 -20
  5. package/dist/colornames.bestof.esm.js +1 -1
  6. package/dist/colornames.bestof.esm.mjs +1 -1
  7. package/dist/colornames.bestof.html +1 -1
  8. package/dist/colornames.bestof.json +1 -1
  9. package/dist/colornames.bestof.min.json +1 -1
  10. package/dist/colornames.bestof.scss +1 -1
  11. package/dist/colornames.bestof.umd.js +1 -1
  12. package/dist/colornames.bestof.xml +59 -51
  13. package/dist/colornames.bestof.yaml +47 -41
  14. package/dist/colornames.csv +38 -135
  15. package/dist/colornames.esm.js +1 -1
  16. package/dist/colornames.esm.mjs +1 -1
  17. package/dist/colornames.html +1 -1
  18. package/dist/colornames.json +1 -1
  19. package/dist/colornames.min.json +1 -1
  20. package/dist/colornames.scss +1 -1
  21. package/dist/colornames.short.csv +12 -17
  22. package/dist/colornames.short.esm.js +1 -1
  23. package/dist/colornames.short.esm.mjs +1 -1
  24. package/dist/colornames.short.html +1 -1
  25. package/dist/colornames.short.json +1 -1
  26. package/dist/colornames.short.min.json +1 -1
  27. package/dist/colornames.short.scss +1 -1
  28. package/dist/colornames.short.umd.js +1 -1
  29. package/dist/colornames.short.xml +25 -45
  30. package/dist/colornames.short.yaml +21 -36
  31. package/dist/colornames.umd.js +1 -1
  32. package/dist/colornames.xml +69 -457
  33. package/dist/colornames.yaml +56 -347
  34. package/dist/history.json +1 -1
  35. package/package.json +1 -1
  36. package/scripts/build.js +33 -161
  37. package/scripts/lib.js +60 -5
  38. package/scripts/sortSrc.js +4 -5
  39. package/src/colornames.csv +42 -139
  40. package/tests/_utils/report.js +76 -0
  41. package/tests/csv-test-data.js +108 -0
  42. package/tests/duplicate-allowlist.json +28 -1
  43. package/tests/duplicate-plurals-allowlist.json +10 -0
  44. package/tests/duplicates.test.js +135 -40
  45. package/tests/formats.test.js +9 -8
  46. package/tests/imports.test.js +12 -14
  47. package/tests/sorting.test.js +10 -12
  48. 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.1.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,23 +131,78 @@ 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 = [] } = 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
  }
150
181
 
182
+ // Optional plural folding: merge keys ending in 's' with their singular if present.
183
+ if (foldPlurals) {
184
+ const pluralAllowSet = new Set(
185
+ (Array.isArray(pluralAllowlist) ? pluralAllowlist : [])
186
+ .filter((v) => typeof v === 'string' && v.trim().length)
187
+ // Use the same normalization as used for keys (respects stopword folding)
188
+ .map((v) => normalizeForOptions(String(v)))
189
+ );
190
+ // We iterate over a snapshot of keys because we'll mutate the map.
191
+ for (const key of Array.from(byNorm.keys())) {
192
+ if (key.length < 4) continue; // avoid over-aggressive folding for tiny words
193
+ if (!key.endsWith('s')) continue;
194
+ if (key.endsWith('ss')) continue; // glass vs glas, moss vs mos, keep
195
+ if (pluralAllowSet.has(key)) continue; // explicit allow: don't fold
196
+ const singular = key.slice(0, -1);
197
+ if (byNorm.has(singular)) {
198
+ // merge arrays
199
+ const merged = [...byNorm.get(singular), ...byNorm.get(key)];
200
+ byNorm.set(singular, merged);
201
+ byNorm.delete(key);
202
+ }
203
+ }
204
+ }
205
+
151
206
  const conflicts = [];
152
207
  for (const [norm, arr] of byNorm.entries()) {
153
208
  if (allowSet.has(norm)) continue; // explicitly allowed
@@ -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);