Skip to content

Commit

Permalink
Rename subclass, move to own file
Browse files Browse the repository at this point in the history
  • Loading branch information
slevithan committed Nov 15, 2024
1 parent 7790427 commit 2d7ad33
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 86 deletions.
90 changes: 4 additions & 86 deletions src/regex.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {backcompatPlugin} from './backcompat.js';
import {flagNPreprocessor} from './flag-n.js';
import {clean, flagXPreprocessor} from './flag-x.js';
import {Pattern, pattern} from './pattern.js';
import {RegExpSubclass} from './subclass.js';
import {subroutines} from './subroutines.js';
import {adjustNumberedBackrefs, capturingDelim, CharClassContext, containsCharClassUnion, countCaptures, emulationGroupMarker, enclosedTokenCharClassContexts, enclosedTokenRegexContexts, escapeV, flagVSupported, getBreakoutChar, getEndContextForIncompleteExpression, patternModsSupported, preprocess, RegexContext, sandboxLoneCharClassCaret, sandboxLoneDoublePunctuatorChar, sandboxUnsafeNulls} from './utils.js';
import {adjustNumberedBackrefs, CharClassContext, containsCharClassUnion, countCaptures, enclosedTokenCharClassContexts, enclosedTokenRegexContexts, escapeV, flagVSupported, getBreakoutChar, getEndContextForIncompleteExpression, patternModsSupported, preprocess, RegexContext, sandboxLoneCharClassCaret, sandboxLoneDoublePunctuatorChar, sandboxUnsafeNulls} from './utils.js';
import {Context, hasUnescaped, replaceUnescaped} from 'regex-utilities';

/**
Expand Down Expand Up @@ -35,7 +36,7 @@ import {Context, hasUnescaped, replaceUnescaped} from 'regex-utilities';
(template: RawTemplate, ...substitutions: ReadonlyArray<InterpolatedValue>): T;
(flags?: string): RegexTag<T>;
(options: RegexTagOptions & {subclass?: false}): RegexTag<T>;
(options: RegexTagOptions & {subclass: true}): RegexTag<WrappedRegExp>;
(options: RegexTagOptions & {subclass: true}): RegexTag<RegExpSubclass>;
}}
*/
/**
Expand Down Expand Up @@ -104,7 +105,7 @@ const regexFromTemplate = (options, template, ...substitutions) => {
expression = handlePlugins(expression, opts);
try {
return opts.subclass ?
new WrappedRegExp(expression, opts.flags, {unmarkEmulationGroups: true}) :
new RegExpSubclass(expression, opts.flags, {unmarkEmulationGroups: true}) :
new RegExp(expression, opts.flags);
} catch (err) {
// Improve DX by always including the generated source in the error message. Some browsers
Expand Down Expand Up @@ -210,60 +211,6 @@ function handlePlugins(expression, options) {
return expression;
}

/**
@class
@param {string | WrappedRegExp} expression
@param {string} [flags]
@param {{unmarkEmulationGroups: boolean;}} [options]
*/
class WrappedRegExp extends RegExp {
#captureMap;
constructor(expression, flags, options) {
let captureMap;
if (options?.unmarkEmulationGroups) {
({expression, captureMap} = unmarkEmulationGroups(expression));
}
super(expression, flags);
if (captureMap) {
this.#captureMap = captureMap;
// The third argument `options` isn't provided when regexes are copied as part of the internal
// handling of string methods `matchAll` and `split`
} else if (expression instanceof WrappedRegExp) {
// Can read private properties of the existing object since it was created by this class
this.#captureMap = expression.#captureMap;
}
}
/**
Called internally by all String/RegExp methods that use regexes.
@override
@param {string} str
@returns {RegExpExecArray | null}
*/
exec(str) {
const match = RegExp.prototype.exec.call(this, str);
if (!match || !this.#captureMap) {
return match;
}
const matchCopy = [...match];
// Empty all but the first value of the array while preserving its other properties
match.length = 1;
let indicesCopy;
if (this.hasIndices) {
indicesCopy = [...match.indices];
match.indices.length = 1;
}
for (let i = 1; i < matchCopy.length; i++) {
if (this.#captureMap[i]) {
match.push(matchCopy[i]);
if (this.hasIndices) {
match.indices.push(indicesCopy[i]);
}
}
}
return match;
}
}

/**
@param {InterpolatedValue} value
@param {string} flags
Expand Down Expand Up @@ -395,35 +342,6 @@ function transformForLocalFlags(re, outerFlags) {
return {value};
}

/**
Build the capturing group map (with emulation groups marked as `false` to indicate their submatches
shouldn't appear in results), and remove the markers for anonymous captures which were added to
emulate extended syntax.
@param {string} expression
@returns {{expression: string; captureMap: Array<boolean>;}}
*/
function unmarkEmulationGroups(expression) {
const marker = emulationGroupMarker.replace(/\$/g, '\\$');
const captureMap = [true];
expression = replaceUnescaped(
expression,
`(?:${capturingDelim})(?<mark>${marker})?`,
({0: m, groups: {mark}}) => {
if (mark) {
captureMap.push(false);
return m.slice(0, -emulationGroupMarker.length);
}
captureMap.push(true);
return m;
},
Context.DEFAULT
);
return {
captureMap,
expression,
};
}

export {
pattern,
regex,
Expand Down
89 changes: 89 additions & 0 deletions src/subclass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {capturingDelim, emulationGroupMarker} from './utils.js';
import {Context, replaceUnescaped} from 'regex-utilities';

/**
@class
@param {string | RegExpSubclass} expression
@param {string} [flags]
@param {{unmarkEmulationGroups: boolean;}} [options]
*/
class RegExpSubclass extends RegExp {
#captureMap;
constructor(expression, flags, options) {
let captureMap;
if (options?.unmarkEmulationGroups) {
({expression, captureMap} = unmarkEmulationGroups(expression));
}
super(expression, flags);
if (captureMap) {
this.#captureMap = captureMap;
// The third argument `options` isn't provided when regexes are copied as part of the internal
// handling of string methods `matchAll` and `split`
} else if (expression instanceof RegExpSubclass) {
// Can read private properties of the existing object since it was created by this class
this.#captureMap = expression.#captureMap;
}
}
/**
Called internally by all String/RegExp methods that use regexes.
@override
@param {string} str
@returns {RegExpExecArray | null}
*/
exec(str) {
const match = RegExp.prototype.exec.call(this, str);
if (!match || !this.#captureMap) {
return match;
}
const matchCopy = [...match];
// Empty all but the first value of the array while preserving its other properties
match.length = 1;
let indicesCopy;
if (this.hasIndices) {
indicesCopy = [...match.indices];
match.indices.length = 1;
}
for (let i = 1; i < matchCopy.length; i++) {
if (this.#captureMap[i]) {
match.push(matchCopy[i]);
if (this.hasIndices) {
match.indices.push(indicesCopy[i]);
}
}
}
return match;
}
}

/**
Build the capturing group map (with emulation groups marked as `false` to indicate their submatches
shouldn't appear in results), and remove the markers for anonymous captures which were added to
emulate extended syntax.
@param {string} expression
@returns {{expression: string; captureMap: Array<boolean>;}}
*/
function unmarkEmulationGroups(expression) {
const marker = emulationGroupMarker.replace(/\$/g, '\\$');
const captureMap = [true];
expression = replaceUnescaped(
expression,
`(?:${capturingDelim})(?<mark>${marker})?`,
({0: m, groups: {mark}}) => {
if (mark) {
captureMap.push(false);
return m.slice(0, -emulationGroupMarker.length);
}
captureMap.push(true);
return m;
},
Context.DEFAULT
);
return {
captureMap,
expression,
};
}

export {
RegExpSubclass,
};

0 comments on commit 2d7ad33

Please sign in to comment.