Skip to content
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

refactor: encoder structure #28

Closed
wants to merge 5 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { buildPattern, buildRegex } from '../../builders';
import { buildPattern, buildRegex } from '../builders';
import {
one,
oneOrMore,
optionally,
zeroOrMore,
} from '../../components/quantifiers';
import { repeat } from '../../components/repeat';
} from '../components/quantifiers';
import { repeat } from '../components/repeat';

test('basic quantifies', () => {
expect(buildPattern('a')).toEqual('a');
Expand Down Expand Up @@ -55,15 +55,8 @@ test('"buildPattern" escapes special characters', () => {
);
});

test('buildRegex throws error on unknown element', () => {
expect(() =>
// @ts-expect-error intentionally passing incorrect object
buildRegex({ type: 'unknown' })
).toThrowErrorMatchingInlineSnapshot(`"Unknown elements type unknown"`);
});

test('buildPattern throws on empty text', () => {
expect(() => buildPattern('')).toThrowErrorMatchingInlineSnapshot(
`"\`encodeText\`: received text should not be empty"`
`"\\"encodeText\\": received text should not be empty"`
);
});
8 changes: 4 additions & 4 deletions src/builders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RegexElement } from './components/types';
import { encodeSequence } from './encoder/encoder';
import { isRegexElement } from './utils';
import type { RegexElement } from './types';
import { encodeSequence } from './encoder';
import { isValidElement } from './utils';

export interface RegexFlags {
/** Global search. */
Expand Down Expand Up @@ -30,7 +30,7 @@ export function buildRegex(
first: RegexFlags | RegexElement | string,
...rest: Array<RegexElement | string>
): RegExp {
if (typeof first === 'string' || isRegexElement(first)) {
if (typeof first === 'string' || isValidElement(first)) {
return buildRegex({}, first, ...rest);
}

Expand Down
20 changes: 1 addition & 19 deletions src/components/__tests__/character-class.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { buildPattern } from '../../builders';
import { one, oneOrMore } from '../quantifiers';
import {
any,
anyOf,
digit,
encodeCharacterClass,
whitespace,
word,
} from '../character-class';
import { any, anyOf, digit, whitespace, word } from '../character-class';

test('"whitespace" character class', () => {
expect(buildPattern(whitespace)).toEqual(`\\s`);
Expand Down Expand Up @@ -65,14 +58,3 @@ test('`anyOf` throws on empty text', () => {
`"\`anyOf\` should received at least one character"`
);
});

test('buildPattern throws on empty text', () => {
expect(() =>
encodeCharacterClass({
type: 'characterClass',
characters: [],
})
).toThrowErrorMatchingInlineSnapshot(
`"Character class should contain at least one character"`
);
});
2 changes: 1 addition & 1 deletion src/components/__tests__/choice-of.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ test('"choiceOf" using nested regex', () => {

test('`anyOf` throws on empty options', () => {
expect(() => choiceOf()).toThrowErrorMatchingInlineSnapshot(
`"\`choiceOf\` should receive at least one option"`
`"\\"choiceOf\\" should receive at least one option"`
);
});
2 changes: 1 addition & 1 deletion src/components/__tests__/repeat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ test('"repeat"" optimizes grouping for atoms', () => {

test('`repeat` throws on no children', () => {
expect(() => repeat({ count: 1 })).toThrowErrorMatchingInlineSnapshot(
`"\`repeat\` should receive at least one element"`
`"\\"repeat\\" should receive at least one element"`
);
});
34 changes: 22 additions & 12 deletions src/components/capture.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import type { Capture, RegexElement } from './types';
import { encodeSequence } from '../encoder';
import {
EncoderPrecedence,
type EncoderResult,
type RegexElement,
} from '../types';

export function capture(...children: Array<RegexElement | string>): Capture {
return {
type: 'capture',
children,
};
export class Capture implements RegexElement {
public children: Array<RegexElement | string>;

constructor(children: Array<RegexElement | string>) {
this.children = children;
}

encode(): EncoderResult {
const children = encodeSequence(this.children);
return {
precedence: EncoderPrecedence.Atom,
pattern: `(${children.pattern})`,
};
}
}

export function encodeCapture(node: EncoderNode): EncoderNode {
return {
precedence: EncoderPrecedence.Atom,
pattern: `(${node.pattern})`,
};
export function capture(...children: Array<RegexElement | string>): Capture {
return new Capture(children);
}
75 changes: 33 additions & 42 deletions src/components/character-class.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,48 @@
import { type EncoderNode, EncoderPrecedence } from '../encoder/types';
import { escapeText } from '../utils';
import type { CharacterClass } from './types';
import {
EncoderPrecedence,
type EncoderResult,
type RegexElement,
} from '../types';

export const any: CharacterClass = {
type: 'characterClass',
characters: ['.'],
};
export class CharacterClass implements RegexElement {
public characters: string[];

export const whitespace: CharacterClass = {
type: 'characterClass',
characters: ['\\s'],
};
constructor(characters: string[]) {
if (characters.length === 0) {
throw new Error('Character class should contain at least one character');
}

export const digit: CharacterClass = {
type: 'characterClass',
characters: ['\\d'],
};

export const word: CharacterClass = {
type: 'characterClass',
characters: ['\\w'],
};

export function anyOf(characters: string): CharacterClass {
const charactersArray = characters.split('').map(escapeText);
if (charactersArray.length === 0) {
throw new Error('`anyOf` should received at least one character');
this.characters = characters;
}

return {
type: 'characterClass',
characters: charactersArray,
};
}

export function encodeCharacterClass({
characters,
}: CharacterClass): EncoderNode {
if (characters.length === 0) {
throw new Error('Character class should contain at least one character');
}
encode(): EncoderResult {
if (this.characters.length === 1) {
return {
precedence: EncoderPrecedence.Atom,
pattern: this.characters[0]!,
};
}

if (characters.length === 1) {
return {
precedence: EncoderPrecedence.Atom,
pattern: characters[0]!,
pattern: `[${reorderHyphen(this.characters).join('')}]`,
};
}
}

export const any = new CharacterClass(['.']);
export const whitespace = new CharacterClass(['\\s']);
export const digit = new CharacterClass(['\\d']);
export const word = new CharacterClass(['\\w']);

export function anyOf(characters: string): CharacterClass {
const charactersArray = characters.split('').map(escapeText);
if (charactersArray.length === 0) {
throw new Error('`anyOf` should received at least one character');
}

return {
precedence: EncoderPrecedence.Atom,
pattern: `[${reorderHyphen(characters).join('')}]`,
};
return new CharacterClass(charactersArray);
}

// If passed characters includes hyphen (`-`) it need to be moved to
Expand Down
48 changes: 25 additions & 23 deletions src/components/choice-of.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import { encodeElement } from '../encoder';
import {
type EncodeElement,
type EncoderNode,
EncoderPrecedence,
} from '../encoder/types';
import type { ChoiceOf, RegexElement } from './types';
type EncoderResult,
type RegexElement,
} from '../types';

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

constructor(children: Array<RegexElement | string>) {
if (children.length === 0) {
throw new Error('"choiceOf" should receive at least one option');
}

this.children = children;
}

return {
type: 'choiceOf',
children,
};
}
encode(): EncoderResult {
const children = this.children.map(encodeElement);
if (children.length === 1) {
return children[0]!;
}

export function encodeChoiceOf(
element: ChoiceOf,
encodeElement: EncodeElement
): EncoderNode {
const encodedNodes = element.children.map(encodeElement);
if (encodedNodes.length === 1) {
return encodedNodes[0]!;
return {
precedence: EncoderPrecedence.Alternation,
pattern: children.map((n) => n.pattern).join('|'),
};
}
}

return {
precedence: EncoderPrecedence.Alternation,
pattern: encodedNodes.map((n) => n.pattern).join('|'),
};
export function choiceOf(...children: Array<RegexElement | string>): ChoiceOf {
return new ChoiceOf(children);
}
Loading