diff --git a/src/__tests__/compiler.test.tsx b/src/__tests__/compiler.test.tsx index 06e2316..dcdd8e4 100644 --- a/src/__tests__/compiler.test.tsx +++ b/src/__tests__/compiler.test.tsx @@ -1,5 +1,5 @@ import { buildPattern, buildRegex } from '../compiler'; -import { oneOrMore, optionally, one, zeroOrMore } from '../quantifiers'; +import { oneOrMore, optionally, one, zeroOrMore, repeat } from '../quantifiers'; test('basic quantifies', () => { expect(buildPattern('a')).toEqual('a'); @@ -12,6 +12,8 @@ test('basic quantifies', () => { expect(buildPattern('a', oneOrMore('bc'))).toEqual('a(?:bc)+'); expect(buildPattern('a', oneOrMore('bc'))).toEqual('a(?:bc)+'); + expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); + expect(buildPattern('a', zeroOrMore('b'))).toEqual('ab*'); expect(buildPattern('a', zeroOrMore('bc'))).toEqual('a(?:bc)*'); expect(buildPattern('a', zeroOrMore('bc'))).toEqual('a(?:bc)*'); diff --git a/src/__tests__/repeat.test.tsx b/src/__tests__/repeat.test.tsx new file mode 100644 index 0000000..5c8776f --- /dev/null +++ b/src/__tests__/repeat.test.tsx @@ -0,0 +1,15 @@ +import { buildPattern } from '../compiler'; +import { repeat, zeroOrMore, oneOrMore } from '../quantifiers'; + +test('"repeat" quantifier', () => { + expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); + expect(buildPattern('a', repeat({ min: 1 }, 'b'))).toEqual('ab{1,}'); + expect(buildPattern('a', repeat({ count: 1 }, 'b'))).toEqual('ab{1}'); + + expect(buildPattern('a', repeat({ count: 1 }, 'a', zeroOrMore('b')))).toEqual( + 'a(?:ab*){1}' + ); + expect( + buildPattern(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))) + ).toEqual('(?:text d+){5}'); +}); diff --git a/src/compiler.ts b/src/compiler.ts index ac3de1f..45fe05b 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,5 +1,6 @@ import type { RegexElement } from './types'; import { compilers as quantifiers } from './quantifiers'; +import { compileRepeat } from './quantifiers/repeat'; /** * Generate RegExp object for elements. @@ -32,11 +33,16 @@ function compileSingle(elements: RegexElement): string { return elements; } + const compiledChildren = compileList(elements.children); + + if (elements.type === 'repeat') { + return compileRepeat(elements.config, compiledChildren); + } + const elementCompiler = quantifiers[elements.type]; if (!elementCompiler) { throw new Error(`Unknown elements type ${elements.type}`); } - const children = compileList(elements.children); - return elementCompiler(children); + return elementCompiler(compiledChildren); } diff --git a/src/quantifiers.ts b/src/quantifiers/index.ts similarity index 62% rename from src/quantifiers.ts rename to src/quantifiers/index.ts index 4007990..c6d170d 100644 --- a/src/quantifiers.ts +++ b/src/quantifiers/index.ts @@ -3,10 +3,12 @@ import type { OneOrMore, Optionally, RegexElement, + Repeat, + RepeatConfig, ZeroOrMore, -} from './types'; -import type { CompilerMap } from './types-internal'; -import { wrapGroup } from './utils'; +} from '../types'; +import type { CompilerMap } from '../types-internal'; +import { wrapGroup } from '../utils'; export function oneOrMore(...children: RegexElement[]): OneOrMore { return { @@ -36,9 +38,20 @@ export function zeroOrMore(...children: RegexElement[]): ZeroOrMore { }; } +export function repeat( + config: RepeatConfig, + ...children: RegexElement[] +): Repeat { + return { + type: 'repeat', + children, + config, + }; +} + export const compilers = { one: (compiledChildren) => compiledChildren, oneOrMore: (compiledChildren) => `${wrapGroup(compiledChildren)}+`, - optionally: (compiledChildren: string) => `${wrapGroup(compiledChildren)}?`, - zeroOrMore: (compiledChildren: string) => `${wrapGroup(compiledChildren)}*`, + optionally: (compiledChildren) => `${wrapGroup(compiledChildren)}?`, + zeroOrMore: (compiledChildren) => `${wrapGroup(compiledChildren)}*`, } satisfies CompilerMap; diff --git a/src/quantifiers/repeat.ts b/src/quantifiers/repeat.ts new file mode 100644 index 0000000..4aa379a --- /dev/null +++ b/src/quantifiers/repeat.ts @@ -0,0 +1,17 @@ +import type { RepeatConfig } from '../types'; +import { wrapGroup } from '../utils'; + +export function compileRepeat( + config: RepeatConfig, + compiledChildren: string +): string { + if ('count' in config && typeof config.count === 'number') { + return `${wrapGroup(compiledChildren)}{${config.count}}`; + } + + if ('min' in config && typeof config.min === 'number') { + return `${wrapGroup(compiledChildren)}{${config.min},${config?.max ?? ''}}`; + } + + return `${wrapGroup(compiledChildren)}`; +} diff --git a/src/types.ts b/src/types.ts index dd6f023..b8ea674 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,11 @@ export type RegexElement = string | RegexQuantifier; -export type RegexQuantifier = One | OneOrMore | Optionally | ZeroOrMore; +export type RegexQuantifier = + | One + | OneOrMore + | Optionally + | ZeroOrMore + | Repeat; // Quantifiers export type One = { @@ -18,6 +23,18 @@ export type Optionally = { children: RegexElement[]; }; +export type RepeatConfig = + | { min: number; max?: number } + | { + count: number; + }; + +export type Repeat = { + type: 'repeat'; + children: RegexElement[]; + config: RepeatConfig; +}; + export type ZeroOrMore = { type: 'zeroOrMore'; children: RegexElement[];