190 lines
7.5 KiB
JavaScript
190 lines
7.5 KiB
JavaScript
/*!
|
|
* XRegExp.build 3.1.0
|
|
* <xregexp.com>
|
|
* Steven Levithan (c) 2012-2016 MIT License
|
|
* Inspired by Lea Verou's RegExp.create <lea.verou.me>
|
|
*/
|
|
|
|
module.exports = function(XRegExp) {
|
|
'use strict';
|
|
|
|
var REGEX_DATA = 'xregexp',
|
|
subParts = /(\()(?!\?)|\\([1-9]\d*)|\\[\s\S]|\[(?:[^\\\]]|\\[\s\S])*]/g,
|
|
parts = XRegExp.union([/\({{([\w$]+)}}\)|{{([\w$]+)}}/, subParts], 'g');
|
|
|
|
/**
|
|
* Strips a leading `^` and trailing unescaped `$`, if both are present.
|
|
*
|
|
* @private
|
|
* @param {String} pattern Pattern to process.
|
|
* @returns {String} Pattern with edge anchors removed.
|
|
*/
|
|
function deanchor(pattern) {
|
|
// Allow any number of empty noncapturing groups before/after anchors, because regexes
|
|
// built/generated by XRegExp sometimes include them
|
|
var leadingAnchor = /^(?:\(\?:\))*\^/,
|
|
trailingAnchor = /\$(?:\(\?:\))*$/;
|
|
|
|
if (
|
|
leadingAnchor.test(pattern) &&
|
|
trailingAnchor.test(pattern) &&
|
|
// Ensure that the trailing `$` isn't escaped
|
|
trailingAnchor.test(pattern.replace(/\\[\s\S]/g, ''))
|
|
) {
|
|
return pattern.replace(leadingAnchor, '').replace(trailingAnchor, '');
|
|
}
|
|
|
|
return pattern;
|
|
}
|
|
|
|
/**
|
|
* Converts the provided value to an XRegExp. Native RegExp flags are not preserved.
|
|
*
|
|
* @private
|
|
* @param {String|RegExp} value Value to convert.
|
|
* @returns {RegExp} XRegExp object with XRegExp syntax applied.
|
|
*/
|
|
function asXRegExp(value) {
|
|
return XRegExp.isRegExp(value) ?
|
|
(value[REGEX_DATA] && value[REGEX_DATA].captureNames ?
|
|
// Don't recompile, to preserve capture names
|
|
value :
|
|
// Recompile as XRegExp
|
|
XRegExp(value.source)
|
|
) :
|
|
// Compile string as XRegExp
|
|
XRegExp(value);
|
|
}
|
|
|
|
/**
|
|
* Builds regexes using named subpatterns, for readability and pattern reuse. Backreferences in the
|
|
* outer pattern and provided subpatterns are automatically renumbered to work correctly. Native
|
|
* flags used by provided subpatterns are ignored in favor of the `flags` argument.
|
|
*
|
|
* @memberOf XRegExp
|
|
* @param {String} pattern XRegExp pattern using `{{name}}` for embedded subpatterns. Allows
|
|
* `({{name}})` as shorthand for `(?<name>{{name}})`. Patterns cannot be embedded within
|
|
* character classes.
|
|
* @param {Object} subs Lookup object for named subpatterns. Values can be strings or regexes. A
|
|
* leading `^` and trailing unescaped `$` are stripped from subpatterns, if both are present.
|
|
* @param {String} [flags] Any combination of XRegExp flags.
|
|
* @returns {RegExp} Regex with interpolated subpatterns.
|
|
* @example
|
|
*
|
|
* var time = XRegExp.build('(?x)^ {{hours}} ({{minutes}}) $', {
|
|
* hours: XRegExp.build('{{h12}} : | {{h24}}', {
|
|
* h12: /1[0-2]|0?[1-9]/,
|
|
* h24: /2[0-3]|[01][0-9]/
|
|
* }, 'x'),
|
|
* minutes: /^[0-5][0-9]$/
|
|
* });
|
|
* time.test('10:59'); // -> true
|
|
* XRegExp.exec('10:59', time).minutes; // -> '59'
|
|
*/
|
|
XRegExp.build = function(pattern, subs, flags) {
|
|
var inlineFlags = /^\(\?([\w$]+)\)/.exec(pattern),
|
|
data = {},
|
|
numCaps = 0, // 'Caps' is short for captures
|
|
numPriorCaps,
|
|
numOuterCaps = 0,
|
|
outerCapsMap = [0],
|
|
outerCapNames,
|
|
sub,
|
|
p;
|
|
|
|
// Add flags within a leading mode modifier to the overall pattern's flags
|
|
if (inlineFlags) {
|
|
flags = flags || '';
|
|
inlineFlags[1].replace(/./g, function(flag) {
|
|
// Don't add duplicates
|
|
flags += (flags.indexOf(flag) > -1 ? '' : flag);
|
|
});
|
|
}
|
|
|
|
for (p in subs) {
|
|
if (subs.hasOwnProperty(p)) {
|
|
// Passing to XRegExp enables extended syntax and ensures independent validity,
|
|
// lest an unescaped `(`, `)`, `[`, or trailing `\` breaks the `(?:)` wrapper. For
|
|
// subpatterns provided as native regexes, it dies on octals and adds the property
|
|
// used to hold extended regex instance data, for simplicity
|
|
sub = asXRegExp(subs[p]);
|
|
data[p] = {
|
|
// Deanchoring allows embedding independently useful anchored regexes. If you
|
|
// really need to keep your anchors, double them (i.e., `^^...$$`)
|
|
pattern: deanchor(sub.source),
|
|
names: sub[REGEX_DATA].captureNames || []
|
|
};
|
|
}
|
|
}
|
|
|
|
// Passing to XRegExp dies on octals and ensures the outer pattern is independently valid;
|
|
// helps keep this simple. Named captures will be put back
|
|
pattern = asXRegExp(pattern);
|
|
outerCapNames = pattern[REGEX_DATA].captureNames || [];
|
|
pattern = pattern.source.replace(parts, function($0, $1, $2, $3, $4) {
|
|
var subName = $1 || $2,
|
|
capName,
|
|
intro,
|
|
localCapIndex;
|
|
// Named subpattern
|
|
if (subName) {
|
|
if (!data.hasOwnProperty(subName)) {
|
|
throw new ReferenceError('Undefined property ' + $0);
|
|
}
|
|
// Named subpattern was wrapped in a capturing group
|
|
if ($1) {
|
|
capName = outerCapNames[numOuterCaps];
|
|
outerCapsMap[++numOuterCaps] = ++numCaps;
|
|
// If it's a named group, preserve the name. Otherwise, use the subpattern name
|
|
// as the capture name
|
|
intro = '(?<' + (capName || subName) + '>';
|
|
} else {
|
|
intro = '(?:';
|
|
}
|
|
numPriorCaps = numCaps;
|
|
return intro + data[subName].pattern.replace(subParts, function(match, paren, backref) {
|
|
// Capturing group
|
|
if (paren) {
|
|
capName = data[subName].names[numCaps - numPriorCaps];
|
|
++numCaps;
|
|
// If the current capture has a name, preserve the name
|
|
if (capName) {
|
|
return '(?<' + capName + '>';
|
|
}
|
|
// Backreference
|
|
} else if (backref) {
|
|
localCapIndex = +backref - 1;
|
|
// Rewrite the backreference
|
|
return data[subName].names[localCapIndex] ?
|
|
// Need to preserve the backreference name in case using flag `n`
|
|
'\\k<' + data[subName].names[localCapIndex] + '>' :
|
|
'\\' + (+backref + numPriorCaps);
|
|
}
|
|
return match;
|
|
}) + ')';
|
|
}
|
|
// Capturing group
|
|
if ($3) {
|
|
capName = outerCapNames[numOuterCaps];
|
|
outerCapsMap[++numOuterCaps] = ++numCaps;
|
|
// If the current capture has a name, preserve the name
|
|
if (capName) {
|
|
return '(?<' + capName + '>';
|
|
}
|
|
// Backreference
|
|
} else if ($4) {
|
|
localCapIndex = +$4 - 1;
|
|
// Rewrite the backreference
|
|
return outerCapNames[localCapIndex] ?
|
|
// Need to preserve the backreference name in case using flag `n`
|
|
'\\k<' + outerCapNames[localCapIndex] + '>' :
|
|
'\\' + outerCapsMap[+$4];
|
|
}
|
|
return $0;
|
|
});
|
|
|
|
return XRegExp(pattern, flags);
|
|
};
|
|
|
|
};
|