diff --git a/src/character-classes.ts b/src/character-classes.ts deleted file mode 100644 index 9a25c83..0000000 --- a/src/character-classes.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/character-classes/__tests__/any-of.test.ts b/src/character-classes/__tests__/any-of.test.ts new file mode 100644 index 0000000..0876929 --- /dev/null +++ b/src/character-classes/__tests__/any-of.test.ts @@ -0,0 +1,23 @@ +import { buildPattern as p } from '../../compiler'; +import { oneOrMore } from '../../quantifiers/base'; +import { anyOf } from '../any-of'; + +test('"anyOf" base cases', () => { + expect(p(anyOf(''))).toBe(''); + expect(p(anyOf('a'))).toBe('a'); + expect(p(anyOf('abc'))).toBe('[abc]'); +}); + +test('"anyOf" in context', () => { + expect(p('x', anyOf('a'), 'x')).toBe('xax'); + expect(p('x', anyOf('abc'), 'x')).toBe('x[abc]x'); + expect(p('x', oneOrMore(anyOf('abc')), 'x')).toBe('x(?:[abc])+x'); +}); + +test('"anyOf" escapes special characters', () => { + expect(p(anyOf('abc-+.'))).toBe('[-abc\\+\\.]'); +}); + +test('"anyOf" moves hyphen to the first position', () => { + expect(p(anyOf('a-bc'))).toBe('[-abc]'); +}); diff --git a/src/__tests__/characterClasses.test.tsx b/src/character-classes/__tests__/base.test.ts similarity index 85% rename from src/__tests__/characterClasses.test.tsx rename to src/character-classes/__tests__/base.test.ts index 6081cf5..0638662 100644 --- a/src/__tests__/characterClasses.test.tsx +++ b/src/character-classes/__tests__/base.test.ts @@ -1,6 +1,6 @@ -import { any, digit, whitespace, word } from '../character-classes'; -import { buildPattern } from '../compiler'; -import { one } from '../quantifiers/base'; +import { any, digit, whitespace, word } from '../base'; +import { buildPattern } from '../../compiler'; +import { one } from '../../quantifiers/base'; test('"whitespace" character class', () => { expect(buildPattern(whitespace)).toEqual(`\\s`); diff --git a/src/character-classes/any-of.ts b/src/character-classes/any-of.ts new file mode 100644 index 0000000..6776b38 --- /dev/null +++ b/src/character-classes/any-of.ts @@ -0,0 +1,9 @@ +import type { CharacterClass } from '../types'; +import { escapeText } from '../utils'; + +export function anyOf(characters: string): CharacterClass { + return { + type: 'characterClass', + characters: characters.split('').map(escapeText), + }; +} diff --git a/src/character-classes/base.ts b/src/character-classes/base.ts new file mode 100644 index 0000000..6042cff --- /dev/null +++ b/src/character-classes/base.ts @@ -0,0 +1,21 @@ +import type { CharacterClass } from '../types'; + +export const whitespace: CharacterClass = { + type: 'characterClass', + characters: ['\\s'], +}; + +export const digit: CharacterClass = { + type: 'characterClass', + characters: ['\\d'], +}; + +export const word: CharacterClass = { + type: 'characterClass', + characters: ['\\w'], +}; + +export const any: CharacterClass = { + type: 'characterClass', + characters: ['.'], +}; diff --git a/src/character-classes/compiler.ts b/src/character-classes/compiler.ts new file mode 100644 index 0000000..5ce0a46 --- /dev/null +++ b/src/character-classes/compiler.ts @@ -0,0 +1,24 @@ +import type { CharacterClass } from '../types'; + +export function compileCharacterClass({ characters }: CharacterClass): string { + if (characters.length === 0) { + return ''; + } + + if (characters.length === 1) { + return characters[0]!; + } + + return `[${escapeHyphen(characters).join('')}]`; +} + +// 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 +function escapeHyphen(characters: string[]) { + if (characters.includes('-')) { + return ['-', ...characters.filter((c) => c !== '-')]; + } + + return characters; +} diff --git a/src/compiler.ts b/src/compiler.ts index 5775166..c3cbf82 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,6 +1,6 @@ import type { RegexElement } from './types'; -import { characterClasses, isCharacterClass } from './character-classes'; import { compileChoiceOf } from './components/choiceOf'; +import { compileCharacterClass } from './character-classes/compiler'; import { baseQuantifiers, isBaseQuantifier } from './quantifiers/base'; import { compileRepeat } from './quantifiers/repeat'; import { escapeText } from './utils'; @@ -36,8 +36,8 @@ function compileSingle(element: RegexElement): string { return escapeText(element); } - if (isCharacterClass(element)) { - return characterClasses[element.type]; + if (element.type === 'characterClass') { + return compileCharacterClass(element); } if (element.type === 'choiceOf') { diff --git a/src/index.ts b/src/index.ts index bf99e6a..9e536e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ export type * from './types'; -export { any, digit, whitespace, word } from './character-classes'; export { buildRegex, buildPattern } from './compiler'; + +export { any, digit, whitespace, word } from './character-classes/base'; +export { anyOf } from './character-classes/any-of'; export { one, oneOrMore, optionally, zeroOrMore } from './quantifiers/base'; export { repeat } from './quantifiers/repeat'; export { choiceOf } from './components/choiceOf'; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..abc20f1 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,5 @@ +export type * from './types'; + +export { whitespace } from './character-classes/base'; +export { buildRegex, buildPattern } from './compiler'; +export { oneOrMore, optionally } from './quantifiers/base'; diff --git a/src/__tests__/quantifiers.test.tsx b/src/quantifiers/__tests__/base.test.tsx similarity index 91% rename from src/__tests__/quantifiers.test.tsx rename to src/quantifiers/__tests__/base.test.tsx index 2788ae1..05cd750 100644 --- a/src/__tests__/quantifiers.test.tsx +++ b/src/quantifiers/__tests__/base.test.tsx @@ -1,5 +1,5 @@ -import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers/base'; -import { buildPattern, buildRegex } from '../compiler'; +import { one, oneOrMore, optionally, zeroOrMore } from '../base'; +import { buildPattern, buildRegex } from '../../compiler'; test('"oneOrMore" quantifier', () => { expect(buildPattern(oneOrMore('a'))).toEqual('a+'); diff --git a/src/__tests__/repeat.test.tsx b/src/quantifiers/__tests__/repeat.test.tsx similarity index 75% rename from src/__tests__/repeat.test.tsx rename to src/quantifiers/__tests__/repeat.test.tsx index 2305ea8..d0015c7 100644 --- a/src/__tests__/repeat.test.tsx +++ b/src/quantifiers/__tests__/repeat.test.tsx @@ -1,6 +1,6 @@ -import { buildPattern } from '../compiler'; -import { zeroOrMore, oneOrMore } from '../quantifiers/base'; -import { repeat } from '../quantifiers/repeat'; +import { buildPattern } from '../../compiler'; +import { zeroOrMore, oneOrMore } from '../base'; +import { repeat } from '../repeat'; test('"repeat" quantifier', () => { expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); diff --git a/src/quantifiers/base.ts b/src/quantifiers/base.ts index 0564322..7f12221 100644 --- a/src/quantifiers/base.ts +++ b/src/quantifiers/base.ts @@ -8,23 +8,23 @@ import type { } from '../types'; import { wrapGroup } from '../utils'; -export function oneOrMore(...children: RegexElement[]): OneOrMore { +export function one(...children: RegexElement[]): One { return { - type: 'oneOrMore', + type: 'one', children, }; } -export function optionally(...children: RegexElement[]): Optionally { +export function oneOrMore(...children: RegexElement[]): OneOrMore { return { - type: 'optionally', + type: 'oneOrMore', children, }; } -export function one(...children: RegexElement[]): One { +export function optionally(...children: RegexElement[]): Optionally { return { - type: 'one', + type: 'optionally', children, }; } diff --git a/src/types.ts b/src/types.ts index ee4beaa..32b4ed6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,11 @@ export type RegexElement = string | ChoiceOf | 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' }; +export type CharacterClass = { + type: 'characterClass'; + characters: string[]; +}; // Components export type ChoiceOf = {