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
@@ -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
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { parseCSVString } from '../scripts/lib.js';
4
+
5
+ /**
6
+ * Shared CSV test data loader
7
+ * Provides consistent CSV data across all test files
8
+ */
9
+ class CSVTestData {
10
+ constructor() {
11
+ this._data = null;
12
+ this._raw = null;
13
+ this._lines = null;
14
+ this._items = null;
15
+ }
16
+
17
+ /**
18
+ * Load and parse the CSV file once
19
+ */
20
+ load() {
21
+ if (this._data) return; // Already loaded
22
+
23
+ const csvPath = path.resolve('./src/colornames.csv');
24
+ this._raw = fs.readFileSync(csvPath, 'utf8').replace(/\r\n/g, '\n').trimEnd();
25
+
26
+ // Parse with parseCSVString for structured access
27
+ this._data = parseCSVString(this._raw);
28
+
29
+ // Parse for line-by-line access (used by duplicates/sorting tests)
30
+ this._lines = this._raw.split('\n');
31
+ const header = this._lines.shift();
32
+
33
+ if (!header.startsWith('name,hex,good name')) {
34
+ throw new Error('Invalid CSV header format');
35
+ }
36
+
37
+ // Create items array with line numbers for duplicate checking
38
+ this._items = this._lines
39
+ .map((l, idx) => {
40
+ const lineNumber = idx + 2; // +1 for header, +1 because idx is 0-based
41
+ if (!l.trim()) return null;
42
+ const firstComma = l.indexOf(',');
43
+ const name = firstComma === -1 ? l : l.slice(0, firstComma);
44
+ return { name, lineNumber };
45
+ })
46
+ .filter(Boolean);
47
+ }
48
+
49
+ /**
50
+ * Get the parsed CSV data (using parseCSVString format)
51
+ */
52
+ get data() {
53
+ this.load();
54
+ return this._data;
55
+ }
56
+
57
+ /**
58
+ * Get the raw CSV content
59
+ */
60
+ get raw() {
61
+ this.load();
62
+ return this._raw;
63
+ }
64
+
65
+ /**
66
+ * Get CSV lines (without header)
67
+ */
68
+ get lines() {
69
+ this.load();
70
+ return this._lines;
71
+ }
72
+
73
+ /**
74
+ * Get items array with line numbers for duplicate checking
75
+ */
76
+ get items() {
77
+ this.load();
78
+ return this._items;
79
+ }
80
+
81
+ /**
82
+ * Get colors in simple format (name, hex)
83
+ */
84
+ get colors() {
85
+ this.load();
86
+ return this._data.entries.map((entry) => ({
87
+ name: entry.name,
88
+ hex: entry.hex,
89
+ }));
90
+ }
91
+
92
+ /**
93
+ * Get line count including header
94
+ */
95
+ get lineCount() {
96
+ this.load();
97
+ return this._lines.length + 1; // +1 for header
98
+ }
99
+ }
100
+
101
+ // Export a singleton instance
102
+ export const csvTestData = new CSVTestData();
103
+
104
+ // Export a convenience function to ensure data is loaded
105
+ export function loadCSVTestData() {
106
+ csvTestData.load();
107
+ return csvTestData;
108
+ }
@@ -8,5 +8,32 @@
8
8
  "Sable",
9
9
  "Sablé",
10
10
  "Rosé",
11
- "Fresh Water"
11
+ "Fresh Water",
12
+ "Undersea",
13
+ "In the Twilight",
14
+ "In the Tropics",
15
+ "Teal With It",
16
+ "In the Spotlight",
17
+ "In the Shadows",
18
+ "By the Sea",
19
+ "Sail On",
20
+ "In the Red",
21
+ "In the Pink",
22
+ "In A Pickle",
23
+ "In for a Penny",
24
+ "On the Nile",
25
+ "In the Navy",
26
+ "Mint to Be",
27
+ "Mauve It",
28
+ "At the Beach",
29
+ "In the Blue",
30
+ "Blue by You",
31
+ "In the Buff",
32
+ "Buff It",
33
+ "Buzz-In",
34
+ "Coffee With Cream",
35
+ "In the Dark",
36
+ "Dark as Night",
37
+ "Green With Envy",
38
+ "Lost in Space"
12
39
  ]
@@ -0,0 +1,10 @@
1
+ [
2
+ "Tropics",
3
+ "Blues",
4
+ "Greens",
5
+ "Nighthawks",
6
+ "Spearmints",
7
+ "In the Vines",
8
+ "On the Rocks",
9
+ "Sea of Stars"
10
+ ]
@@ -1,43 +1,71 @@
1
- import { describe, it, expect } from 'vitest';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { findNearDuplicateNameConflicts } from '../scripts/lib.js';
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { findNearDuplicateNameConflicts, findDuplicates } from '../scripts/lib.js';
5
3
  import allowlist from './duplicate-allowlist.json';
4
+ import pluralAllowlist from './duplicate-plurals-allowlist.json';
5
+ import { csvTestData } from './csv-test-data.js';
6
+ import { buildFailureMessage } from './_utils/report.js';
6
7
 
7
8
  describe('Duplicate-like color names', () => {
8
- it('should not contain names that only differ by spacing/punctuation/case/accents', () => {
9
- const csvPath = path.resolve('./src/colornames.csv');
10
- const raw = fs.readFileSync(csvPath, 'utf8').replace(/\r\n/g, '\n').trimEnd();
11
- const lines = raw.split('\n');
12
- expect(lines.length).toBeGreaterThan(1);
9
+ beforeAll(() => {
10
+ // Load CSV data once for all tests
11
+ csvTestData.load();
12
+ });
13
+
14
+ it('should not contain the same name twice', () => {
15
+ expect(csvTestData.lineCount).toBeGreaterThan(1);
16
+
17
+ const duplicates = findDuplicates(csvTestData.data.values['name']);
13
18
 
14
- const header = lines.shift();
15
- expect(header.startsWith('name,hex')).toBe(true);
19
+ if (duplicates.length) {
20
+ throw new Error(
21
+ buildFailureMessage({
22
+ title: 'Found {n} duplicate {items}:',
23
+ offenders: duplicates,
24
+ offenderLabel: 'name',
25
+ details: [
26
+ 'Exact duplicate names are not allowed.',
27
+ 'Please remove duplicates or consolidate to a single preferred name.',
28
+ ],
29
+ tips: [
30
+ 'Edit src/colornames.csv and keep only one entry per name',
31
+ 'When in doubt, prefer the most common or descriptive name',
32
+ ],
33
+ })
34
+ );
35
+ }
16
36
 
17
- const items = lines.map((l, idx) => {
18
- const lineNumber = idx + 2; // +1 for header, +1 because idx is 0-based
19
- if (!l.trim()) return null;
20
- const firstComma = l.indexOf(',');
21
- const name = firstComma === -1 ? l : l.slice(0, firstComma);
22
- return { name, lineNumber };
23
- }).filter(Boolean);
37
+ expect(duplicates.length).toBe(0);
38
+ });
24
39
 
25
- const conflicts = findNearDuplicateNameConflicts(items, { allowlist });
40
+ it('should not contain names that only differ by spacing/punctuation/case/accents/stopwords', () => {
41
+ expect(csvTestData.lineCount).toBeGreaterThan(1);
42
+
43
+ const conflicts = findNearDuplicateNameConflicts(csvTestData.items, {
44
+ allowlist,
45
+ foldPlurals: true,
46
+ pluralAllowlist,
47
+ foldStopwords: true,
48
+ stopwords: [
49
+ 'of', 'the', 'and', 'a', 'an',
50
+ 'in', 'on', 'at', 'to', 'for',
51
+ 'by', 'with', 'from', 'as', 'is',
52
+ 'it', 'this', 'that', 'these', 'those',
53
+ 'be', 'are', 'was', 'were', 'or'
54
+ ],
55
+ });
26
56
 
27
57
  if (conflicts.length) {
28
- // Create a helpful error message with examples and hints.
29
- const groupCount = conflicts.length;
30
- const msgLines = [
31
- `Found ${groupCount} duplicate-like group${groupCount === 1 ? '' : 's'} (case/accents/punctuation-insensitive):`,
32
- '',
33
- ];
58
+ // Create a summary of all names across conflict groups
59
+ const allOffendingNames = new Set();
60
+ conflicts.forEach(({ entries }) => entries.forEach((e) => allOffendingNames.add(e.name)));
61
+
62
+ // Build detailed section: groups with line numbers (stable + unique)
63
+ const details = [];
34
64
  conflicts
35
- // make message deterministic by sorting
36
65
  .sort((a, b) => a.norm.localeCompare(b.norm))
37
66
  .forEach(({ norm, entries }) => {
38
67
  const unique = [];
39
68
  const seen = new Set();
40
- // De-duplicate exact same name+line pairs in output
41
69
  for (const e of entries) {
42
70
  const key = `${e.name}@${e.lineNumber}`;
43
71
  if (!seen.has(key)) {
@@ -45,26 +73,93 @@ describe('Duplicate-like color names', () => {
45
73
  unique.push(e);
46
74
  }
47
75
  }
48
- // sort entries by line number for readability
49
76
  unique.sort((a, b) => a.lineNumber - b.lineNumber);
50
- msgLines.push(` • ${norm}:`);
51
- unique.forEach((e) => msgLines.push(` - line ${e.lineNumber}: "${e.name}"`));
77
+ details.push(` • ${norm}:`);
78
+ unique.forEach((e) => details.push(` - line ${e.lineNumber}: "${e.name}"`));
79
+ details.push('');
52
80
  });
53
81
 
54
- msgLines.push(
55
- '',
56
- 'This typically indicates near-duplicates that only differ by spacing/punctuation, like "Snow Pink" vs "Snowpink".',
57
- 'Please unify or remove duplicates to keep the dataset clean.',
58
- '',
59
- 'Tip:',
60
- ' - Edit src/colornames.csv and keep a single preferred spelling. When in doubt, prefer the most common or simplest form or the British spelling.',
61
- ' - After changes, run: npm run sort-colors',
82
+ throw new Error(
83
+ buildFailureMessage({
84
+ title: 'Found {n} duplicate-like {items} (case/accents/punctuation/stopwords-insensitive):',
85
+ offenders: [...allOffendingNames],
86
+ offenderLabel: 'name',
87
+ details: [
88
+ ...details,
89
+ 'This typically indicates near-duplicates that only differ by spacing/punctuation, like "Snow Pink" vs "Snowpink".',
90
+ 'Please unify or remove duplicates to keep the dataset clean.',
91
+ ],
92
+ tips: [
93
+ 'Edit src/colornames.csv and keep a single preferred spelling. When in doubt, prefer the most common or simplest form or the British spelling.',
94
+ 'After changes, run: npm run sort-colors',
95
+ ],
96
+ count: conflicts.length,
97
+ })
62
98
  );
63
-
64
- throw new Error(msgLines.join('\n'));
65
99
  }
66
100
 
67
101
  // If we reach here, no conflicts were found.
68
102
  expect(conflicts.length).toBe(0);
69
103
  });
104
+
105
+ it('should not contain duplicate hex codes', () => {
106
+ // Find duplicates in hex values
107
+ const hexDuplicates = findDuplicates(csvTestData.data.values['hex']);
108
+
109
+ if (hexDuplicates.length) {
110
+ const details = [];
111
+ hexDuplicates.forEach((duplicateHex) => {
112
+ const entriesWithHex = csvTestData.data.entries
113
+ .map((entry, index) => ({ ...entry, lineNumber: index + 2 }))
114
+ .filter((entry) => entry.hex === duplicateHex);
115
+
116
+ details.push(` • ${duplicateHex}:`);
117
+ entriesWithHex.forEach((entry) => {
118
+ details.push(` - line ${entry.lineNumber}: "${entry.name}" (${entry.hex})`);
119
+ });
120
+ details.push('');
121
+ });
122
+
123
+ throw new Error(
124
+ buildFailureMessage({
125
+ title: 'Found {n} duplicate {items}:',
126
+ offenders: hexDuplicates,
127
+ offenderLabel: 'hex code',
128
+ details: [
129
+ ...details,
130
+ 'Duplicate hex codes indicate multiple color names pointing to the same exact color.',
131
+ 'Please remove duplicates or consolidate to a single preferred name.',
132
+ ],
133
+ tips: [
134
+ 'Edit src/colornames.csv and keep only one entry per hex code',
135
+ 'When in doubt, prefer the most common or descriptive name',
136
+ ],
137
+ })
138
+ );
139
+ }
140
+
141
+ expect(hexDuplicates.length).toBe(0);
142
+ });
143
+
144
+ it('should detect names that only differ by stopwords when enabled', () => {
145
+ const items = [
146
+ { name: 'Heart Gold' },
147
+ { name: 'Heart of Gold' },
148
+ ];
149
+ const stopwords = [
150
+ 'of', 'the', 'and', 'a', 'an',
151
+ 'in', 'on', 'at', 'to', 'for',
152
+ 'by', 'with', 'from', 'as', 'is',
153
+ 'it', 'this', 'that', 'these', 'those',
154
+ 'be', 'are', 'was', 'were', 'or',
155
+ ];
156
+
157
+ const conflicts = findNearDuplicateNameConflicts(items, {
158
+ foldStopwords: true,
159
+ stopwords,
160
+ });
161
+
162
+ expect(conflicts.length).toBe(1);
163
+ expect(conflicts[0].entries.length).toBe(2);
164
+ });
70
165
  });
@@ -1,16 +1,17 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { parseCSVString } from '../scripts/lib.js';
5
+ import { csvTestData } from './csv-test-data.js';
5
6
 
6
7
  describe('Other Format Tests', () => {
7
- // Load CSV data for comparison
8
- const csvSource = fs.readFileSync(path.resolve('./src/colornames.csv'), 'utf8').toString();
9
- const csvData = parseCSVString(csvSource);
10
- const csvColors = csvData.entries.map(entry => ({
11
- name: entry.name,
12
- hex: entry.hex
13
- }));
8
+ let csvColors;
9
+
10
+ beforeAll(() => {
11
+ // Load CSV data once for all tests
12
+ csvTestData.load();
13
+ csvColors = csvTestData.colors;
14
+ });
14
15
 
15
16
  describe('CSV Output', () => {
16
17
  it('should correctly generate CSV files', () => {
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
2
 
3
3
  import * as esmColors from '../dist/colornames.esm.js';
4
4
  import * as esmBestOfColors from '../dist/colornames.bestof.esm.js';
@@ -15,18 +15,16 @@ import jsonMinBestOfColors from '../dist/colornames.bestof.min.json' assert { ty
15
15
  import jsonMinShortColors from '../dist/colornames.short.min.json' assert { type: 'json' };
16
16
 
17
17
  // Also import the source CSV file for verification
18
- import fs from 'fs';
19
- import path from 'path';
20
- import { parseCSVString } from '../scripts/lib.js';
18
+ import { csvTestData } from './csv-test-data.js';
21
19
 
22
20
  describe('Color Names Import Tests', () => {
23
- // Load CSV data for comparison
24
- const csvSource = fs.readFileSync(path.resolve('./src/colornames.csv'), 'utf8').toString();
25
- const csvData = parseCSVString(csvSource);
26
- const csvColors = csvData.entries.map(entry => ({
27
- name: entry.name,
28
- hex: entry.hex
29
- }));
21
+ let csvColors;
22
+
23
+ beforeAll(() => {
24
+ // Load CSV data for comparison
25
+ csvTestData.load();
26
+ csvColors = csvTestData.colors;
27
+ });
30
28
 
31
29
  describe('JSON Files', () => {
32
30
  it('should import main JSON file correctly', () => {
@@ -108,11 +106,11 @@ describe('Color Names Import Tests', () => {
108
106
  const commonColors = ['black', 'white', 'red', 'blue', 'green', 'yellow', 'purple', 'pink'];
109
107
 
110
108
  // Convert to lowercase for easier comparison
111
- const allNames = jsonColors.map(color => color.name.toLowerCase());
109
+ const allNames = jsonColors.map((color) => color.name.toLowerCase());
112
110
 
113
- commonColors.forEach(color => {
111
+ commonColors.forEach((color) => {
114
112
  // Check if at least one entry contains this common color name
115
- expect(allNames.some(name => name.includes(color))).toBe(true);
113
+ expect(allNames.some((name) => name.includes(color))).toBe(true);
116
114
  });
117
115
  });
118
116
  });
@@ -1,22 +1,20 @@
1
- import { describe, it, expect } from 'vitest';
2
- import fs from 'fs';
3
- import path from 'path';
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { csvTestData } from './csv-test-data.js';
4
3
 
5
4
  /**
6
5
  * Ensures that the source CSV file is already sorted alphabetically (case-insensitive)
7
6
  * by the color name. If not, it throws with a helpful message telling how to fix it.
8
7
  */
9
8
  describe('Source CSV sorting', () => {
10
- it('colornames.csv should be sorted by name (case-insensitive)', () => {
11
- const csvPath = path.resolve('./src/colornames.csv');
12
- const raw = fs.readFileSync(csvPath, 'utf8').replace(/\r\n/g, '\n').trimEnd();
13
- const lines = raw.split('\n');
14
- expect(lines.length).toBeGreaterThan(1);
9
+ beforeAll(() => {
10
+ // Load CSV data once
11
+ csvTestData.load();
12
+ });
15
13
 
16
- const header = lines.shift();
17
- expect(header.startsWith('name,hex')).toBe(true);
14
+ it('colornames.csv should be sorted by name (case-insensitive)', () => {
15
+ expect(csvTestData.lineCount).toBeGreaterThan(1);
18
16
 
19
- const entries = lines
17
+ const entries = csvTestData.lines
20
18
  .filter((l) => l.trim().length)
21
19
  .map((l, idx) => {
22
20
  const [name, hex] = l.split(',');
@@ -36,7 +34,7 @@ describe('Source CSV sorting', () => {
36
34
  'To fix automatically run:',
37
35
  ' npm run sort-colors',
38
36
  '',
39
- 'Commit the updated src/colornames.csv after sorting.'
37
+ 'Commit the updated src/colornames.csv after sorting.',
40
38
  ].join('\n')
41
39
  );
42
40
  }