From a2e611483498bc51bffa4e2fe3e78f0f2c0eb7e1 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 19 Dec 2023 09:42:59 +0100 Subject: [PATCH 1/5] refactor: rename EncoderResult --- src/components/capture.ts | 4 ++-- src/components/character-class.ts | 4 ++-- src/components/choice-of.ts | 4 ++-- src/components/quantifiers.ts | 10 +++++----- src/components/repeat.ts | 6 +++--- src/encoder/encoder.ts | 8 ++++---- src/encoder/types.ts | 4 ++-- src/utils.ts | 6 +++--- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/capture.ts b/src/components/capture.ts index bb33e9e..c0bddaf 100644 --- a/src/components/capture.ts +++ b/src/components/capture.ts @@ -1,4 +1,4 @@ -import { type EncoderNode, EncoderPrecedence } from '../encoder/types'; +import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; import type { Capture, RegexElement } from './types'; export function capture(...children: Array): Capture { @@ -8,7 +8,7 @@ export function capture(...children: Array): Capture { }; } -export function encodeCapture(node: EncoderNode): EncoderNode { +export function encodeCapture(node: EncoderResult): EncoderResult { return { precedence: EncoderPrecedence.Atom, pattern: `(${node.pattern})`, diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 7c6dbd3..d3ebc89 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -1,4 +1,4 @@ -import { type EncoderNode, EncoderPrecedence } from '../encoder/types'; +import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; import { escapeText } from '../utils'; import type { CharacterClass } from './types'; @@ -36,7 +36,7 @@ export function anyOf(characters: string): CharacterClass { export function encodeCharacterClass({ characters, -}: CharacterClass): EncoderNode { +}: CharacterClass): EncoderResult { if (characters.length === 0) { throw new Error('Character class should contain at least one character'); } diff --git a/src/components/choice-of.ts b/src/components/choice-of.ts index 9f21f31..691f7e9 100644 --- a/src/components/choice-of.ts +++ b/src/components/choice-of.ts @@ -1,6 +1,6 @@ import { type EncodeElement, - type EncoderNode, + type EncoderResult, EncoderPrecedence, } from '../encoder/types'; import type { ChoiceOf, RegexElement } from './types'; @@ -19,7 +19,7 @@ export function choiceOf(...children: Array): ChoiceOf { export function encodeChoiceOf( element: ChoiceOf, encodeElement: EncodeElement -): EncoderNode { +): EncoderResult { const encodedNodes = element.children.map(encodeElement); if (encodedNodes.length === 1) { return encodedNodes[0]!; diff --git a/src/components/quantifiers.ts b/src/components/quantifiers.ts index 1ba6816..0ad8b3b 100644 --- a/src/components/quantifiers.ts +++ b/src/components/quantifiers.ts @@ -1,4 +1,4 @@ -import { type EncoderNode, EncoderPrecedence } from '../encoder/types'; +import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; import { toAtom } from '../utils'; import type { One, @@ -42,25 +42,25 @@ export function zeroOrMore( }; } -export function encodeOne(node: EncoderNode) { +export function encodeOne(node: EncoderResult) { return node; } -export function encodeOneOrMore(node: EncoderNode): EncoderNode { +export function encodeOneOrMore(node: EncoderResult): EncoderResult { return { precedence: EncoderPrecedence.Sequence, pattern: `${toAtom(node)}+`, }; } -export function encodeOptionally(node: EncoderNode): EncoderNode { +export function encodeOptionally(node: EncoderResult): EncoderResult { return { precedence: EncoderPrecedence.Sequence, pattern: `${toAtom(node)}?`, }; } -export function encodeZeroOrMore(node: EncoderNode): EncoderNode { +export function encodeZeroOrMore(node: EncoderResult): EncoderResult { return { precedence: EncoderPrecedence.Sequence, pattern: `${toAtom(node)}*`, diff --git a/src/components/repeat.ts b/src/components/repeat.ts index 17b53d0..aa1aef1 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -1,4 +1,4 @@ -import { type EncoderNode, EncoderPrecedence } from '../encoder/types'; +import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; import { toAtom } from '../utils'; import type { RegexElement, Repeat, RepeatConfig } from './types'; @@ -19,8 +19,8 @@ export function repeat( export function encodeRepeat( config: RepeatConfig, - node: EncoderNode -): EncoderNode { + node: EncoderResult +): EncoderResult { if ('count' in config) { return { precedence: EncoderPrecedence.Sequence, diff --git a/src/encoder/encoder.ts b/src/encoder/encoder.ts index 5a586aa..ee6de6e 100644 --- a/src/encoder/encoder.ts +++ b/src/encoder/encoder.ts @@ -10,15 +10,15 @@ import { } from '../components/quantifiers'; import { encodeRepeat } from '../components/repeat'; import { concatNodes, escapeText } from '../utils'; -import { type EncoderNode, EncoderPrecedence } from './types'; +import { type EncoderResult, EncoderPrecedence } from './types'; export function encodeSequence( elements: Array -): EncoderNode { +): EncoderResult { return concatNodes(elements.map((c) => encodeElement(c))); } -export function encodeElement(element: RegexElement | string): EncoderNode { +export function encodeElement(element: RegexElement | string): EncoderResult { if (typeof element === 'string') { return encodeText(element); } @@ -59,7 +59,7 @@ export function encodeElement(element: RegexElement | string): EncoderNode { throw new Error(`Unknown elements type ${element.type}`); } -function encodeText(text: string): EncoderNode { +function encodeText(text: string): EncoderResult { if (text.length === 0) { throw new Error('`encodeText`: received text should not be empty'); } diff --git a/src/encoder/types.ts b/src/encoder/types.ts index 8a9125a..ecf2c3d 100644 --- a/src/encoder/types.ts +++ b/src/encoder/types.ts @@ -3,7 +3,7 @@ import type { RegexElement } from '../components/types'; /** * Encoded regex pattern with information about its type (atom, sequence) */ -export interface EncoderNode { +export interface EncoderResult { precedence: EncoderPrecedence; pattern: string; } @@ -26,4 +26,4 @@ export const EncoderPrecedence = { type ValueOf = T[keyof T]; type EncoderPrecedence = ValueOf; -export type EncodeElement = (element: RegexElement | string) => EncoderNode; +export type EncodeElement = (element: RegexElement | string) => EncoderResult; diff --git a/src/utils.ts b/src/utils.ts index d0d930d..a9c9c2d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import type { RegexElement } from './components/types'; -import { type EncoderNode, EncoderPrecedence } from './encoder/types'; +import { type EncoderResult, EncoderPrecedence } from './encoder/types'; /** * Returns atomic pattern for given node. @@ -7,7 +7,7 @@ import { type EncoderNode, EncoderPrecedence } from './encoder/types'; * @param node * @returns */ -export function toAtom(node: EncoderNode): string { +export function toAtom(node: EncoderResult): string { if (node.precedence === EncoderPrecedence.Atom) { return node.pattern; } @@ -15,7 +15,7 @@ export function toAtom(node: EncoderNode): string { return `(?:${node.pattern})`; } -export function concatNodes(nodes: EncoderNode[]): EncoderNode { +export function concatNodes(nodes: EncoderResult[]): EncoderResult { if (nodes.length === 1) { return nodes[0]!; } From ea1ee3ec0ae47c9ecee447a72e5580491900e043 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 19 Dec 2023 09:58:25 +0100 Subject: [PATCH 2/5] refactor: move encode function onto elements --- .../__tests__/character-class.test.ts | 20 +------- src/components/capture.ts | 8 ++-- src/components/character-class.ts | 19 ++++---- src/components/choice-of.ts | 15 ++---- src/components/quantifiers.ts | 26 ++++++---- src/components/repeat.ts | 18 +++---- src/components/types.ts | 10 ++++ src/encoder/__tests__/encoder.test.tsx | 7 --- src/encoder/encoder.ts | 47 +------------------ src/utils.ts | 2 +- 10 files changed, 62 insertions(+), 110 deletions(-) diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index 68f47a4..83fa3e1 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -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`); @@ -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"` - ); -}); diff --git a/src/components/capture.ts b/src/components/capture.ts index c0bddaf..0868e65 100644 --- a/src/components/capture.ts +++ b/src/components/capture.ts @@ -1,16 +1,18 @@ -import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; +import { encodeSequence } from '../encoder/encoder'; +import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import type { Capture, RegexElement } from './types'; export function capture(...children: Array): Capture { return { type: 'capture', children, + encode: encodeCapture, }; } -export function encodeCapture(node: EncoderResult): EncoderResult { +function encodeCapture(this: Capture): EncoderResult { return { precedence: EncoderPrecedence.Atom, - pattern: `(${node.pattern})`, + pattern: `(${encodeSequence(this.children).pattern})`, }; } diff --git a/src/components/character-class.ts b/src/components/character-class.ts index d3ebc89..9436ce0 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -1,25 +1,29 @@ -import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; +import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import { escapeText } from '../utils'; import type { CharacterClass } from './types'; export const any: CharacterClass = { type: 'characterClass', characters: ['.'], + encode: encodeCharacterClass, }; export const whitespace: CharacterClass = { type: 'characterClass', characters: ['\\s'], + encode: encodeCharacterClass, }; export const digit: CharacterClass = { type: 'characterClass', characters: ['\\d'], + encode: encodeCharacterClass, }; export const word: CharacterClass = { type: 'characterClass', characters: ['\\w'], + encode: encodeCharacterClass, }; export function anyOf(characters: string): CharacterClass { @@ -31,26 +35,25 @@ export function anyOf(characters: string): CharacterClass { return { type: 'characterClass', characters: charactersArray, + encode: encodeCharacterClass, }; } -export function encodeCharacterClass({ - characters, -}: CharacterClass): EncoderResult { - if (characters.length === 0) { +function encodeCharacterClass(this: CharacterClass): EncoderResult { + if (this.characters.length === 0) { throw new Error('Character class should contain at least one character'); } - if (characters.length === 1) { + if (this.characters.length === 1) { return { precedence: EncoderPrecedence.Atom, - pattern: characters[0]!, + pattern: this.characters[0]!, }; } return { precedence: EncoderPrecedence.Atom, - pattern: `[${reorderHyphen(characters).join('')}]`, + pattern: `[${reorderHyphen(this.characters).join('')}]`, }; } diff --git a/src/components/choice-of.ts b/src/components/choice-of.ts index 691f7e9..01a98f1 100644 --- a/src/components/choice-of.ts +++ b/src/components/choice-of.ts @@ -1,8 +1,5 @@ -import { - type EncodeElement, - type EncoderResult, - EncoderPrecedence, -} from '../encoder/types'; +import { encodeElement } from '../encoder/encoder'; +import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import type { ChoiceOf, RegexElement } from './types'; export function choiceOf(...children: Array): ChoiceOf { @@ -13,14 +10,12 @@ export function choiceOf(...children: Array): ChoiceOf { return { type: 'choiceOf', children, + encode: encodeChoiceOf, }; } -export function encodeChoiceOf( - element: ChoiceOf, - encodeElement: EncodeElement -): EncoderResult { - const encodedNodes = element.children.map(encodeElement); +function encodeChoiceOf(this: ChoiceOf): EncoderResult { + const encodedNodes = this.children.map(encodeElement); if (encodedNodes.length === 1) { return encodedNodes[0]!; } diff --git a/src/components/quantifiers.ts b/src/components/quantifiers.ts index 0ad8b3b..530e9bf 100644 --- a/src/components/quantifiers.ts +++ b/src/components/quantifiers.ts @@ -1,4 +1,5 @@ -import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; +import { encodeSequence } from '../encoder/encoder'; +import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import { toAtom } from '../utils'; import type { One, @@ -12,6 +13,7 @@ export function one(...children: Array): One { return { type: 'one', children, + encode: encodeOne, }; } @@ -21,6 +23,7 @@ export function oneOrMore( return { type: 'oneOrMore', children, + encode: encodeOneOrMore, }; } @@ -30,6 +33,7 @@ export function optionally( return { type: 'optionally', children, + encode: encodeOptionally, }; } @@ -39,30 +43,34 @@ export function zeroOrMore( return { type: 'zeroOrMore', children, + encode: encodeZeroOrMore, }; } -export function encodeOne(node: EncoderResult) { - return node; +function encodeOne(this: One): EncoderResult { + return encodeSequence(this.children); } -export function encodeOneOrMore(node: EncoderResult): EncoderResult { +function encodeOneOrMore(this: OneOrMore): EncoderResult { + const children = encodeSequence(this.children); return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(node)}+`, + pattern: `${toAtom(children)}+`, }; } -export function encodeOptionally(node: EncoderResult): EncoderResult { +function encodeOptionally(this: Optionally): EncoderResult { + const children = encodeSequence(this.children); return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(node)}?`, + pattern: `${toAtom(children)}?`, }; } -export function encodeZeroOrMore(node: EncoderResult): EncoderResult { +function encodeZeroOrMore(this: ZeroOrMore): EncoderResult { + const children = encodeSequence(this.children); return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(node)}*`, + pattern: `${toAtom(children)}*`, }; } diff --git a/src/components/repeat.ts b/src/components/repeat.ts index aa1aef1..821587e 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -1,4 +1,5 @@ -import { type EncoderResult, EncoderPrecedence } from '../encoder/types'; +import { encodeSequence } from '../encoder/encoder'; +import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import { toAtom } from '../utils'; import type { RegexElement, Repeat, RepeatConfig } from './types'; @@ -14,22 +15,23 @@ export function repeat( type: 'repeat', children, config, + encode: encodeRepeat, }; } -export function encodeRepeat( - config: RepeatConfig, - node: EncoderResult -): EncoderResult { - if ('count' in config) { +function encodeRepeat(this: Repeat): EncoderResult { + const children = encodeSequence(this.children); + if ('count' in this.config) { return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(node)}{${config.count}}`, + pattern: `${toAtom(children)}{${this.config.count}}`, }; } return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(node)}{${config.min},${config?.max ?? ''}}`, + pattern: `${toAtom(children)}{${this.config.min},${ + this.config?.max ?? '' + }}`, }; } diff --git a/src/components/types.ts b/src/components/types.ts index 648b679..fb69798 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,3 +1,5 @@ +import type { EncoderResult } from '../encoder/types'; + export type RegexElement = Capture | CharacterClass | ChoiceOf | Quantifier; export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; @@ -5,39 +7,46 @@ export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; export type CharacterClass = { type: 'characterClass'; characters: string[]; + encode: () => EncoderResult; }; // Components export type ChoiceOf = { type: 'choiceOf'; children: Array; + encode: () => EncoderResult; }; // Quantifiers export type One = { type: 'one'; children: Array; + encode: () => EncoderResult; }; export type OneOrMore = { type: 'oneOrMore'; children: Array; + encode: () => EncoderResult; }; export type Optionally = { type: 'optionally'; children: Array; + encode: () => EncoderResult; }; export type ZeroOrMore = { type: 'zeroOrMore'; children: Array; + encode: () => EncoderResult; }; export type Repeat = { type: 'repeat'; children: Array; config: RepeatConfig; + encode: () => EncoderResult; }; export type RepeatConfig = { count: number } | { min: number; max?: number }; @@ -46,4 +55,5 @@ export type RepeatConfig = { count: number } | { min: number; max?: number }; export type Capture = { type: 'capture'; children: Array; + encode: () => EncoderResult; }; diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index 5867c20..5c85a1b 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -55,13 +55,6 @@ 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"` diff --git a/src/encoder/encoder.ts b/src/encoder/encoder.ts index ee6de6e..3fdbc6c 100644 --- a/src/encoder/encoder.ts +++ b/src/encoder/encoder.ts @@ -1,16 +1,6 @@ import type { RegexElement } from '../components/types'; -import { encodeCapture } from '../components/capture'; -import { encodeCharacterClass } from '../components/character-class'; -import { encodeChoiceOf } from '../components/choice-of'; -import { - encodeOne, - encodeOneOrMore, - encodeOptionally, - encodeZeroOrMore, -} from '../components/quantifiers'; -import { encodeRepeat } from '../components/repeat'; import { concatNodes, escapeText } from '../utils'; -import { type EncoderResult, EncoderPrecedence } from './types'; +import { EncoderPrecedence, type EncoderResult } from './types'; export function encodeSequence( elements: Array @@ -23,40 +13,7 @@ export function encodeElement(element: RegexElement | string): EncoderResult { return encodeText(element); } - if (element.type === 'characterClass') { - return encodeCharacterClass(element); - } - - if (element.type === 'choiceOf') { - return encodeChoiceOf(element, encodeElement); - } - - if (element.type === 'repeat') { - return encodeRepeat(element.config, encodeSequence(element.children)); - } - - if (element.type === 'one') { - return encodeOne(encodeSequence(element.children)); - } - - if (element.type === 'oneOrMore') { - return encodeOneOrMore(encodeSequence(element.children)); - } - - if (element.type === 'optionally') { - return encodeOptionally(encodeSequence(element.children)); - } - - if (element.type === 'zeroOrMore') { - return encodeZeroOrMore(encodeSequence(element.children)); - } - - if (element.type === 'capture') { - return encodeCapture(encodeSequence(element.children)); - } - - // @ts-expect-error User passed incorrect type - throw new Error(`Unknown elements type ${element.type}`); + return element.encode(); } function encodeText(text: string): EncoderResult { diff --git a/src/utils.ts b/src/utils.ts index a9c9c2d..c621704 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import type { RegexElement } from './components/types'; -import { type EncoderResult, EncoderPrecedence } from './encoder/types'; +import { EncoderPrecedence, type EncoderResult } from './encoder/types'; /** * Returns atomic pattern for given node. From c023fb21069556bc4735d641c95310330c81f930 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 19 Dec 2023 10:17:26 +0100 Subject: [PATCH 3/5] refactor: decentralize component types --- src/{encoder => }/__tests__/encoder.test.tsx | 6 +- src/builders.ts | 4 +- src/components/capture.ts | 15 ++++- src/components/character-class.ts | 13 ++++- src/components/choice-of.ts | 15 ++++- src/components/quantifiers.ts | 39 ++++++++++--- src/components/repeat.ts | 18 +++++- src/components/types.ts | 59 -------------------- src/{encoder => }/encoder.ts | 9 ++- src/index.ts | 2 +- src/test-utils.ts | 2 +- src/{encoder => }/types.ts | 10 ++-- src/utils.ts | 7 ++- 13 files changed, 103 insertions(+), 96 deletions(-) rename src/{encoder => }/__tests__/encoder.test.tsx (92%) delete mode 100644 src/components/types.ts rename src/{encoder => }/encoder.ts (80%) rename src/{encoder => }/types.ts (69%) diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/__tests__/encoder.test.tsx similarity index 92% rename from src/encoder/__tests__/encoder.test.tsx rename to src/__tests__/encoder.test.tsx index 5c85a1b..329459c 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/__tests__/encoder.test.tsx @@ -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'); diff --git a/src/builders.ts b/src/builders.ts index 13ad226..c6e303c 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -1,5 +1,5 @@ -import type { RegexElement } from './components/types'; -import { encodeSequence } from './encoder/encoder'; +import type { RegexElement } from './types'; +import { encodeSequence } from './encoder'; import { isRegexElement } from './utils'; export interface RegexFlags { diff --git a/src/components/capture.ts b/src/components/capture.ts index 0868e65..c0e0f01 100644 --- a/src/components/capture.ts +++ b/src/components/capture.ts @@ -1,6 +1,15 @@ -import { encodeSequence } from '../encoder/encoder'; -import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; -import type { Capture, RegexElement } from './types'; +import { encodeSequence } from '../encoder'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from '../types'; + +export interface Capture extends RegexElement { + type: 'capture'; + children: Array; + encode: () => EncoderResult; +} export function capture(...children: Array): Capture { return { diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 9436ce0..49c0897 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -1,6 +1,15 @@ -import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; import { escapeText } from '../utils'; -import type { CharacterClass } from './types'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from '../types'; + +export interface CharacterClass extends RegexElement { + type: 'characterClass'; + characters: string[]; + encode: () => EncoderResult; +} export const any: CharacterClass = { type: 'characterClass', diff --git a/src/components/choice-of.ts b/src/components/choice-of.ts index 01a98f1..7cc6a85 100644 --- a/src/components/choice-of.ts +++ b/src/components/choice-of.ts @@ -1,6 +1,15 @@ -import { encodeElement } from '../encoder/encoder'; -import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; -import type { ChoiceOf, RegexElement } from './types'; +import { encodeElement } from '../encoder'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from '../types'; + +export interface ChoiceOf extends RegexElement { + type: 'choiceOf'; + children: Array; + encode: () => EncoderResult; +} export function choiceOf(...children: Array): ChoiceOf { if (children.length === 0) { diff --git a/src/components/quantifiers.ts b/src/components/quantifiers.ts index 530e9bf..42cfab0 100644 --- a/src/components/quantifiers.ts +++ b/src/components/quantifiers.ts @@ -1,13 +1,34 @@ -import { encodeSequence } from '../encoder/encoder'; -import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; +import { encodeSequence } from '../encoder'; import { toAtom } from '../utils'; -import type { - One, - OneOrMore, - Optionally, - RegexElement, - ZeroOrMore, -} from './types'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from '../types'; + +export interface One extends RegexElement { + type: 'one'; + children: Array; + encode: () => EncoderResult; +} + +export interface OneOrMore extends RegexElement { + type: 'oneOrMore'; + children: Array; + encode: () => EncoderResult; +} + +export interface Optionally extends RegexElement { + type: 'optionally'; + children: Array; + encode: () => EncoderResult; +} + +export interface ZeroOrMore extends RegexElement { + type: 'zeroOrMore'; + children: Array; + encode: () => EncoderResult; +} export function one(...children: Array): One { return { diff --git a/src/components/repeat.ts b/src/components/repeat.ts index 821587e..e5718d7 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -1,7 +1,19 @@ -import { encodeSequence } from '../encoder/encoder'; -import { EncoderPrecedence, type EncoderResult } from '../encoder/types'; +import { encodeSequence } from '../encoder'; import { toAtom } from '../utils'; -import type { RegexElement, Repeat, RepeatConfig } from './types'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from '../types'; + +export interface Repeat extends RegexElement { + type: 'repeat'; + children: Array; + config: RepeatConfig; + encode: () => EncoderResult; +} + +export type RepeatConfig = { count: number } | { min: number; max?: number }; export function repeat( config: RepeatConfig, diff --git a/src/components/types.ts b/src/components/types.ts deleted file mode 100644 index fb69798..0000000 --- a/src/components/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { EncoderResult } from '../encoder/types'; - -export type RegexElement = Capture | CharacterClass | ChoiceOf | Quantifier; - -export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; - -export type CharacterClass = { - type: 'characterClass'; - characters: string[]; - encode: () => EncoderResult; -}; - -// Components -export type ChoiceOf = { - type: 'choiceOf'; - children: Array; - encode: () => EncoderResult; -}; - -// Quantifiers -export type One = { - type: 'one'; - children: Array; - encode: () => EncoderResult; -}; - -export type OneOrMore = { - type: 'oneOrMore'; - children: Array; - encode: () => EncoderResult; -}; - -export type Optionally = { - type: 'optionally'; - children: Array; - encode: () => EncoderResult; -}; - -export type ZeroOrMore = { - type: 'zeroOrMore'; - children: Array; - encode: () => EncoderResult; -}; - -export type Repeat = { - type: 'repeat'; - children: Array; - config: RepeatConfig; - encode: () => EncoderResult; -}; - -export type RepeatConfig = { count: number } | { min: number; max?: number }; - -// Captures -export type Capture = { - type: 'capture'; - children: Array; - encode: () => EncoderResult; -}; diff --git a/src/encoder/encoder.ts b/src/encoder.ts similarity index 80% rename from src/encoder/encoder.ts rename to src/encoder.ts index 3fdbc6c..d30fc10 100644 --- a/src/encoder/encoder.ts +++ b/src/encoder.ts @@ -1,6 +1,9 @@ -import type { RegexElement } from '../components/types'; -import { concatNodes, escapeText } from '../utils'; -import { EncoderPrecedence, type EncoderResult } from './types'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from './types'; +import { concatNodes, escapeText } from './utils'; export function encodeSequence( elements: Array diff --git a/src/index.ts b/src/index.ts index 652875f..df4ad3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export type * from './components/types'; +export type * from './types'; export { buildPattern, buildRegex } from './builders'; diff --git a/src/test-utils.ts b/src/test-utils.ts index cea4dde..29b7c02 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,5 +1,5 @@ import { buildRegex } from './builders'; -import type { RegexElement } from './components/types'; +import type { RegexElement } from './types'; export function execRegex( text: string, diff --git a/src/encoder/types.ts b/src/types.ts similarity index 69% rename from src/encoder/types.ts rename to src/types.ts index ecf2c3d..96a048e 100644 --- a/src/encoder/types.ts +++ b/src/types.ts @@ -1,4 +1,6 @@ -import type { RegexElement } from '../components/types'; +export interface RegexElement { + encode: () => EncoderResult; +} /** * Encoded regex pattern with information about its type (atom, sequence) @@ -23,7 +25,5 @@ export const EncoderPrecedence = { Alternation: 3, } as const; -type ValueOf = T[keyof T]; -type EncoderPrecedence = ValueOf; - -export type EncodeElement = (element: RegexElement | string) => EncoderResult; +export type ValueOf = T[keyof T]; +export type EncoderPrecedence = ValueOf; diff --git a/src/utils.ts b/src/utils.ts index c621704..e2e1b2d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ -import type { RegexElement } from './components/types'; -import { EncoderPrecedence, type EncoderResult } from './encoder/types'; +import { + EncoderPrecedence, + type EncoderResult, + type RegexElement, +} from './types'; /** * Returns atomic pattern for given node. From 180afd7d2692b29efba5cd54aa64968168f57a73 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 19 Dec 2023 10:23:26 +0100 Subject: [PATCH 4/5] refactor: rename RegexComponent --- src/builders.ts | 12 ++++++------ src/components/capture.ts | 8 ++++---- src/components/character-class.ts | 4 ++-- src/components/choice-of.ts | 10 ++++++---- src/components/quantifiers.ts | 26 +++++++++++++------------- src/components/repeat.ts | 8 ++++---- src/encoder.ts | 6 +++--- src/test-utils.ts | 4 ++-- src/types.ts | 2 +- src/utils.ts | 4 ++-- 10 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/builders.ts b/src/builders.ts index c6e303c..5966ba5 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -1,4 +1,4 @@ -import type { RegexElement } from './types'; +import type { RegexComponent } from './types'; import { encodeSequence } from './encoder'; import { isRegexElement } from './utils'; @@ -21,14 +21,14 @@ export interface RegexFlags { * @param elements * @returns */ -export function buildRegex(...elements: Array): RegExp; +export function buildRegex(...elements: Array): RegExp; export function buildRegex( flags: RegexFlags, - ...elements: Array + ...elements: Array ): RegExp; export function buildRegex( - first: RegexFlags | RegexElement | string, - ...rest: Array + first: RegexFlags | RegexComponent | string, + ...rest: Array ): RegExp { if (typeof first === 'string' || isRegexElement(first)) { return buildRegex({}, first, ...rest); @@ -45,7 +45,7 @@ export function buildRegex( * @returns */ export function buildPattern( - ...elements: Array + ...elements: Array ): string { return encodeSequence(elements).pattern; } diff --git a/src/components/capture.ts b/src/components/capture.ts index c0e0f01..3186771 100644 --- a/src/components/capture.ts +++ b/src/components/capture.ts @@ -2,16 +2,16 @@ import { encodeSequence } from '../encoder'; import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from '../types'; -export interface Capture extends RegexElement { +export interface Capture extends RegexComponent { type: 'capture'; - children: Array; + children: Array; encode: () => EncoderResult; } -export function capture(...children: Array): Capture { +export function capture(...children: Array): Capture { return { type: 'capture', children, diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 49c0897..0827fae 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -2,10 +2,10 @@ import { escapeText } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from '../types'; -export interface CharacterClass extends RegexElement { +export interface CharacterClass extends RegexComponent { type: 'characterClass'; characters: string[]; encode: () => EncoderResult; diff --git a/src/components/choice-of.ts b/src/components/choice-of.ts index 7cc6a85..992fe5d 100644 --- a/src/components/choice-of.ts +++ b/src/components/choice-of.ts @@ -2,16 +2,18 @@ import { encodeElement } from '../encoder'; import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from '../types'; -export interface ChoiceOf extends RegexElement { +export interface ChoiceOf extends RegexComponent { type: 'choiceOf'; - children: Array; + children: Array; encode: () => EncoderResult; } -export function choiceOf(...children: Array): ChoiceOf { +export function choiceOf( + ...children: Array +): ChoiceOf { if (children.length === 0) { throw new Error('`choiceOf` should receive at least one option'); } diff --git a/src/components/quantifiers.ts b/src/components/quantifiers.ts index 42cfab0..d55f158 100644 --- a/src/components/quantifiers.ts +++ b/src/components/quantifiers.ts @@ -3,34 +3,34 @@ import { toAtom } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from '../types'; -export interface One extends RegexElement { +export interface One extends RegexComponent { type: 'one'; - children: Array; + children: Array; encode: () => EncoderResult; } -export interface OneOrMore extends RegexElement { +export interface OneOrMore extends RegexComponent { type: 'oneOrMore'; - children: Array; + children: Array; encode: () => EncoderResult; } -export interface Optionally extends RegexElement { +export interface Optionally extends RegexComponent { type: 'optionally'; - children: Array; + children: Array; encode: () => EncoderResult; } -export interface ZeroOrMore extends RegexElement { +export interface ZeroOrMore extends RegexComponent { type: 'zeroOrMore'; - children: Array; + children: Array; encode: () => EncoderResult; } -export function one(...children: Array): One { +export function one(...children: Array): One { return { type: 'one', children, @@ -39,7 +39,7 @@ export function one(...children: Array): One { } export function oneOrMore( - ...children: Array + ...children: Array ): OneOrMore { return { type: 'oneOrMore', @@ -49,7 +49,7 @@ export function oneOrMore( } export function optionally( - ...children: Array + ...children: Array ): Optionally { return { type: 'optionally', @@ -59,7 +59,7 @@ export function optionally( } export function zeroOrMore( - ...children: Array + ...children: Array ): ZeroOrMore { return { type: 'zeroOrMore', diff --git a/src/components/repeat.ts b/src/components/repeat.ts index e5718d7..312c4de 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -3,12 +3,12 @@ import { toAtom } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from '../types'; -export interface Repeat extends RegexElement { +export interface Repeat extends RegexComponent { type: 'repeat'; - children: Array; + children: Array; config: RepeatConfig; encode: () => EncoderResult; } @@ -17,7 +17,7 @@ export type RepeatConfig = { count: number } | { min: number; max?: number }; export function repeat( config: RepeatConfig, - ...children: Array + ...children: Array ): Repeat { if (children.length === 0) { throw new Error('`repeat` should receive at least one element'); diff --git a/src/encoder.ts b/src/encoder.ts index d30fc10..1e7ef00 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -1,17 +1,17 @@ import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from './types'; import { concatNodes, escapeText } from './utils'; export function encodeSequence( - elements: Array + elements: Array ): EncoderResult { return concatNodes(elements.map((c) => encodeElement(c))); } -export function encodeElement(element: RegexElement | string): EncoderResult { +export function encodeElement(element: RegexComponent | string): EncoderResult { if (typeof element === 'string') { return encodeText(element); } diff --git a/src/test-utils.ts b/src/test-utils.ts index 29b7c02..dc92ced 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,9 +1,9 @@ import { buildRegex } from './builders'; -import type { RegexElement } from './types'; +import type { RegexComponent } from './types'; export function execRegex( text: string, - elements: Array + elements: Array ) { const regex = buildRegex(...elements); return [...regex.exec(text)!]; diff --git a/src/types.ts b/src/types.ts index 96a048e..a05b136 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export interface RegexElement { +export interface RegexComponent { encode: () => EncoderResult; } diff --git a/src/utils.ts b/src/utils.ts index e2e1b2d..3c9c0f3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import { EncoderPrecedence, type EncoderResult, - type RegexElement, + type RegexComponent, } from './types'; /** @@ -33,7 +33,7 @@ export function concatNodes(nodes: EncoderResult[]): EncoderResult { }; } -export function isRegexElement(element: unknown): element is RegexElement { +export function isRegexElement(element: unknown): element is RegexComponent { return typeof element === 'object' && element !== null && 'type' in element; } From 716a810163086f96ae7de68fe54ee4e94193c55e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 19 Dec 2023 12:02:58 +0100 Subject: [PATCH 5/5] refactor: class-based components --- src/__tests__/encoder.test.tsx | 2 +- src/builders.ts | 16 +-- src/components/__tests__/choice-of.test.ts | 2 +- src/components/__tests__/repeat.test.tsx | 2 +- src/components/capture.ts | 33 +++-- src/components/character-class.ts | 77 ++++------- src/components/choice-of.ts | 46 +++---- src/components/quantifiers.ts | 144 +++++++++++---------- src/components/repeat.ts | 61 +++++---- src/encoder.ts | 8 +- src/test-utils.ts | 4 +- src/types.ts | 2 +- src/utils.ts | 6 +- 13 files changed, 191 insertions(+), 212 deletions(-) diff --git a/src/__tests__/encoder.test.tsx b/src/__tests__/encoder.test.tsx index 329459c..a17459e 100644 --- a/src/__tests__/encoder.test.tsx +++ b/src/__tests__/encoder.test.tsx @@ -57,6 +57,6 @@ test('"buildPattern" escapes special characters', () => { test('buildPattern throws on empty text', () => { expect(() => buildPattern('')).toThrowErrorMatchingInlineSnapshot( - `"\`encodeText\`: received text should not be empty"` + `"\\"encodeText\\": received text should not be empty"` ); }); diff --git a/src/builders.ts b/src/builders.ts index 5966ba5..4dfb5c2 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -1,6 +1,6 @@ -import type { RegexComponent } from './types'; +import type { RegexElement } from './types'; import { encodeSequence } from './encoder'; -import { isRegexElement } from './utils'; +import { isValidElement } from './utils'; export interface RegexFlags { /** Global search. */ @@ -21,16 +21,16 @@ export interface RegexFlags { * @param elements * @returns */ -export function buildRegex(...elements: Array): RegExp; +export function buildRegex(...elements: Array): RegExp; export function buildRegex( flags: RegexFlags, - ...elements: Array + ...elements: Array ): RegExp; export function buildRegex( - first: RegexFlags | RegexComponent | string, - ...rest: Array + first: RegexFlags | RegexElement | string, + ...rest: Array ): RegExp { - if (typeof first === 'string' || isRegexElement(first)) { + if (typeof first === 'string' || isValidElement(first)) { return buildRegex({}, first, ...rest); } @@ -45,7 +45,7 @@ export function buildRegex( * @returns */ export function buildPattern( - ...elements: Array + ...elements: Array ): string { return encodeSequence(elements).pattern; } diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index 3748dab..734d45d 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -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"` ); }); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index 0bb2225..5f388cd 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -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"` ); }); diff --git a/src/components/capture.ts b/src/components/capture.ts index 3186771..d67fefc 100644 --- a/src/components/capture.ts +++ b/src/components/capture.ts @@ -2,26 +2,25 @@ import { encodeSequence } from '../encoder'; import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from '../types'; -export interface Capture extends RegexComponent { - type: 'capture'; - children: Array; - encode: () => EncoderResult; -} +export class Capture implements RegexElement { + public children: Array; + + constructor(children: Array) { + this.children = children; + } -export function capture(...children: Array): Capture { - return { - type: 'capture', - children, - encode: encodeCapture, - }; + encode(): EncoderResult { + const children = encodeSequence(this.children); + return { + precedence: EncoderPrecedence.Atom, + pattern: `(${children.pattern})`, + }; + } } -function encodeCapture(this: Capture): EncoderResult { - return { - precedence: EncoderPrecedence.Atom, - pattern: `(${encodeSequence(this.children).pattern})`, - }; +export function capture(...children: Array): Capture { + return new Capture(children); } diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 0827fae..ffbf360 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -2,38 +2,39 @@ import { escapeText } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from '../types'; -export interface CharacterClass extends RegexComponent { - type: 'characterClass'; - characters: string[]; - encode: () => EncoderResult; -} +export class CharacterClass implements RegexElement { + public characters: string[]; -export const any: CharacterClass = { - type: 'characterClass', - characters: ['.'], - encode: encodeCharacterClass, -}; + constructor(characters: string[]) { + if (characters.length === 0) { + throw new Error('Character class should contain at least one character'); + } -export const whitespace: CharacterClass = { - type: 'characterClass', - characters: ['\\s'], - encode: encodeCharacterClass, -}; + this.characters = characters; + } -export const digit: CharacterClass = { - type: 'characterClass', - characters: ['\\d'], - encode: encodeCharacterClass, -}; + encode(): EncoderResult { + if (this.characters.length === 1) { + return { + precedence: EncoderPrecedence.Atom, + pattern: this.characters[0]!, + }; + } -export const word: CharacterClass = { - type: 'characterClass', - characters: ['\\w'], - encode: encodeCharacterClass, -}; + return { + precedence: EncoderPrecedence.Atom, + 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); @@ -41,29 +42,7 @@ export function anyOf(characters: string): CharacterClass { throw new Error('`anyOf` should received at least one character'); } - return { - type: 'characterClass', - characters: charactersArray, - encode: encodeCharacterClass, - }; -} - -function encodeCharacterClass(this: CharacterClass): EncoderResult { - if (this.characters.length === 0) { - throw new Error('Character class should contain at least one character'); - } - - if (this.characters.length === 1) { - return { - precedence: EncoderPrecedence.Atom, - pattern: this.characters[0]!, - }; - } - - return { - precedence: EncoderPrecedence.Atom, - pattern: `[${reorderHyphen(this.characters).join('')}]`, - }; + return new CharacterClass(charactersArray); } // If passed characters includes hyphen (`-`) it need to be moved to diff --git a/src/components/choice-of.ts b/src/components/choice-of.ts index 992fe5d..307652c 100644 --- a/src/components/choice-of.ts +++ b/src/components/choice-of.ts @@ -2,37 +2,33 @@ import { encodeElement } from '../encoder'; import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from '../types'; -export interface ChoiceOf extends RegexComponent { - type: 'choiceOf'; - children: Array; - encode: () => EncoderResult; -} +export class ChoiceOf implements RegexElement { + public children: Array; + + constructor(children: Array) { + if (children.length === 0) { + throw new Error('"choiceOf" should receive at least one option'); + } -export function choiceOf( - ...children: Array -): ChoiceOf { - if (children.length === 0) { - throw new Error('`choiceOf` should receive at least one option'); + this.children = children; } - return { - type: 'choiceOf', - children, - encode: encodeChoiceOf, - }; -} + encode(): EncoderResult { + const children = this.children.map(encodeElement); + if (children.length === 1) { + return children[0]!; + } -function encodeChoiceOf(this: ChoiceOf): EncoderResult { - const encodedNodes = this.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): ChoiceOf { + return new ChoiceOf(children); } diff --git a/src/components/quantifiers.ts b/src/components/quantifiers.ts index d55f158..8cd971f 100644 --- a/src/components/quantifiers.ts +++ b/src/components/quantifiers.ts @@ -3,95 +3,103 @@ import { toAtom } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from '../types'; -export interface One extends RegexComponent { - type: 'one'; - children: Array; - encode: () => EncoderResult; -} +export class One implements RegexElement { + public children: Array; -export interface OneOrMore extends RegexComponent { - type: 'oneOrMore'; - children: Array; - encode: () => EncoderResult; -} + constructor(children: Array) { + if (children.length === 0) { + throw new Error('"one" should receive at least one element'); + } + + this.children = children; + } -export interface Optionally extends RegexComponent { - type: 'optionally'; - children: Array; - encode: () => EncoderResult; + encode(): EncoderResult { + return encodeSequence(this.children); + } } -export interface ZeroOrMore extends RegexComponent { - type: 'zeroOrMore'; - children: Array; - encode: () => EncoderResult; +export function one(...children: Array): One { + return new One(children); } -export function one(...children: Array): One { - return { - type: 'one', - children, - encode: encodeOne, - }; +export class OneOrMore implements RegexElement { + public children: Array; + + constructor(children: Array) { + if (children.length === 0) { + throw new Error('"oneOrMore" should receive at least one element'); + } + + this.children = children; + } + + encode(): EncoderResult { + const children = encodeSequence(this.children); + return { + precedence: EncoderPrecedence.Sequence, + pattern: `${toAtom(children)}+`, + }; + } } export function oneOrMore( - ...children: Array + ...children: Array ): OneOrMore { - return { - type: 'oneOrMore', - children, - encode: encodeOneOrMore, - }; + return new OneOrMore(children); +} + +export class Optionally implements RegexElement { + public children: Array; + + constructor(children: Array) { + if (children.length === 0) { + throw new Error('"optionally" should receive at least one element'); + } + + this.children = children; + } + + encode(): EncoderResult { + const children = encodeSequence(this.children); + return { + precedence: EncoderPrecedence.Sequence, + pattern: `${toAtom(children)}?`, + }; + } } export function optionally( - ...children: Array + ...children: Array ): Optionally { - return { - type: 'optionally', - children, - encode: encodeOptionally, - }; + return new Optionally(children); } -export function zeroOrMore( - ...children: Array -): ZeroOrMore { - return { - type: 'zeroOrMore', - children, - encode: encodeZeroOrMore, - }; -} +export class ZeroOrMore implements RegexElement { + public children: Array; -function encodeOne(this: One): EncoderResult { - return encodeSequence(this.children); -} + constructor(children: Array) { + if (children.length === 0) { + throw new Error('"zeroOrMore" should receive at least one element'); + } -function encodeOneOrMore(this: OneOrMore): EncoderResult { - const children = encodeSequence(this.children); - return { - precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(children)}+`, - }; -} + this.children = children; + } -function encodeOptionally(this: Optionally): EncoderResult { - const children = encodeSequence(this.children); - return { - precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(children)}?`, - }; + encode(): EncoderResult { + const children = encodeSequence(this.children); + return { + precedence: EncoderPrecedence.Sequence, + pattern: `${toAtom(children)}*`, + }; + } } -function encodeZeroOrMore(this: ZeroOrMore): EncoderResult { - const children = encodeSequence(this.children); - return { - precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(children)}*`, - }; +export function zeroOrMore( + ...children: Array +): ZeroOrMore { + return new ZeroOrMore(children); } diff --git a/src/components/repeat.ts b/src/components/repeat.ts index 312c4de..51824e5 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -3,47 +3,44 @@ import { toAtom } from '../utils'; import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from '../types'; -export interface Repeat extends RegexComponent { - type: 'repeat'; - children: Array; - config: RepeatConfig; - encode: () => EncoderResult; -} - export type RepeatConfig = { count: number } | { min: number; max?: number }; -export function repeat( - config: RepeatConfig, - ...children: Array -): Repeat { - if (children.length === 0) { - throw new Error('`repeat` should receive at least one element'); +export class Repeat implements RegexElement { + public children: Array; + public config: RepeatConfig; + + constructor(children: Array, config: RepeatConfig) { + if (children.length === 0) { + throw new Error('"repeat" should receive at least one element'); + } + + this.children = children; + this.config = config; } - return { - type: 'repeat', - children, - config, - encode: encodeRepeat, - }; -} + encode(): EncoderResult { + const children = encodeSequence(this.children); + if ('count' in this.config) { + return { + precedence: EncoderPrecedence.Sequence, + pattern: `${toAtom(children)}{${this.config.count}}`, + }; + } -function encodeRepeat(this: Repeat): EncoderResult { - const children = encodeSequence(this.children); - if ('count' in this.config) { return { precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(children)}{${this.config.count}}`, + pattern: `${toAtom(children)}{${this.config.min},${ + this.config?.max ?? '' + }}`, }; } - - return { - precedence: EncoderPrecedence.Sequence, - pattern: `${toAtom(children)}{${this.config.min},${ - this.config?.max ?? '' - }}`, - }; +} +export function repeat( + config: RepeatConfig, + ...children: Array +): Repeat { + return new Repeat(children, config); } diff --git a/src/encoder.ts b/src/encoder.ts index 1e7ef00..84057f1 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -1,17 +1,17 @@ import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from './types'; import { concatNodes, escapeText } from './utils'; export function encodeSequence( - elements: Array + elements: Array ): EncoderResult { return concatNodes(elements.map((c) => encodeElement(c))); } -export function encodeElement(element: RegexComponent | string): EncoderResult { +export function encodeElement(element: RegexElement | string): EncoderResult { if (typeof element === 'string') { return encodeText(element); } @@ -21,7 +21,7 @@ export function encodeElement(element: RegexComponent | string): EncoderResult { function encodeText(text: string): EncoderResult { if (text.length === 0) { - throw new Error('`encodeText`: received text should not be empty'); + throw new Error('"encodeText": received text should not be empty'); } if (text.length === 1) { diff --git a/src/test-utils.ts b/src/test-utils.ts index dc92ced..29b7c02 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,9 +1,9 @@ import { buildRegex } from './builders'; -import type { RegexComponent } from './types'; +import type { RegexElement } from './types'; export function execRegex( text: string, - elements: Array + elements: Array ) { const regex = buildRegex(...elements); return [...regex.exec(text)!]; diff --git a/src/types.ts b/src/types.ts index a05b136..96a048e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export interface RegexComponent { +export interface RegexElement { encode: () => EncoderResult; } diff --git a/src/utils.ts b/src/utils.ts index 3c9c0f3..4575be5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import { EncoderPrecedence, type EncoderResult, - type RegexComponent, + type RegexElement, } from './types'; /** @@ -33,8 +33,8 @@ export function concatNodes(nodes: EncoderResult[]): EncoderResult { }; } -export function isRegexElement(element: unknown): element is RegexComponent { - return typeof element === 'object' && element !== null && 'type' in element; +export function isValidElement(element: unknown): element is RegexElement { + return typeof element === 'object' && element !== null && 'encode' in element; } // Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping