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
@@ -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
  ]
@@ -3,5 +3,8 @@
3
3
  "Blues",
4
4
  "Greens",
5
5
  "Nighthawks",
6
- "Spearmints"
6
+ "Spearmints",
7
+ "In the Vines",
8
+ "On the Rocks",
9
+ "Sea of Stars"
7
10
  ]
@@ -1,44 +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';
6
4
  import pluralAllowlist from './duplicate-plurals-allowlist.json';
5
+ import { csvTestData } from './csv-test-data.js';
6
+ import { buildFailureMessage } from './_utils/report.js';
7
7
 
8
8
  describe('Duplicate-like color names', () => {
9
- it('should not contain names that only differ by spacing/punctuation/case/accents', () => {
10
- const csvPath = path.resolve('./src/colornames.csv');
11
- const raw = fs.readFileSync(csvPath, 'utf8').replace(/\r\n/g, '\n').trimEnd();
12
- const lines = raw.split('\n');
13
- 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']);
14
18
 
15
- const header = lines.shift();
16
- 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
+ }
17
36
 
18
- const items = lines.map((l, idx) => {
19
- const lineNumber = idx + 2; // +1 for header, +1 because idx is 0-based
20
- if (!l.trim()) return null;
21
- const firstComma = l.indexOf(',');
22
- const name = firstComma === -1 ? l : l.slice(0, firstComma);
23
- return { name, lineNumber };
24
- }).filter(Boolean);
37
+ expect(duplicates.length).toBe(0);
38
+ });
25
39
 
26
- const conflicts = findNearDuplicateNameConflicts(items, { allowlist, foldPlurals: true, pluralAllowlist });
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
+ });
27
56
 
28
57
  if (conflicts.length) {
29
- // Create a helpful error message with examples and hints.
30
- const groupCount = conflicts.length;
31
- const msgLines = [
32
- `Found ${groupCount} duplicate-like group${groupCount === 1 ? '' : 's'} (case/accents/punctuation-insensitive):`,
33
- '',
34
- ];
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 = [];
35
64
  conflicts
36
- // make message deterministic by sorting
37
65
  .sort((a, b) => a.norm.localeCompare(b.norm))
38
66
  .forEach(({ norm, entries }) => {
39
67
  const unique = [];
40
68
  const seen = new Set();
41
- // De-duplicate exact same name+line pairs in output
42
69
  for (const e of entries) {
43
70
  const key = `${e.name}@${e.lineNumber}`;
44
71
  if (!seen.has(key)) {
@@ -46,26 +73,93 @@ describe('Duplicate-like color names', () => {
46
73
  unique.push(e);
47
74
  }
48
75
  }
49
- // sort entries by line number for readability
50
76
  unique.sort((a, b) => a.lineNumber - b.lineNumber);
51
- msgLines.push(` • ${norm}:`);
52
- 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('');
53
80
  });
54
81
 
55
- msgLines.push(
56
- '',
57
- 'This typically indicates near-duplicates that only differ by spacing/punctuation, like "Snow Pink" vs "Snowpink".',
58
- 'Please unify or remove duplicates to keep the dataset clean.',
59
- '',
60
- 'Tip:',
61
- ' - Edit src/colornames.csv and keep a single preferred spelling. When in doubt, prefer the most common or simplest form or the British spelling.',
62
- ' - 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
+ })
63
98
  );
64
-
65
- throw new Error(msgLines.join('\n'));
66
99
  }
67
100
 
68
101
  // If we reach here, no conflicts were found.
69
102
  expect(conflicts.length).toBe(0);
70
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
+ });
71
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
  }