Skip to content

Commit

Permalink
feat: display built regexp in TS tooltip (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Jul 17, 2022
1 parent 63d8dd8 commit 051e219
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 51 deletions.
35 changes: 26 additions & 9 deletions src/core/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { createInput, Input } from './internal'
import type { GetValue, EscapeChar } from './types/escape'
import type { Join } from './types/join'
import type { MapToGroups, MapToValues, InputSource } from './types/sources'

export type { Input }

/** This matches any character in the string provided */
export const charIn = (chars: string) => createInput(`[${chars.replace(/[-\\^\]]/g, '\\$&')}]`)
export const charIn = <T extends string>(chars: T) =>
createInput(`[${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[${EscapeChar<T>}]`>

/** This matches any character that is not in the string provided */
export const charNotIn = (chars: string) => createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`)
export const charNotIn = <T extends string>(chars: T) =>
createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[^${EscapeChar<T>}]`>

/** This takes an array of inputs and matches any of them. */
export const anyOf = <T extends string = never>(...args: Array<string | Input<T>>) =>
createInput<T>(`(${args.map(a => exactly(a)).join('|')})`)
export const anyOf = <New extends InputSource<V, T>[], V extends string, T extends string>(
...args: New
) =>
createInput(`(${args.map(a => exactly(a)).join('|')})`) as Input<
`(${Join<MapToValues<New>>})`,
MapToGroups<New>
>

export const char = createInput('.')
export const word = createInput('\\w')
Expand All @@ -30,9 +42,14 @@ export const not = {
}

/** Equivalent to `?` - this marks the input as optional */
export const maybe = (str: string | Input) => createInput(`(${exactly(str)})?`)
export const maybe = <New extends InputSource<string>>(str: New) =>
createInput(`(${exactly(str)})?`) as Input<`(${GetValue<New>})?`>

/** This escapes a string input to match it exactly */
export const exactly = (str: string | Input) =>
typeof str === 'string' ? createInput(str.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')) : str
export const oneOrMore = (str: string | Input) => createInput(`(${exactly(str)})+`)
// export const = (str: string | Input) => createInput(`(${exactly(str)})+`)
export const exactly = <New extends InputSource<string>>(input: New): Input<GetValue<New>> =>
typeof input === 'string'
? (createInput(input.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')) as any)
: input

export const oneOrMore = <New extends InputSource<string>>(str: New) =>
createInput(`(${exactly(str)})+`) as Input<`(${GetValue<New>})+`>
45 changes: 28 additions & 17 deletions src/core/internal.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
import { exactly } from './inputs'
import type { GetValue } from './types/escape'
import type { InputSource } from './types/sources'

export interface Input<T extends string = never> {
export interface Input<V extends string, G extends string = never> {
/** this adds a new pattern to the current input */
and: <X extends string = never>(input: string | Input<X>) => Input<T | X>
and: <I extends InputSource<string, G>, Groups extends string = never>(
input: I
) => Input<`${V}${GetValue<I>}`, G | Groups>
/** this provides an alternative to the current input */
or: <X extends string = never>(input: string | Input<X>) => Input<T | X>
or: <I extends InputSource<string, G>, Groups extends string = never>(
input: I
) => Input<`(${V}|${GetValue<I>})`, G | Groups>
/** 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) */
after: (input: string | Input) => Input<T>
after: <I extends InputSource<string>>(input: I) => Input<`(?<=${GetValue<I>})${V}`, G>
/** this is a positive lookahead */
before: (input: string | Input) => Input<T>
before: <I extends InputSource<string>>(input: I) => Input<`${V}(?=${GetValue<I>})`, G>
/** 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) */
notAfter: (input: string | Input) => Input<T>
notAfter: <I extends InputSource<string>>(input: I) => Input<`(?<!${GetValue<I>})${V}`, G>
/** this is a negative lookahead */
notBefore: (input: string | Input) => Input<T>
notBefore: <I extends InputSource<string>>(input: I) => Input<`${V}(?!${GetValue<I>})`, G>
times: {
/** repeat the previous pattern an exact number of times */
(number: number): Input<T>
/** specify a range of times to repeat the previous pattern */
between: (min: number, max: number) => Input<T>
<N extends number>(number: N): Input<`(${V}){${N}}`, G>
/** specify that the expression can repeat any number of times, _including none_ */
any: () => Input<T>
any: () => Input<`(${V})*`, G>
/** specify a range of times to repeat the previous pattern */
between: <Min extends number, Max extends number>(
min: Min,
max: Max
) => Input<`(${V}){${Min},${Max}}`, G>
/** specify that the expression must occur at least x times */
atLeast: (min: number) => Input<T>
atLeast: <N extends number>(number: N) => Input<`(${V}){${N},}`, G>
}
/** 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()` */
as: <K extends string>(key: K) => Input<T | K>
as: <K extends string>(key: K) => Input<`(?<${K}>${V})`, G | K>
/** this allows you to match beginning/ends of lines with `at.lineStart()` and `at.lineEnd()` */
at: {
lineStart: () => Input<T>
lineEnd: () => Input<T>
lineStart: () => Input<`^${V}`, G>
lineEnd: () => Input<`${V}$`, G>
}
/** this allows you to mark the input so far as optional */
optionally: () => Input<T>
optionally: () => Input<`(${V})?`, G>
toString: () => string
}

export const createInput = <T extends string = never>(s: string | Input<T>): Input<T> => {
export const createInput = <Value extends string, Groups extends string = never>(
s: Value | Input<Groups, Value>
): Input<Value, Groups> => {
return {
toString: () => s.toString(),
and: input => createInput(`${s}${exactly(input)}`),
Expand Down
28 changes: 28 additions & 0 deletions src/core/types/escape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Input } from '../inputs'
import { InputSource } from './sources'

// prettier-ignore
type ExactEscapeChar = '.' | '*' | '+' | '?' | '^' | '$' | '{' | '}' | '(' | ')' | '|' | '[' | ']' | '/'
type Escape<
T extends string,
EscapeChar extends string
> = T extends `${infer Start}${EscapeChar}${string}`
? Start extends `${string}${EscapeChar}${string}`
? never
: T extends `${Start}${infer Char}${string}`
? Char extends EscapeChar
? T extends `${Start}${Char}${infer Rest}`
? `${Start}\\${Char}${Escape<Rest, EscapeChar>}`
: never
: never
: never
: T

type CharEscapeCharacter = '\\' | '^' | '-' | ']'
export type EscapeChar<T extends string> = Escape<T, CharEscapeCharacter>

export type GetValue<T extends InputSource<string>> = T extends string
? Escape<T, ExactEscapeChar>
: T extends Input<infer R>
? R
: never
9 changes: 9 additions & 0 deletions src/core/types/join.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type Join<
T extends string[],
Prefix extends string = '',
Joiner extends string = '|'
> = T extends [infer F, ...infer R]
? F extends string
? `${Prefix}${F}${R extends string[] ? Join<R, Joiner, Joiner> : ''}`
: ''
: ''
17 changes: 17 additions & 0 deletions src/core/types/sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Input } from '../internal'

export type InputSource<S extends string = never, T extends string = never> = S | Input<S, T>
export type MapToValues<T extends InputSource<any, any>[]> = T extends [infer First, ...infer Rest]
? First extends InputSource<infer K>
? [K, ...MapToValues<Rest>]
: []
: []

export type MapToGroups<T extends InputSource<any, string>[]> = T extends [
infer First,
...infer Rest
]
? First extends Input<any, infer K>
? K | MapToGroups<Rest>
: MapToGroups<Rest>
: never
16 changes: 8 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { Input, exactly } from './core/inputs'

const MagicRegExpSymbol = Symbol('MagicRegExp')

export type MagicRegExp<T = never> = RegExp & {
[MagicRegExpSymbol]: T
export type MagicRegExp<Value extends string, T = never> = RegExp & {
[MagicRegExpSymbol]: T & Value
}

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

export * from './core/flags'
export * from './core/inputs'
Expand All @@ -19,10 +19,10 @@ export * from './core/inputs'
declare global {
interface String {
match<T extends string>(
regexp: MagicRegExp<T>
regexp: MagicRegExp<any, T>
): (Omit<RegExpMatchArray, 'groups'> & { groups: Record<T, string | undefined> }) | null
matchAll<T extends string>(
regexp: MagicRegExp<T>
regexp: MagicRegExp<any, T>
): IterableIterator<
Omit<RegExpMatchArray, 'groups'> & { groups: Record<T, string | undefined> }
>
Expand Down

0 comments on commit 051e219

Please sign in to comment.