diff --git a/docs/content/2.getting-started/2.usage.md b/docs/content/2.getting-started/2.usage.md index 64311635..dacbd7cb 100644 --- a/docs/content/2.getting-started/2.usage.md +++ b/docs/content/2.getting-started/2.usage.md @@ -15,7 +15,7 @@ console.log(regExp) Every pattern you create with the library should be wrapped in `createRegExp`, which enables the build-time transform. -The first argument is either a string to match exactly, or an input pattern built up using helpers from `magic-regexp`. It also takes a second argument, which is an array of flags or flags string. +`createRegExp` accepts an arbitrary number of arguments of type `string` or `Input` (built up using helpers from `magic-regexp`), and an optional final argument of an array of flags or a flags string. It creates a `MagicRegExp`, which concatenates all the patterns from the arguments that were passed in. ```js import { createRegExp, global, multiline, exactly } from 'magic-regexp' @@ -25,6 +25,16 @@ createRegExp(exactly('foo').or('bar')) createRegExp('string-to-match', [global, multiline]) // you can also pass flags directly as strings or Sets createRegExp('string-to-match', ['g', 'm']) + +// or pass in multiple `string` and `input patterns`, +// all inputs will be concatenated to one RegExp pattern +createRegExp( + 'foo', + maybe('bar').groupedAs('g1'), + 'baz', + [global, multiline] +) +// equivalent to /foo(?(?:bar)?)baz/gm ``` ::alert @@ -38,12 +48,16 @@ There are a range of helpers that can be used to activate pattern matching, and | | | | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `charIn`, `charNotIn` | this matches or doesn't match any character in the string provided. | -| `anyOf` | this takes an array of inputs and matches any of them. | +| `anyOf` | this takes a variable number of inputs and matches any of them. | | `char`, `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` and `carriageReturn` | these are helpers for specific RegExp characters. | | `not` | this can prefix `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` or `carriageReturn`. For example `createRegExp(not.letter)`. | -| `maybe` | equivalent to `?` - this marks the input as optional. | -| `oneOrMore` | Equivalent to `+` - this marks the input as repeatable, any number of times but at least once. | -| `exactly` | This escapes a string input to match it exactly. | +| `maybe` | equivalent to `?` - this takes a variable number of inputs and marks them as optional. | +| `oneOrMore` | Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once. | +| `exactly` | This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly. | + +::alert +All helpers that takes `string` and `Input` are variadic functions, so you can pass in one or multiple arguments of `string` or `Input` to them and they will be concatenated to one pattern. for example,s `exactly('foo', maybe('bar'))` is equivalent to `exactly('foo').and(maybe('bar'))`. +:: ## Chaining inputs @@ -51,9 +65,9 @@ All of the helpers above return an object of type `Input` that can be chained wi | | | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `and` | this adds a new pattern to the current input, or you can use `and.referenceTo(groupName)` to adds a new pattern referencing to a named group. | -| `or` | this provides an alternative to the current input. | -| `after`, `before`, `notAfter` and `notBefore` | these activate positive/negative lookahead/lookbehinds. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari). | +| `and` | this takes a variable number of inputs and adds them as new pattern to the current input, or you can use `and.referenceTo(groupName)` to adds a new pattern referencing to a named group. | +| `or` | this takes a variable number of inputs and provides as an alternative to the current input. | +| `after`, `before`, `notAfter` and `notBefore` | these takes a variable number of inputs and activate positive/negative lookahead/lookbehinds. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari). | | `times` | this is a function you can call directly to repeat the previous pattern an exact number of times, or you can use `times.between(min, max)` to specify a range, `times.atLeast(x)` to indicate it must repeat at least x times, `times.atMost(x)` to indicate it must repeat at most x times or `times.any()` to indicate it can repeat any number of times, _including none_. | | `optionally` | this is a function you can call to mark the current input as optional. | | `as` | alias for `groupedAs` | diff --git a/docs/content/2.getting-started/3.examples.md b/docs/content/2.getting-started/3.examples.md index e9a3adfc..9714e7a0 100644 --- a/docs/content/2.getting-started/3.examples.md +++ b/docs/content/2.getting-started/3.examples.md @@ -5,14 +5,13 @@ title: Examples ### Quick-and-dirty semver ```js -import { createRegExp, exactly, oneOrMore, digit, char } from 'magic-regexp' +import { createRegExp, exactly, maybe, oneOrMore, digit, char } from 'magic-regexp' createRegExp( - oneOrMore(digit) - .groupedAs('major') - .and('.') - .and(oneOrMore(digit).groupedAs('minor')) - .and(exactly('.').and(oneOrMore(char).groupedAs('patch')).optionally()) + oneOrMore(digit).groupedAs('major'), + '.', + oneOrMore(digit).groupedAs('minor'), + maybe('.', oneOrMore(char).groupedAs('patch')) ) // /(?\d+)\.(?\d+)(?:\.(?.+))?/ ``` diff --git a/src/core/inputs.ts b/src/core/inputs.ts index 597e882c..276a086a 100644 --- a/src/core/inputs.ts +++ b/src/core/inputs.ts @@ -1,14 +1,7 @@ import { createInput, Input } from './internal' -import type { GetValue, EscapeChar } from './types/escape' +import type { EscapeChar } from './types/escape' import type { Join } from './types/join' -import type { - MapToGroups, - MapToValues, - InputSource, - GetGroup, - MapToCapturedGroupsArr, - GetCapturedGroupsArr, -} from './types/sources' +import type { MapToGroups, MapToValues, InputSource, MapToCapturedGroupsArr } from './types/sources' import { IfUnwrapped, wrap } from './wrap' export type { Input } @@ -23,13 +16,15 @@ export const charIn = (chars: T) => export const charNotIn = (chars: T) => createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[^${EscapeChar}]`> -/** This takes an array of inputs and matches any of them */ -export const anyOf = (...args: New) => - createInput(`(?:${args.map(a => exactly(a)).join('|')})`) as Input< - `(?:${Join>})`, - MapToGroups, - MapToCapturedGroupsArr - > +/** This takes a variable number of inputs and matches any of them + * @example + * anyOf('foo', maybe('bar'), 'baz') // => /(?:foo|(?:bar)?|baz)/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ +export const anyOf = ( + ...inputs: Inputs +): Input<`(?:${Join>})`, MapToGroups, MapToCapturedGroupsArr> => + createInput(`(?:${inputs.map(a => exactly(a)).join('|')})`) export const char = createInput('.') export const word = createInput('\\b\\w+\\b') @@ -59,24 +54,48 @@ export const not = { carriageReturn: createInput('[^\\r]'), } -/** Equivalent to `?` - this marks the input as optional */ -export const maybe = (str: New) => - createInput(`${wrap(exactly(str))}?`) as Input< - IfUnwrapped, `(?:${GetValue})?`, `${GetValue}?`>, - GetGroup, - GetCapturedGroupsArr - > +/** Equivalent to `?` - takes a variable number of inputs and marks them as optional + * @example + * maybe('foo', excatly('ba?r')) // => /(?:fooba\?r)?/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ +export const maybe = < + Inputs extends InputSource[], + Value extends string = Join, '', ''> +>( + ...inputs: Inputs +): Input< + IfUnwrapped, + MapToGroups, + MapToCapturedGroupsArr +> => createInput(`${wrap(exactly(...inputs))}?`) -/** This escapes a string input to match it exactly */ -export const exactly = ( - input: New -): Input, GetGroup, GetCapturedGroupsArr> => - typeof input === 'string' ? (createInput(input.replace(ESCAPE_REPLACE_RE, '\\$&')) as any) : input +/** This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly + * @example + * exactly('fo?o', maybe('bar')) // => /fo\?o(?:bar)?/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ +export const exactly = ( + ...inputs: Inputs +): Input, '', ''>, MapToGroups, MapToCapturedGroupsArr> => + createInput( + inputs + .map(input => (typeof input === 'string' ? input.replace(ESCAPE_REPLACE_RE, '\\$&') : input)) + .join('') + ) -/** Equivalent to `+` - this marks the input as repeatable, any number of times but at least once */ -export const oneOrMore = (str: New) => - createInput(`${wrap(exactly(str))}+`) as Input< - IfUnwrapped, `(?:${GetValue})+`, `${GetValue}+`>, - GetGroup, - GetCapturedGroupsArr - > +/** Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once + * @example + * oneOrMore('foo', maybe('bar')) // => /(?:foo(?:bar)?)+/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ +export const oneOrMore = < + Inputs extends InputSource[], + Value extends string = Join, '', ''> +>( + ...inputs: Inputs +): Input< + IfUnwrapped, + MapToGroups, + MapToCapturedGroupsArr +> => createInput(`${wrap(exactly(...inputs))}+`) diff --git a/src/core/internal.ts b/src/core/internal.ts index 09dac307..e364d4c2 100644 --- a/src/core/internal.ts +++ b/src/core/internal.ts @@ -1,6 +1,6 @@ import { exactly } from './inputs' -import type { GetValue } from './types/escape' -import type { GetCapturedGroupsArr, InputSource } from './types/sources' +import { Join } from './types/join' +import type { InputSource, MapToCapturedGroupsArr, MapToGroups, MapToValues } from './types/sources' import { IfUnwrapped, wrap } from './wrap' const GROUPED_AS_REPLACE_RE = /^(?:\(\?:(.+)\)|(\(?.+\)?))$/ @@ -11,40 +11,60 @@ export interface Input< G extends string = never, C extends (string | undefined)[] = [] > { - /** this adds a new pattern to the current input */ + /** this takes a variable number of inputs and adds them as new pattern to the current input, or you can use `and.referenceTo(groupName)` to adds a new pattern referencing to a named group + * @example + * exactly('foo').and('bar', maybe('baz')) // => /foobar(?:baz)?/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ and: { - (input: I): Input< - `${V}${GetValue}`, - G | (I extends Input ? NewGroups : never), - [...C, ...GetCapturedGroupsArr] + >(...inputs: I): Input< + `${V}${Join, '', ''>}`, + G | MapToGroups, + [...C, ...CG] > /** this adds a new pattern to the current input, with the pattern reference to a named group. */ referenceTo: (groupName: N) => Input<`${V}\\k<${N}>`, G, C> } - /** this provides an alternative to the current input */ - or: ( - input: I - ) => Input< - `(?:${V}|${GetValue})`, - G | (I extends Input ? NewGroups : never), - [...C, ...GetCapturedGroupsArr] - > - /** this is a positive lookbehind. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari) */ - after: ( - input: I - ) => Input<`(?<=${GetValue})${V}`, G, [...GetCapturedGroupsArr, ...C]> - /** this is a positive lookahead */ - before: ( - input: I - ) => Input<`${V}(?=${GetValue})`, G, [...C, ...GetCapturedGroupsArr]> - /** these is a negative lookbehind. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari) */ - notAfter: ( - input: I - ) => Input<`(?})${V}`, G, [...GetCapturedGroupsArr, ...C]> - /** this is a negative lookahead */ - notBefore: ( - input: I - ) => Input<`${V}(?!${GetValue})`, G, [...C, ...GetCapturedGroupsArr]> + /** this takes a variable number of inputs and provides as an alternative to the current input + * @example + * exactly('foo').or('bar', maybe('baz')) // => /foo|bar(?:baz)?/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ + or: >( + ...inputs: I + ) => Input<`(?:${V}|${Join, '', ''>})`, G | MapToGroups, [...C, ...CG]> + /** this takes a variable number of inputs and activate a positive lookbehind. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari) + * @example + * exactly('foo').after('bar', maybe('baz')) // => /(?<=bar(?:baz)?)foo/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ + after: >( + ...inputs: I + ) => Input<`(?<=${Join, '', ''>})${V}`, G | MapToGroups, [...CG, ...C]> + /** this takes a variable number of inputs and activate a positive lookahead + * @example + * exactly('foo').before('bar', maybe('baz')) // => /foo(?=bar(?:baz)?)/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ + before: >( + ...inputs: I + ) => Input<`${V}(?=${Join, '', ''>})`, G, [...C, ...CG]> + /** these takes a variable number of inputs and activate a negative lookbehind. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari) + * @example + * exactly('foo').notAfter('bar', maybe('baz')) // => /(?>( + ...inputs: I + ) => Input<`(?, '', ''>})${V}`, G, [...CG, ...C]> + /** this takes a variable number of inputs and activate a negative lookahead + * @example + * exactly('foo').notBefore('bar', maybe('baz')) // => /foo(?!bar(?:baz)?)/ + * @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped + */ + notBefore: >( + ...inputs: I + ) => Input<`${V}(?!${Join, '', ''>})`, G, [...C, ...CG]> /** repeat the previous pattern an exact number of times */ times: { (number: N): Input, G, C> @@ -108,21 +128,21 @@ export const createInput = < return { toString: () => s.toString(), - and: Object.assign((input: InputSource) => createInput(`${s}${exactly(input)}`), { + and: Object.assign((...inputs: InputSource[]) => createInput(`${s}${exactly(...inputs)}`), { referenceTo: (groupName: string) => createInput(`${s}\\k<${groupName}>`), }), - or: input => createInput(`(?:${s}|${exactly(input)})`), - after: input => createInput(`(?<=${exactly(input)})${s}`), - before: input => createInput(`${s}(?=${exactly(input)})`), - notAfter: input => createInput(`(? createInput(`${s}(?!${exactly(input)})`), - times: Object.assign((number: number) => createInput(`${wrap(s)}{${number}}`) as any, { - any: () => createInput(`${wrap(s)}*`) as any, - atLeast: (min: number) => createInput(`${wrap(s)}{${min},}`) as any, - atMost: (max: number) => createInput(`${wrap(s)}{0,${max}}`) as any, - between: (min: number, max: number) => createInput(`${wrap(s)}{${min},${max}}`) as any, + or: (...inputs) => createInput(`(?:${s}|${exactly(...inputs)})`), + after: (...input) => createInput(`(?<=${exactly(...input)})${s}`), + before: (...input) => createInput(`${s}(?=${exactly(...input)})`), + notAfter: (...input) => createInput(`(? createInput(`${s}(?!${exactly(...input)})`), + times: Object.assign((number: number) => createInput(`${wrap(s)}{${number}}`), { + any: () => createInput(`${wrap(s)}*`), + atLeast: (min: number) => createInput(`${wrap(s)}{${min},}`), + atMost: (max: number) => createInput(`${wrap(s)}{0,${max}}`), + between: (min: number, max: number) => createInput(`${wrap(s)}{${min},${max}}`), }), - optionally: () => createInput(`${wrap(s)}?`) as any, + optionally: () => createInput(`${wrap(s)}?`), as: groupedAsFn, groupedAs: groupedAsFn, grouped: () => createInput(`${s}`.replace(GROUPED_REPLACE_RE, '($1$3)$2')), diff --git a/src/core/types/join.ts b/src/core/types/join.ts index 79408c3b..4af6cc3a 100644 --- a/src/core/types/join.ts +++ b/src/core/types/join.ts @@ -7,3 +7,15 @@ export type Join< ? `${Prefix}${F}${R extends string[] ? Join : ''}` : '' : '' + +type UnionToIntersection = (Union extends Union ? (a: Union) => any : never) extends ( + a: infer I +) => any + ? I + : never + +export type UnionToTuple = UnionToIntersection< + Union extends any ? () => Union : never +> extends () => infer Item + ? UnionToTuple, [...Tuple, Item]> + : Tuple diff --git a/src/core/types/sources.ts b/src/core/types/sources.ts index 2b41eaa0..04bd06c5 100644 --- a/src/core/types/sources.ts +++ b/src/core/types/sources.ts @@ -2,15 +2,7 @@ import type { Input } from '../internal' import type { GetValue } from './escape' export type InputSource = S | Input -export type GetGroup = T extends Input ? Group : never -export type GetCapturedGroupsArr< - T extends InputSource, - MapToUndefined extends boolean = false -> = T extends Input - ? MapToUndefined extends true - ? { [K in keyof CapturedGroupArr]: undefined } - : CapturedGroupArr - : [] + export type MapToValues = T extends [ infer First, ...infer Rest extends InputSource[] @@ -29,12 +21,27 @@ export type MapToGroups = T extends [ : MapToGroups : never -type Flatten = T extends [infer L, ...infer R] - ? L extends any[] - ? [...Flatten, ...Flatten] - : [L, ...Flatten] - : [] - -export type MapToCapturedGroupsArr = Flatten<{ - [K in keyof T]: T[K] extends Input ? C : string[] -}> +export type MapToCapturedGroupsArr< + Inputs extends any[], + MapToUndefined extends boolean = false, + CapturedGroupsArr extends any[] = [], + Count extends any[] = [] +> = Count['length'] extends Inputs['length'] + ? CapturedGroupsArr + : Inputs[Count['length']] extends Input + ? [CaptureGroups] extends [never] + ? MapToCapturedGroupsArr + : MapToUndefined extends true + ? MapToCapturedGroupsArr< + Inputs, + MapToUndefined, + [...CapturedGroupsArr, undefined], + [...Count, ''] + > + : MapToCapturedGroupsArr< + Inputs, + MapToUndefined, + [...CapturedGroupsArr, ...CaptureGroups], + [...Count, ''] + > + : MapToCapturedGroupsArr diff --git a/src/index.ts b/src/index.ts index a579bdfe..fe4b7dc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,46 @@ import type { Flag } from './core/flags' -import { Input, exactly } from './core/inputs' -import type { Join } from './core/types/join' +import { exactly } from './core/inputs' +import type { Join, UnionToTuple } from './core/types/join' import type { MagicRegExp, MagicRegExpMatchArray } from './core/types/magic-regexp' -import type { Escape, ExactEscapeChar } from './core/types/escape' +import { InputSource, MapToCapturedGroupsArr, MapToGroups, MapToValues } from './core/types/sources' export const createRegExp: { - /** Create Magic RegExp from Input helper */ + /** Create Magic RegExp from Input helpers and strin (string will be sanitized) */ + (...inputs: Inputs): MagicRegExp< + `/${Join, '', ''>}/`, + MapToGroups, + MapToCapturedGroupsArr, + never + > + ( + ...inputs: [...Inputs, [...Flags]] + ): MagicRegExp< + `/${Join, '', ''>}/${Join}`, + MapToGroups, + MapToCapturedGroupsArr, + Flags[number] + > < - Value extends string, - NamedGroups extends string = never, - CapturedGroupsArr extends (string | undefined)[] = [], - Flags extends Flag[] = never[] + Inputs extends InputSource[], + FlagUnion extends Flag = never, + Flags extends Flag[] = UnionToTuple extends infer F extends Flag[] ? F : never >( - raw: Input, - flags?: [...Flags] | string | Set - ): MagicRegExp<`/${Value}/${Join}`, NamedGroups, CapturedGroupsArr, Flags[number]> - /** Create Magic RegExp from string, string will be sanitized */ - ( - raw: Value, - flags?: [...Flags] | string | Set + ...inputs: [...Inputs, Set] ): MagicRegExp< - `/${Escape}/${Join}`, - never, - [], + `/${Join, '', ''>}/${Join}`, + MapToGroups, + MapToCapturedGroupsArr, Flags[number] > -} = (raw: any, flags?: any) => - new RegExp(exactly(raw).toString(), [...(flags || '')].join('')) as any +} = (...inputs: any[]) => { + const flags = + inputs.length > 1 && + (Array.isArray(inputs[inputs.length - 1]) || inputs[inputs.length - 1] instanceof Set) + ? inputs.pop() + : undefined + return new RegExp(exactly(...inputs).toString(), [...(flags || '')].join('')) as any +} export * from './core/flags' export * from './core/inputs' diff --git a/test/index.test.ts b/test/index.test.ts index db391bf9..c98cd2ce 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -13,6 +13,8 @@ import { MagicRegExp, MagicRegExpMatchArray, StringCapturedBy, + oneOrMore, + caseInsensitive, } from '../src' import { createInput } from '../src/core/internal' @@ -25,8 +27,11 @@ describe('magic-regexp', () => { expectTypeOf(regExp).not.toEqualTypeOf(RegExp) }) it('collects flag type', () => { - const re = createRegExp('.', [global, multiline]) - expectTypeOf(re).toEqualTypeOf>() + const re_array_flag = createRegExp('.', [global, multiline]) + expectTypeOf(re_array_flag).toEqualTypeOf>() + + const re_set_flag = createRegExp('.', new Set([global] as const)) + expectTypeOf(re_set_flag).toEqualTypeOf>() }) it('sanitize string input', () => { const escapeChars = '.*+?^${}()[]/' @@ -49,6 +54,32 @@ describe('inputs', () => { MagicRegExp<'/(?\\s)/', 'groupName', ['(?\\s)'], never> >() }) + it('takes variadic args and flags', () => { + const regExp = createRegExp( + oneOrMore(digit).as('major'), + '.', + oneOrMore(digit).as('minor'), + maybe('.', oneOrMore(char).groupedAs('patch')), + [caseInsensitive] + ) + const result = '3.4.1-beta'.match(regExp) + expect(Array.isArray(result)).toBeTruthy() + expect(result?.groups).toMatchInlineSnapshot(` + { + "major": "3", + "minor": "4", + "patch": "1-beta", + } + `) + expectTypeOf(regExp).toEqualTypeOf< + MagicRegExp< + '/(?\\d+)\\.(?\\d+)(?:\\.(?.+))?/i', + 'major' | 'minor' | 'patch', + ['(?\\d+)', '(?\\d+)', '(?.+)'], + 'i' + > + >() + }) it('any', () => { const regExp = createRegExp(anyOf('foo', 'bar')) expect(regExp).toMatchInlineSnapshot('/\\(\\?:foo\\|bar\\)/') diff --git a/test/inputs.test.ts b/test/inputs.test.ts index 5d7b6c9b..c795983b 100644 --- a/test/inputs.test.ts +++ b/test/inputs.test.ts @@ -61,6 +61,13 @@ describe('inputs', () => { expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf< MagicRegExp<'/(?foo)?/', 'groupName', ['(?foo)'], never> >() + + const multi = maybe('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot( + '/\\(\\?:foo\\(\\?\\(\\?:foo\\)\\?\\)bar\\)\\?/' + ) + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'(?:foo(?(?:foo)?)bar)?'>() }) it('oneOrMore', () => { const input = oneOrMore('foo') @@ -72,6 +79,13 @@ describe('inputs', () => { expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf< MagicRegExp<'/(?foo)+/', 'groupName', ['(?foo)'], never> >() + + const multi = oneOrMore('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot( + '/\\(\\?:foo\\(\\?\\(\\?:foo\\)\\+\\)bar\\)\\+/' + ) + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'(?:foo(?(?:foo)+)bar)+'>() }) it('exactly', () => { const input = exactly('fo?[a-z]{2}/o?') @@ -89,6 +103,15 @@ describe('inputs', () => { never > >() + + const multi = exactly('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot( + '/foo\\(\\?fo\\\\\\?\\\\\\[a-z\\\\\\]\\\\\\{2\\\\\\}\\\\/o\\\\\\?\\)bar/' + ) + expectTypeOf( + extractRegExp(multi) + ).toEqualTypeOf<'foo(?fo\\?\\[a-z\\]\\{2\\}\\/o\\?)bar'>() }) it('word', () => { const input = word @@ -193,6 +216,11 @@ describe('chained inputs', () => { const regexp = new RegExp(val as any) expect(regexp).toMatchInlineSnapshot('/\\\\\\?test\\\\\\.js/') expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?test\\.js'>() + + const multi = multichar.and('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot('/abfoo\\(\\?\\\\\\?\\)bar/') + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'abfoo(?\\?)bar'>() }) it('and.referenceTo', () => { const val = input.groupedAs('namedGroup').and(exactly('any')).and.referenceTo('namedGroup') @@ -208,30 +236,59 @@ describe('chained inputs', () => { expect(regexp.test(test)).toBeTruthy() expect(regexp.exec(test)?.[1]).toBeUndefined() expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(?:\\?|test\\.js)'>() + + const multi = multichar.or('foo', input.groupedAs('groupName'), exactly('bar').or(test)) + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot( + '/\\(\\?:ab\\|foo\\(\\?\\\\\\?\\)\\(\\?:bar\\|test\\\\\\.js\\)\\)/' + ) + expectTypeOf( + extractRegExp(multi) + ).toEqualTypeOf<'(?:ab|foo(?\\?)(?:bar|test\\.js))'>() }) it('after', () => { const val = input.after('test.js') const regexp = new RegExp(val as any) expect(regexp).toMatchInlineSnapshot('/\\(\\?<=test\\\\\\.js\\)\\\\\\?/') expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(?<=test\\.js)\\?'>() + + const multi = multichar.after('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot('/\\(\\?<=foo\\(\\?\\\\\\?\\)bar\\)ab/') + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'(?<=foo(?\\?)bar)ab'>() }) it('before', () => { const val = input.before('test.js') const regexp = new RegExp(val as any) expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\(\\?=test\\\\\\.js\\)/') expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?(?=test\\.js)'>() + + const multi = multichar.before('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot('/ab\\(\\?=foo\\(\\?\\\\\\?\\)bar\\)/') + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'ab(?=foo(?\\?)bar)'>() }) it('notAfter', () => { const val = input.notAfter('test.js') const regexp = new RegExp(val as any) expect(regexp).toMatchInlineSnapshot('/\\(\\?() + + const multi = multichar.notAfter('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot('/\\(\\?\\\\\\?\\)bar\\)ab/') + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'(?\\?)bar)ab'>() }) it('notBefore', () => { const val = input.notBefore('test.js') const regexp = new RegExp(val as any) expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\(\\?!test\\\\\\.js\\)/') expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?(?!test\\.js)'>() + + const multi = multichar.notBefore('foo', input.groupedAs('groupName'), 'bar') + const regexp2 = new RegExp(multi as any) + expect(regexp2).toMatchInlineSnapshot('/ab\\(\\?!foo\\(\\?\\\\\\?\\)bar\\)/') + expectTypeOf(extractRegExp(multi)).toEqualTypeOf<'ab(?!foo(?\\?)bar)'>() }) it('times', () => { const val = input.times(500)