Skip to content

Commit a02645b

Browse files
authored
feat!: do not create wrap single chars in parentheses (#27)
1 parent fa09325 commit a02645b

File tree

6 files changed

+83
-30
lines changed

6 files changed

+83
-30
lines changed

docs/content/2.getting-started/3.examples.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@ import assert from 'node:assert'
2424
import { createRegExp, word, char, oneOrMore } from 'magic-regexp'
2525

2626
const TENET_RE = createRegExp(
27-
word
28-
.as('firstWord')
29-
.and(word.as('secondWord'))
27+
wordChar
28+
.as('firstChar')
29+
.and(wordChar.as('secondChar'))
3030
.and(oneOrMore(char))
31-
.and.referenceTo('secondWord')
32-
.and.referenceTo('firstWord')
31+
.and.referenceTo('secondChar')
32+
.and.referenceTo('firstChar')
3333
)
34-
// /(?<firstWord>\w)(?<secondWord>\w)(.)+\k<secondWord>\k<firstWord>/
34+
// /(?<firstChar>\w)(?<secondChar>\w).+\k<secondChar>\k<firstChar>/
3535

3636
assert.equal(TENET_RE.test('TEN<==O==>NET'), true)
3737
```
38-

src/core/inputs.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +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 } from './types/sources'
5+
import { IfSingle, wrap } from './wrap'
56

67
export type { Input }
78

@@ -46,7 +47,11 @@ export const not = {
4647

4748
/** Equivalent to `?` - this marks the input as optional */
4849
export const maybe = <New extends InputSource<string>>(str: New) =>
49-
createInput(`(${exactly(str)})?`) as Input<`(${GetValue<New>})?`>
50+
createInput(`${wrap(exactly(str))}?`) as IfSingle<
51+
GetValue<New>,
52+
Input<`${GetValue<New>}?`>,
53+
Input<`(${GetValue<New>})?`>
54+
>
5055

5156
/** This escapes a string input to match it exactly */
5257
export const exactly = <New extends InputSource<string>>(input: New): Input<GetValue<New>> =>
@@ -55,4 +60,8 @@ export const exactly = <New extends InputSource<string>>(input: New): Input<GetV
5560
: input
5661

5762
export const oneOrMore = <New extends InputSource<string>>(str: New) =>
58-
createInput(`(${exactly(str)})+`) as Input<`(${GetValue<New>})+`>
63+
createInput(`${wrap(exactly(str))}+`) as IfSingle<
64+
GetValue<New>,
65+
Input<`${GetValue<New>}+`>,
66+
Input<`(${GetValue<New>})+`>
67+
>

src/core/internal.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +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'
45

56
export interface Input<V extends string, G extends string = never> {
67
and: {
@@ -29,16 +30,18 @@ export interface Input<V extends string, G extends string = never> {
2930
notBefore: <I extends InputSource<string>>(input: I) => Input<`${V}(?!${GetValue<I>})`, G>
3031
times: {
3132
/** repeat the previous pattern an exact number of times */
32-
<N extends number>(number: N): Input<`(${V}){${N}}`, G>
33+
<N extends number>(number: N): IfSingle<V, Input<`${V}{${N}}`, G>, Input<`(${V}){${N}}`, G>>
3334
/** specify that the expression can repeat any number of times, _including none_ */
34-
any: () => Input<`(${V})*`, G>
35+
any: () => IfSingle<V, Input<`${V}*`, G>, Input<`(${V})*`, G>>
36+
/** specify that the expression must occur at least x times */
37+
atLeast: <N extends number>(
38+
number: N
39+
) => IfSingle<V, Input<`${V}{${N},}`, G>, Input<`(${V}){${N},}`, G>>
3540
/** specify a range of times to repeat the previous pattern */
3641
between: <Min extends number, Max extends number>(
3742
min: Min,
3843
max: Max
39-
) => Input<`(${V}){${Min},${Max}}`, G>
40-
/** specify that the expression must occur at least x times */
41-
atLeast: <N extends number>(number: N) => Input<`(${V}){${N},}`, G>
44+
) => IfSingle<V, Input<`${V}{${Min},${Max}}`, G>, Input<`(${V}){${Min},${Max}}`, G>>
4245
}
4346
/** 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()` */
4447
as: <K extends string>(key: K) => Input<`(?<${K}>${V})`, G | K>
@@ -48,7 +51,7 @@ export interface Input<V extends string, G extends string = never> {
4851
lineEnd: () => Input<`${V}$`, G>
4952
}
5053
/** this allows you to mark the input so far as optional */
51-
optionally: () => Input<`(${V})?`, G>
54+
optionally: () => IfSingle<V, Input<`${V}?`, G>, Input<`(${V})?`, G>>
5255
toString: () => string
5356
}
5457

@@ -65,12 +68,12 @@ export const createInput = <Value extends string, Groups extends string = never>
6568
before: input => createInput(`${s}(?=${exactly(input)})`),
6669
notAfter: input => createInput(`(?<!${exactly(input)})${s}`),
6770
notBefore: input => createInput(`${s}(?!${exactly(input)})`),
68-
times: Object.assign((number: number) => createInput(`(${s}){${number}}`), {
69-
any: () => createInput(`(${s})*`),
70-
atLeast: (min: number) => createInput(`(${s}){${min},}`),
71-
between: (min: number, max: number) => createInput(`(${s}){${min},${max}}`),
71+
times: Object.assign((number: number) => createInput(`${wrap(s)}{${number}}`) as any, {
72+
any: () => createInput(`${wrap(s)}*`) as any,
73+
atLeast: (min: number) => createInput(`${wrap(s)}{${min},}`) as any,
74+
between: (min: number, max: number) => createInput(`${wrap(s)}{${min},${max}}`) as any,
7275
}),
73-
optionally: () => createInput(`(${s})?`),
76+
optionally: () => createInput(`${wrap(s)}?`) as any,
7477
as: key => createInput(`(?<${key}>${s})`),
7578
at: {
7679
lineStart: () => createInput(`^${s}`),

src/core/types/escape.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Escape<
2020

2121
type CharEscapeCharacter = '\\' | '^' | '-' | ']'
2222
export type EscapeChar<T extends string> = Escape<T, CharEscapeCharacter>
23+
export type StripEscapes<T extends string> = T extends `${infer A}\\${infer B}` ? `${A}${B}` : T
2324

2425
export type GetValue<T extends InputSource<string>> = T extends string
2526
? Escape<T, ExactEscapeChar>

src/core/wrap.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Input } from './internal'
2+
import { StripEscapes } from './types/escape'
3+
4+
export type IfSingle<T extends string, Yes, No> = StripEscapes<T> extends `${infer A}${infer B}`
5+
? A extends ''
6+
? Yes
7+
: B extends ''
8+
? Yes
9+
: No
10+
: never
11+
12+
export const wrap = (s: string | Input<any>) => {
13+
const v = s.toString()
14+
return v.replace(/^\\/, '').length === 1 ? v : `(${v})`
15+
}

test/inputs.test.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ describe('inputs', () => {
135135

136136
describe('chained inputs', () => {
137137
const input = exactly('?')
138+
const multichar = exactly('ab')
138139
it('and', () => {
139140
const val = input.and('test.js')
140141
const regexp = new RegExp(val as any)
@@ -180,32 +181,57 @@ describe('chained inputs', () => {
180181
it('times', () => {
181182
const val = input.times(500)
182183
const regexp = new RegExp(val as any)
183-
expect(regexp).toMatchInlineSnapshot('/\\(\\\\\\?\\)\\{500\\}/')
184-
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(\\?){500}'>()
184+
expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{500\\}/')
185+
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{500}'>()
186+
187+
const val2 = multichar.times(500)
188+
const regexp2 = new RegExp(val2 as any)
189+
expect(regexp2).toMatchInlineSnapshot('/\\(ab\\)\\{500\\}/')
190+
expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(ab){500}'>()
185191
})
186192
it('times.any', () => {
187193
const val = input.times.any()
188194
const regexp = new RegExp(val as any)
189-
expect(regexp).toMatchInlineSnapshot('/\\(\\\\\\?\\)\\*/')
190-
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(\\?)*'>()
195+
expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\*/')
196+
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?*'>()
197+
198+
const val2 = multichar.times.any()
199+
const regexp2 = new RegExp(val2 as any)
200+
expect(regexp2).toMatchInlineSnapshot('/\\(ab\\)\\*/')
201+
expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(ab)*'>()
191202
})
192203
it('times.atLeast', () => {
193204
const val = input.times.atLeast(2)
194205
const regexp = new RegExp(val as any)
195-
expect(regexp).toMatchInlineSnapshot('/\\(\\\\\\?\\)\\{2,\\}/')
196-
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(\\?){2,}'>()
206+
expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{2,\\}/')
207+
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{2,}'>()
208+
209+
const val2 = multichar.times.atLeast(2)
210+
const regexp2 = new RegExp(val2 as any)
211+
expect(regexp2).toMatchInlineSnapshot('/\\(ab\\)\\{2,\\}/')
212+
expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(ab){2,}'>()
197213
})
198214
it('times.between', () => {
199215
const val = input.times.between(3, 5)
200216
const regexp = new RegExp(val as any)
201-
expect(regexp).toMatchInlineSnapshot('/\\(\\\\\\?\\)\\{3,5\\}/')
202-
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(\\?){3,5}'>()
217+
expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\{3,5\\}/')
218+
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\?{3,5}'>()
219+
220+
const val2 = multichar.times.between(3, 5)
221+
const regexp2 = new RegExp(val2 as any)
222+
expect(regexp2).toMatchInlineSnapshot('/\\(ab\\)\\{3,5\\}/')
223+
expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(ab){3,5}'>()
203224
})
204225
it('optionally', () => {
205226
const val = input.optionally()
206227
const regexp = new RegExp(val as any)
207-
expect(regexp).toMatchInlineSnapshot('/\\(\\\\\\?\\)\\?/')
208-
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'(\\?)?'>()
228+
expect(regexp).toMatchInlineSnapshot('/\\\\\\?\\?/')
229+
expectTypeOf(extractRegExp(val)).toEqualTypeOf<'\\??'>()
230+
231+
const val2 = multichar.optionally()
232+
const regexp2 = new RegExp(val2 as any)
233+
expect(regexp2).toMatchInlineSnapshot('/\\(ab\\)\\?/')
234+
expectTypeOf(extractRegExp(val2)).toEqualTypeOf<'(ab)?'>()
209235
})
210236
it('as', () => {
211237
const val = input.as('test')

0 commit comments

Comments
 (0)