color-name-list 13.0.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.
@@ -25,12 +25,12 @@ jobs:
25
25
  - name: Install dependencies
26
26
  run: npm ci
27
27
 
28
- - name: Test
29
- run: npm test
30
-
31
28
  - name: Build
32
29
  run: npm run build
33
30
 
31
+ - name: Test
32
+ run: npm test
33
+
34
34
  - name: Generate history file
35
35
  run: npm run history
36
36
 
@@ -22,8 +22,8 @@ jobs:
22
22
  node-version: ${{ matrix.node-version }}
23
23
  cache: 'npm'
24
24
  - run: npm ci
25
- - run: npm test
26
25
  - run: npm run build
26
+ - run: npm test
27
27
  - name: Check for changes
28
28
  id: git-check
29
29
  run: |
package/.husky/pre-commit CHANGED
@@ -1,3 +1,3 @@
1
1
  npm run lint
2
2
  npm run sort-colors
3
- npm run test:precommit
3
+ npm run test
package/CONTRIBUTING.md CHANGED
@@ -27,7 +27,7 @@ interactions with the project.
27
27
  - No protected brand-names (`Facebook Blue`, `Coca Cola Red`)
28
28
  - No enumerations (`Grey 1`, `Grey 2`, `Grey 3`, `Grey 4`)
29
29
  - British English spelling (ex. `Grey` not `Gray`), unless its something U.S. typical.
30
- - Capitalize colors: `Kind of Orange`
30
+ - Capitalize colors: `Kind of Orange` following [APA style](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)
31
31
  - Prefer common names, especially when adding colors of flora and fauna
32
32
  (plants and animals ;) ) ex. `Venus Slipper Orchid` instead of `Paphiopedilum`.
33
33
  - Avoid ethnic & racial assumptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "color-name-list",
3
- "version": "13.0.0",
3
+ "version": "13.1.0",
4
4
  "description": "long list of color names",
5
5
  "main": "dist/colornames.json",
6
6
  "browser": "dist/colornames.umd.js",
@@ -24,10 +24,8 @@
24
24
  "scripts": {
25
25
  "commit": "git-cz",
26
26
  "pull-colors": "curl -L 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQube6Y0wHyEtJnjg0eU3N7VseoxVnD4L9uDqvWZdl_tzzrHDVN10IPP7cdFipX8j70atNMLfPCB0Q6/pub?gid=40578722&single=true&output=csv' -o src/colornames.csv",
27
- "test": "npm run test:precommit && vitest run",
27
+ "test": "vitest run",
28
28
  "test:watch": "vitest",
29
- "test:precommit": "npm run build:test",
30
- "build:test": "node scripts/build.js --testonly",
31
29
  "build": "node scripts/build.js && npm run prettier",
32
30
  "prettier": "prettier --write ./dist/*",
33
31
  "lint": "npm run lint:scripts && npm run lint:markdown",
@@ -45,7 +45,7 @@ const readAndSortCSV = () => {
45
45
  });
46
46
 
47
47
  // Combine header & sorted lines (no blank line). Ensure exactly one final newline.
48
- const sortedData = [header, ...sortedColorLines].join('\n') + '\n';
48
+ const sortedData = [header, ...sortedColorLines].join('\n');
49
49
 
50
50
  // Write back
51
51
  fs.writeFileSync(csvPath, sortedData, 'utf8');
@@ -30117,4 +30117,4 @@ Zumthor,#cdd5d5,
30117
30117
  Zunda Green,#6bc026,x
30118
30118
  Zuni,#008996,
30119
30119
  Zürich Blue,#248bcc,
30120
- Zürich White,#e6e1d9,
30120
+ Zürich White,#e6e1d9,
@@ -35,5 +35,6 @@
35
35
  "In the Dark",
36
36
  "Dark as Night",
37
37
  "Green With Envy",
38
- "Lost in Space"
38
+ "Lost in Space",
39
+ "Fruit of Passion"
39
40
  ]
@@ -11,6 +11,88 @@ describe('Duplicate-like color names', () => {
11
11
  csvTestData.load();
12
12
  });
13
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
+ };
37
+
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
+ }
55
+
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
+ }
92
+
93
+ return conflicts;
94
+ }
95
+
14
96
  it('should not contain the same name twice', () => {
15
97
  expect(csvTestData.lineCount).toBeGreaterThan(1);
16
98
 
@@ -45,13 +127,7 @@ describe('Duplicate-like color names', () => {
45
127
  foldPlurals: true,
46
128
  pluralAllowlist,
47
129
  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
- ],
130
+ stopwords: STOPWORDS,
55
131
  });
56
132
 
57
133
  if (conflicts.length) {
@@ -146,20 +222,56 @@ describe('Duplicate-like color names', () => {
146
222
  { name: 'Heart Gold' },
147
223
  { name: 'Heart of Gold' },
148
224
  ];
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
225
  const conflicts = findNearDuplicateNameConflicts(items, {
158
226
  foldStopwords: true,
159
- stopwords,
227
+ stopwords: STOPWORDS,
160
228
  });
161
229
 
162
230
  expect(conflicts.length).toBe(1);
163
231
  expect(conflicts[0].entries.length).toBe(2);
164
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
+ });
165
277
  });
@@ -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
+ });