Skip to content

Commit 4ed7a56

Browse files
refactor: open element types (#33)
1 parent 76c1f75 commit 4ed7a56

21 files changed

+278
-343
lines changed

src/builders.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { RegexElement } from './components/types';
1+
import type { RegexNode } from './types';
22
import { encodeSequence } from './encoder/encoder';
3-
import { asElementArray } from './utils/elements';
3+
import { asNodeArray } from './utils/nodes';
44
import { optionalFirstArg } from './utils/optional-arg';
55

66
export interface RegexFlags {
@@ -26,7 +26,7 @@ export interface RegexFlags {
2626
* @param elements Single regex element or array of elements
2727
* @returns
2828
*/
29-
export function buildRegex(elements: RegexElement | RegexElement[]): RegExp;
29+
export function buildRegex(elements: RegexNode | RegexNode[]): RegExp;
3030

3131
/**
3232
* Generate RegExp object from elements with passed flags.
@@ -37,7 +37,7 @@ export function buildRegex(elements: RegexElement | RegexElement[]): RegExp;
3737
*/
3838
export function buildRegex(
3939
flags: RegexFlags,
40-
elements: RegexElement | RegexElement[]
40+
elements: RegexNode | RegexNode[]
4141
): RegExp;
4242

4343
export function buildRegex(first: any, second?: any): RegExp {
@@ -46,9 +46,9 @@ export function buildRegex(first: any, second?: any): RegExp {
4646

4747
export function _buildRegex(
4848
flags: RegexFlags,
49-
elements: RegexElement | RegexElement[]
49+
elements: RegexNode | RegexNode[]
5050
): RegExp {
51-
const pattern = encodeSequence(asElementArray(elements)).pattern;
51+
const pattern = encodeSequence(asNodeArray(elements)).pattern;
5252
const flagsString = encodeFlags(flags ?? {});
5353
return new RegExp(pattern, flagsString);
5454
}
@@ -58,8 +58,8 @@ export function _buildRegex(
5858
* @param elements Single regex element or array of elements
5959
* @returns regex pattern string
6060
*/
61-
export function buildPattern(elements: RegexElement | RegexElement[]): string {
62-
return encodeSequence(asElementArray(elements)).pattern;
61+
export function buildPattern(elements: RegexNode | RegexNode[]): string {
62+
return encodeSequence(asNodeArray(elements)).pattern;
6363
}
6464

6565
function encodeFlags(flags: RegexFlags): string {

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
characterClass,
66
characterRange,
77
digit,
8-
encodeCharacterClass,
98
inverted,
109
whitespace,
1110
word,
1211
} from '../character-class';
12+
import { buildRegex } from '../../builders';
1313

1414
test('`any` character class', () => {
1515
expect(any).toHavePattern('.');
@@ -64,7 +64,7 @@ test('`characterRange` base cases', () => {
6464

6565
test('`characterRange` throws on incorrect arguments', () => {
6666
expect(() => characterRange('z', 'a')).toThrowErrorMatchingInlineSnapshot(
67-
`"\`start\` should be less or equal to \`end\`"`
67+
`"\`start\` should be before or equal to \`end\`"`
6868
);
6969
expect(() => characterRange('aa', 'z')).toThrowErrorMatchingInlineSnapshot(
7070
`"\`characterRange\` should receive only single character \`start\` string"`
@@ -119,12 +119,15 @@ test('`inverted` character class execution', () => {
119119

120120
test('`encodeCharacterClass` throws on empty text', () => {
121121
expect(() =>
122-
encodeCharacterClass({
123-
type: 'characterClass',
124-
characters: [],
125-
ranges: [],
126-
isInverted: false,
127-
})
122+
buildRegex(
123+
// @ts-expect-error
124+
inverted({
125+
type: 'characterClass',
126+
characters: [],
127+
ranges: [],
128+
isInverted: false,
129+
})
130+
)
128131
).toThrowErrorMatchingInlineSnapshot(
129132
`"Character class should contain at least one character or character range"`
130133
);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ test('`choiceOf` using nested regex', () => {
3838

3939
test('`choiceOf` throws on empty options', () => {
4040
expect(() => choiceOf()).toThrowErrorMatchingInlineSnapshot(
41-
`"\`choiceOf\` should receive at least one option"`
41+
`"\`choiceOf\` should receive at least one alternative"`
4242
);
4343
});

src/components/anchors.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1-
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
2-
import type { Anchor } from './types';
1+
import type { EncodeOutput } from '../encoder/types';
2+
import type { RegexElement } from '../types';
3+
4+
export interface Anchor extends RegexElement {
5+
type: 'anchor';
6+
symbol: string;
7+
}
38

49
export const startOfString: Anchor = {
510
type: 'anchor',
611
symbol: '^',
12+
encode: encodeAnchor,
713
};
814

915
export const endOfString: Anchor = {
1016
type: 'anchor',
1117
symbol: '$',
18+
encode: encodeAnchor,
1219
};
1320

14-
export function encodeAnchor(anchor: Anchor): EncoderNode {
21+
function encodeAnchor(this: Anchor): EncodeOutput {
1522
return {
16-
precedence: EncoderPrecedence.Sequence,
17-
pattern: anchor.symbol,
23+
precedence: 'sequence',
24+
pattern: this.symbol,
1825
};
1926
}

src/components/capture.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
2-
import { asElementArray } from '../utils/elements';
3-
import type { Capture, RegexElement } from './types';
1+
import { encodeSequence } from '../encoder/encoder';
2+
import type { EncodeOutput } from '../encoder/types';
3+
import { asNodeArray } from '../utils/nodes';
4+
import type { RegexElement, RegexNode } from '../types';
45

5-
export function capture(children: RegexElement | RegexElement[]): Capture {
6+
export interface Capture extends RegexElement {
7+
type: 'capture';
8+
children: RegexNode[];
9+
}
10+
11+
export function capture(nodes: RegexNode | RegexNode[]): Capture {
612
return {
713
type: 'capture',
8-
children: asElementArray(children),
14+
children: asNodeArray(nodes),
15+
encode: encodeCapture,
916
};
1017
}
1118

12-
export function encodeCapture(node: EncoderNode): EncoderNode {
19+
function encodeCapture(this: Capture): EncodeOutput {
1320
return {
14-
precedence: EncoderPrecedence.Atom,
15-
pattern: `(${node.pattern})`,
21+
precedence: 'atom',
22+
pattern: `(${encodeSequence(this.children).pattern})`,
1623
};
1724
}

src/components/character-class.ts

+45-26
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,52 @@
1-
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
1+
import type { EncodeOutput } from '../encoder/types';
22
import { escapeText } from '../utils/text';
3-
import type { CharacterClass } from './types';
3+
4+
export interface CharacterClass {
5+
type: 'characterClass';
6+
characters: string[];
7+
ranges: CharacterRange[];
8+
isInverted: boolean;
9+
encode: () => EncodeOutput;
10+
}
11+
12+
/**
13+
* Character range from start to end (inclusive).
14+
*/
15+
export interface CharacterRange {
16+
start: string;
17+
end: string;
18+
}
419

520
export const any: CharacterClass = {
621
type: 'characterClass',
722
characters: ['.'],
823
ranges: [],
924
isInverted: false,
25+
encode: encodeCharacterClass,
1026
};
1127

1228
export const digit: CharacterClass = {
1329
type: 'characterClass',
1430
characters: ['\\d'],
1531
ranges: [],
1632
isInverted: false,
33+
encode: encodeCharacterClass,
1734
};
1835

1936
export const word: CharacterClass = {
2037
type: 'characterClass',
2138
characters: ['\\w'],
2239
ranges: [],
2340
isInverted: false,
41+
encode: encodeCharacterClass,
2442
};
2543

2644
export const whitespace: CharacterClass = {
2745
type: 'characterClass',
2846
characters: ['\\s'],
2947
ranges: [],
3048
isInverted: false,
49+
encode: encodeCharacterClass,
3150
};
3251

3352
export function characterClass(...elements: CharacterClass[]): CharacterClass {
@@ -44,6 +63,7 @@ export function characterClass(...elements: CharacterClass[]): CharacterClass {
4463
characters: elements.map((c) => c.characters).flat(),
4564
ranges: elements.map((c) => c.ranges).flat(),
4665
isInverted: false,
66+
encode: encodeCharacterClass,
4767
};
4868
}
4969

@@ -61,7 +81,7 @@ export function characterRange(start: string, end: string): CharacterClass {
6181
}
6282

6383
if (start > end) {
64-
throw new Error('`start` should be less or equal to `end`');
84+
throw new Error('`start` should be before or equal to `end`');
6585
}
6686

6787
const range = {
@@ -74,6 +94,7 @@ export function characterRange(start: string, end: string): CharacterClass {
7494
characters: [],
7595
ranges: [range],
7696
isInverted: false,
97+
encode: encodeCharacterClass,
7798
};
7899
}
79100

@@ -88,53 +109,51 @@ export function anyOf(characters: string): CharacterClass {
88109
characters: charactersArray,
89110
ranges: [],
90111
isInverted: false,
112+
encode: encodeCharacterClass,
91113
};
92114
}
93115

94-
export function inverted({
95-
characters,
96-
ranges,
97-
isInverted,
98-
}: CharacterClass): CharacterClass {
116+
export function inverted(element: CharacterClass): CharacterClass {
99117
return {
100118
type: 'characterClass',
101-
characters: characters,
102-
ranges: ranges,
103-
isInverted: !isInverted,
119+
characters: element.characters,
120+
ranges: element.ranges,
121+
isInverted: !element.isInverted,
122+
encode: encodeCharacterClass,
104123
};
105124
}
106125

107-
export function encodeCharacterClass({
108-
characters,
109-
ranges,
110-
isInverted,
111-
}: CharacterClass): EncoderNode {
112-
if (characters.length === 0 && ranges.length === 0) {
126+
function encodeCharacterClass(this: CharacterClass): EncodeOutput {
127+
if (this.characters.length === 0 && this.ranges.length === 0) {
113128
throw new Error(
114129
'Character class should contain at least one character or character range'
115130
);
116131
}
117132

118133
// Direct rendering for single-character class
119-
if (characters.length === 1 && ranges?.length === 0 && !isInverted) {
134+
if (
135+
this.characters.length === 1 &&
136+
this.ranges?.length === 0 &&
137+
!this.isInverted
138+
) {
120139
return {
121-
precedence: EncoderPrecedence.Atom,
122-
pattern: characters[0]!,
140+
precedence: 'atom',
141+
pattern: this.characters[0]!,
123142
};
124143
}
125144

126145
// If passed characters includes hyphen (`-`) it need to be moved to
127146
// first (or last) place in order to treat it as hyphen character and not a range.
128147
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes#types
129-
const hyphenString = characters.includes('-') ? '-' : '';
130-
const charactersString = characters.filter((c) => c !== '-').join('');
131-
const rangesString = ranges
148+
const hyphen = this.characters.includes('-') ? '-' : '';
149+
const otherCharacters = this.characters.filter((c) => c !== '-').join('');
150+
const ranges = this.ranges
132151
.map(({ start, end }) => `${start}-${end}`)
133152
.join('');
134-
const invertedString = isInverted ? '^' : '';
153+
const isInverted = this.isInverted ? '^' : '';
135154

136155
return {
137-
precedence: EncoderPrecedence.Atom,
138-
pattern: `[${invertedString}${hyphenString}${rangesString}${charactersString}]`,
156+
precedence: 'atom',
157+
pattern: `[${isInverted}${hyphen}${ranges}${otherCharacters}]`,
139158
};
140159
}

src/components/choice-of.ts

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
1-
import {
2-
type EncoderNode,
3-
EncoderPrecedence,
4-
type EncodeSequence,
5-
} from '../encoder/types';
6-
import { asElementArray } from '../utils/elements';
7-
import type { ChoiceOf, RegexElement } from './types';
1+
import { encodeSequence } from '../encoder/encoder';
2+
import type { EncodeOutput } from '../encoder/types';
3+
import { asNodeArray } from '../utils/nodes';
4+
import type { RegexElement, RegexNode } from '../types';
5+
6+
export interface ChoiceOf extends RegexElement {
7+
type: 'choiceOf';
8+
alternatives: RegexNode[][];
9+
}
810

911
export function choiceOf(
10-
...children: Array<RegexElement | RegexElement[]>
12+
...alternatives: Array<RegexNode | RegexNode[]>
1113
): ChoiceOf {
12-
if (children.length === 0) {
13-
throw new Error('`choiceOf` should receive at least one option');
14+
if (alternatives.length === 0) {
15+
throw new Error('`choiceOf` should receive at least one alternative');
1416
}
1517

1618
return {
1719
type: 'choiceOf',
18-
children: children.map((c) => asElementArray(c)),
20+
alternatives: alternatives.map((c) => asNodeArray(c)),
21+
encode: encodeChoiceOf,
1922
};
2023
}
2124

22-
export function encodeChoiceOf(
23-
element: ChoiceOf,
24-
encodeSequence: EncodeSequence
25-
): EncoderNode {
26-
const encodedNodes = element.children.map((c) => encodeSequence(c));
27-
if (encodedNodes.length === 1) {
28-
return encodedNodes[0]!;
25+
function encodeChoiceOf(this: ChoiceOf): EncodeOutput {
26+
const encodedAlternatives = this.alternatives.map((c) => encodeSequence(c));
27+
if (encodedAlternatives.length === 1) {
28+
return encodedAlternatives[0]!;
2929
}
3030

3131
return {
32-
precedence: EncoderPrecedence.Alternation,
33-
pattern: encodedNodes.map((n) => n.pattern).join('|'),
32+
precedence: 'alternation',
33+
pattern: encodedAlternatives.map((n) => n.pattern).join('|'),
3434
};
3535
}

0 commit comments

Comments
 (0)