Skip to content

refactor: migrate component signatures to use array for multiple items #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ npm install ts-regex-builder
import { buildRegex, capture, oneOrMore } from 'ts-regex-builder';

// /Hello (\w+)/
const regex = buildRegex('Hello ', capture(oneOrMore(word)));
const regex = buildRegex(['Hello ', capture(oneOrMore(word))]);
```

## Contributing
Expand Down
77 changes: 40 additions & 37 deletions src/builders.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,75 @@
import type { RegexElement } from './components/types';
import { encodeSequence } from './encoder/encoder';
import { isRegexElement } from './utils';
import { asElementArray } from './utils/elements';
import { optionalFirstArg } from './utils/optional-arg';

export interface RegexFlags {
/** Global search. */
global?: boolean;

/** Case-insensitive search. */
ignoreCase?: boolean;

/** Allows ^ and $ to match newline characters. */
multiline?: boolean;

/** Generate indices for substring matches. */
hasIndices?: boolean;

/** Perform a "sticky" search that matches starting at the current position in the target string. */
sticky?: boolean;
}

/**
* Generate RegExp object for elements.
* Generate RegExp object from elements.
*
* @param elements
* @param elements Single regex element or array of elements
* @returns
*/
export function buildRegex(...elements: Array<RegexElement | string>): RegExp;
export function buildRegex(elements: RegexElement | RegexElement[]): RegExp;

/**
* Generate RegExp object from elements with passed flags.
*
* @param elements Single regex element or array of elements
* @param flags RegExp flags object
* @returns RegExp object
*/
export function buildRegex(
flags: RegexFlags,
...elements: Array<RegexElement | string>
elements: RegexElement | RegexElement[]
): RegExp;
export function buildRegex(
first: RegexFlags | RegexElement | string,
...rest: Array<RegexElement | string>
): RegExp {
if (typeof first === 'string' || isRegexElement(first)) {
return buildRegex({}, first, ...rest);
}

const pattern = encodeSequence(rest).pattern;
const flags = encodeFlags(first);
return new RegExp(pattern, flags);
export function buildRegex(first: any, second?: any): RegExp {
return _buildRegex(...optionalFirstArg(first, second));
}

export function _buildRegex(
flags: RegexFlags,
elements: RegexElement | RegexElement[]
): RegExp {
const pattern = encodeSequence(asElementArray(elements)).pattern;
const flagsString = encodeFlags(flags ?? {});
return new RegExp(pattern, flagsString);
}

/**
* Generate regex pattern for elements.
* @param elements
* @returns
* Generate regex pattern from elements.
* @param elements Single regex element or array of elements
* @returns regex pattern string
*/
export function buildPattern(
...elements: Array<RegexElement | string>
): string {
return encodeSequence(elements).pattern;
export function buildPattern(elements: RegexElement | RegexElement[]): string {
return encodeSequence(asElementArray(elements)).pattern;
}

function encodeFlags(flags: RegexFlags): string {
let result = '';
if (flags.global) {
result += 'g';
}
if (flags.ignoreCase) {
result += 'i';
}
if (flags.multiline) {
result += 'm';
}
if (flags.hasIndices) {
result += 'd';
}
if (flags.sticky) {
result += 'y';
}

if (flags.global) result += 'g';
if (flags.ignoreCase) result += 'i';
if (flags.multiline) result += 'm';
if (flags.hasIndices) result += 'd';
if (flags.sticky) result += 'y';

return result;
}
8 changes: 8 additions & 0 deletions src/components/__tests__/choice-of.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ test('`choiceOf` used in sequence', () => {
expect(choiceOf('aaa', 'bbb')).toHavePattern('aaa|bbb');
});

test('`choiceOf` with sequence options', () => {
expect([choiceOf(['a', 'b'])]).toHavePattern('ab');
expect([choiceOf(['a', 'b'], ['c', 'd'])]).toHavePattern('ab|cd');
expect([
choiceOf(['a', zeroOrMore('b')], [oneOrMore('c'), 'd']),
]).toHavePattern('ab*|c+d');
});

test('`choiceOf` using nested regex', () => {
expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern('a+|b*');
expect(
Expand Down
6 changes: 3 additions & 3 deletions src/components/__tests__/repeat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ test('`repeat` quantifier', () => {
expect(['a', repeat({ min: 1 }, 'b')]).toHavePattern('ab{1,}');
expect(['a', repeat({ count: 1 }, 'b')]).toHavePattern('ab{1}');

expect(['a', repeat({ count: 1 }, 'a', zeroOrMore('b'))]).toHavePattern(
expect(['a', repeat({ count: 1 }, ['a', zeroOrMore('b')])]).toHavePattern(
'a(?:ab*){1}'
);
expect(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))).toHavePattern(
expect(repeat({ count: 5 }, ['text', ' ', oneOrMore('d')])).toHavePattern(
'(?:text d+){5}'
);
});
Expand All @@ -22,7 +22,7 @@ test('`repeat` optimizes grouping for atoms', () => {
});

test('`repeat` throws on no children', () => {
expect(() => repeat({ count: 1 })).toThrowErrorMatchingInlineSnapshot(
expect(() => repeat({ count: 1 }, [])).toThrowErrorMatchingInlineSnapshot(
`"\`repeat\` should receive at least one element"`
);
});
5 changes: 3 additions & 2 deletions src/components/capture.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import { asElementArray } from '../utils/elements';
import type { Capture, RegexElement } from './types';

export function capture(...children: Array<RegexElement | string>): Capture {
export function capture(children: RegexElement | RegexElement[]): Capture {
return {
type: 'capture',
children,
children: asElementArray(children),
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/character-class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import { escapeText } from '../utils';
import { escapeText } from '../utils/text';
import type { CharacterClass } from './types';

export const any: CharacterClass = {
Expand Down Expand Up @@ -126,7 +126,7 @@ export function encodeCharacterClass({
// If passed characters includes hyphen (`-`) it need to be moved to
// first (or last) place in order to treat it as hyphen character and not a range.
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes#types
const hypenString = characters.includes('-') ? '-' : '';
const hyphenString = characters.includes('-') ? '-' : '';
const charactersString = characters.filter((c) => c !== '-').join('');
const rangesString = ranges
.map(({ start, end }) => `${start}-${end}`)
Expand All @@ -135,6 +135,6 @@ export function encodeCharacterClass({

return {
precedence: EncoderPrecedence.Atom,
pattern: `[${invertedString}${hypenString}${rangesString}${charactersString}]`,
pattern: `[${invertedString}${hyphenString}${rangesString}${charactersString}]`,
};
}
13 changes: 8 additions & 5 deletions src/components/choice-of.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import {
type EncodeElement,
type EncoderNode,
EncoderPrecedence,
type EncodeSequence,
} from '../encoder/types';
import { asElementArray } from '../utils/elements';
import type { ChoiceOf, RegexElement } from './types';

export function choiceOf(...children: Array<RegexElement | string>): ChoiceOf {
export function choiceOf(
...children: Array<RegexElement | RegexElement[]>
): ChoiceOf {
if (children.length === 0) {
throw new Error('`choiceOf` should receive at least one option');
}

return {
type: 'choiceOf',
children,
children: children.map((c) => asElementArray(c)),
};
}

export function encodeChoiceOf(
element: ChoiceOf,
encodeElement: EncodeElement
encodeSequence: EncodeSequence
): EncoderNode {
const encodedNodes = element.children.map(encodeElement);
const encodedNodes = element.children.map((c) => encodeSequence(c));
if (encodedNodes.length === 1) {
return encodedNodes[0]!;
}
Expand Down
21 changes: 10 additions & 11 deletions src/components/quantifiers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import { toAtom } from '../utils';
import { toAtom } from '../encoder/utils';
import { asElementArray } from '../utils/elements';
import type {
One,
OneOrMore,
Expand All @@ -8,37 +9,35 @@ import type {
ZeroOrMore,
} from './types';

export function one(...children: Array<RegexElement | string>): One {
export function one(children: RegexElement | RegexElement[]): One {
return {
type: 'one',
children,
children: asElementArray(children),
};
}

export function oneOrMore(
...children: Array<RegexElement | string>
): OneOrMore {
export function oneOrMore(children: RegexElement | RegexElement[]): OneOrMore {
return {
type: 'oneOrMore',
children,
children: asElementArray(children),
};
}

export function optionally(
...children: Array<RegexElement | string>
children: RegexElement | RegexElement[]
): Optionally {
return {
type: 'optionally',
children,
children: asElementArray(children),
};
}

export function zeroOrMore(
...children: Array<RegexElement | string>
children: RegexElement | RegexElement[]
): ZeroOrMore {
return {
type: 'zeroOrMore',
children,
children: asElementArray(children),
};
}

Expand Down
7 changes: 5 additions & 2 deletions src/components/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import { toAtom } from '../utils';
import { toAtom } from '../encoder/utils';
import { asElementArray } from '../utils/elements';
import type { RegexElement, Repeat, RepeatConfig } from './types';

export function repeat(
config: RepeatConfig,
...children: Array<RegexElement | string>
children: RegexElement | RegexElement[]
): Repeat {
children = asElementArray(children);

if (children.length === 0) {
throw new Error('`repeat` should receive at least one element');
}
Expand Down
21 changes: 13 additions & 8 deletions src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export type RegexElement = Capture | CharacterClass | ChoiceOf | Quantifier;
export type RegexElement =
| string
| Capture
| CharacterClass
| ChoiceOf
| Quantifier;

export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat;

Expand All @@ -20,33 +25,33 @@ export type CharacterRange = {
// Components
export type ChoiceOf = {
type: 'choiceOf';
children: Array<RegexElement | string>;
children: RegexElement[][];
};

// Quantifiers
export type One = {
type: 'one';
children: Array<RegexElement | string>;
children: RegexElement[];
};

export type OneOrMore = {
type: 'oneOrMore';
children: Array<RegexElement | string>;
children: RegexElement[];
};

export type Optionally = {
type: 'optionally';
children: Array<RegexElement | string>;
children: RegexElement[];
};

export type ZeroOrMore = {
type: 'zeroOrMore';
children: Array<RegexElement | string>;
children: RegexElement[];
};

export type Repeat = {
type: 'repeat';
children: Array<RegexElement | string>;
children: RegexElement[];
config: RepeatConfig;
};

Expand All @@ -55,5 +60,5 @@ export type RepeatConfig = { count: number } | { min: number; max?: number };
// Captures
export type Capture = {
type: 'capture';
children: Array<RegexElement | string>;
children: RegexElement[];
};
11 changes: 5 additions & 6 deletions src/encoder/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ import {
encodeZeroOrMore,
} from '../components/quantifiers';
import { encodeRepeat } from '../components/repeat';
import { concatNodes, escapeText } from '../utils';
import { escapeText } from '../utils/text';
import { type EncoderNode, EncoderPrecedence } from './types';
import { concatNodes } from './utils';

export function encodeSequence(
elements: Array<RegexElement | string>
): EncoderNode {
export function encodeSequence(elements: RegexElement[]): EncoderNode {
return concatNodes(elements.map((c) => encodeElement(c)));
}

export function encodeElement(element: RegexElement | string): EncoderNode {
export function encodeElement(element: RegexElement): EncoderNode {
if (typeof element === 'string') {
return encodeText(element);
}
Expand All @@ -28,7 +27,7 @@ export function encodeElement(element: RegexElement | string): EncoderNode {
}

if (element.type === 'choiceOf') {
return encodeChoiceOf(element, encodeElement);
return encodeChoiceOf(element, encodeSequence);
}

if (element.type === 'repeat') {
Expand Down
3 changes: 2 additions & 1 deletion src/encoder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export const EncoderPrecedence = {
type ValueOf<T> = T[keyof T];
type EncoderPrecedence = ValueOf<typeof EncoderPrecedence>;

export type EncodeElement = (element: RegexElement | string) => EncoderNode;
export type EncodeSequence = (elements: RegexElement[]) => EncoderNode;
export type EncodeElement = (element: RegexElement) => EncoderNode;
Loading