Last active
September 22, 2020 05:25
-
-
Save disco0/58db260cb34d44f2bc9378296712aa15 to your computer and use it in GitHub Desktop.
typescript-regexp-groups
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Casting regexp match/exec results will allow for autocompletion of the keys supplied in union | |
* to `Groups` generic parameter: | |
* ``` ts | |
* const execResult = /(?<test>capture)/.exec('test') as GroupedRegExpExecArray<'test'>; | |
* execResult.groups. // <TAB> => test?: string | |
* | |
* const matchResult = 'test'.match(/(?<test>capture)/) as GroupedRegExpMatchArray<'test'|'capture'>; | |
* matchResult.groups. // <TAB> => (property) capture?: string | |
* // (property) test?: string | |
* ``` | |
*/ | |
//#region RegExp Grouped Result Types | |
/** | |
* Extension of RegExpExecArray that adds explicit capture group names via unioning of | |
* group name string literals passed via `Groups` generic parameter. | |
*/ | |
export type GroupedRegExpExecArray<Groups extends string> = GroupedRegExpResult<RegExpExecArray, Groups>; | |
/** | |
* Extension of RegExpMatchArray that adds explicit capture group names via unioning of | |
* group name string literals passed via `Groups` generic parameter. | |
*/ | |
export type GroupedRegExpMatchArray<Groups extends string> = GroupedRegExpResult<RegExpMatchArray, Groups>; | |
//#endregion RegExp grouped result utility types | |
//#region Util subtypes | |
type PartialRecord<K extends PropertyKey, V> = Partial<Record<K, V>>; | |
type GroupRecords<Groups extends string> = PartialRecord<Groups, string> | |
type RegExpResultArray = RegExpExecArray | RegExpMatchArray; | |
/** | |
* Base for `GroupedRegExpExecArray` and `GroupedRegExpMatchArray` | |
*/ | |
type GroupedRegExpResult<RA extends RegExpResultArray, Groups extends string> | |
= RA & { groups: GroupRecords<Groups> }; | |
/** | |
* Base for `GroupedRegExpExecArray` and `GroupedRegExpMatchArray` without | |
* use of type aliases | |
*/ | |
type GroupedRegExpResultUnaliased< | |
RegExpResultType extends RegExpExecArray | RegExpMatchArray, | |
Groups extends string | |
> = RegExpResultType & { groups: Partial<Record<Groups, string>> }; | |
//#endregion Util subtypes | |
//#region Test | |
const regexpExecArrayResult = /(?<test>capture)/.exec('test') as GroupedRegExpExecArray<'test'>; | |
regexpExecArrayResult.groups. | |
const regexpMatchArrayResult = 'test'.match(/(?<test>capture)/) as GroupedRegExpMatchArray<'test'|'capture'>; | |
regexpMatchArrayResult.groups. | |
//#endregion Test | |
/** | |
* ADDENDUM: A terser and equally successful (part of the) defintion | |
* | |
* ``` ts | |
* | |
* type RegexResultType = RegExpExecArray | RegExpMatchArray; | |
* type TypedRegexResultArray<ResultType extends RegexResultType, GroupKeys extends string> = | |
* ResultType & { groups: { [key in GroupKeys]?: string } }; | |
* | |
* type TypedRegexExecArray<GroupKeys extends string> = TypedRegexResultArray<RegExpExecArray, GroupKeys>; | |
* type TypedRegexMatchArray<GroupKeys extends string> = TypedRegexResultArray<RegExpMatchArray, GroupKeys>; | |
* | |
* ``` | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///<reference lib="esnext"/> | |
/** | |
* NOTE: | |
* RegExp.prototype.source can be shimmed (with reasonable confidence) | |
* with the function: | |
* | |
* ``` ts | |
* (regex: RegExp) => regex.toString().replace(/(^\/)|(\/[a-z]*$)/gmi, ''); | |
* ``` | |
*/ | |
//#region Main | |
/** | |
* Matches the `(?<group>` component of a regexp named capture, with check that | |
* the group opening `(` is not escaped[1] | |
* | |
* [1] Starting at right before `(` lookbehind confirms one of the valid cases: | |
* - At beginning of regexp / new line (should work for (?x) I think?) | |
* - Previous character is not backslash | |
* - Amount of backslashes prefixing delimiting `(` is even, and therefore not | |
* escaping `(` | |
* | |
*/ | |
let regexpGroupPattern = /(?<=^|[^\\]|(?<!([\\]{2})*)[\\])\(\?<(?<name>[a-z_]+)>/gmi | |
function regexGroupParser(regex: RegExp): Array<string> | |
{ | |
let source = regex.source; | |
let msgText = '%cRegExp body:%c%s', | |
msgBody = [ 'color: #04C; ', '', source ]; | |
console.debug(msgText, ...msgBody); | |
return [...source.matchAll(regexpGroupPattern)] | |
.map(({groups: {name}}) => name ?? false) | |
// Catch for explicit failure mapping group | |
.filter((_): _ is string => typeof _ === 'string') | |
} | |
//#endregion Main | |
//#region Tests | |
/** | |
* Compare two arrays | |
* | |
* From [Stack Overflow answer](https://stackoverflow.com/a/14853974/12697514) | |
* | |
* @TODO Possible to narrow generic type? Attempts error out in nesting | |
*/ | |
function compareArray(array1: Array<any>, array2: Array<any>): boolean | |
{ | |
// if the other array2 is a falsy value, return | |
if (!array2) return false; | |
// compare lengths - can save a lot of time | |
if (array1.length != array2.length) return false; | |
for (var i = 0, l = array1.length; i < l; i++) | |
{ | |
// Check if we have nested arrays | |
if (array1[i] instanceof Array && array2[i] instanceof Array) | |
{ | |
// recurse into the nested arrays | |
if (!compareArray(array1[i], (array2[i]))) return false; | |
} | |
else if (array1[i] != array2[i]) { | |
// Warning - two different object instances will never be equal: {x:20} != {x:20} | |
return false; | |
} | |
} | |
return true; | |
} | |
const groupTests: Array<[Name: string, Groups: Array<string>, Test: RegExp]> = | |
[ | |
[ "Basic", 'test group'.split(/ +/), /(?<test>a test)(?<group>group)\(?<not_a_group>\)/ ], | |
[ "Empty", [], /(\(?<test>)/ ] | |
] | |
const testStyle = { | |
heading: 'color: #26E; font-weight: 600; ', | |
regex: 'color: #1A0; font-style: oblique;', | |
result: (testResult: boolean): string => | |
`background-color: ${testResult ? "#0C0" : "#C00"}; color: white; border-radius: 0.15em` | |
} | |
groupTests.forEach(([name, groups, test]) => { | |
const parsed = regexGroupParser(test); | |
const result: boolean = compareArray(groups,parsed); | |
console.log('%c%s:%c %c%s', | |
testStyle.heading, name, '', testStyle.regex, test.source); | |
console.log('%c%s%c | %s', | |
testStyle.result(result), result ? "VALID" : "INVALID", '', | |
[ | |
`[${groups.join(', ') || 'None'}]`, | |
`[${parsed.join(', ') || 'None'}]` | |
].join(" === ") + " ?", | |
) | |
}) | |
//#endregion Tests |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment