diff --git a/README.md b/README.md index 35580cc..d5769fd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ const hexColor = buildRegex( optionally('#'), capture( choiceOf( - repeat({ count: 6 }, hexDigit), // #rrggbb - repeat({ count: 3 }, hexDigit), // #rgb + repeat(hexDigit, { count: 6 }), // #rrggbb + repeat(hexDigit, { count: 3 }), // #rgb ), ), endOfString, @@ -90,7 +90,7 @@ const currencyAmount = buildRegex([ | Regex Component | Regex Pattern | Description | | --------------------------------------- | ------------- | ----------------------------------- | | `buildRegex(...)` | `/.../` | Create `RegExp` instance | -| `buildRegex({ ignoreCase: true }, ...)` | `/.../i` | Create `RegExp` instance with flags | +| `buildRegex(..., { ignoreCase: true })` | `/.../i` | Create `RegExp` instance with flags | ### Components @@ -111,9 +111,9 @@ Notes: | `zeroOrMore(x)` | `x*` | Zero or more occurence of a pattern | | `oneOrMore(x)` | `x+` | One or more occurence of a pattern | | `optionally(x)` | `x?` | Zero or one occurence of a pattern | -| `repeat({ count: n }, x)` | `x{n}` | Pattern repeats exact number of times | -| `repeat({ min: n, }, x)` | `x{n,}` | Pattern repeats at least given number of times | -| `repeat({ min: n, max: n2 }, x)` | `x{n1,n2}` | Pattern repeats between n1 and n2 number of times | +| `repeat(x, { count: n })` | `x{n}` | Pattern repeats exact number of times | +| `repeat(x, { min: n, })` | `x{n,}` | Pattern repeats at least given number of times | +| `repeat(x, { min: n, max: n2 })` | `x{n1,n2}` | Pattern repeats between n1 and n2 number of times | All quantifiers accept sequence of elements @@ -133,7 +133,7 @@ All quantifiers accept sequence of elements Notes: - `any`, `word`, `digit`, `whitespace` are objects, no need to call them -- `anyof` accepts a single string of characters to match +- `anyOf` accepts a single string of characters to match - `charRange` accepts exactly **two single character** strings representing range start and end (inclusive) - `charClass` accepts a variable number of character classes to join into a single class - `inverted` accepts a single character class to be inverted diff --git a/docs/Examples.md b/docs/Examples.md index 32505d7..910fd32 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -14,14 +14,9 @@ const octet = choiceOf( // Match const regex = buildRegex([ - startOfString, - capture(octet), - '.', - capture(octet), - '.', - capture(octet), - '.', - capture(octet), + startOfString, // + repeat([octet, '.'], { count: 3 }), + octet, endOfString, ]); ``` @@ -30,12 +25,5 @@ This code generates the following regex pattern: ```ts const regex = - /^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/; -``` - -This pattern uses repetition of the `capture(octet)` elements to generate capture groups for each of the IPv4 octets: - -```ts -// Matched groups ['192.168.0.1', '192', '168', '0', '1',] -const match = regex.exec('192.168.0.1'); + /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/; ``` diff --git a/src/__tests__/builder.test.ts b/src/__tests__/builder.test.ts index 2488a42..42d8b8e 100644 --- a/src/__tests__/builder.test.ts +++ b/src/__tests__/builder.test.ts @@ -2,22 +2,28 @@ import { buildRegex } from '../builders'; test('`regexBuilder` flags', () => { expect(buildRegex('a').flags).toBe(''); - expect(buildRegex({}, 'a').flags).toBe(''); + expect(buildRegex('a', {}).flags).toBe(''); - expect(buildRegex({ global: true }, 'a').flags).toBe('g'); - expect(buildRegex({ global: false }, 'a').flags).toBe(''); + expect(buildRegex('a', { global: true }).flags).toBe('g'); + expect(buildRegex('a', { global: false }).flags).toBe(''); - expect(buildRegex({ ignoreCase: true }, 'a').flags).toBe('i'); - expect(buildRegex({ ignoreCase: false }, 'a').flags).toBe(''); + expect(buildRegex('a', { ignoreCase: true }).flags).toBe('i'); + expect(buildRegex('a', { ignoreCase: false }).flags).toBe(''); - expect(buildRegex({ multiline: true }, 'a').flags).toBe('m'); - expect(buildRegex({ multiline: false }, 'a').flags).toBe(''); + expect(buildRegex('a', { multiline: true }).flags).toBe('m'); + expect(buildRegex('a', { multiline: false }).flags).toBe(''); - expect(buildRegex({ hasIndices: true }, 'a').flags).toBe('d'); - expect(buildRegex({ hasIndices: false }, 'a').flags).toBe(''); + expect(buildRegex('a', { hasIndices: true }).flags).toBe('d'); + expect(buildRegex('a', { hasIndices: false }).flags).toBe(''); - expect(buildRegex({ sticky: true }, 'a').flags).toBe('y'); - expect(buildRegex({ sticky: false }, 'a').flags).toBe(''); + expect(buildRegex('a', { sticky: true }).flags).toBe('y'); + expect(buildRegex('a', { sticky: false }).flags).toBe(''); - expect(buildRegex({ global: true, ignoreCase: true, multiline: false }, 'a').flags).toBe('gi'); + expect( + buildRegex('a', { + global: true, // + ignoreCase: true, + multiline: false, + }).flags + ).toBe('gi'); }); diff --git a/src/__tests__/examples.test.ts b/src/__tests__/examples.test.ts new file mode 100644 index 0000000..c7845fe --- /dev/null +++ b/src/__tests__/examples.test.ts @@ -0,0 +1,43 @@ +import { + buildRegex, + charRange, + choiceOf, + digit, + endOfString, + repeat, + startOfString, +} from '../index'; + +test('example: IPv4 address validator', () => { + const octet = choiceOf( + [digit], + [charRange('1', '9'), digit], + ['1', repeat(digit, { count: 2 })], + ['2', charRange('0', '4'), digit], + ['25', charRange('0', '5')] + ); + + const regex = buildRegex([ + startOfString, // + repeat([octet, '.'], { count: 3 }), + octet, + endOfString, + ]); + + expect(regex).toMatchString('0.0.0.0'); + expect(regex).toMatchString('192.168.0.1'); + expect(regex).toMatchString('1.99.100.249'); + expect(regex).toMatchString('255.255.255.255'); + expect(regex).toMatchString('123.45.67.89'); + + expect(regex).not.toMatchString('0.0.0.'); + expect(regex).not.toMatchString('0.0.0.0.'); + expect(regex).not.toMatchString('0.-1.0.0'); + expect(regex).not.toMatchString('0.1000.0.0'); + expect(regex).not.toMatchString('0.0.300.0'); + expect(regex).not.toMatchString('255.255.255.256'); + + expect(regex).toHavePattern( + /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ + ); +}); diff --git a/src/__tests__/examples.ts b/src/__tests__/examples.ts deleted file mode 100644 index 4848d9f..0000000 --- a/src/__tests__/examples.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - buildRegex, - capture, - charRange, - choiceOf, - digit, - endOfString, - repeat, - startOfString, -} from '../index'; - -test('example: IPv4 address validator', () => { - const octet = choiceOf( - [digit], - [charRange('1', '9'), digit], - ['1', repeat({ count: 2 }, digit)], - ['2', charRange('0', '4'), digit], - ['25', charRange('0', '5')] - ); - - const regex = buildRegex([ - startOfString, - capture(octet), - '.', - capture(octet), - '.', - capture(octet), - '.', - capture(octet), - endOfString, - ]); - - expect(regex).toMatchGroups('0.0.0.0', ['0.0.0.0', '0', '0', '0', '0']); - expect(regex).toMatchGroups('192.168.0.1', ['192.168.0.1', '192', '168', '0', '1']); - expect(regex).toMatchGroups('1.99.100.249', ['1.99.100.249', '1', '99', '100', '249']); - expect(regex).toMatchGroups('255.255.255.255', ['255.255.255.255', '255', '255', '255', '255']); - expect(regex).toMatchGroups('123.45.67.89', ['123.45.67.89', '123', '45', '67', '89']); - - expect(regex).not.toMatchString('0.0.0.'); - expect(regex).not.toMatchString('0.0.0.0.'); - expect(regex).not.toMatchString('0.-1.0.0'); - expect(regex).not.toMatchString('0.1000.0.0'); - expect(regex).not.toMatchString('0.0.300.0'); - expect(regex).not.toMatchString('255.255.255.256'); - - expect(regex).toHavePattern( - /^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ - ); -}); diff --git a/src/builders.ts b/src/builders.ts index e230a6f..e63f36f 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -1,7 +1,6 @@ import type { RegexSequence } from './types'; import { encodeSequence } from './encoder/encoder'; import { asNodeArray } from './utils/nodes'; -import { optionalFirstArg } from './utils/optional-arg'; export interface RegexFlags { /** Global search. */ @@ -21,27 +20,13 @@ export interface RegexFlags { } /** - * Generate RegExp object from elements. - * - * @param elements Single regex element or array of elements - * @returns - */ -export function buildRegex(sequence: RegexSequence): RegExp; - -/** - * Generate RegExp object from elements with passed flags. + * Generate RegExp object from elements with optional flags. * * @param elements Single regex element or array of elements * @param flags RegExp flags object * @returns RegExp object */ -export function buildRegex(flags: RegexFlags, sequence: RegexSequence): RegExp; - -export function buildRegex(first: any, second?: any): RegExp { - return _buildRegex(...optionalFirstArg(first, second)); -} - -export function _buildRegex(flags: RegexFlags, sequence: RegexSequence): RegExp { +export function buildRegex(sequence: RegexSequence, flags?: RegexFlags): RegExp { const pattern = encodeSequence(asNodeArray(sequence)).pattern; const flagsString = encodeFlags(flags ?? {}); return new RegExp(pattern, flagsString); diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index 9ce9324..6533663 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -29,7 +29,7 @@ test('`choiceOf` with sequence options', () => { test('`choiceOf` using nested regex', () => { expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern(/a+|b*/); - expect(choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx'))).toHavePattern( + expect(choiceOf(repeat('a', { min: 1, max: 3 }), repeat('bx', { count: 5 }))).toHavePattern( /a{1,3}|(?:bx){5}/ ); }); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index 7941b82..33d738a 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -3,22 +3,22 @@ import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; test('`repeat` quantifier', () => { - expect(['a', repeat({ min: 1, max: 5 }, 'b')]).toHavePattern(/ab{1,5}/); - expect(['a', repeat({ min: 1 }, 'b')]).toHavePattern(/ab{1,}/); - expect(['a', repeat({ count: 1 }, 'b')]).toHavePattern(/ab{1}/); + expect(['a', repeat('b', { min: 1, max: 5 })]).toHavePattern(/ab{1,5}/); + expect(['a', repeat('b', { min: 1 })]).toHavePattern(/ab{1,}/); + expect(['a', repeat('b', { count: 1 })]).toHavePattern(/ab{1}/); - expect(['a', repeat({ count: 1 }, ['a', zeroOrMore('b')])]).toHavePattern(/a(?:ab*){1}/); - expect(repeat({ count: 5 }, ['text', ' ', oneOrMore('d')])).toHavePattern(/(?:text d+){5}/); + expect(['a', repeat(['a', zeroOrMore('b')], { count: 1 })]).toHavePattern(/a(?:ab*){1}/); + expect(repeat(['text', ' ', oneOrMore('d')], { count: 5 })).toHavePattern(/(?:text d+){5}/); }); test('`repeat` optimizes grouping for atoms', () => { - expect(repeat({ count: 2 }, digit)).toHavePattern(/\d{2}/); - expect(repeat({ min: 2 }, digit)).toHavePattern(/\d{2,}/); - expect(repeat({ min: 1, max: 5 }, digit)).toHavePattern(/\d{1,5}/); + expect(repeat(digit, { count: 2 })).toHavePattern(/\d{2}/); + expect(repeat(digit, { min: 2 })).toHavePattern(/\d{2,}/); + expect(repeat(digit, { min: 1, max: 5 })).toHavePattern(/\d{1,5}/); }); test('`repeat` throws on no children', () => { - expect(() => repeat({ count: 1 }, [])).toThrowErrorMatchingInlineSnapshot( + expect(() => repeat([], { count: 1 })).toThrowErrorMatchingInlineSnapshot( `"\`repeat\` should receive at least one element"` ); }); diff --git a/src/components/repeat.ts b/src/components/repeat.ts index 7d20a79..286c252 100644 --- a/src/components/repeat.ts +++ b/src/components/repeat.ts @@ -11,7 +11,7 @@ export interface Repeat extends RegexElement { export type RepeatOptions = { count: number } | { min: number; max?: number }; -export function repeat(options: RepeatOptions, sequence: RegexSequence): Repeat { +export function repeat(sequence: RegexSequence, options: RepeatOptions): Repeat { const children = asNodeArray(sequence); if (children.length === 0) { diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index 0418e01..cd730aa 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -13,7 +13,7 @@ test('basic quantifies', () => { expect(['a', oneOrMore('bc')]).toHavePattern(/a(?:bc)+/); expect(['a', oneOrMore('bc')]).toHavePattern(/a(?:bc)+/); - expect(['a', repeat({ min: 1, max: 5 }, 'b')]).toHavePattern(/ab{1,5}/); + expect(['a', repeat('b', { min: 1, max: 5 })]).toHavePattern(/ab{1,5}/); expect(['a', zeroOrMore('b')]).toHavePattern(/ab*/); expect(['a', zeroOrMore('bc')]).toHavePattern(/a(?:bc)*/); diff --git a/src/utils/optional-arg.ts b/src/utils/optional-arg.ts deleted file mode 100644 index dc47f81..0000000 --- a/src/utils/optional-arg.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Re-arranges passed arguments to allow for optional first parameter. - * - * @param first first argument received by the function - * @param second second argument received by the function - * - * @example - * function _capture(options: CaptureOptions, children: RegexElement[]): Capture { - * return { - * type: 'capture', - * options, - * children, - * }; - * } - * - * // Without `config`` - * function capture(element: RegexElement | RegexElement[]): Capture - * - * // With `config`` - * function capture(config: CaptureConfig, element: RegexElement | RegexElement[]): Capture - * - * function capture(first: any, second: any) { - * return _capture(...componentArgs(first, second)); - * } - * - * @category Function - */ -export function optionalFirstArg(first: any, second?: any) { - if (second === undefined) { - return [{}, first] as const; - } - - return [first, second] as const; -}