From 89d6c2d1f35cbf7a6da3ef0bd8efd718b36e38f7 Mon Sep 17 00:00:00 2001
From: David Tai <didavid61202@hotmail.com>
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 = <New extends InputSource>(str: New) =>
     GetCapturedGroupsArr<New>
   >
 
+/** Equivalent to `??` - this marks the input as (Lazy) optional */
+export const maybeLazy = <New extends InputSource>(str: New) =>
+  createInput(`${wrap(exactly(str))}??`) as Input<
+    IfUnwrapped<GetValue<New>, `(?:${GetValue<New>})??`, `${GetValue<New>}??`>,
+    GetGroup<New>,
+    GetCapturedGroupsArr<New>
+  >
+
 /** This escapes a string input to match it exactly */
 export const exactly = <New extends InputSource>(
   input: New
@@ -80,3 +88,11 @@ export const oneOrMore = <New extends InputSource>(str: New) =>
     GetGroup<New>,
     GetCapturedGroupsArr<New>
   >
+
+/** Equivalent to `+?` - this marks the input as repeatable, any number of times but at least once (Lazy) */
+export const oneOrMoreLazy = <New extends InputSource>(str: New) =>
+  createInput(`${wrap(exactly(str))}+?`) as Input<
+    IfUnwrapped<GetValue<New>, `(?:${GetValue<New>})+?`, `${GetValue<New>}+?`>,
+    GetGroup<New>,
+    GetCapturedGroupsArr<New>
+  >
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<
     <N extends number>(number: N): Input<IfUnwrapped<V, `(?:${V}){${N}}`, `${V}{${N}}`>, G, C>
     /** specify that the expression can repeat any number of times, _including none_ */
     any: () => Input<IfUnwrapped<V, `(?:${V})*`, `${V}*`>, G, C>
+    /** (Lazy Mode) specify that the expression can repeat any number of times, _including none_ */
+    anyLazy: () => Input<IfUnwrapped<V, `(?:${V})*?`, `${V}*?`>, G, C>
     /** specify that the expression must occur at least `N` times */
     atLeast: <N extends number>(
       number: N
     ) => Input<IfUnwrapped<V, `(?:${V}){${N},}`, `${V}{${N},}`>, G, C>
+    /** (Lazy Mode) specify that the expression must occur at least `N` times */
+    atLeastLazy: <N extends number>(
+      number: N
+    ) => Input<IfUnwrapped<V, `(?:${V}){${N},}?`, `${V}{${N},}?`>, G, C>
     /** specify that the expression must occur at most `N` times */
     atMost: <N extends number>(
       number: N
     ) => Input<IfUnwrapped<V, `(?:${V}){0,${N}}`, `${V}{0,${N}}`>, G, C>
+    /** (Lazy Mode) specify that the expression must occur at most `N` times */
+    atMostLazy: <N extends number>(
+      number: N
+    ) => Input<IfUnwrapped<V, `(?:${V}){0,${N}}?`, `${V}{0,${N}}?`>, G, C>
     /** specify a range of times to repeat the previous pattern */
     between: <Min extends number, Max extends number>(
       min: Min,
       max: Max
     ) => Input<IfUnwrapped<V, `(?:${V}){${Min},${Max}}`, `${V}{${Min},${Max}}`>, G, C>
+    /** (Lazy Mode) specify a range of times to repeat the previous pattern */
+    betweenLazy: <Min extends number, Max extends number>(
+      min: Min,
+      max: Max
+    ) => Input<IfUnwrapped<V, `(?:${V}){${Min},${Max}}?`, `${V}{${Min},${Max}}?`>, 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: <K extends string>(
@@ -93,6 +108,7 @@ export interface Input<
   }
   /** this allows you to mark the input so far as optional */
   optionally: () => Input<IfUnwrapped<V, `(?:${V})?`, `${V}?`>, G, C>
+  optionallyLazy: () => Input<IfUnwrapped<V, `(?:${V})??`, `${V}??`>, G, C>
   toString: () => string
 }
 
@@ -117,12 +133,17 @@ export const createInput = <
     notAfter: input => createInput(`(?<!${exactly(input)})${s}`),
     notBefore: input => 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 <didavid61202@hotmail.com>
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<'/(?<groupName>foo)?/', 'groupName', ['(?<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<'/(?<groupName>foo)??/', 'groupName', ['(?<groupName>foo)'], never>
+    >()
+  })
   it('oneOrMore', () => {
     const input = oneOrMore('foo')
     const regexp = new RegExp(input as any)
@@ -73,6 +86,17 @@ describe('inputs', () => {
       MagicRegExp<'/(?<groupName>foo)+/', 'groupName', ['(?<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<'/(?<groupName>foo)+?/', 'groupName', ['(?<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)