Skip to content

Commit 8cd15ee

Browse files
committed
fix(cz-commitlint): support multiple cases & fix to sentence-case
1 parent a5b8605 commit 8cd15ee

File tree

6 files changed

+131
-76
lines changed

6 files changed

+131
-76
lines changed

@commitlint/cz-commitlint/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
}
3737
},
3838
"dependencies": {
39+
"@commitlint/ensure": "^13.2.0",
3940
"@commitlint/load": "^13.2.1",
4041
"@commitlint/types": "^13.2.0",
4142
"chalk": "^4.1.0",
@@ -47,6 +48,7 @@
4748
"inquirer": "^8.0.0"
4849
},
4950
"devDependencies": {
50-
"@types/inquirer": "^8.0.0"
51+
"@types/inquirer": "^8.0.0",
52+
"commitizen": "^4.2.4"
5153
}
5254
}

@commitlint/cz-commitlint/src/Question.test.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const QUESTION_CONFIG = {
1515
messages: MESSAGES,
1616
};
1717

18+
const caseFn = (input: string | string[], delimiter?: string) =>
19+
(Array.isArray(input) ? input : [input])
20+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
21+
.join(delimiter);
22+
1823
describe('name', () => {
1924
test('should throw error when name is not a meaningful string', () => {
2025
expect(
@@ -195,55 +200,44 @@ describe('filter', () => {
195200
test('should auto fix case and full-stop', () => {
196201
const question = new Question('body', {
197202
...QUESTION_CONFIG,
198-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
203+
caseFn,
199204
fullStopFn: (input: string) => input + '!',
200205
}).question;
201206

202207
expect(question.filter?.('xxxx', {})).toBe('Xxxx!');
203208
});
204209

205-
test('should split the string to items when multipleValueDelimiters is defined', () => {
206-
const question = new Question('body', {
207-
...QUESTION_CONFIG,
208-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
209-
fullStopFn: (input: string) => input + '!',
210-
multipleValueDelimiters: /,|\|/g,
211-
}).question;
212-
213-
expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!');
214-
});
215-
216-
test('should delimiters keep origin when multipleSelectDefaultDelimiter is defined', () => {
210+
test('should transform each item with same case when input is array', () => {
217211
const question = new Question('body', {
218212
...QUESTION_CONFIG,
219-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
213+
caseFn,
220214
fullStopFn: (input: string) => input + '!',
221-
multipleValueDelimiters: /,|\|/g,
222-
multipleSelectDefaultDelimiter: '/',
223215
}).question;
224216

225-
expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!');
217+
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx,Yyyy!');
226218
});
227219

228-
test('should fix each item when input is array', () => {
220+
test('should concat items with multipleSelectDefaultDelimiter when input is array', () => {
229221
const question = new Question('body', {
230222
...QUESTION_CONFIG,
231-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
223+
caseFn,
232224
fullStopFn: (input: string) => input + '!',
225+
multipleSelectDefaultDelimiter: '|',
233226
}).question;
234227

235-
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx,Yyyy!');
228+
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx|Yyyy!');
236229
});
237230

238-
test('should concat items with multipleSelectDefaultDelimiter when input is array', () => {
231+
test('should split the string to items when multipleValueDelimiters is defined', () => {
239232
const question = new Question('body', {
240233
...QUESTION_CONFIG,
241-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
234+
caseFn,
242235
fullStopFn: (input: string) => input + '!',
243-
multipleSelectDefaultDelimiter: '|',
236+
multipleValueDelimiters: /,|\|/g,
244237
}).question;
245238

246-
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx|Yyyy!');
239+
expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!');
240+
expect(question.filter?.('xxxx-yyyy-zzzz', {})).toBe('Xxxx-yyyy-zzzz!');
247241
});
248242

249243
test('should works well when does not pass caseFn/fullStopFn', () => {
@@ -307,7 +301,7 @@ describe('transformer', () => {
307301
test('should auto transform case and full-stop', () => {
308302
const question = new Question('body', {
309303
...QUESTION_CONFIG,
310-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
304+
caseFn,
311305
fullStopFn: (input: string) => input + '!',
312306
}).question;
313307

@commitlint/cz-commitlint/src/Question.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ export default class Question {
5959
this.title = title ?? '';
6060
this.skip = skip ?? false;
6161
this.fullStopFn = fullStopFn ?? ((_: string) => _);
62-
this.caseFn = caseFn ?? ((_: string) => _);
62+
this.caseFn =
63+
caseFn ??
64+
((input: string | string[], delimiter?: string) =>
65+
Array.isArray(input) ? input.join(delimiter) : input);
6366
this.multipleValueDelimiters = multipleValueDelimiters;
6467
this.multipleSelectDefaultDelimiter = multipleSelectDefaultDelimiter;
6568

@@ -149,17 +152,30 @@ export default class Question {
149152
}
150153

151154
protected filter(input: string | string[]): string {
152-
// Array
153-
// String,is enable MultiSelect -> input.replace(delimeter, this.case); -> Array
154-
155-
const toCased = Array.isArray(input)
156-
? input.map(this.caseFn).join(this.multipleSelectDefaultDelimiter)
157-
: this.multipleValueDelimiters
158-
? input.replace(
159-
new RegExp(`[^${this.multipleValueDelimiters.source}]+`, 'g'),
160-
this.caseFn
161-
)
162-
: this.caseFn(input);
155+
let toCased;
156+
157+
// array
158+
// - multipleSelectDefaultDelimiter
159+
// - no multipleSelectDefaultDelimiter
160+
// string + multipleValueDelimiters
161+
// - split
162+
// - not split
163+
// string
164+
if (Array.isArray(input)) {
165+
toCased = this.caseFn(input, this.multipleSelectDefaultDelimiter);
166+
} else if (this.multipleValueDelimiters) {
167+
const segments = input.split(this.multipleValueDelimiters);
168+
const casedString = this.caseFn(segments, ',');
169+
const casedSegments = casedString.split(',');
170+
toCased = input.replace(
171+
new RegExp(`[^${this.multipleValueDelimiters.source}]+`, 'g'),
172+
(segment) => {
173+
return casedSegments[segments.indexOf(segment)];
174+
}
175+
);
176+
} else {
177+
toCased = this.caseFn(input);
178+
}
163179

164180
return this.fullStopFn(toCased);
165181
}

@commitlint/cz-commitlint/src/utils/case-fn.test.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {RuleConfigSeverity} from '@commitlint/types';
22
import getCaseFn from './case-fn';
33

4-
test('should not apply', () => {
4+
test('should not transform when rule is disabled', () => {
55
let rule = getCaseFn([RuleConfigSeverity.Disabled]);
66
expect(rule('test')).toBe('test');
77
expect(rule('test-foo')).toBe('test-foo');
@@ -19,29 +19,17 @@ test('should not apply', () => {
1919
expect(rule('test-foo')).toBe('test-foo');
2020
expect(rule('testFoo')).toBe('testFoo');
2121
expect(rule('TEST_FOO')).toBe('TEST_FOO');
22-
23-
rule = getCaseFn([
24-
RuleConfigSeverity.Warning,
25-
'always',
26-
['camel-case', 'lowercase'],
27-
]);
28-
expect(rule('test')).toBe('test');
29-
expect(rule('test-foo')).toBe('test-foo');
30-
expect(rule('testFoo')).toBe('testFoo');
31-
expect(rule('TEST_FOO')).toBe('TEST_FOO');
3222
});
3323

3424
test('should throw error on invalid casing', () => {
35-
expect(() => getCaseFn([RuleConfigSeverity.Warning, 'always'])).toThrow(
36-
'Unknown target case "undefined"'
37-
);
25+
let rule = getCaseFn([RuleConfigSeverity.Warning, 'always']);
26+
expect(() => rule('test')).toThrow('Unknown target case "undefined"');
3827

39-
expect(() =>
40-
getCaseFn([RuleConfigSeverity.Warning, 'always', 'foo'])
41-
).toThrow('Unknown target case "foo"');
28+
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'foo']);
29+
expect(() => rule('test')).toThrow('Unknown target case "foo"');
4230
});
4331

44-
test('should convert text correctly', () => {
32+
test('should transform text correctly with single case', () => {
4533
let rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'camel-case']);
4634
expect(rule('TEST_FOOBar-baz baz')).toBe('testFooBarBazBaz');
4735

@@ -64,10 +52,10 @@ test('should convert text correctly', () => {
6452
expect(rule('TEST_FOOBar-baz baz')).toBe('TEST_FOOBAR-BAZ BAZ');
6553

6654
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'sentence-case']);
67-
expect(rule('TEST_FOOBar-baz baz')).toBe('Test_foobar-baz baz');
55+
expect(rule('tEST_FOOBar-baz baz')).toBe('TEST_FOOBar-baz baz');
6856

6957
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'sentencecase']);
70-
expect(rule('TEST_FOOBar-baz baz')).toBe('Test_foobar-baz baz');
58+
expect(rule('tEST_FOOBar-baz baz')).toBe('TEST_FOOBar-baz baz');
7159

7260
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lower-case']);
7361
expect(rule('TEST_FOOBar-baz baz')).toBe('test_foobar-baz baz');
@@ -77,4 +65,27 @@ test('should convert text correctly', () => {
7765

7866
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']);
7967
expect(rule('TEST_FOOBar-baz baz')).toBe('test_foobar-baz baz');
68+
69+
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']);
70+
expect(rule(['TEST_FOOBar-baz', 'bAz'])).toBe('test_foobar-baz,baz');
71+
72+
rule = getCaseFn([RuleConfigSeverity.Warning, 'always', 'lowerCase']);
73+
expect(rule(['TEST_FOOBar-baz', 'bAz'], '|')).toBe('test_foobar-baz|baz');
74+
});
75+
76+
test('should transform text correctly with multiple cases', () => {
77+
const rule = getCaseFn([
78+
RuleConfigSeverity.Warning,
79+
'always',
80+
['camel-case', 'lowercase'],
81+
]);
82+
expect(rule('test')).toBe('test');
83+
expect(rule('test-foo')).toBe('test-foo');
84+
expect(rule('testFoo')).toBe('testFoo');
85+
expect(rule('TEST_FOO')).toBe('testFoo');
86+
87+
expect(rule(['testFoo', 'test_foo'])).toBe('testFoo,testFoo');
88+
expect(rule(['TEST_foo', 'test_foo'])).toBe('test_foo,test_foo');
89+
expect(rule(['TEST_FOO', 'Test_foo'])).toBe('testFoo,testFoo');
90+
expect(rule(['TEST_FOO', 'Test_foo'], '|')).toBe('testFoo|testFoo');
8091
});

@commitlint/cz-commitlint/src/utils/case-fn.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,75 @@
1+
import _ from 'lodash';
12
import camelCase from 'lodash/camelCase';
23
import kebabCase from 'lodash/kebabCase';
34
import snakeCase from 'lodash/snakeCase';
45
import startCase from 'lodash/startCase';
56
import upperFirst from 'lodash/upperFirst';
6-
import {Rule} from '../types';
77
import {ruleIsActive, ruleIsNotApplicable} from './rules';
8+
import {TargetCaseType} from '@commitlint/types';
9+
import {case as ensureCase} from '@commitlint/ensure';
10+
import {Rule} from '../types';
811

9-
export type CaseFn = (input: string) => string;
12+
export type CaseFn = (input: string | string[], delimiter?: string) => string;
1013

1114
/**
1215
* Get forced case for rule
1316
* @param rule to parse
1417
* @return transform function applying the enforced case
1518
*/
1619
export default function getCaseFn(rule?: Rule): CaseFn {
17-
const noop = (input: string) => input;
20+
const noop = (input: string | string[], delimiter?: string) =>
21+
Array.isArray(input) ? input.join(delimiter) : input;
1822

1923
if (!rule || !ruleIsActive(rule) || ruleIsNotApplicable(rule)) {
2024
return noop;
2125
}
2226

23-
const target = rule[2];
27+
const value = rule[2];
2428

25-
if (Array.isArray(target)) {
26-
return noop;
27-
}
29+
const caseList = Array.isArray(value) ? value : [value];
30+
31+
return (input: string | string[], delimiter?: string) => {
32+
let matchedCase: TargetCaseType = caseList[0];
33+
const segments = Array.isArray(input) ? input : [input];
34+
35+
for (const segment of segments) {
36+
const check = caseList.find((a) => ensureCase(segment, a));
37+
if (check) {
38+
matchedCase = check;
39+
break;
40+
}
41+
}
42+
43+
return segments
44+
.map((segment) => {
45+
return toCase(segment, matchedCase);
46+
})
47+
.join(delimiter);
48+
};
49+
}
2850

51+
function toCase(input: string, target: TargetCaseType): string {
2952
switch (target) {
3053
case 'camel-case':
31-
return (input: string) => camelCase(input);
54+
return camelCase(input);
3255
case 'kebab-case':
33-
return (input: string) => kebabCase(input);
56+
return kebabCase(input);
3457
case 'snake-case':
35-
return (input: string) => snakeCase(input);
58+
return snakeCase(input);
3659
case 'pascal-case':
37-
return (input: string) => upperFirst(camelCase(input));
60+
return upperFirst(camelCase(input));
3861
case 'start-case':
39-
return (input: string) => startCase(input);
62+
return startCase(input);
4063
case 'upper-case':
4164
case 'uppercase':
42-
return (input: string) => input.toUpperCase();
65+
return input.toUpperCase();
4366
case 'sentence-case':
4467
case 'sentencecase':
45-
return (input: string) =>
46-
`${input.charAt(0).toUpperCase()}${input.substring(1).toLowerCase()}`;
68+
return input.charAt(0).toUpperCase() + input.slice(1);
4769
case 'lower-case':
4870
case 'lowercase':
4971
case 'lowerCase': // Backwards compat config-angular v4
50-
return (input: string) => input.toLowerCase();
72+
return input.toLowerCase();
5173
default:
5274
throw new TypeError(`Unknown target case "${target}"`);
5375
}

yarn.lock

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3798,7 +3798,7 @@ commander@~2.20.3:
37983798
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
37993799
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
38003800

3801-
3801+
[email protected], commitizen@^4.2.4:
38023802
version "4.2.4"
38033803
resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.4.tgz#a3e5b36bd7575f6bf6e7aa19dbbf06b0d8f37165"
38043804
integrity sha512-LlZChbDzg3Ir3O2S7jSo/cgWp5/QwylQVr59K4xayVq8S4/RdKzSyJkghAiZZHfhh5t4pxunUoyeg0ml1q/7aw==
@@ -7352,11 +7352,21 @@ lodash.truncate@^4.4.2:
73527352
resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
73537353
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
73547354

7355-
[email protected], [email protected], lodash@^3.3.1, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.5.1, lodash@^4.7.0:
7355+
7356+
version "4.17.15"
7357+
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
7358+
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
7359+
7360+
[email protected], lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.5.1, lodash@^4.7.0:
73567361
version "4.17.21"
73577362
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
73587363
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
73597364

7365+
lodash@^3.3.1:
7366+
version "3.10.1"
7367+
resolved "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
7368+
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
7369+
73607370
log-update@^1.0.2:
73617371
version "1.0.2"
73627372
resolved "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"

0 commit comments

Comments
 (0)