From 89d6c2d1f35cbf7a6da3ef0bd8efd718b36e38f7 Mon Sep 17 00:00:00 2001 From: David Tai Date: Thu, 30 Mar 2023 17:10:46 +0800 Subject: [PATCH 1/2] feat: add new helpers --- src/core/inputs.ts | 16 ++++++++++++++++ src/core/internal.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/core/inputs.ts b/src/core/inputs.ts index 597e882c..52a926e0 100644 --- a/src/core/inputs.ts +++ b/src/core/inputs.ts @@ -67,6 +67,14 @@ export const maybe = (str: New) => GetCapturedGroupsArr > +/** Equivalent to `??` - this marks the input as (Lazy) optional */ +export const maybeLazy = (str: New) => + createInput(`${wrap(exactly(str))}??`) as Input< + IfUnwrapped, `(?:${GetValue})??`, `${GetValue}??`>, + GetGroup, + GetCapturedGroupsArr + > + /** This escapes a string input to match it exactly */ export const exactly = ( input: New @@ -80,3 +88,11 @@ export const oneOrMore = (str: New) => GetGroup, GetCapturedGroupsArr > + +/** Equivalent to `+?` - this marks the input as repeatable, any number of times but at least once (Lazy) */ +export const oneOrMoreLazy = (str: New) => + createInput(`${wrap(exactly(str))}+?`) as Input< + IfUnwrapped, `(?:${GetValue})+?`, `${GetValue}+?`>, + GetGroup, + GetCapturedGroupsArr + > diff --git a/src/core/internal.ts b/src/core/internal.ts index 09dac307..231ae2b6 100644 --- a/src/core/internal.ts +++ b/src/core/internal.ts @@ -50,19 +50,34 @@ export interface Input< (number: N): Input, G, C> /** specify that the expression can repeat any number of times, _including none_ */ any: () => Input, G, C> + /** (Lazy Mode) specify that the expression can repeat any number of times, _including none_ */ + anyLazy: () => Input, G, C> /** specify that the expression must occur at least `N` times */ atLeast: ( number: N ) => Input, G, C> + /** (Lazy Mode) specify that the expression must occur at least `N` times */ + atLeastLazy: ( + number: N + ) => Input, G, C> /** specify that the expression must occur at most `N` times */ atMost: ( number: N ) => Input, G, C> + /** (Lazy Mode) specify that the expression must occur at most `N` times */ + atMostLazy: ( + number: N + ) => Input, G, C> /** specify a range of times to repeat the previous pattern */ between: ( min: Min, max: Max ) => Input, G, C> + /** (Lazy Mode) specify a range of times to repeat the previous pattern */ + betweenLazy: ( + min: Min, + max: Max + ) => Input, G, C> } /** this defines the entire input so far as a named capture group. You will get type safety when using the resulting RegExp with `String.match()`. Alias for `groupedAs` */ as: ( @@ -93,6 +108,7 @@ export interface Input< } /** this allows you to mark the input so far as optional */ optionally: () => Input, G, C> + optionallyLazy: () => Input, G, C> toString: () => string } @@ -117,12 +133,17 @@ export const createInput = < 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, + any: () => createInput(`${wrap(s)}*`), + anyLazy: () => createInput(`${wrap(s)}*?`), + atLeast: (min: number) => createInput(`${wrap(s)}{${min},}`), + atLeastLazy: (min: number) => createInput(`${wrap(s)}{${min},}?`), + atMost: (max: number) => createInput(`${wrap(s)}{0,${max}}`), + atMostLazy: (max: number) => createInput(`${wrap(s)}{0,${max}}?`), + between: (min: number, max: number) => createInput(`${wrap(s)}{${min},${max}}`), + betweenLazy: (min: number, max: number) => createInput(`${wrap(s)}{${min},${max}}?`), }), optionally: () => createInput(`${wrap(s)}?`) as any, + optionallyLazy: () => createInput(`${wrap(s)}??`) as any, as: groupedAsFn, groupedAs: groupedAsFn, grouped: () => createInput(`${s}`.replace(GROUPED_REPLACE_RE, '($1$3)$2')), From 1bcf386bc1eb90435a827554e871afdb0a22de50 Mon Sep 17 00:00:00 2001 From: David Tai Date: Thu, 30 Mar 2023 17:11:06 +0800 Subject: [PATCH 2/2] add tests --- test/inputs.test.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/inputs.test.ts b/test/inputs.test.ts index 5d7b6c9b..66ade6a8 100644 --- a/test/inputs.test.ts +++ b/test/inputs.test.ts @@ -9,7 +9,9 @@ import { charIn, not, maybe, + maybeLazy, oneOrMore, + oneOrMoreLazy, word, wordChar, wordBoundary, @@ -62,6 +64,17 @@ describe('inputs', () => { MagicRegExp<'/(?foo)?/', 'groupName', ['(?foo)'], never> >() }) + it('maybeLazy', () => { + const input = maybeLazy('foo') + const regexp = new RegExp(input as any) + expect(regexp).toMatchInlineSnapshot('/\\(\\?:foo\\)\\?\\?/') + expectTypeOf(extractRegExp(input)).toEqualTypeOf<'(?:foo)??'>() + + const nestedInputWithGroup = maybeLazy(exactly('foo').groupedAs('groupName')) + expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf< + MagicRegExp<'/(?foo)??/', 'groupName', ['(?foo)'], never> + >() + }) it('oneOrMore', () => { const input = oneOrMore('foo') const regexp = new RegExp(input as any) @@ -73,6 +86,17 @@ describe('inputs', () => { MagicRegExp<'/(?foo)+/', 'groupName', ['(?foo)'], never> >() }) + it('oneOrMoreLazy', () => { + const input = oneOrMoreLazy('foo') + const regexp = new RegExp(input as any) + expect(regexp).toMatchInlineSnapshot('/\\(\\?:foo\\)\\+\\?/') + expectTypeOf(extractRegExp(input)).toEqualTypeOf<'(?:foo)+?'>() + + const nestedInputWithGroup = oneOrMoreLazy(exactly('foo').groupedAs('groupName')) + expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf< + MagicRegExp<'/(?foo)+?/', 'groupName', ['(?foo)'], never> + >() + }) it('exactly', () => { const input = exactly('fo?[a-z]{2}/o?') expect(new RegExp(input as any)).toMatchInlineSnapshot( @@ -255,6 +279,12 @@ describe('chained inputs', () => { expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\*/') expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab)*'>() }) + it('times.anyLazy', () => { + const val = input.times.anyLazy() + const regexp = new RegExp(val as any) + expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\*\\?/') + expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?*?'>() + }) it('times.atLeast', () => { const val = input.times.atLeast(2) const regexp = new RegExp(val as any) @@ -266,6 +296,12 @@ describe('chained inputs', () => { expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\{2,\\}/') expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab){2,}'>() }) + it('times.atLeastLazy', () => { + const val = input.times.atLeastLazy(2) + const regexp = new RegExp(val as any) + expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{2,\\}\\?/') + expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{2,}?'>() + }) it('times.atMost', () => { const val = input.times.atMost(2) const regexp = new RegExp(val as any) @@ -277,7 +313,17 @@ describe('chained inputs', () => { expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\{0,2\\}/') expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab){0,2}'>() }) + it('times.atMostLazy', () => { + const val = input.times.atMostLazy(2) + const regexp = new RegExp(val as any) + expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{0,2\\}\\?/') + expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{0,2}?'>() + const val2 = multichar.times.atMost(2) + const regexp2 = new RegExp(val2 as any) + expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\{0,2\\}/') + expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab){0,2}'>() + }) it('times.between', () => { const val = input.times.between(3, 5) const regexp = new RegExp(val as any) @@ -289,6 +335,17 @@ describe('chained inputs', () => { expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\{3,5\\}/') expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab){3,5}'>() }) + it('times.betweenLazy', () => { + const val = input.times.betweenLazy(3, 5) + const regexp = new RegExp(val as any) + expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{3,5\\}\\?/') + expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{3,5}?'>() + + const val2 = multichar.times.between(3, 5) + const regexp2 = new RegExp(val2 as any) + expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\{3,5\\}/') + expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab){3,5}'>() + }) it('optionally', () => { const val = input.optionally() const regexp = new RegExp(val as any) @@ -300,6 +357,12 @@ describe('chained inputs', () => { expect(regexp2).toMatchInlineSnapshot('/\\(\\?:ab\\)\\?/') expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(?:ab)?'>() }) + it('optionallyLazy', () => { + const val = input.optionallyLazy() + const regexp = new RegExp(val as any) + expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\?\\?/') + expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\???'>() + }) it('as', () => { const val = input.as('test') const regexp = new RegExp(val as any)