Skip to content

Commit 051e219

Browse files
authored
feat: display built regexp in TS tooltip (#6)
1 parent 63d8dd8 commit 051e219

File tree

8 files changed

+174
-51
lines changed

8 files changed

+174
-51
lines changed

src/core/inputs.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import { createInput, Input } from './internal'
2+
import type { GetValue, EscapeChar } from './types/escape'
3+
import type { Join } from './types/join'
4+
import type { MapToGroups, MapToValues, InputSource } from './types/sources'
25

36
export type { Input }
47

58
/** This matches any character in the string provided */
6-
export const charIn = (chars: string) => createInput(`[${chars.replace(/[-\\^\]]/g, '\\$&')}]`)
9+
export const charIn = <T extends string>(chars: T) =>
10+
createInput(`[${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[${EscapeChar<T>}]`>
11+
712
/** This matches any character that is not in the string provided */
8-
export const charNotIn = (chars: string) => createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`)
13+
export const charNotIn = <T extends string>(chars: T) =>
14+
createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[^${EscapeChar<T>}]`>
15+
916
/** This takes an array of inputs and matches any of them. */
10-
export const anyOf = <T extends string = never>(...args: Array<string | Input<T>>) =>
11-
createInput<T>(`(${args.map(a => exactly(a)).join('|')})`)
17+
export const anyOf = <New extends InputSource<V, T>[], V extends string, T extends string>(
18+
...args: New
19+
) =>
20+
createInput(`(${args.map(a => exactly(a)).join('|')})`) as Input<
21+
`(${Join<MapToValues<New>>})`,
22+
MapToGroups<New>
23+
>
1224

1325
export const char = createInput('.')
1426
export const word = createInput('\\w')
@@ -30,9 +42,14 @@ export const not = {
3042
}
3143

3244
/** Equivalent to `?` - this marks the input as optional */
33-
export const maybe = (str: string | Input) => createInput(`(${exactly(str)})?`)
45+
export const maybe = <New extends InputSource<string>>(str: New) =>
46+
createInput(`(${exactly(str)})?`) as Input<`(${GetValue<New>})?`>
47+
3448
/** This escapes a string input to match it exactly */
35-
export const exactly = (str: string | Input) =>
36-
typeof str === 'string' ? createInput(str.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')) : str
37-
export const oneOrMore = (str: string | Input) => createInput(`(${exactly(str)})+`)
38-
// export const = (str: string | Input) => createInput(`(${exactly(str)})+`)
49+
export const exactly = <New extends InputSource<string>>(input: New): Input<GetValue<New>> =>
50+
typeof input === 'string'
51+
? (createInput(input.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')) as any)
52+
: input
53+
54+
export const oneOrMore = <New extends InputSource<string>>(str: New) =>
55+
createInput(`(${exactly(str)})+`) as Input<`(${GetValue<New>})+`>

src/core/internal.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,52 @@
11
import { exactly } from './inputs'
2+
import type { GetValue } from './types/escape'
3+
import type { InputSource } from './types/sources'
24

3-
export interface Input<T extends string = never> {
5+
export interface Input<V extends string, G extends string = never> {
46
/** this adds a new pattern to the current input */
5-
and: <X extends string = never>(input: string | Input<X>) => Input<T | X>
7+
and: <I extends InputSource<string, G>, Groups extends string = never>(
8+
input: I
9+
) => Input<`${V}${GetValue<I>}`, G | Groups>
610
/** this provides an alternative to the current input */
7-
or: <X extends string = never>(input: string | Input<X>) => Input<T | X>
11+
or: <I extends InputSource<string, G>, Groups extends string = never>(
12+
input: I
13+
) => Input<`(${V}|${GetValue<I>})`, G | Groups>
814
/** 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) */
9-
after: (input: string | Input) => Input<T>
15+
after: <I extends InputSource<string>>(input: I) => Input<`(?<=${GetValue<I>})${V}`, G>
1016
/** this is a positive lookahead */
11-
before: (input: string | Input) => Input<T>
17+
before: <I extends InputSource<string>>(input: I) => Input<`${V}(?=${GetValue<I>})`, G>
1218
/** 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) */
13-
notAfter: (input: string | Input) => Input<T>
19+
notAfter: <I extends InputSource<string>>(input: I) => Input<`(?<!${GetValue<I>})${V}`, G>
1420
/** this is a negative lookahead */
15-
notBefore: (input: string | Input) => Input<T>
21+
notBefore: <I extends InputSource<string>>(input: I) => Input<`${V}(?!${GetValue<I>})`, G>
1622
times: {
1723
/** repeat the previous pattern an exact number of times */
18-
(number: number): Input<T>
19-
/** specify a range of times to repeat the previous pattern */
20-
between: (min: number, max: number) => Input<T>
24+
<N extends number>(number: N): Input<`(${V}){${N}}`, G>
2125
/** specify that the expression can repeat any number of times, _including none_ */
22-
any: () => Input<T>
26+
any: () => Input<`(${V})*`, G>
27+
/** specify a range of times to repeat the previous pattern */
28+
between: <Min extends number, Max extends number>(
29+
min: Min,
30+
max: Max
31+
) => Input<`(${V}){${Min},${Max}}`, G>
2332
/** specify that the expression must occur at least x times */
24-
atLeast: (min: number) => Input<T>
33+
atLeast: <N extends number>(number: N) => Input<`(${V}){${N},}`, G>
2534
}
2635
/** 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()` */
27-
as: <K extends string>(key: K) => Input<T | K>
36+
as: <K extends string>(key: K) => Input<`(?<${K}>${V})`, G | K>
2837
/** this allows you to match beginning/ends of lines with `at.lineStart()` and `at.lineEnd()` */
2938
at: {
30-
lineStart: () => Input<T>
31-
lineEnd: () => Input<T>
39+
lineStart: () => Input<`^${V}`, G>
40+
lineEnd: () => Input<`${V}$`, G>
3241
}
3342
/** this allows you to mark the input so far as optional */
34-
optionally: () => Input<T>
43+
optionally: () => Input<`(${V})?`, G>
3544
toString: () => string
3645
}
3746

38-
export const createInput = <T extends string = never>(s: string | Input<T>): Input<T> => {
47+
export const createInput = <Value extends string, Groups extends string = never>(
48+
s: Value | Input<Groups, Value>
49+
): Input<Value, Groups> => {
3950
return {
4051
toString: () => s.toString(),
4152
and: input => createInput(`${s}${exactly(input)}`),

src/core/types/escape.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Input } from '../inputs'
2+
import { InputSource } from './sources'
3+
4+
// prettier-ignore
5+
type ExactEscapeChar = '.' | '*' | '+' | '?' | '^' | '$' | '{' | '}' | '(' | ')' | '|' | '[' | ']' | '/'
6+
type Escape<
7+
T extends string,
8+
EscapeChar extends string
9+
> = T extends `${infer Start}${EscapeChar}${string}`
10+
? Start extends `${string}${EscapeChar}${string}`
11+
? never
12+
: T extends `${Start}${infer Char}${string}`
13+
? Char extends EscapeChar
14+
? T extends `${Start}${Char}${infer Rest}`
15+
? `${Start}\\${Char}${Escape<Rest, EscapeChar>}`
16+
: never
17+
: never
18+
: never
19+
: T
20+
21+
type CharEscapeCharacter = '\\' | '^' | '-' | ']'
22+
export type EscapeChar<T extends string> = Escape<T, CharEscapeCharacter>
23+
24+
export type GetValue<T extends InputSource<string>> = T extends string
25+
? Escape<T, ExactEscapeChar>
26+
: T extends Input<infer R>
27+
? R
28+
: never

src/core/types/join.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type Join<
2+
T extends string[],
3+
Prefix extends string = '',
4+
Joiner extends string = '|'
5+
> = T extends [infer F, ...infer R]
6+
? F extends string
7+
? `${Prefix}${F}${R extends string[] ? Join<R, Joiner, Joiner> : ''}`
8+
: ''
9+
: ''

src/core/types/sources.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Input } from '../internal'
2+
3+
export type InputSource<S extends string = never, T extends string = never> = S | Input<S, T>
4+
export type MapToValues<T extends InputSource<any, any>[]> = T extends [infer First, ...infer Rest]
5+
? First extends InputSource<infer K>
6+
? [K, ...MapToValues<Rest>]
7+
: []
8+
: []
9+
10+
export type MapToGroups<T extends InputSource<any, string>[]> = T extends [
11+
infer First,
12+
...infer Rest
13+
]
14+
? First extends Input<any, infer K>
15+
? K | MapToGroups<Rest>
16+
: MapToGroups<Rest>
17+
: never

src/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { Input, exactly } from './core/inputs'
33

44
const MagicRegExpSymbol = Symbol('MagicRegExp')
55

6-
export type MagicRegExp<T = never> = RegExp & {
7-
[MagicRegExpSymbol]: T
6+
export type MagicRegExp<Value extends string, T = never> = RegExp & {
7+
[MagicRegExpSymbol]: T & Value
88
}
99

10-
export const createRegExp = <T extends string = never>(
11-
raw: Input<T> | string,
12-
flags: Flag[] = []
13-
): MagicRegExp<T> => new RegExp(exactly(raw).toString(), flags.join('')) as MagicRegExp<T>
10+
export const createRegExp = <Value extends string, NamedGroups extends string = never>(
11+
raw: Input<Value, NamedGroups> | Value,
12+
flags?: Flag[]
13+
) => new RegExp(exactly(raw).toString(), flags?.join('')) as MagicRegExp<`/${Value}/`, NamedGroups>
1414

1515
export * from './core/flags'
1616
export * from './core/inputs'
@@ -19,10 +19,10 @@ export * from './core/inputs'
1919
declare global {
2020
interface String {
2121
match<T extends string>(
22-
regexp: MagicRegExp<T>
22+
regexp: MagicRegExp<any, T>
2323
): (Omit<RegExpMatchArray, 'groups'> & { groups: Record<T, string | undefined> }) | null
2424
matchAll<T extends string>(
25-
regexp: MagicRegExp<T>
25+
regexp: MagicRegExp<any, T>
2626
): IterableIterator<
2727
Omit<RegExpMatchArray, 'groups'> & { groups: Record<T, string | undefined> }
2828
>

0 commit comments

Comments
 (0)