Skip to content

Commit 26a7cc5

Browse files
fix: character class escaping (#35)
1 parent 89d320f commit 26a7cc5

18 files changed

+159
-116
lines changed

.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ root = true
99
indent_style = space
1010
indent_size = 2
1111

12+
max_line_length = 100
13+
1214
end_of_line = lf
1315
charset = utf-8
1416
trim_trailing_whitespace = true

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ const hexDigit = characterClass(
1919
characterRange('0', '9')
2020
);
2121

22+
// prettier-ignore
2223
const hexColor = buildRegex(
2324
startOfString,
2425
optionally('#'),
2526
capture(
26-
choiceOf(repeat({ count: 6 }, hexDigit), repeat({ count: 3 }, hexDigit))
27+
choiceOf(
28+
repeat({ count: 6 }, hexDigit),
29+
repeat({ count: 3 }, hexDigit)
30+
)
2731
),
2832
endOfString
2933
);
@@ -44,6 +48,10 @@ import { buildRegex, capture, oneOrMore } from 'ts-regex-builder';
4448
const regex = buildRegex(['Hello ', capture(oneOrMore(word))]);
4549
```
4650

51+
## Examples
52+
53+
See [Examples document](./docs/Examples.md).
54+
4755
## Contributing
4856

4957
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

docs/Examples.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Regex Examples
2+
3+
## IPv4 address validation
4+
5+
```ts
6+
// Match integers from 0-255
7+
const octet = choiceOf(
8+
[digit],
9+
[characterRange('1', '9'), digit],
10+
['1', repeat({ count: 2 }, digit)],
11+
['2', characterRange('0', '4'), digit],
12+
['25', characterRange('0', '5')]
13+
);
14+
15+
// Match
16+
const regex = buildRegex([
17+
startOfString,
18+
capture(octet),
19+
'.',
20+
capture(octet),
21+
'.',
22+
capture(octet),
23+
'.',
24+
capture(octet),
25+
endOfString,
26+
]);
27+
```
28+
29+
This code generates the following regex pattern:
30+
31+
```ts
32+
const regex =
33+
/^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/;
34+
```
35+
36+
This pattern uses repetition of the `capture(octet)` elements to generate capture groups for each of the IPv4 octets:
37+
38+
```ts
39+
// Matched groups ['192.168.0.1', '192', '168', '0', '1',]
40+
const match = regex.exec('192.168.0.1');
41+
```

src/__tests__/builder.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,5 @@ test('`regexBuilder` flags', () => {
1919
expect(buildRegex({ sticky: true }, 'a').flags).toBe('y');
2020
expect(buildRegex({ sticky: false }, 'a').flags).toBe('');
2121

22-
expect(
23-
buildRegex({ global: true, ignoreCase: true, multiline: false }, 'a').flags
24-
).toBe('gi');
22+
expect(buildRegex({ global: true, ignoreCase: true, multiline: false }, 'a').flags).toBe('gi');
2523
});

src/__tests__/examples.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
buildRegex,
3+
capture,
4+
characterRange,
5+
choiceOf,
6+
digit,
7+
endOfString,
8+
repeat,
9+
startOfString,
10+
} from '../index';
11+
12+
test('example: IPv4 address validator', () => {
13+
const octet = choiceOf(
14+
[digit],
15+
[characterRange('1', '9'), digit],
16+
['1', repeat({ count: 2 }, digit)],
17+
['2', characterRange('0', '4'), digit],
18+
['25', characterRange('0', '5')]
19+
);
20+
21+
const regex = buildRegex([
22+
startOfString,
23+
capture(octet),
24+
'.',
25+
capture(octet),
26+
'.',
27+
capture(octet),
28+
'.',
29+
capture(octet),
30+
endOfString,
31+
]);
32+
33+
expect(regex).toMatchGroups('0.0.0.0', ['0.0.0.0', '0', '0', '0', '0']);
34+
expect(regex).toMatchGroups('192.168.0.1', ['192.168.0.1', '192', '168', '0', '1']);
35+
expect(regex).toMatchGroups('1.99.100.249', ['1.99.100.249', '1', '99', '100', '249']);
36+
expect(regex).toMatchGroups('255.255.255.255', ['255.255.255.255', '255', '255', '255', '255']);
37+
expect(regex).toMatchGroups('123.45.67.89', ['123.45.67.89', '123', '45', '67', '89']);
38+
39+
expect(regex.test('0.0.0.')).toBe(false);
40+
expect(regex.test('0.0.0.0.')).toBe(false);
41+
expect(regex.test('0.-1.0.0')).toBe(false);
42+
expect(regex.test('0.1000.0.0')).toBe(false);
43+
expect(regex.test('0.0.300.0')).toBe(false);
44+
expect(regex.test('255.255.255.256')).toBe(false);
45+
46+
expect(regex.source).toEqual(
47+
'^(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])$'
48+
);
49+
});

src/builders.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,13 @@ export function buildRegex(elements: RegexNode | RegexNode[]): RegExp;
3535
* @param flags RegExp flags object
3636
* @returns RegExp object
3737
*/
38-
export function buildRegex(
39-
flags: RegexFlags,
40-
elements: RegexNode | RegexNode[]
41-
): RegExp;
38+
export function buildRegex(flags: RegexFlags, elements: RegexNode | RegexNode[]): RegExp;
4239

4340
export function buildRegex(first: any, second?: any): RegExp {
4441
return _buildRegex(...optionalFirstArg(first, second));
4542
}
4643

47-
export function _buildRegex(
48-
flags: RegexFlags,
49-
elements: RegexNode | RegexNode[]
50-
): RegExp {
44+
export function _buildRegex(flags: RegexFlags, elements: RegexNode | RegexNode[]): RegExp {
5145
const pattern = encodeSequence(asNodeArray(elements)).pattern;
5246
const flagsString = encodeFlags(flags ?? {});
5347
return new RegExp(pattern, flagsString);

src/components/__tests__/capture.test.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,5 @@ test('`capture` base cases', () => {
1111
test('`capture` captures group', () => {
1212
expect(capture('b')).toMatchGroups('ab', ['b', 'b']);
1313
expect(['a', capture('b')]).toMatchGroups('ab', ['ab', 'b']);
14-
expect(['a', capture('b'), capture('c')]).toMatchGroups('abc', [
15-
'abc',
16-
'b',
17-
'c',
18-
]);
14+
expect(['a', capture('b'), capture('c')]).toMatchGroups('abc', ['abc', 'b', 'c']);
1915
});

src/components/__tests__/character-class.test.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,17 @@ test('`whitespace` character class', () => {
3737

3838
test('`characterClass` base cases', () => {
3939
expect(characterClass(characterRange('a', 'z'))).toHavePattern('[a-z]');
40-
expect(
41-
characterClass(characterRange('a', 'z'), characterRange('A', 'Z'))
42-
).toHavePattern('[a-zA-Z]');
43-
expect(characterClass(characterRange('a', 'z'), anyOf('05'))).toHavePattern(
44-
'[a-z05]'
40+
expect(characterClass(characterRange('a', 'z'), characterRange('A', 'Z'))).toHavePattern(
41+
'[a-zA-Z]'
42+
);
43+
expect(characterClass(characterRange('a', 'z'), anyOf('05'))).toHavePattern('[a-z05]');
44+
expect(characterClass(characterRange('a', 'z'), whitespace, anyOf('05'))).toHavePattern(
45+
'[a-z\\s05]'
4546
);
46-
expect(
47-
characterClass(characterRange('a', 'z'), whitespace, anyOf('05'))
48-
).toHavePattern('[a-z\\s05]');
4947
});
5048

5149
test('`characterClass` throws on inverted arguments', () => {
52-
expect(() =>
53-
characterClass(inverted(whitespace))
54-
).toThrowErrorMatchingInlineSnapshot(
50+
expect(() => characterClass(inverted(whitespace))).toThrowErrorMatchingInlineSnapshot(
5551
`"\`characterClass\` should receive only non-inverted character classes"`
5652
);
5753
});
@@ -89,11 +85,11 @@ test('`anyOf` with quantifiers', () => {
8985
});
9086

9187
test('`anyOf` escapes special characters', () => {
92-
expect(anyOf('abc-+.')).toHavePattern('[-abc\\+\\.]');
88+
expect(anyOf('abc-+.]\\')).toHavePattern('[abc+.\\]\\\\-]');
9389
});
9490

95-
test('`anyOf` moves hyphen to the first position', () => {
96-
expect(anyOf('a-bc')).toHavePattern('[-abc]');
91+
test('`anyOf` moves hyphen to the last position', () => {
92+
expect(anyOf('a-bc')).toHavePattern('[abc-]');
9793
});
9894

9995
test('`anyOf` throws on empty text', () => {

src/components/__tests__/choice-of.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,14 @@ test('`choiceOf` used in sequence', () => {
2424
test('`choiceOf` with sequence options', () => {
2525
expect([choiceOf(['a', 'b'])]).toHavePattern('ab');
2626
expect([choiceOf(['a', 'b'], ['c', 'd'])]).toHavePattern('ab|cd');
27-
expect([
28-
choiceOf(['a', zeroOrMore('b')], [oneOrMore('c'), 'd']),
29-
]).toHavePattern('ab*|c+d');
27+
expect([choiceOf(['a', zeroOrMore('b')], [oneOrMore('c'), 'd'])]).toHavePattern('ab*|c+d');
3028
});
3129

3230
test('`choiceOf` using nested regex', () => {
3331
expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern('a+|b*');
34-
expect(
35-
choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx'))
36-
).toHavePattern('a{1,3}|(?:bx){5}');
32+
expect(choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx'))).toHavePattern(
33+
'a{1,3}|(?:bx){5}'
34+
);
3735
});
3836

3937
test('`choiceOf` throws on empty options', () => {

src/components/__tests__/repeat.test.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ test('`repeat` quantifier', () => {
77
expect(['a', repeat({ min: 1 }, 'b')]).toHavePattern('ab{1,}');
88
expect(['a', repeat({ count: 1 }, 'b')]).toHavePattern('ab{1}');
99

10-
expect(['a', repeat({ count: 1 }, ['a', zeroOrMore('b')])]).toHavePattern(
11-
'a(?:ab*){1}'
12-
);
13-
expect(repeat({ count: 5 }, ['text', ' ', oneOrMore('d')])).toHavePattern(
14-
'(?:text d+){5}'
15-
);
10+
expect(['a', repeat({ count: 1 }, ['a', zeroOrMore('b')])]).toHavePattern('a(?:ab*){1}');
11+
expect(repeat({ count: 5 }, ['text', ' ', oneOrMore('d')])).toHavePattern('(?:text d+){5}');
1612
});
1713

1814
test('`repeat` optimizes grouping for atoms', () => {

0 commit comments

Comments
 (0)