Skip to content

Commit 5f83423

Browse files
authored
feat(rules): expand Latin-only characters limitation for subject-case with Unicode support (#3575)
1 parent 8940ccc commit 5f83423

File tree

2 files changed

+157
-2
lines changed

2 files changed

+157
-2
lines changed

@commitlint/rules/src/subject-case.test.ts

+139-1
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,44 @@ const messages = {
55
empty: 'test:\n',
66
numeric: 'test: 1.0.0',
77
lowercase: 'test: subject',
8+
lowercase_unicode: 'test: тема', // Bulgarian for `subject`
89
mixedcase: 'test: sUbJeCt',
910
uppercase: 'test: SUBJECT',
11+
uppercase_unicode: 'test: ÛNDERWERP', // Frisian for `SUBJECT`
1012
camelcase: 'test: subJect',
13+
camelcase_unicode: 'test: θέΜα', // Greek for `subJect`
1114
kebabcase: 'test: sub-ject',
15+
kebabcase_unicode: 'test: áb-har', // Irish for `sub-ject`
1216
pascalcase: 'test: SubJect',
17+
pascalcase_unicode: 'test: ТақыРып', // Kazakh for `SubJect`
1318
snakecase: 'test: sub_ject',
19+
snakecase_unicode: 'test: сэ_дэв', // Mongolian for `sub_ject`
1420
startcase: 'test: Sub Ject',
21+
startcase_unicode: 'test: Äm Ne', // Swedish for `Sub Ject`
22+
sentencecase: 'test: Sub ject',
23+
sentencecase_unicode: 'test: Мав зуъ', // Tajik for `Sub ject`
1524
};
1625

1726
const parsed = {
1827
empty: parse(messages.empty),
1928
numeric: parse(messages.numeric),
2029
lowercase: parse(messages.lowercase),
30+
lowercase_unicode: parse(messages.lowercase_unicode),
2131
mixedcase: parse(messages.mixedcase),
2232
uppercase: parse(messages.uppercase),
33+
uppercase_unicode: parse(messages.uppercase_unicode),
2334
camelcase: parse(messages.camelcase),
35+
camelcase_unicode: parse(messages.camelcase_unicode),
2436
kebabcase: parse(messages.kebabcase),
37+
kebabcase_unicode: parse(messages.kebabcase_unicode),
2538
pascalcase: parse(messages.pascalcase),
39+
pascalcase_unicode: parse(messages.pascalcase_unicode),
2640
snakecase: parse(messages.snakecase),
41+
snakecase_unicode: parse(messages.snakecase_unicode),
2742
startcase: parse(messages.startcase),
43+
startcase_unicode: parse(messages.startcase_unicode),
44+
sentencecase: parse(messages.sentencecase),
45+
sentencecase_unicode: parse(messages.sentencecase_unicode),
2846
};
2947

3048
test('with empty subject should succeed for "never lowercase"', async () => {
@@ -63,6 +81,16 @@ test('with lowercase subject should succeed for "always lowercase"', async () =>
6381
expect(actual).toEqual(expected);
6482
});
6583

84+
test('with lowercase unicode subject should fail for "always uppercase"', async () => {
85+
const [actual] = subjectCase(
86+
await parsed.lowercase_unicode,
87+
'always',
88+
'upper-case'
89+
);
90+
const expected = false;
91+
expect(actual).toEqual(expected);
92+
});
93+
6694
test('with mixedcase subject should succeed for "never lowercase"', async () => {
6795
const [actual] = subjectCase(await parsed.mixedcase, 'never', 'lowercase');
6896
const expected = true;
@@ -93,12 +121,22 @@ test('with uppercase subject should fail for "never uppercase"', async () => {
93121
expect(actual).toEqual(expected);
94122
});
95123

96-
test('with lowercase subject should succeed for "always uppercase"', async () => {
124+
test('with uppercase subject should succeed for "always uppercase"', async () => {
97125
const [actual] = subjectCase(await parsed.uppercase, 'always', 'uppercase');
98126
const expected = true;
99127
expect(actual).toEqual(expected);
100128
});
101129

130+
test('with uppercase unicode subject should fail for "always lowercase"', async () => {
131+
const [actual] = subjectCase(
132+
await parsed.uppercase_unicode,
133+
'always',
134+
'lower-case'
135+
);
136+
const expected = false;
137+
expect(actual).toEqual(expected);
138+
});
139+
102140
test('with camelcase subject should fail for "always uppercase"', async () => {
103141
const [actual] = subjectCase(await parsed.camelcase, 'always', 'uppercase');
104142
const expected = false;
@@ -135,6 +173,26 @@ test('with camelcase subject should succeed for "always camelcase"', async () =>
135173
expect(actual).toEqual(expected);
136174
});
137175

176+
test('with camelcase unicode subject should fail for "always sentencecase"', async () => {
177+
const [actual] = subjectCase(
178+
await parsed.camelcase_unicode,
179+
'always',
180+
'sentence-case'
181+
);
182+
const expected = false;
183+
expect(actual).toEqual(expected);
184+
});
185+
186+
test('with kebabcase unicode subject should fail for "always camelcase"', async () => {
187+
const [actual] = subjectCase(
188+
await parsed.kebabcase_unicode,
189+
'always',
190+
'camel-case'
191+
);
192+
const expected = false;
193+
expect(actual).toEqual(expected);
194+
});
195+
138196
test('with pascalcase subject should fail for "always uppercase"', async () => {
139197
const [actual] = subjectCase(await parsed.pascalcase, 'always', 'uppercase');
140198
const expected = false;
@@ -175,6 +233,16 @@ test('with pascalcase subject should fail for "always camelcase"', async () => {
175233
expect(actual).toEqual(expected);
176234
});
177235

236+
test('with pascalcase unicode subject should fail for "always uppercase"', async () => {
237+
const [actual] = subjectCase(
238+
await parsed.pascalcase_unicode,
239+
'always',
240+
'upper-case'
241+
);
242+
const expected = false;
243+
expect(actual).toEqual(expected);
244+
});
245+
178246
test('with snakecase subject should fail for "always uppercase"', async () => {
179247
const [actual] = subjectCase(await parsed.snakecase, 'always', 'uppercase');
180248
const expected = false;
@@ -211,6 +279,16 @@ test('with snakecase subject should fail for "always camelcase"', async () => {
211279
expect(actual).toEqual(expected);
212280
});
213281

282+
test('with snakecase unicode subject should fail for "never lowercase"', async () => {
283+
const [actual] = subjectCase(
284+
await parsed.snakecase_unicode,
285+
'never',
286+
'lower-case'
287+
);
288+
const expected = false;
289+
expect(actual).toEqual(expected);
290+
});
291+
214292
test('with startcase subject should fail for "always uppercase"', async () => {
215293
const [actual] = subjectCase(await parsed.startcase, 'always', 'uppercase');
216294
const expected = false;
@@ -253,6 +331,66 @@ test('with startcase subject should succeed for "always startcase"', async () =>
253331
expect(actual).toEqual(expected);
254332
});
255333

334+
test('with startcase unicode subject should fail for "always pascalcase"', async () => {
335+
const [actual] = subjectCase(
336+
await parsed.startcase_unicode,
337+
'always',
338+
'pascal-case'
339+
);
340+
const expected = false;
341+
expect(actual).toEqual(expected);
342+
});
343+
344+
test('with sentencecase subject should succeed for "always sentence-case"', async () => {
345+
const [actual] = subjectCase(
346+
await parsed.sentencecase,
347+
'always',
348+
'sentence-case'
349+
);
350+
const expected = true;
351+
expect(actual).toEqual(expected);
352+
});
353+
354+
test('with sentencecase subject should fail for "never sentencecase"', async () => {
355+
const [actual] = subjectCase(
356+
await parsed.sentencecase,
357+
'never',
358+
'sentence-case'
359+
);
360+
const expected = false;
361+
expect(actual).toEqual(expected);
362+
});
363+
364+
test('with sentencecase subject should fail for "always pascalcase"', async () => {
365+
const [actual] = subjectCase(
366+
await parsed.sentencecase,
367+
'always',
368+
'pascal-case'
369+
);
370+
const expected = false;
371+
expect(actual).toEqual(expected);
372+
});
373+
374+
test('with sentencecase subject should succeed for "never camelcase"', async () => {
375+
const [actual] = subjectCase(
376+
await parsed.sentencecase,
377+
'never',
378+
'camel-case'
379+
);
380+
const expected = true;
381+
expect(actual).toEqual(expected);
382+
});
383+
384+
test('with sentencecase unicode subject should fail for "always camelcase"', async () => {
385+
const [actual] = subjectCase(
386+
await parsed.sentencecase_unicode,
387+
'always',
388+
'camel-case'
389+
);
390+
const expected = false;
391+
expect(actual).toEqual(expected);
392+
});
393+
256394
test('should use expected message with "always"', async () => {
257395
const [, message] = subjectCase(
258396
await parsed.uppercase,

@commitlint/rules/src/subject-case.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ import {case as ensureCase} from '@commitlint/ensure';
22
import message from '@commitlint/message';
33
import {TargetCaseType, SyncRule} from '@commitlint/types';
44

5+
/**
6+
* Since the rule requires first symbol of a subject to be a letter, use
7+
* combination of Unicode `Cased_Letter` and `Other_Letter` categories now to
8+
* allow non-Latin alphabets as well.
9+
*
10+
* Do not use `Letter` category directly to avoid capturing `Modifier_Letter`
11+
* (which just modifiers letters, so we probably shouldn't anyway) and to stay
12+
* close to previous implementation.
13+
*
14+
* Also, typescript does not seem to support almost any longhand category name
15+
* (and even short for `Cased_Letter` too) so list all required letter
16+
* categories manually just to prevent it from complaining about unknown stuff.
17+
*
18+
* @see [Unicode Categories]{@link https://www.regular-expressions.info/unicode.html}
19+
*/
20+
const startsWithLetterRegex = /^[\p{Ll}\p{Lu}\p{Lt}\p{Lo}]/iu;
21+
522
const negated = (when?: string) => when === 'never';
623

724
export const subjectCase: SyncRule<TargetCaseType | TargetCaseType[]> = (
@@ -11,7 +28,7 @@ export const subjectCase: SyncRule<TargetCaseType | TargetCaseType[]> = (
1128
) => {
1229
const {subject} = parsed;
1330

14-
if (typeof subject !== 'string' || !subject.match(/^[a-z]/i)) {
31+
if (typeof subject !== 'string' || !subject.match(startsWithLetterRegex)) {
1532
return [true];
1633
}
1734

0 commit comments

Comments
 (0)