diff --git a/src/__tests__/example-js-number.ts b/src/__tests__/example-js-number.ts index 683e717..9c92ff0 100644 --- a/src/__tests__/example-js-number.ts +++ b/src/__tests__/example-js-number.ts @@ -6,15 +6,16 @@ import { endOfString, oneOrMore, optional, + regex, startOfString, zeroOrMore, } from '..'; test('example: validate JavaScript number', () => { const sign = anyOf('+-'); - const exponent = [anyOf('eE'), optional(sign), oneOrMore(digit)]; + const exponent = regex([anyOf('eE'), optional(sign), oneOrMore(digit)]); - const regex = buildRegExp([ + const numberValidator = buildRegExp([ startOfString, optional(sign), choiceOf( @@ -25,26 +26,26 @@ test('example: validate JavaScript number', () => { endOfString, ]); - expect(regex).toMatchString('0'); - expect(regex).toMatchString('-1'); - expect(regex).toMatchString('+1'); - expect(regex).toMatchString('1.0'); - expect(regex).toMatchString('1.1234'); - expect(regex).toMatchString('1.'); - expect(regex).toMatchString('.1'); - expect(regex).toMatchString('-.1234'); - expect(regex).toMatchString('+.5'); - expect(regex).toMatchString('1e21'); - expect(regex).toMatchString('1e-21'); - expect(regex).toMatchString('+1e+42'); - expect(regex).toMatchString('-1e-42'); + expect(numberValidator).toMatchString('0'); + expect(numberValidator).toMatchString('-1'); + expect(numberValidator).toMatchString('+1'); + expect(numberValidator).toMatchString('1.0'); + expect(numberValidator).toMatchString('1.1234'); + expect(numberValidator).toMatchString('1.'); + expect(numberValidator).toMatchString('.1'); + expect(numberValidator).toMatchString('-.1234'); + expect(numberValidator).toMatchString('+.5'); + expect(numberValidator).toMatchString('1e21'); + expect(numberValidator).toMatchString('1e-21'); + expect(numberValidator).toMatchString('+1e+42'); + expect(numberValidator).toMatchString('-1e-42'); - expect(regex).not.toMatchString(''); - expect(regex).not.toMatchString('a'); - expect(regex).not.toMatchString('1a'); - expect(regex).not.toMatchString('1.0.'); - expect(regex).not.toMatchString('.1.1'); - expect(regex).not.toMatchString('.'); + expect(numberValidator).not.toMatchString(''); + expect(numberValidator).not.toMatchString('a'); + expect(numberValidator).not.toMatchString('1a'); + expect(numberValidator).not.toMatchString('1.0.'); + expect(numberValidator).not.toMatchString('.1.1'); + expect(numberValidator).not.toMatchString('.'); - expect(regex).toEqualRegex(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/); + expect(numberValidator).toEqualRegex(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/); }); diff --git a/src/constructs/__tests__/regex.test.tsx b/src/constructs/__tests__/regex.test.tsx new file mode 100644 index 0000000..e093a06 --- /dev/null +++ b/src/constructs/__tests__/regex.test.tsx @@ -0,0 +1,7 @@ +import { regex } from '../..'; + +test('`regex` no-op pattern', () => { + expect(regex('a')).toEqualRegex(/a/); + expect(regex(['a', 'b'])).toEqualRegex(/ab/); + expect([regex('a'), regex(['b', 'c'])]).toEqualRegex(/abc/); +}); diff --git a/src/constructs/regex.ts b/src/constructs/regex.ts new file mode 100644 index 0000000..58f4317 --- /dev/null +++ b/src/constructs/regex.ts @@ -0,0 +1,21 @@ +import { encodeSequence } from '../encoder/encoder'; +import type { EncodeResult } from '../encoder/types'; +import { ensureArray } from '../utils/elements'; +import type { RegexConstruct, RegexElement, RegexSequence } from '../types'; + +export interface Regex extends RegexConstruct { + type: 'sequence'; + children: RegexElement[]; +} + +export function regex(sequence: RegexSequence): Regex { + return { + type: 'sequence', + children: ensureArray(sequence), + encode: encodeRegex, + }; +} + +function encodeRegex(this: Regex): EncodeResult { + return encodeSequence(this.children); +} diff --git a/src/index.ts b/src/index.ts index d07b03b..e0576b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,4 +33,5 @@ export { lookbehind } from './constructs/lookbehind'; export { negativeLookahead } from './constructs/negative-lookahead'; export { negativeLookbehind } from './constructs/negative-lookbehind'; export { oneOrMore, optional, zeroOrMore } from './constructs/quantifiers'; +export { regex } from './constructs/regex'; export { repeat } from './constructs/repeat'; diff --git a/website/docs/api/constructs.md b/website/docs/api/constructs.md index fa3f778..b6ab438 100644 --- a/website/docs/api/constructs.md +++ b/website/docs/api/constructs.md @@ -9,7 +9,7 @@ These functions and objects represent available regex constructs. ```ts function choiceOf( - ...alternatives: RegexSequence[] + ...alternatives: RegexSequence[], ): ChoiceOf { ``` @@ -22,7 +22,9 @@ Example: `choiceOf("color", "colour")` matches either `color` or `colour` patter ### `capture()` ```ts -function capture(sequence: RegexSequence): Capture; +function capture( + sequence: RegexSequence, +): Capture; ``` Regex syntax: `(...)`. @@ -35,3 +37,34 @@ TS Regex Builder does not have a construct for non-capturing groups. Such groups ::: +### `regex()` + +```ts +function regex( + sequence: RegexSequence, +): Regex; +``` + +Regex syntax: the pattern remains unchanged when wrapped by this construct. + +This construct is a no-op operator that groups array of `RegexElements` into a single element for composition purposes. This is particularly useful for defining smaller sequence patterns as separate variables. + +Without `regex()`: + +```ts +const exponent = [anyOf('eE'), optional(anyOf('+-')), oneOrMore(digit)]; +const numberWithExponent = buildRegExp([ + oneOrMore(digit), + ...exponent, // Need to spread "exponent" as it's an array. +]); +``` + +With `regex()`: + +```ts +const exponent = regex([anyOf('eE'), optional(anyOf('+-')), oneOrMore(digit)]); +const numberWithExponent = buildRegExp([ + oneOrMore(digit), + exponent, // Easily compose "exponent" sequence as a single element. +]); +```