Skip to content

Commit cd233ab

Browse files
authored
feat: update IfSingle and wrap to not wrap wrapped input (#33)
* feat: update `IfSingle` and `wrap` to also not wrap if already wrapped * test: update and add test for checking extra wrap * refactor: rename type `IfSingle` to `Wrap` * fix: update and combine `wrap` regexp * fix: update and extract NEEDS_WRAP_RE
1 parent 6131ac6 commit cd233ab

File tree

4 files changed

+23
-13
lines changed

4 files changed

+23
-13
lines changed

src/core/inputs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createInput, Input } from './internal'
22
import type { GetValue, EscapeChar } from './types/escape'
33
import type { Join } from './types/join'
44
import type { MapToGroups, MapToValues, InputSource, GetGroup } from './types/sources'
5-
import { IfSingle, wrap } from './wrap'
5+
import { Wrap, wrap } from './wrap'
66

77
export type { Input }
88

@@ -47,7 +47,7 @@ export const not = {
4747

4848
/** Equivalent to `?` - this marks the input as optional */
4949
export const maybe = <New extends InputSource<string>>(str: New) =>
50-
createInput(`${wrap(exactly(str))}?`) as IfSingle<
50+
createInput(`${wrap(exactly(str))}?`) as Wrap<
5151
GetValue<New>,
5252
Input<`${GetValue<New>}?`, GetGroup<New>>,
5353
Input<`(${GetValue<New>})?`, GetGroup<New>>
@@ -62,7 +62,7 @@ export const exactly = <New extends InputSource<string>>(
6262
: input
6363

6464
export const oneOrMore = <New extends InputSource<string>>(str: New) =>
65-
createInput(`${wrap(exactly(str))}+`) as IfSingle<
65+
createInput(`${wrap(exactly(str))}+`) as Wrap<
6666
GetValue<New>,
6767
Input<`${GetValue<New>}+`, GetGroup<New>>,
6868
Input<`(${GetValue<New>})+`, GetGroup<New>>

src/core/internal.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { exactly } from './inputs'
22
import type { GetValue } from './types/escape'
33
import type { InputSource } from './types/sources'
4-
import { IfSingle, wrap } from './wrap'
4+
import { Wrap, wrap } from './wrap'
55

66
export interface Input<V extends string, G extends string = never> {
77
and: {
@@ -30,18 +30,18 @@ export interface Input<V extends string, G extends string = never> {
3030
notBefore: <I extends InputSource<string>>(input: I) => Input<`${V}(?!${GetValue<I>})`, G>
3131
times: {
3232
/** repeat the previous pattern an exact number of times */
33-
<N extends number>(number: N): IfSingle<V, Input<`${V}{${N}}`, G>, Input<`(${V}){${N}}`, G>>
33+
<N extends number>(number: N): Wrap<V, Input<`${V}{${N}}`, G>, Input<`(${V}){${N}}`, G>>
3434
/** specify that the expression can repeat any number of times, _including none_ */
35-
any: () => IfSingle<V, Input<`${V}*`, G>, Input<`(${V})*`, G>>
35+
any: () => Wrap<V, Input<`${V}*`, G>, Input<`(${V})*`, G>>
3636
/** specify that the expression must occur at least x times */
3737
atLeast: <N extends number>(
3838
number: N
39-
) => IfSingle<V, Input<`${V}{${N},}`, G>, Input<`(${V}){${N},}`, G>>
39+
) => Wrap<V, Input<`${V}{${N},}`, G>, Input<`(${V}){${N},}`, G>>
4040
/** specify a range of times to repeat the previous pattern */
4141
between: <Min extends number, Max extends number>(
4242
min: Min,
4343
max: Max
44-
) => IfSingle<V, Input<`${V}{${Min},${Max}}`, G>, Input<`(${V}){${Min},${Max}}`, G>>
44+
) => Wrap<V, Input<`${V}{${Min},${Max}}`, G>, Input<`(${V}){${Min},${Max}}`, G>>
4545
}
4646
/** 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()` */
4747
as: <K extends string>(key: K) => Input<`(?<${K}>${V})`, G | K>
@@ -51,7 +51,7 @@ export interface Input<V extends string, G extends string = never> {
5151
lineEnd: () => Input<`${V}$`, G>
5252
}
5353
/** this allows you to mark the input so far as optional */
54-
optionally: () => IfSingle<V, Input<`${V}?`, G>, Input<`(${V})?`, G>>
54+
optionally: () => Wrap<V, Input<`${V}?`, G>, Input<`(${V})?`, G>>
5555
toString: () => string
5656
}
5757

src/core/wrap.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { Input } from './internal'
22
import { StripEscapes } from './types/escape'
33

4-
export type IfSingle<T extends string, Yes, No> = StripEscapes<T> extends `${infer A}${infer B}`
4+
export type Wrap<T extends string, Yes, No> = T extends `(${string})`
5+
? Yes
6+
: StripEscapes<T> extends `${infer A}${infer B}`
57
? A extends ''
68
? Yes
79
: B extends ''
810
? Yes
911
: No
1012
: never
1113

14+
const NEEDS_WRAP_RE = /^(\(.*\)|\\?.)$/
15+
1216
export const wrap = (s: string | Input<any>) => {
1317
const v = s.toString()
14-
return v.replace(/^\\/, '').length === 1 ? v : `(${v})`
18+
return NEEDS_WRAP_RE.test(v) ? v : `(${v})`
1519
}

test/inputs.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('inputs', () => {
5757
expectTypeOf(extractRegExp(input)).toEqualTypeOf<'(foo)?'>()
5858
const nestedInputWithGroup = maybe(exactly('foo').as('groupName'))
5959
expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf<
60-
MagicRegExp<'/((?<groupName>foo))?/', 'groupName', never>
60+
MagicRegExp<'/(?<groupName>foo)?/', 'groupName', never>
6161
>()
6262
})
6363
it('oneOrMore', () => {
@@ -67,7 +67,7 @@ describe('inputs', () => {
6767
expectTypeOf(extractRegExp(input)).toEqualTypeOf<'(foo)+'>()
6868
const nestedInputWithGroup = oneOrMore(exactly('foo').as('groupName'))
6969
expectTypeOf(createRegExp(nestedInputWithGroup)).toEqualTypeOf<
70-
MagicRegExp<'/((?<groupName>foo))+/', 'groupName', never>
70+
MagicRegExp<'/(?<groupName>foo)+/', 'groupName', never>
7171
>()
7272
})
7373
it('exactly', () => {
@@ -144,6 +144,12 @@ describe('inputs', () => {
144144
expect(not.carriageReturn.toString()).toMatchInlineSnapshot('"[^\\\\r]"')
145145
expectTypeOf(extractRegExp(not.carriageReturn)).toEqualTypeOf<'[^\\r]'>()
146146
})
147+
it('no extra wrap by ()', () => {
148+
const input = oneOrMore(maybe(exactly('(foo)')).as('groupName'))
149+
const regexp = new RegExp(input as any)
150+
expect(regexp).toMatchInlineSnapshot('/\\(\\?<groupName>\\(\\\\\\(foo\\\\\\)\\)\\?\\)\\+/')
151+
expectTypeOf(extractRegExp(input)).toEqualTypeOf<'(?<groupName>(\\(foo\\))?)+'>()
152+
})
147153
})
148154

149155
describe('chained inputs', () => {

0 commit comments

Comments
 (0)