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.
Files changed (52) hide show
  1. package/.github/workflows/build-and-release.yml +3 -3
  2. package/.github/workflows/build.yml +1 -1
  3. package/.husky/pre-commit +1 -1
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +2 -2
  6. package/changes.svg +3 -3
  7. package/dist/colornames.bestof.csv +5 -4
  8. package/dist/colornames.bestof.esm.js +1 -1
  9. package/dist/colornames.bestof.esm.mjs +1 -1
  10. package/dist/colornames.bestof.html +1 -1
  11. package/dist/colornames.bestof.json +1 -1
  12. package/dist/colornames.bestof.min.json +1 -1
  13. package/dist/colornames.bestof.scss +1 -1
  14. package/dist/colornames.bestof.umd.js +1 -1
  15. package/dist/colornames.bestof.xml +17 -13
  16. package/dist/colornames.bestof.yaml +13 -10
  17. package/dist/colornames.csv +11 -38
  18. package/dist/colornames.esm.js +1 -1
  19. package/dist/colornames.esm.mjs +1 -1
  20. package/dist/colornames.html +1 -1
  21. package/dist/colornames.json +1 -1
  22. package/dist/colornames.min.json +1 -1
  23. package/dist/colornames.scss +1 -1
  24. package/dist/colornames.short.csv +3 -2
  25. package/dist/colornames.short.esm.js +1 -1
  26. package/dist/colornames.short.esm.mjs +1 -1
  27. package/dist/colornames.short.html +1 -1
  28. package/dist/colornames.short.json +1 -1
  29. package/dist/colornames.short.min.json +1 -1
  30. package/dist/colornames.short.scss +1 -1
  31. package/dist/colornames.short.umd.js +1 -1
  32. package/dist/colornames.short.xml +9 -5
  33. package/dist/colornames.short.yaml +7 -4
  34. package/dist/colornames.umd.js +1 -1
  35. package/dist/colornames.xml +17 -125
  36. package/dist/colornames.yaml +15 -96
  37. package/dist/history.json +1 -1
  38. package/package.json +2 -4
  39. package/scripts/build.js +33 -161
  40. package/scripts/lib.js +38 -6
  41. package/scripts/sortSrc.js +5 -6
  42. package/src/colornames.csv +14 -41
  43. package/tests/_utils/report.js +76 -0
  44. package/tests/csv-test-data.js +108 -0
  45. package/tests/duplicate-allowlist.json +29 -1
  46. package/tests/duplicate-plurals-allowlist.json +4 -1
  47. package/tests/duplicates.test.js +246 -40
  48. package/tests/formats.test.js +9 -8
  49. package/tests/imports.test.js +12 -14
  50. package/tests/sorting.test.js +10 -12
  51. package/tests/title-case.test.js +320 -0
  52. package/tests/validations.test.js +219 -0
@@ -0,0 +1,320 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { csvTestData } from './csv-test-data.js';
3
+ import { buildFailureMessage } from './_utils/report.js';
4
+
5
+ describe('APA Title Case Validation', () => {
6
+ beforeAll(() => {
7
+ // Load CSV data once for all tests
8
+ csvTestData.load();
9
+ });
10
+
11
+ /**
12
+ * Determine if a word should be capitalized according to APA title case rules
13
+ * @param {string} word - The word to check
14
+ * @param {boolean} isFirstWordOrAfterPunctuation - True if this is the first word or after punctuation
15
+ * @param {boolean} isAfterPunctuation - True if this word comes after punctuation
16
+ * @param {boolean} isLastWord - True if this is the last word in the title
17
+ * @returns {boolean} - True if the word should be capitalized
18
+ */
19
+ function shouldBeCapitalized(word, isFirstWordOrAfterPunctuation, isAfterPunctuation, isLastWord = false) {
20
+ // Always capitalize first words, words after punctuation, and last words
21
+ if (isFirstWordOrAfterPunctuation || isAfterPunctuation || isLastWord) {
22
+ return true;
23
+ }
24
+
25
+ // Capitalize words of 4 or more letters
26
+ if (word.length >= 4) {
27
+ return true;
28
+ }
29
+
30
+ // Short words (3 letters or fewer) that should be lowercase (unless first, after punctuation, or last)
31
+ const minorWords = new Set([
32
+ // Short conjunctions
33
+ 'and', 'as', 'but', 'for', 'if', 'nor', 'or', 'so', 'yet',
34
+ // Articles
35
+ 'a', 'an', 'the',
36
+ // Short prepositions
37
+ 'at', 'by', 'in', 'of', 'off', 'on', 'per', 'to', 'up', 'via',
38
+ // for short prepositions
39
+ 'de', 'la', 'le', 'les', 'un', 'une', 'du', 'des', 'et', 'ou', 'à', 'au', 'aux'
40
+ ]);
41
+
42
+ return !minorWords.has(word.toLowerCase());
43
+ }
44
+
45
+ /**
46
+ * Convert a color name to proper APA title case
47
+ * @param {string} name - The color name to convert
48
+ * @returns {string} - The properly formatted title case name
49
+ */
50
+ function toTitleCase(name) {
51
+ // Special cases that should maintain their exact capitalization
52
+ const specialCases = new Map([
53
+ // Roman numerals (case-insensitive key, exact case value)
54
+ ['ii', 'II'],
55
+ ['iii', 'III'],
56
+ ['iv', 'IV'],
57
+ ['vi', 'VI'],
58
+ ['vii', 'VII'],
59
+ ['viii', 'VIII'],
60
+ ['ix', 'IX'],
61
+ ['xi', 'XI'],
62
+ ['xii', 'XII'],
63
+
64
+ // Common abbreviations that should stay uppercase
65
+ ['ny', 'NY'],
66
+ ['nyc', 'NYC'],
67
+ ['usa', 'USA'],
68
+ ['uk', 'UK'],
69
+ ['us', 'US'],
70
+ ['bbc', 'BBC'],
71
+ ['cnn', 'CNN'],
72
+ ['fbi', 'FBI'],
73
+ ['cia', 'CIA'],
74
+ ['nasa', 'NASA'],
75
+ ['nato', 'NATO'],
76
+ ['nypd', 'NYPD'],
77
+ ['usmc', 'USMC'],
78
+ ['usc', 'USC'],
79
+ ['ucla', 'UCLA'],
80
+ ['ksu', 'KSU'],
81
+ ['ku', 'KU'],
82
+ ['ou', 'OU'],
83
+ ['msu', 'MSU'],
84
+ ['ua', 'UA'],
85
+
86
+ // Technical terms
87
+ ['cg', 'CG'],
88
+ ['cga', 'CGA'],
89
+ ['ega', 'EGA'],
90
+ ['led', 'LED'],
91
+ ['lcd', 'LCD'],
92
+ ['crt', 'CRT'],
93
+ ['vic', 'VIC'],
94
+ ['mvs', 'MVS'],
95
+ ['lua', 'LUA'],
96
+ ['php', 'PHP'],
97
+ ['sql', 'SQL'],
98
+ ['ufo', 'UFO'],
99
+ ['uv', 'UV'],
100
+ ['pcb', 'PCB'],
101
+ ['ntsc', 'NTSC'],
102
+ ['nes', 'NES'],
103
+ ['gmb', 'GMB'],
104
+ ['ocd', 'OCD'],
105
+ ['bbq', 'BBQ'],
106
+ ['ok', 'OK'],
107
+ ['ff', 'FF'],
108
+ ['po', 'PO'],
109
+
110
+ // Chemical formulas (maintain exact case)
111
+ ['co₂', 'CO₂'],
112
+ ['h₂o', 'H₂O'],
113
+ ['mos₂', 'MoS₂'],
114
+
115
+ // Proper nouns with specific capitalization
116
+ ['mckenzie', 'McKenzie'],
117
+ ['mcnuke', 'McNuke'],
118
+ ['mcquarrie', 'McQuarrie'],
119
+ ["o'brien", "O'Brien"],
120
+ ["o'neal", "O'Neal"],
121
+ ["lechuck's", "LeChuck's"],
122
+ ['davanzo', 'DaVanzo'],
123
+ ['bioshock', 'BioShock'],
124
+ ['microprose', 'MicroProse'],
125
+ ['dodgeroll', 'DodgeRoll'],
126
+ ['aurometalsaurus', 'AuroMetalSaurus'],
127
+ ['yinmn', 'YInMn'],
128
+ ['redяum', 'RedЯum'],
129
+ ['omgreen', 'OMGreen'],
130
+
131
+ // foreign words and acronyms
132
+ ['Vers De Terre', 'Vers de Terre'],
133
+ ]);
134
+
135
+ // Time pattern regex for expressions like 3AM, 12PM, etc.
136
+ const timePattern = /^\d{1,2}[AP]M$/i;
137
+
138
+ /**
139
+ * Check if "LA" in this context likely refers to Los Angeles
140
+ * @param {string} fullName - The complete color name
141
+ * @param {number} wordIndex - Index of the current word
142
+ * @param {string[]} words - Array of all words
143
+ * @returns {boolean} - True if "LA" likely refers to Los Angeles
144
+ */
145
+ function isLosAngelesContext(fullName, wordIndex, words) {
146
+ const lowerName = fullName.toLowerCase();
147
+
148
+ // Patterns that suggest Los Angeles context
149
+ const losAngelesIndicators = [
150
+ 'vibes', 'style', 'sunset', 'beach', 'hollywood', 'california', 'west coast',
151
+ 'city', 'metro', 'downtown', 'freeway', 'boulevard'
152
+ ];
153
+
154
+ // If the name contains any LA indicators, treat "la" as Los Angeles
155
+ if (losAngelesIndicators.some(indicator => lowerName.includes(indicator))) {
156
+ return true;
157
+ }
158
+
159
+ // If "La" is followed by a clearly non-French word, it might be Los Angeles
160
+ const nextWord = words[wordIndex + 2]; // +2 to skip whitespace
161
+ if (nextWord) {
162
+ const nonFrenchPatterns = ['vibes', 'style', 'sunset', 'beach'];
163
+ if (nonFrenchPatterns.some(pattern => nextWord.toLowerCase().includes(pattern))) {
164
+ return true;
165
+ }
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ // Split on spaces and handle punctuation
172
+ const words = name.split(/(\s+)/);
173
+ let result = '';
174
+ let isFirstWord = true;
175
+
176
+ for (let i = 0; i < words.length; i++) {
177
+ const segment = words[i];
178
+
179
+ // Skip whitespace segments
180
+ if (/^\s+$/.test(segment)) {
181
+ result += segment;
182
+ continue;
183
+ }
184
+
185
+ // Check if previous segment ended with punctuation that requires capitalization
186
+ const isAfterPunctuation = i > 0 && /[:—–-]$/.test(words[i - 2]);
187
+
188
+ // Check for alphanumeric patterns that should stay uppercase (like model numbers)
189
+ // Exclude ordinal numbers (1st, 2nd, 3rd, 18th, etc.)
190
+ const isAlphanumericPattern = /^[0-9]+[A-Z]+$/i.test(segment) &&
191
+ !/^\d+(st|nd|rd|th)$/i.test(segment);
192
+
193
+ // Handle hyphenated words - both parts should follow title case rules
194
+ if (segment.includes('-')) {
195
+ const parts = segment.split('-');
196
+ const capitalizedParts = parts.map((part, partIndex) => {
197
+ if (!part) return part; // Handle cases like "--"
198
+
199
+ // Check if this is a special case
200
+ const lowerPart = part.toLowerCase();
201
+ if (specialCases.has(lowerPart)) {
202
+ return specialCases.get(lowerPart);
203
+ }
204
+
205
+ // Check for time patterns
206
+ if (timePattern.test(part)) {
207
+ return part.toUpperCase();
208
+ }
209
+
210
+ // Check for alphanumeric patterns (exclude ordinal numbers)
211
+ if (/^[0-9]+[A-Z]+$/i.test(part) && !/^\d+(st|nd|rd|th)$/i.test(part)) {
212
+ return part.toUpperCase();
213
+ }
214
+
215
+ // Check for Los Angeles context in hyphenated words
216
+ if (part.toLowerCase() === 'la' && isLosAngelesContext(name, i, words)) {
217
+ return 'LA';
218
+ }
219
+
220
+ // Check if this is the last part of the last word in the title
221
+ const isLastPart = partIndex === parts.length - 1;
222
+ const isLastWordInTitle = i === words.length - 1 || (i === words.length - 2 && /^\s+$/.test(words[words.length - 1]));
223
+ const isLastWord = isLastPart && isLastWordInTitle;
224
+
225
+ const shouldCap = shouldBeCapitalized(part, isFirstWord || partIndex > 0, isAfterPunctuation, isLastWord);
226
+ return shouldCap ? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() : part.toLowerCase();
227
+ });
228
+ result += capitalizedParts.join('-');
229
+ } else {
230
+ // Check if this is a special case
231
+ const lowerSegment = segment.toLowerCase();
232
+ if (specialCases.has(lowerSegment)) {
233
+ result += specialCases.get(lowerSegment);
234
+ } else if (timePattern.test(segment)) {
235
+ // Handle time expressions like 3AM, 12PM
236
+ result += segment.toUpperCase();
237
+ } else if (isAlphanumericPattern) {
238
+ // Handle alphanumeric patterns like "400XT"
239
+ result += segment.toUpperCase();
240
+ } else if (lowerSegment === 'la' && isLosAngelesContext(name, i, words)) {
241
+ // Handle Los Angeles context - force uppercase
242
+ result += 'LA';
243
+ } else {
244
+ // Regular word - check if this is the last non-whitespace word
245
+ const isLastWordInTitle = i === words.length - 1 || (i === words.length - 2 && /^\s+$/.test(words[words.length - 1]));
246
+ const shouldCap = shouldBeCapitalized(segment, isFirstWord, isAfterPunctuation, isLastWordInTitle);
247
+ result += shouldCap ? segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase() : segment.toLowerCase();
248
+ }
249
+ }
250
+
251
+ isFirstWord = false;
252
+ }
253
+
254
+ return result;
255
+ }
256
+
257
+ /**
258
+ * Check if a color name follows APA title case rules
259
+ * @param {string} name - The color name to validate
260
+ * @returns {boolean} - True if the name follows proper title case
261
+ */
262
+ function isValidTitleCase(name) {
263
+ const expectedTitleCase = toTitleCase(name);
264
+ return name === expectedTitleCase;
265
+ }
266
+
267
+ it.skip('should follow APA title case capitalization rules', () => {
268
+ const invalidNames = [];
269
+
270
+ csvTestData.data.values['name'].forEach((name, index) => {
271
+ if (!isValidTitleCase(name)) {
272
+ const entry = csvTestData.data.entries[index];
273
+ const expectedTitleCase = toTitleCase(name);
274
+ invalidNames.push({
275
+ name,
276
+ expected: expectedTitleCase,
277
+ hex: entry.hex,
278
+ lineNumber: index + 2, // +2 for header and 0-based index
279
+ });
280
+ }
281
+ });
282
+
283
+ if (invalidNames.length) {
284
+ const details = invalidNames.map(
285
+ ({ name, expected, hex, lineNumber }) =>
286
+ ` * line ${lineNumber}: "${name}" -> should be "${expected}" (${hex})`
287
+ );
288
+
289
+ throw new Error(
290
+ buildFailureMessage({
291
+ title: 'Found {n} color {items} not following APA title case:',
292
+ offenders: invalidNames.map((i) => `"${i.name}"`),
293
+ offenderLabel: 'name',
294
+ details: [
295
+ ...details,
296
+ '',
297
+ 'Color names should follow APA Style title case capitalization rules:',
298
+ '• Capitalize the first word',
299
+ '• Capitalize major words (nouns, verbs, adjectives, adverbs, pronouns)',
300
+ '• Capitalize words of 4+ letters',
301
+ '• Capitalize both parts of hyphenated words',
302
+ '• Lowercase minor words (3 letters or fewer): articles (a, an, the), short conjunctions (and, as, but, for, if, nor, or, so, yet), and short prepositions (at, by, in, of, off, on, per, to, up, via)',
303
+ '• Always capitalize words after colons, em dashes, or end punctuation',
304
+ '',
305
+ 'Reference: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case',
306
+ ],
307
+ tips: [
308
+ 'Edit src/colornames.csv and update the capitalization',
309
+ 'Examples: "Red and Blue" → "Red and Blue" (correct)',
310
+ 'Examples: "A Shade Of Green" → "A Shade of Green" (of should be lowercase)',
311
+ 'Examples: "Self-Report Blue" → "Self-Report Blue" (both parts capitalized)',
312
+ 'After changes, run: npm run sort-colors',
313
+ ],
314
+ })
315
+ );
316
+ }
317
+
318
+ expect(invalidNames.length).toBe(0);
319
+ });
320
+ });
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { csvTestData } from './csv-test-data.js';
3
+ import { buildFailureMessage } from './_utils/report.js';
4
+
5
+ describe('CSV Data Validations', () => {
6
+ const bestOfKey = 'good name';
7
+
8
+ // Validation patterns from build.js
9
+ const hexColorValidation = /^#[0-9a-f]{6}$/;
10
+ const spacesValidation = /^\s+|\s{2,}|\s$/;
11
+ const quoteValidation = /"|'|`/;
12
+
13
+ beforeAll(() => {
14
+ // Load CSV data once for all tests
15
+ csvTestData.load();
16
+ });
17
+
18
+ it('should contain only valid 6-character hex color codes', () => {
19
+ const invalidHexCodes = [];
20
+
21
+ csvTestData.data.values['hex'].forEach((hex, index) => {
22
+ if (!hexColorValidation.test(hex)) {
23
+ const entry = csvTestData.data.entries[index];
24
+ invalidHexCodes.push({
25
+ hex,
26
+ name: entry.name,
27
+ lineNumber: index + 2, // +2 for header and 0-based index
28
+ });
29
+ }
30
+ });
31
+
32
+ if (invalidHexCodes.length) {
33
+ const details = invalidHexCodes.map(
34
+ ({ hex, name, lineNumber }) => ` * line ${lineNumber}: "${name}" -> ${hex}`
35
+ );
36
+ throw new Error(
37
+ buildFailureMessage({
38
+ title: 'Found {n} invalid hex color {items}:',
39
+ offenders: invalidHexCodes.map((i) => i.hex),
40
+ offenderLabel: 'code',
41
+ details: [
42
+ ...details,
43
+ '',
44
+ 'Hex codes must be exactly 6 characters long (no shorthand) and contain only lowercase letters a-f and numbers 0-9.',
45
+ 'Examples of valid hex codes: #ff0000, #a1b2c3, #000000',
46
+ 'Examples of invalid hex codes: #f00 (too short), #FF0000 (uppercase), #gghhii (invalid characters)',
47
+ ],
48
+ tips: [
49
+ 'Edit src/colornames.csv and fix the hex values',
50
+ 'Use lowercase letters only: a-f',
51
+ 'Always use 6 characters: #rrggbb format',
52
+ 'After changes, run: npm run sort-colors',
53
+ ],
54
+ })
55
+ );
56
+ }
57
+
58
+ expect(invalidHexCodes.length).toBe(0);
59
+ });
60
+
61
+ it('should not contain color names with invalid spacing', () => {
62
+ const invalidNames = [];
63
+
64
+ csvTestData.data.values['name'].forEach((name, index) => {
65
+ if (spacesValidation.test(name)) {
66
+ const entry = csvTestData.data.entries[index];
67
+ invalidNames.push({
68
+ name,
69
+ hex: entry.hex,
70
+ lineNumber: index + 2, // +2 for header and 0-based index
71
+ });
72
+ }
73
+ });
74
+
75
+ if (invalidNames.length) {
76
+ const details = invalidNames.map(
77
+ ({ name, hex, lineNumber }) => ` * line ${lineNumber}: "${name}" (${hex})`
78
+ );
79
+ throw new Error(
80
+ buildFailureMessage({
81
+ title: 'Found {n} color {items} with invalid spacing:',
82
+ offenders: invalidNames.map((i) => `"${i.name}"`),
83
+ offenderLabel: 'name',
84
+ details: [
85
+ ...details,
86
+ '',
87
+ 'Color names cannot have leading spaces, trailing spaces, or multiple consecutive spaces.',
88
+ 'This ensures consistent formatting and prevents parsing issues.',
89
+ ],
90
+ tips: [
91
+ 'Edit src/colornames.csv and remove extra spaces',
92
+ 'Use single spaces between words only',
93
+ 'After changes, run: npm run sort-colors',
94
+ ],
95
+ })
96
+ );
97
+ }
98
+
99
+ expect(invalidNames.length).toBe(0);
100
+ });
101
+
102
+ it('should not contain color names with quote characters', () => {
103
+ const invalidNames = [];
104
+
105
+ csvTestData.data.values['name'].forEach((name, index) => {
106
+ if (quoteValidation.test(name)) {
107
+ const entry = csvTestData.data.entries[index];
108
+ invalidNames.push({
109
+ name,
110
+ hex: entry.hex,
111
+ lineNumber: index + 2, // +2 for header and 0-based index
112
+ });
113
+ }
114
+ });
115
+
116
+ if (invalidNames.length) {
117
+ const details = invalidNames.map(
118
+ ({ name, hex, lineNumber }) => ` * line ${lineNumber}: "${name}" (${hex})`
119
+ );
120
+ throw new Error(
121
+ buildFailureMessage({
122
+ title: 'Found {n} color {items} with quote characters:',
123
+ offenders: invalidNames.map((i) => `"${i.name}"`),
124
+ offenderLabel: 'name',
125
+ details: [
126
+ ...details,
127
+ '',
128
+ 'Color names should not contain double quotes or backticks.',
129
+ 'Use apostrophes instead of double quotes for possessives or contractions.',
130
+ ],
131
+ tips: [
132
+ 'Edit src/colornames.csv and replace quote characters',
133
+ 'Use apostrophes for possessives; avoid double quotes in names',
134
+ 'After changes, run: npm run sort-colors',
135
+ ],
136
+ })
137
+ );
138
+ }
139
+
140
+ expect(invalidNames.length).toBe(0);
141
+ });
142
+
143
+ it('should have valid "good name" markers', () => {
144
+ const invalidMarkers = [];
145
+
146
+ if (csvTestData.data.values[bestOfKey]) {
147
+ csvTestData.data.values[bestOfKey].forEach((marker, index) => {
148
+ // Check for spacing issues
149
+ if (spacesValidation.test(marker)) {
150
+ const entry = csvTestData.data.entries[index];
151
+ invalidMarkers.push({
152
+ marker,
153
+ name: entry.name,
154
+ hex: entry.hex,
155
+ lineNumber: index + 2,
156
+ issue: 'invalid spacing',
157
+ });
158
+ }
159
+
160
+ // Check for invalid values (must be 'x' or empty)
161
+ if (!(marker === 'x' || marker === '')) {
162
+ const entry = csvTestData.data.entries[index];
163
+ invalidMarkers.push({
164
+ marker,
165
+ name: entry.name,
166
+ hex: entry.hex,
167
+ lineNumber: index + 2,
168
+ issue: 'invalid value',
169
+ });
170
+ }
171
+ });
172
+ }
173
+
174
+ if (invalidMarkers.length) {
175
+ const spacingIssues = invalidMarkers.filter((item) => item.issue === 'invalid spacing');
176
+ const valueIssues = invalidMarkers.filter((item) => item.issue === 'invalid value');
177
+
178
+ const details = [];
179
+ if (spacingIssues.length) {
180
+ details.push(' Spacing issues:');
181
+ spacingIssues.forEach(({ marker, name, lineNumber }) => {
182
+ details.push(` * line ${lineNumber}: "${name}" -> "${marker}"`);
183
+ });
184
+ details.push('');
185
+ }
186
+ if (valueIssues.length) {
187
+ details.push(' Invalid values:');
188
+ valueIssues.forEach(({ marker, name, lineNumber }) => {
189
+ details.push(` * line ${lineNumber}: "${name}" -> "${marker}"`);
190
+ });
191
+ details.push('');
192
+ }
193
+
194
+ throw new Error(
195
+ buildFailureMessage({
196
+ title: 'Found {n} invalid "good name" {items}:',
197
+ offenders: invalidMarkers.map((m) => `"${m.marker}"`),
198
+ offenderLabel: 'marker',
199
+ details: [
200
+ ...details,
201
+ '',
202
+ 'The "good name" column must contain either:',
203
+ ' - lowercase "x" to mark a color as a good/recommended name',
204
+ ' - empty string (blank) for regular colors',
205
+ '',
206
+ 'No spaces, uppercase letters, or other characters are allowed.',
207
+ ],
208
+ tips: [
209
+ 'Edit src/colornames.csv and fix the "good name" column',
210
+ 'Use lowercase "x" or leave blank',
211
+ 'After changes, run: npm run sort-colors',
212
+ ],
213
+ })
214
+ );
215
+ }
216
+
217
+ expect(invalidMarkers.length).toBe(0);
218
+ });
219
+ });