Skip to content

Commit 264d123

Browse files
committed
feat: null element support
1 parent d218336 commit 264d123

17 files changed

+121
-66
lines changed

src/builders.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import { encode } from './encoder';
88
* @param flags RegExp flags object
99
* @returns RegExp object
1010
*/
11-
export function buildRegExp(sequence: RegexSequence, flags?: RegexFlags): RegExp {
12-
const pattern = encode(sequence).pattern;
11+
export function buildRegExp(sequence: RegexSequence, flags?: RegexFlags): RegExp | undefined {
12+
const pattern = encode(sequence)?.pattern;
13+
if (!pattern) {
14+
return undefined;
15+
}
16+
1317
ensureUnicodeFlagIfNeeded(pattern, flags);
1418

1519
const flagsString = encodeFlags(flags ?? {});
@@ -21,8 +25,8 @@ export function buildRegExp(sequence: RegexSequence, flags?: RegexFlags): RegExp
2125
* @param elements Single regex element or array of elements
2226
* @returns regex pattern string
2327
*/
24-
export function buildPattern(sequence: RegexSequence): string {
25-
return encode(sequence).pattern;
28+
export function buildPattern(sequence: RegexSequence): string | undefined {
29+
return encode(sequence)?.pattern;
2630
}
2731

2832
function encodeFlags(flags: RegexFlags): string {

src/constructs/__tests__/char-class.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ test('`charClass` joins character escapes', () => {
4343
expect(charClass(word, nonDigit)).toEqualRegex(/[\w\D]/);
4444
});
4545

46-
test('`charClass` throws on empty text', () => {
47-
expect(() => charClass()).toThrowErrorMatchingInlineSnapshot(`"Expected at least one element"`);
46+
test('`charClass` on empty text', () => {
47+
expect(charClass()).toBeNull();
4848
});
4949

5050
test('`charRange` pattern', () => {
@@ -96,10 +96,6 @@ test('`anyOf` handles basic cases pattern', () => {
9696
expect(['x', anyOf('ab'), 'x']).toEqualRegex(/x[ab]x/);
9797
});
9898

99-
test('`anyOf` throws on empty text', () => {
100-
expect(() => anyOf('')).toThrowErrorMatchingInlineSnapshot(`"Expected at least one character"`);
101-
});
102-
10399
test('`anyOf` pattern with quantifiers', () => {
104100
expect(['x', oneOrMore(anyOf('abc')), 'x']).toEqualRegex(/x[abc]+x/);
105101
expect(['x', optional(anyOf('abc')), 'x']).toEqualRegex(/x[abc]?x/);

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ test('`choiceOf` pattern using nested regex', () => {
3232
);
3333
});
3434

35-
test('`choiceOf` throws on empty options', () => {
36-
expect(() => choiceOf()).toThrowErrorMatchingInlineSnapshot(
37-
`"Expected at least one alternative"`,
38-
);
35+
test('`choiceOf` on empty options', () => {
36+
expect(choiceOf()).toBeNull();
3937
});

src/constructs/__tests__/encoder.test.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ test('`buildRegExp` throws error on unknown element', () => {
8181
`);
8282
});
8383

84-
test('`buildPattern` throws on empty text', () => {
85-
expect(() => buildPattern('')).toThrowErrorMatchingInlineSnapshot(
86-
`"Expected at least one character"`,
87-
);
84+
test('`buildPattern` on empty text', () => {
85+
expect(buildPattern('')).toBeUndefined();
8886
});

src/constructs/__tests__/repeat.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ test('`repeat` pattern optimizes grouping for atoms', () => {
1515
expect(repeat(digit, { min: 1, max: 5 })).toEqualRegex(/\d{1,5}/);
1616
});
1717

18-
test('`repeat` throws on no children', () => {
19-
expect(() => repeat([], 1)).toThrowErrorMatchingInlineSnapshot(`"Expected at least one element"`);
18+
test('`repeat` accepts no children', () => {
19+
expect(repeat([], 1)).toBeNull();
2020
});
2121

2222
test('greedy `repeat` quantifier pattern', () => {

src/constructs/capture.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { encode } from '../encoder';
22
import type { EncodedRegex, RegexSequence } from '../types';
3+
import { ensureElements } from '../utils';
34

45
export type CaptureOptions = {
56
/**
@@ -17,18 +18,23 @@ export interface Reference extends EncodedRegex {
1718
* - in the match results (`String.match`, `String.matchAll`, or `RegExp.exec`)
1819
* - in the regex itself, through {@link ref}
1920
*/
20-
export function capture(sequence: RegexSequence, options?: CaptureOptions): EncodedRegex {
21+
export function capture(sequence: RegexSequence, options?: CaptureOptions): EncodedRegex | null {
22+
const elements = ensureElements(sequence);
23+
if (elements.length === 0) {
24+
return null;
25+
}
26+
2127
const name = options?.name;
2228
if (name) {
2329
return {
2430
precedence: 'atom',
25-
pattern: `(?<${name}>${encode(sequence).pattern})`,
31+
pattern: `(?<${name}>${encode(elements).pattern})`,
2632
};
2733
}
2834

2935
return {
3036
precedence: 'atom',
31-
pattern: `(${encode(sequence).pattern})`,
37+
pattern: `(${encode(elements).pattern})`,
3238
};
3339
}
3440

src/constructs/char-class.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import type { CharacterClass, CharacterEscape, EncodedRegex } from '../types';
2-
import { ensureText } from '../utils';
32

43
/**
54
* Creates a character class which matches any one of the given characters.
65
*
76
* @param elements - Member characters or character ranges.
87
* @returns Character class.
98
*/
10-
export function charClass(...elements: Array<CharacterClass | CharacterEscape>): CharacterClass {
11-
if (!elements.length) {
12-
throw new Error('Expected at least one element');
9+
export function charClass(
10+
...elements: Array<CharacterClass | CharacterEscape | null>
11+
): CharacterClass | null {
12+
const allElements = elements.flatMap((c) => c?.elements).filter((c) => c != null);
13+
if (allElements.length === 0) {
14+
return null;
1315
}
1416

1517
return {
16-
elements: elements.map((c) => c.elements).flat(),
18+
elements: allElements,
1719
encode: encodeCharClass,
1820
};
1921
}
@@ -46,9 +48,7 @@ export function charRange(start: string, end: string): CharacterClass {
4648
* @param chars - Characters to match.
4749
* @returns Character class.
4850
*/
49-
export function anyOf(chars: string): CharacterClass {
50-
ensureText(chars);
51-
51+
export function anyOf(chars: string): CharacterClass | null {
5252
return {
5353
elements: chars.split('').map(escapeChar),
5454
encode: encodeCharClass,
@@ -61,7 +61,7 @@ export function anyOf(chars: string): CharacterClass {
6161
* @param element - Character class or character escape to negate.
6262
* @returns Negated character class.
6363
*/
64-
export function negated(element: CharacterClass | CharacterEscape): EncodedRegex {
64+
export function negated(element: CharacterClass | CharacterEscape): EncodedRegex | null {
6565
return encodeCharClass.call(element, true);
6666
}
6767

@@ -79,7 +79,11 @@ function escapeChar(text: string): string {
7979
function encodeCharClass(
8080
this: CharacterClass | CharacterEscape,
8181
isNegated?: boolean,
82-
): EncodedRegex {
82+
): EncodedRegex | null {
83+
if (this.elements.length === 0) {
84+
return null;
85+
}
86+
8387
return {
8488
precedence: 'atom',
8589
pattern: `[${isNegated ? '^' : ''}${this.elements.join('')}]`,

src/constructs/choice-of.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import type { EncodedRegex, RegexSequence } from '../types';
77
* @param alternatives - Alternatives to choose from.
88
* @returns Choice of alternatives.
99
*/
10-
export function choiceOf(...alternatives: RegexSequence[]): EncodedRegex {
10+
export function choiceOf(...alternatives: RegexSequence[]): EncodedRegex | null {
1111
if (alternatives.length === 0) {
12-
throw new Error('Expected at least one alternative');
12+
return null;
1313
}
1414

15-
const encodedAlternatives = alternatives.map((c) => encode(c));
15+
const encodedAlternatives = alternatives.map((c) => encode(c)).filter((c) => c != null);
1616
if (encodedAlternatives.length === 1) {
1717
return encodedAlternatives[0]!;
1818
}

src/constructs/lookahead.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { encode } from '../encoder';
22
import type { EncodedRegex, RegexSequence } from '../types';
3+
import { ensureElements } from '../utils';
34

45
/**
56
* Positive lookahead assertion.
@@ -15,7 +16,12 @@ import type { EncodedRegex, RegexSequence } from '../types';
1516
* // /(?=abc)/
1617
* ```
1718
*/
18-
export function lookahead(sequence: RegexSequence): EncodedRegex {
19+
export function lookahead(sequence: RegexSequence): EncodedRegex | null {
20+
const elements = ensureElements(sequence);
21+
if (elements.length === 0) {
22+
return null;
23+
}
24+
1925
return {
2026
precedence: 'atom',
2127
pattern: `(?=${encode(sequence).pattern})`,

src/constructs/lookbehind.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { encode } from '../encoder';
22
import type { EncodedRegex, RegexSequence } from '../types';
3+
import { ensureElements } from '../utils';
34

45
/**
56
* Positive lookbehind assertion.
@@ -15,7 +16,12 @@ import type { EncodedRegex, RegexSequence } from '../types';
1516
* // /(?<=abc)/
1617
* ```
1718
*/
18-
export function lookbehind(sequence: RegexSequence): EncodedRegex {
19+
export function lookbehind(sequence: RegexSequence): EncodedRegex | null {
20+
const elements = ensureElements(sequence);
21+
if (elements.length === 0) {
22+
return null;
23+
}
24+
1925
return {
2026
precedence: 'atom',
2127
pattern: `(?<=${encode(sequence).pattern})`,

src/constructs/negative-lookahead.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { encode } from '../encoder';
22
import type { EncodedRegex, RegexSequence } from '../types';
3+
import { ensureElements } from '../utils';
34

45
/**
56
* Negative lookahead assertion.
@@ -15,7 +16,12 @@ import type { EncodedRegex, RegexSequence } from '../types';
1516
* // /(?=abc)/
1617
* ```
1718
*/
18-
export function negativeLookahead(sequence: RegexSequence): EncodedRegex {
19+
export function negativeLookahead(sequence: RegexSequence): EncodedRegex | null {
20+
const elements = ensureElements(sequence);
21+
if (elements.length === 0) {
22+
return null;
23+
}
24+
1925
return {
2026
precedence: 'atom',
2127
pattern: `(?!${encode(sequence).pattern})`,

src/constructs/negative-lookbehind.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { encode } from '../encoder';
22
import type { EncodedRegex, RegexSequence } from '../types';
3-
3+
import { ensureElements } from '../utils';
44
/**
55
* Negative lookbehind assertion.
66
*
@@ -15,7 +15,12 @@ import type { EncodedRegex, RegexSequence } from '../types';
1515
* // /(?<!abc)/
1616
* ```
1717
*/
18-
export function negativeLookbehind(sequence: RegexSequence): EncodedRegex {
18+
export function negativeLookbehind(sequence: RegexSequence): EncodedRegex | null {
19+
const elements = ensureElements(sequence);
20+
if (elements.length === 0) {
21+
return null;
22+
}
23+
1924
return {
2025
precedence: 'atom',
2126
pattern: `(?<!${encode(sequence).pattern})`,

src/constructs/quantifiers.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ export interface QuantifierOptions {
1212
* @param sequence - Elements to match zero or more of.
1313
* @param options - Quantifier options.
1414
*/
15-
export function zeroOrMore(sequence: RegexSequence, options?: QuantifierOptions): EncodedRegex {
15+
export function zeroOrMore(
16+
sequence: RegexSequence,
17+
options?: QuantifierOptions,
18+
): EncodedRegex | null {
1619
const elements = ensureElements(sequence);
20+
if (elements.length === 0) {
21+
return null;
22+
}
23+
1724
return {
1825
precedence: 'sequence',
1926
pattern: `${encodeAtomic(elements)}*${options?.greedy === false ? '?' : ''}`,
@@ -26,8 +33,15 @@ export function zeroOrMore(sequence: RegexSequence, options?: QuantifierOptions)
2633
* @param sequence - Elements to match one or more of.
2734
* @param options - Quantifier options.
2835
*/
29-
export function oneOrMore(sequence: RegexSequence, options?: QuantifierOptions): EncodedRegex {
36+
export function oneOrMore(
37+
sequence: RegexSequence,
38+
options?: QuantifierOptions,
39+
): EncodedRegex | null {
3040
const elements = ensureElements(sequence);
41+
if (elements.length === 0) {
42+
return null;
43+
}
44+
3145
return {
3246
precedence: 'sequence',
3347
pattern: `${encodeAtomic(elements)}+${options?.greedy === false ? '?' : ''}`,
@@ -40,8 +54,15 @@ export function oneOrMore(sequence: RegexSequence, options?: QuantifierOptions):
4054
* @param sequence - Elements to match zero or one of.
4155
* @param options - Quantifier options.
4256
*/
43-
export function optional(sequence: RegexSequence, options?: QuantifierOptions): EncodedRegex {
57+
export function optional(
58+
sequence: RegexSequence,
59+
options?: QuantifierOptions,
60+
): EncodedRegex | null {
4461
const elements = ensureElements(sequence);
62+
if (elements.length === 0) {
63+
return null;
64+
}
65+
4566
return {
4667
precedence: 'sequence',
4768
pattern: `${encodeAtomic(elements)}?${options?.greedy === false ? '?' : ''}`,

src/constructs/repeat.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ export type RepeatOptions = number | { min: number; max?: number; greedy?: boole
1717
* @param sequence - Sequence to match.
1818
* @param options - Quantifier options.
1919
*/
20-
export function repeat(sequence: RegexSequence, options: RepeatOptions): EncodedRegex {
20+
export function repeat(sequence: RegexSequence, options: RepeatOptions): EncodedRegex | null {
2121
const elements = ensureElements(sequence);
22+
if (elements.length === 0) {
23+
return null;
24+
}
2225

2326
if (typeof options === 'number') {
2427
return {

src/encoder.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { EncodedRegex, RegexElement, RegexSequence } from './types';
2-
import { ensureElements, ensureText } from './utils';
2+
import { ensureElements } from './utils';
33

4-
export function encode(sequence: RegexSequence): EncodedRegex {
4+
export function encode(sequence: RegexSequence): EncodedRegex | null {
55
const elements = ensureElements(sequence);
6-
const encoded = elements.map((n) => encodeElement(n));
6+
const encoded = elements.map((n) => encodeElement(n)).filter((n) => n != null);
7+
if (encoded.length === 0) {
8+
return null;
9+
}
710

811
if (encoded.length === 1) {
912
return encoded[0]!;
@@ -17,12 +20,20 @@ export function encode(sequence: RegexSequence): EncodedRegex {
1720
};
1821
}
1922

20-
export function encodeAtomic(sequence: RegexSequence): string {
23+
export function encodeAtomic(sequence: RegexSequence): string | null {
2124
const encoded = encode(sequence);
25+
if (encoded == null) {
26+
return null;
27+
}
28+
2229
return encoded.precedence === 'atom' ? encoded.pattern : `(?:${encoded.pattern})`;
2330
}
2431

25-
function encodeElement(element: RegexElement): EncodedRegex {
32+
function encodeElement(element: RegexElement): EncodedRegex | null {
33+
if (element == null) {
34+
return null;
35+
}
36+
2637
if (typeof element === 'string') {
2738
return encodeText(element);
2839
}
@@ -46,8 +57,10 @@ function encodeElement(element: RegexElement): EncodedRegex {
4657
throw new Error(`Unsupported element. Received: ${JSON.stringify(element, null, 2)}`);
4758
}
4859

49-
function encodeText(text: string): EncodedRegex {
50-
ensureText(text);
60+
function encodeText(text: string): EncodedRegex | null {
61+
if (text.length === 0) {
62+
return null;
63+
}
5164

5265
return {
5366
// Optimize for single character case

0 commit comments

Comments
 (0)