diff --git a/.gitignore b/.gitignore index 24b30e1..c75e6b3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ buck-out/ # generated by bob lib/ + +/.idea diff --git a/src/__tests__/characterClasses.test.tsx b/src/__tests__/characterClasses.test.tsx new file mode 100644 index 0000000..6081cf5 --- /dev/null +++ b/src/__tests__/characterClasses.test.tsx @@ -0,0 +1,35 @@ +import { any, digit, whitespace, word } from '../character-classes'; +import { buildPattern } from '../compiler'; +import { one } from '../quantifiers/base'; + +test('"whitespace" character class', () => { + expect(buildPattern(whitespace)).toEqual(`\\s`); + + expect(buildPattern(one('ab'), whitespace)).toEqual(`ab\\s`); + + expect(buildPattern(one('ab'), whitespace, one('c'))).toEqual(`ab\\sc`); +}); + +test('"digit" character class', () => { + expect(buildPattern(digit)).toEqual(`\\d`); + + expect(buildPattern(one('ab'), digit)).toEqual(`ab\\d`); + + expect(buildPattern(one('ab'), digit, one('c'))).toEqual(`ab\\dc`); +}); + +test('"word" character class', () => { + expect(buildPattern(word)).toEqual(`\\w`); + + expect(buildPattern(one('ab'), word)).toEqual(`ab\\w`); + + expect(buildPattern(one('ab'), word, one('c'))).toEqual(`ab\\wc`); +}); + +test('"any" character class', () => { + expect(buildPattern(any)).toEqual(`.`); + + expect(buildPattern(one('ab'), any)).toEqual(`ab.`); + + expect(buildPattern(one('ab'), any, one('c'))).toEqual(`ab.c`); +}); diff --git a/src/__tests__/compiler.test.tsx b/src/__tests__/compiler.test.tsx index dcdd8e4..96ecb5b 100644 --- a/src/__tests__/compiler.test.tsx +++ b/src/__tests__/compiler.test.tsx @@ -1,5 +1,6 @@ import { buildPattern, buildRegex } from '../compiler'; -import { oneOrMore, optionally, one, zeroOrMore, repeat } from '../quantifiers'; +import { oneOrMore, optionally, one, zeroOrMore } from '../quantifiers/base'; +import { repeat } from '../quantifiers/repeat'; test('basic quantifies', () => { expect(buildPattern('a')).toEqual('a'); diff --git a/src/__tests__/quantifiers.test.tsx b/src/__tests__/quantifiers.test.tsx index 219c0c0..2788ae1 100644 --- a/src/__tests__/quantifiers.test.tsx +++ b/src/__tests__/quantifiers.test.tsx @@ -1,4 +1,4 @@ -import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers'; +import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers/base'; import { buildPattern, buildRegex } from '../compiler'; test('"oneOrMore" quantifier', () => { diff --git a/src/__tests__/repeat.test.tsx b/src/__tests__/repeat.test.tsx index 5c8776f..2305ea8 100644 --- a/src/__tests__/repeat.test.tsx +++ b/src/__tests__/repeat.test.tsx @@ -1,5 +1,6 @@ import { buildPattern } from '../compiler'; -import { repeat, zeroOrMore, oneOrMore } from '../quantifiers'; +import { zeroOrMore, oneOrMore } from '../quantifiers/base'; +import { repeat } from '../quantifiers/repeat'; test('"repeat" quantifier', () => { expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); diff --git a/src/character-classes.ts b/src/character-classes.ts new file mode 100644 index 0000000..9a25c83 --- /dev/null +++ b/src/character-classes.ts @@ -0,0 +1,26 @@ +import type { + Any, + CharacterClass, + Digit, + RegexElement, + Whitespace, + Word, +} from './types'; + +export const whitespace: Whitespace = { type: 'whitespace' }; +export const digit: Digit = { type: 'digit' }; +export const word: Word = { type: 'word' }; +export const any: Any = { type: 'any' }; + +export const characterClasses = { + whitespace: '\\s', + digit: '\\d', + word: '\\w', + any: '.', +} as const satisfies Record; + +export function isCharacterClass( + element: Exclude +): element is CharacterClass { + return element.type in characterClasses; +} diff --git a/src/compiler.ts b/src/compiler.ts index 45fe05b..18de405 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 { characterClasses, isCharacterClass } from './character-classes'; +import { baseQuantifiers, isBaseQuantifier } from './quantifiers/base'; import { compileRepeat } from './quantifiers/repeat'; /** @@ -28,21 +29,26 @@ function compileList(elements: RegexElement[]): string { return elements.map((c) => compileSingle(c)).join(''); } -function compileSingle(elements: RegexElement): string { - if (typeof elements === 'string') { - return elements; +function compileSingle(element: RegexElement): string { + if (typeof element === 'string') { + return element; } - const compiledChildren = compileList(elements.children); + if (isCharacterClass(element)) { + return characterClasses[element.type]; + } + + const compiledChildren = compileList(element.children); - if (elements.type === 'repeat') { - return compileRepeat(elements.config, compiledChildren); + if (element.type === 'repeat') { + return compileRepeat(element.config, compiledChildren); } - const elementCompiler = quantifiers[elements.type]; - if (!elementCompiler) { - throw new Error(`Unknown elements type ${elements.type}`); + if (isBaseQuantifier(element)) { + const compiler = baseQuantifiers[element.type]; + return compiler(compiledChildren); } - return elementCompiler(compiledChildren); + // @ts-expect-error User passed incorrect type + throw new Error(`Unknown elements type ${element.type}`); } diff --git a/src/index.tsx b/src/index.tsx index 3335d4c..fd12f03 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,5 @@ export type * from './types'; +export { whitespace } from './character-classes'; export { buildRegex, buildPattern } from './compiler'; -export { oneOrMore, optionally } from './quantifiers'; +export { oneOrMore, optionally } from './quantifiers/base'; diff --git a/src/quantifiers/index.ts b/src/quantifiers/base.ts similarity index 75% rename from src/quantifiers/index.ts rename to src/quantifiers/base.ts index c6d170d..0564322 100644 --- a/src/quantifiers/index.ts +++ b/src/quantifiers/base.ts @@ -2,12 +2,10 @@ import type { One, OneOrMore, Optionally, + Quantifier, RegexElement, - Repeat, - RepeatConfig, ZeroOrMore, } from '../types'; -import type { CompilerMap } from '../types-internal'; import { wrapGroup } from '../utils'; export function oneOrMore(...children: RegexElement[]): OneOrMore { @@ -38,20 +36,15 @@ export function zeroOrMore(...children: RegexElement[]): ZeroOrMore { }; } -export function repeat( - config: RepeatConfig, - ...children: RegexElement[] -): Repeat { - return { - type: 'repeat', - children, - config, - }; -} - -export const compilers = { +export const baseQuantifiers = { one: (compiledChildren) => compiledChildren, oneOrMore: (compiledChildren) => `${wrapGroup(compiledChildren)}+`, optionally: (compiledChildren) => `${wrapGroup(compiledChildren)}?`, zeroOrMore: (compiledChildren) => `${wrapGroup(compiledChildren)}*`, -} satisfies CompilerMap; +} as const satisfies Record string>; + +export function isBaseQuantifier( + element: Exclude +): element is Quantifier { + return element.type in baseQuantifiers; +} diff --git a/src/quantifiers/repeat.ts b/src/quantifiers/repeat.ts index 4aa379a..c6c6288 100644 --- a/src/quantifiers/repeat.ts +++ b/src/quantifiers/repeat.ts @@ -1,17 +1,24 @@ -import type { RepeatConfig } from '../types'; +import type { RegexElement, Repeat, RepeatConfig } from '../types'; import { wrapGroup } from '../utils'; +export function repeat( + config: RepeatConfig, + ...children: RegexElement[] +): Repeat { + return { + type: 'repeat', + children, + config, + }; +} + export function compileRepeat( config: RepeatConfig, compiledChildren: string ): string { - if ('count' in config && typeof config.count === 'number') { + if ('count' in config) { return `${wrapGroup(compiledChildren)}{${config.count}}`; } - if ('min' in config && typeof config.min === 'number') { - return `${wrapGroup(compiledChildren)}{${config.min},${config?.max ?? ''}}`; - } - - return `${wrapGroup(compiledChildren)}`; + return `${wrapGroup(compiledChildren)}{${config.min},${config?.max ?? ''}}`; } diff --git a/src/types-internal.ts b/src/types-internal.ts deleted file mode 100644 index 9d41c14..0000000 --- a/src/types-internal.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Compilation -export type ElementCompiler = (compiledChildren: string) => string; -export type CompilerMap = Record; diff --git a/src/types.ts b/src/types.ts index b8ea674..0da8d35 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,14 @@ -export type RegexElement = string | RegexQuantifier; - -export type RegexQuantifier = - | One - | OneOrMore - | Optionally - | ZeroOrMore - | Repeat; +export type RegexElement = string | CharacterClass | Quantifier; + +export type CharacterClass = Whitespace | Digit | Word | Any; + +export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; + +// Character classes +export type Whitespace = { type: 'whitespace' }; +export type Digit = { type: 'digit' }; +export type Word = { type: 'word' }; +export type Any = { type: 'any' }; // Quantifiers export type One = {