From 2d7ad33b42d93a1e19ef331acaff70c0bade4033 Mon Sep 17 00:00:00 2001 From: Steven Levithan Date: Fri, 15 Nov 2024 14:14:40 +0100 Subject: [PATCH] Rename subclass, move to own file --- src/regex.js | 90 +++---------------------------------------------- src/subclass.js | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 86 deletions(-) create mode 100644 src/subclass.js diff --git a/src/regex.js b/src/regex.js index 42df465..ecb6e40 100644 --- a/src/regex.js +++ b/src/regex.js @@ -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'; /** @@ -35,7 +36,7 @@ import {Context, hasUnescaped, replaceUnescaped} from 'regex-utilities'; (template: RawTemplate, ...substitutions: ReadonlyArray): T; (flags?: string): RegexTag; (options: RegexTagOptions & {subclass?: false}): RegexTag; - (options: RegexTagOptions & {subclass: true}): RegexTag; + (options: RegexTagOptions & {subclass: true}): RegexTag; }} */ /** @@ -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 @@ -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 @@ -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;}} -*/ -function unmarkEmulationGroups(expression) { - const marker = emulationGroupMarker.replace(/\$/g, '\\$'); - const captureMap = [true]; - expression = replaceUnescaped( - expression, - `(?:${capturingDelim})(?${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, diff --git a/src/subclass.js b/src/subclass.js new file mode 100644 index 0000000..4b77155 --- /dev/null +++ b/src/subclass.js @@ -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;}} +*/ +function unmarkEmulationGroups(expression) { + const marker = emulationGroupMarker.replace(/\$/g, '\\$'); + const captureMap = [true]; + expression = replaceUnescaped( + expression, + `(?:${capturingDelim})(?${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, +};