color-name-list 12.2.0 → 13.1.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.
- package/.github/workflows/build-and-release.yml +3 -3
- package/.github/workflows/build.yml +1 -1
- package/.husky/pre-commit +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +2 -2
- package/changes.svg +3 -3
- package/dist/colornames.bestof.csv +5 -4
- package/dist/colornames.bestof.esm.js +1 -1
- package/dist/colornames.bestof.esm.mjs +1 -1
- package/dist/colornames.bestof.html +1 -1
- package/dist/colornames.bestof.json +1 -1
- package/dist/colornames.bestof.min.json +1 -1
- package/dist/colornames.bestof.scss +1 -1
- package/dist/colornames.bestof.umd.js +1 -1
- package/dist/colornames.bestof.xml +17 -13
- package/dist/colornames.bestof.yaml +13 -10
- package/dist/colornames.csv +11 -38
- package/dist/colornames.esm.js +1 -1
- package/dist/colornames.esm.mjs +1 -1
- package/dist/colornames.html +1 -1
- package/dist/colornames.json +1 -1
- package/dist/colornames.min.json +1 -1
- package/dist/colornames.scss +1 -1
- package/dist/colornames.short.csv +3 -2
- package/dist/colornames.short.esm.js +1 -1
- package/dist/colornames.short.esm.mjs +1 -1
- package/dist/colornames.short.html +1 -1
- package/dist/colornames.short.json +1 -1
- package/dist/colornames.short.min.json +1 -1
- package/dist/colornames.short.scss +1 -1
- package/dist/colornames.short.umd.js +1 -1
- package/dist/colornames.short.xml +9 -5
- package/dist/colornames.short.yaml +7 -4
- package/dist/colornames.umd.js +1 -1
- package/dist/colornames.xml +17 -125
- package/dist/colornames.yaml +15 -96
- package/dist/history.json +1 -1
- package/package.json +2 -4
- package/scripts/build.js +33 -161
- package/scripts/lib.js +38 -6
- package/scripts/sortSrc.js +5 -6
- package/src/colornames.csv +14 -41
- package/tests/_utils/report.js +76 -0
- package/tests/csv-test-data.js +108 -0
- package/tests/duplicate-allowlist.json +29 -1
- package/tests/duplicate-plurals-allowlist.json +4 -1
- package/tests/duplicates.test.js +246 -40
- package/tests/formats.test.js +9 -8
- package/tests/imports.test.js +12 -14
- package/tests/sorting.test.js +10 -12
- package/tests/title-case.test.js +320 -0
- 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,33 @@
|
|
|
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",
|
|
39
|
+
"Fruit of Passion"
|
|
12
40
|
]
|
package/tests/duplicates.test.js
CHANGED
|
@@ -1,44 +1,147 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
// Load CSV data once for all tests
|
|
11
|
+
csvTestData.load();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Shared stopwords for normalization across tests
|
|
15
|
+
const STOPWORDS = [
|
|
16
|
+
'of', 'the', 'and', 'a', 'an',
|
|
17
|
+
'in', 'on', 'at', 'to', 'for',
|
|
18
|
+
'by', 'with', 'from', 'as', 'is',
|
|
19
|
+
'it', 'this', 'that', 'these', 'those',
|
|
20
|
+
'be', 'are', 'was', 'were', 'or',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Helper: normalize a phrase similar to scripts/lib normalization but keeping token boundaries
|
|
24
|
+
const normalize = (s) => String(s)
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.normalize('NFD')
|
|
27
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
28
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
const tokenize = (name) => {
|
|
32
|
+
const base = normalize(name);
|
|
33
|
+
const tokens = base.match(/[a-z0-9]+/g) || [];
|
|
34
|
+
const stopSet = new Set(STOPWORDS.map((w) => normalize(w)));
|
|
35
|
+
return tokens.filter((t) => t && !stopSet.has(t));
|
|
36
|
+
};
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
// Detect two-word names that are exact reversals of each other (after stopword filtering)
|
|
39
|
+
function findTwoWordReversedPairs(items) {
|
|
40
|
+
const groups = new Map(); // key: sorted pair "a|b" -> list of { name, lineNumber, order: "a b" }
|
|
41
|
+
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
if (!item || typeof item.name !== 'string') continue;
|
|
44
|
+
const tokens = tokenize(item.name);
|
|
45
|
+
if (tokens.length !== 2) continue; // only 2-token (after stopword removal)
|
|
46
|
+
const [a, b] = tokens;
|
|
47
|
+
if (!a || !b) continue;
|
|
48
|
+
if (a === b) continue; // "blue blue" reversed is the same – ignore
|
|
49
|
+
|
|
50
|
+
const key = [a, b].sort().join('|');
|
|
51
|
+
const order = `${a} ${b}`;
|
|
52
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
53
|
+
groups.get(key).push({ name: item.name, lineNumber: item.lineNumber, order });
|
|
54
|
+
}
|
|
17
55
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
const conflicts = [];
|
|
57
|
+
for (const [key, entries] of groups.entries()) {
|
|
58
|
+
// We have a potential conflict if we see both orders "a b" and "b a"
|
|
59
|
+
const uniqOrders = [...new Set(entries.map((e) => e.order))];
|
|
60
|
+
if (uniqOrders.length < 2) continue;
|
|
61
|
+
|
|
62
|
+
const [t1, t2] = key.split('|');
|
|
63
|
+
const forward = `${t1} ${t2}`;
|
|
64
|
+
const backward = `${t2} ${t1}`;
|
|
65
|
+
|
|
66
|
+
const hasForward = uniqOrders.includes(forward);
|
|
67
|
+
const hasBackward = uniqOrders.includes(backward);
|
|
68
|
+
|
|
69
|
+
if (hasForward && hasBackward) {
|
|
70
|
+
// Keep entries unique by name@line and sorted by line number for stable output
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const unique = entries
|
|
73
|
+
.filter((e) => {
|
|
74
|
+
const k = `${e.name}@${e.lineNumber}`;
|
|
75
|
+
if (seen.has(k)) return false;
|
|
76
|
+
seen.add(k);
|
|
77
|
+
return true;
|
|
78
|
+
})
|
|
79
|
+
.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
80
|
+
|
|
81
|
+
// Respect allowlist: if either direction string is allowlisted, skip
|
|
82
|
+
const allowSet = new Set(
|
|
83
|
+
(Array.isArray(allowlist) ? allowlist : [])
|
|
84
|
+
.filter((v) => typeof v === 'string' && v.trim().length)
|
|
85
|
+
.map((v) => normalize(v))
|
|
86
|
+
);
|
|
87
|
+
if (allowSet.has(forward) || allowSet.has(backward)) continue;
|
|
88
|
+
|
|
89
|
+
conflicts.push({ key, tokens: [t1, t2], entries: unique });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
25
92
|
|
|
26
|
-
|
|
93
|
+
return conflicts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it('should not contain the same name twice', () => {
|
|
97
|
+
expect(csvTestData.lineCount).toBeGreaterThan(1);
|
|
98
|
+
|
|
99
|
+
const duplicates = findDuplicates(csvTestData.data.values['name']);
|
|
100
|
+
|
|
101
|
+
if (duplicates.length) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
buildFailureMessage({
|
|
104
|
+
title: 'Found {n} duplicate {items}:',
|
|
105
|
+
offenders: duplicates,
|
|
106
|
+
offenderLabel: 'name',
|
|
107
|
+
details: [
|
|
108
|
+
'Exact duplicate names are not allowed.',
|
|
109
|
+
'Please remove duplicates or consolidate to a single preferred name.',
|
|
110
|
+
],
|
|
111
|
+
tips: [
|
|
112
|
+
'Edit src/colornames.csv and keep only one entry per name',
|
|
113
|
+
'When in doubt, prefer the most common or descriptive name',
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
expect(duplicates.length).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should not contain names that only differ by spacing/punctuation/case/accents/stopwords', () => {
|
|
123
|
+
expect(csvTestData.lineCount).toBeGreaterThan(1);
|
|
124
|
+
|
|
125
|
+
const conflicts = findNearDuplicateNameConflicts(csvTestData.items, {
|
|
126
|
+
allowlist,
|
|
127
|
+
foldPlurals: true,
|
|
128
|
+
pluralAllowlist,
|
|
129
|
+
foldStopwords: true,
|
|
130
|
+
stopwords: STOPWORDS,
|
|
131
|
+
});
|
|
27
132
|
|
|
28
133
|
if (conflicts.length) {
|
|
29
|
-
// Create a
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
];
|
|
134
|
+
// Create a summary of all names across conflict groups
|
|
135
|
+
const allOffendingNames = new Set();
|
|
136
|
+
conflicts.forEach(({ entries }) => entries.forEach((e) => allOffendingNames.add(e.name)));
|
|
137
|
+
|
|
138
|
+
// Build detailed section: groups with line numbers (stable + unique)
|
|
139
|
+
const details = [];
|
|
35
140
|
conflicts
|
|
36
|
-
// make message deterministic by sorting
|
|
37
141
|
.sort((a, b) => a.norm.localeCompare(b.norm))
|
|
38
142
|
.forEach(({ norm, entries }) => {
|
|
39
143
|
const unique = [];
|
|
40
144
|
const seen = new Set();
|
|
41
|
-
// De-duplicate exact same name+line pairs in output
|
|
42
145
|
for (const e of entries) {
|
|
43
146
|
const key = `${e.name}@${e.lineNumber}`;
|
|
44
147
|
if (!seen.has(key)) {
|
|
@@ -46,26 +149,129 @@ describe('Duplicate-like color names', () => {
|
|
|
46
149
|
unique.push(e);
|
|
47
150
|
}
|
|
48
151
|
}
|
|
49
|
-
// sort entries by line number for readability
|
|
50
152
|
unique.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
51
|
-
|
|
52
|
-
unique.forEach((e) =>
|
|
153
|
+
details.push(` • ${norm}:`);
|
|
154
|
+
unique.forEach((e) => details.push(` - line ${e.lineNumber}: "${e.name}"`));
|
|
155
|
+
details.push('');
|
|
53
156
|
});
|
|
54
157
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
158
|
+
throw new Error(
|
|
159
|
+
buildFailureMessage({
|
|
160
|
+
title: 'Found {n} duplicate-like {items} (case/accents/punctuation/stopwords-insensitive):',
|
|
161
|
+
offenders: [...allOffendingNames],
|
|
162
|
+
offenderLabel: 'name',
|
|
163
|
+
details: [
|
|
164
|
+
...details,
|
|
165
|
+
'This typically indicates near-duplicates that only differ by spacing/punctuation, like "Snow Pink" vs "Snowpink".',
|
|
166
|
+
'Please unify or remove duplicates to keep the dataset clean.',
|
|
167
|
+
],
|
|
168
|
+
tips: [
|
|
169
|
+
'Edit src/colornames.csv and keep a single preferred spelling. When in doubt, prefer the most common or simplest form or the British spelling.',
|
|
170
|
+
'After changes, run: npm run sort-colors',
|
|
171
|
+
],
|
|
172
|
+
count: conflicts.length,
|
|
173
|
+
})
|
|
63
174
|
);
|
|
64
|
-
|
|
65
|
-
throw new Error(msgLines.join('\n'));
|
|
66
175
|
}
|
|
67
176
|
|
|
68
177
|
// If we reach here, no conflicts were found.
|
|
69
178
|
expect(conflicts.length).toBe(0);
|
|
70
179
|
});
|
|
180
|
+
|
|
181
|
+
it('should not contain duplicate hex codes', () => {
|
|
182
|
+
// Find duplicates in hex values
|
|
183
|
+
const hexDuplicates = findDuplicates(csvTestData.data.values['hex']);
|
|
184
|
+
|
|
185
|
+
if (hexDuplicates.length) {
|
|
186
|
+
const details = [];
|
|
187
|
+
hexDuplicates.forEach((duplicateHex) => {
|
|
188
|
+
const entriesWithHex = csvTestData.data.entries
|
|
189
|
+
.map((entry, index) => ({ ...entry, lineNumber: index + 2 }))
|
|
190
|
+
.filter((entry) => entry.hex === duplicateHex);
|
|
191
|
+
|
|
192
|
+
details.push(` • ${duplicateHex}:`);
|
|
193
|
+
entriesWithHex.forEach((entry) => {
|
|
194
|
+
details.push(` - line ${entry.lineNumber}: "${entry.name}" (${entry.hex})`);
|
|
195
|
+
});
|
|
196
|
+
details.push('');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
throw new Error(
|
|
200
|
+
buildFailureMessage({
|
|
201
|
+
title: 'Found {n} duplicate {items}:',
|
|
202
|
+
offenders: hexDuplicates,
|
|
203
|
+
offenderLabel: 'hex code',
|
|
204
|
+
details: [
|
|
205
|
+
...details,
|
|
206
|
+
'Duplicate hex codes indicate multiple color names pointing to the same exact color.',
|
|
207
|
+
'Please remove duplicates or consolidate to a single preferred name.',
|
|
208
|
+
],
|
|
209
|
+
tips: [
|
|
210
|
+
'Edit src/colornames.csv and keep only one entry per hex code',
|
|
211
|
+
'When in doubt, prefer the most common or descriptive name',
|
|
212
|
+
],
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
expect(hexDuplicates.length).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should detect names that only differ by stopwords when enabled', () => {
|
|
221
|
+
const items = [
|
|
222
|
+
{ name: 'Heart Gold' },
|
|
223
|
+
{ name: 'Heart of Gold' },
|
|
224
|
+
];
|
|
225
|
+
const conflicts = findNearDuplicateNameConflicts(items, {
|
|
226
|
+
foldStopwords: true,
|
|
227
|
+
stopwords: STOPWORDS,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(conflicts.length).toBe(1);
|
|
231
|
+
expect(conflicts[0].entries.length).toBe(2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it.skip('should not contain two-word names that are exact reversals of each other', () => {
|
|
235
|
+
expect(csvTestData.lineCount).toBeGreaterThan(1);
|
|
236
|
+
|
|
237
|
+
const conflicts = findTwoWordReversedPairs(csvTestData.items);
|
|
238
|
+
|
|
239
|
+
if (conflicts.length) {
|
|
240
|
+
// Build helpful details with line numbers
|
|
241
|
+
const details = [];
|
|
242
|
+
const offenderNames = new Set();
|
|
243
|
+
|
|
244
|
+
conflicts
|
|
245
|
+
.sort((a, b) => a.tokens.join(' ').localeCompare(b.tokens.join(' ')))
|
|
246
|
+
.forEach(({ tokens, entries }) => {
|
|
247
|
+
const [a, b] = tokens;
|
|
248
|
+
details.push(` • ${a} / ${b}:`);
|
|
249
|
+
entries.forEach((e) => {
|
|
250
|
+
offenderNames.add(e.name);
|
|
251
|
+
details.push(` - line ${e.lineNumber}: "${e.name}"`);
|
|
252
|
+
});
|
|
253
|
+
details.push('');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
throw new Error(
|
|
257
|
+
buildFailureMessage({
|
|
258
|
+
title: 'Found {n} word-order reversed {items}:',
|
|
259
|
+
offenders: [...offenderNames],
|
|
260
|
+
offenderLabel: 'name',
|
|
261
|
+
details: [
|
|
262
|
+
...details,
|
|
263
|
+
'Names that only differ by word order (e.g., "Beach Sand" vs "Sand Beach") should be unified.',
|
|
264
|
+
'Please keep a single preferred order and remove the other.',
|
|
265
|
+
],
|
|
266
|
+
tips: [
|
|
267
|
+
'Edit src/colornames.csv and keep only one order for each two-word pair.',
|
|
268
|
+
'After changes, run: npm run sort-colors',
|
|
269
|
+
],
|
|
270
|
+
count: conflicts.length,
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(conflicts.length).toBe(0);
|
|
276
|
+
});
|
|
71
277
|
});
|
package/tests/formats.test.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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', () => {
|
package/tests/imports.test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
});
|
package/tests/sorting.test.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(lines.length).toBeGreaterThan(1);
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
// Load CSV data once
|
|
11
|
+
csvTestData.load();
|
|
12
|
+
});
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
expect(
|
|
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
|
}
|