Skip to content

Instantly share code, notes, and snippets.

@disco0
Last active September 22, 2020 05:25
Show Gist options
  • Save disco0/58db260cb34d44f2bc9378296712aa15 to your computer and use it in GitHub Desktop.
Save disco0/58db260cb34d44f2bc9378296712aa15 to your computer and use it in GitHub Desktop.
typescript-regexp-groups
/**
* 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>;
*
* ```
*/
///<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