UNPKG

3.81 kBJavaScriptView Raw
1/**
2 * @param {string} value
3 * @returns {RegExp}
4 * */
5function escape(value) {
6 return new RegExp(value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm');
7}
8
9/**
10 * @param {RegExp | string } re
11 * @returns {string}
12 */
13function source(re) {
14 if (!re) return null;
15 if (typeof re === "string") return re;
16
17 return re.source;
18}
19
20/**
21 * @param {RegExp | string } re
22 * @returns {string}
23 */
24function lookahead(re) {
25 return concat('(?=', re, ')');
26}
27
28/**
29 * @param {RegExp | string } re
30 * @returns {string}
31 */
32function anyNumberOfTimes(re) {
33 return concat('(?:', re, ')*');
34}
35
36/**
37 * @param {RegExp | string } re
38 * @returns {string}
39 */
40function optional(re) {
41 return concat('(?:', re, ')?');
42}
43
44/**
45 * @param {...(RegExp | string) } args
46 * @returns {string}
47 */
48function concat(...args) {
49 const joined = args.map((x) => source(x)).join("");
50 return joined;
51}
52
53/**
54 * @param { Array<string | RegExp | Object> } args
55 * @returns {object}
56 */
57function stripOptionsFromArgs(args) {
58 const opts = args[args.length - 1];
59
60 if (typeof opts === 'object' && opts.constructor === Object) {
61 args.splice(args.length - 1, 1);
62 return opts;
63 } else {
64 return {};
65 }
66}
67
68/**
69 * Any of the passed expresssions may match
70 *
71 * Creates a huge this | this | that | that match
72 * @param {(RegExp | string)[] } args
73 * @returns {string}
74 */
75function either(...args) {
76 /** @type { object & {capture?: boolean} } */
77 const opts = stripOptionsFromArgs(args);
78 const joined = '('
79 + (opts.capture ? "" : "?:")
80 + args.map((x) => source(x)).join("|") + ")";
81 return joined;
82}
83
84/**
85 * @param {RegExp | string} re
86 * @returns {number}
87 */
88function countMatchGroups(re) {
89 return (new RegExp(re.toString() + '|')).exec('').length - 1;
90}
91
92/**
93 * Does lexeme start with a regular expression match at the beginning
94 * @param {RegExp} re
95 * @param {string} lexeme
96 */
97function startsWith(re, lexeme) {
98 const match = re && re.exec(lexeme);
99 return match && match.index === 0;
100}
101
102// BACKREF_RE matches an open parenthesis or backreference. To avoid
103// an incorrect parse, it additionally matches the following:
104// - [...] elements, where the meaning of parentheses and escapes change
105// - other escape sequences, so we do not misparse escape sequences as
106// interesting elements
107// - non-matching or lookahead parentheses, which do not capture. These
108// follow the '(' with a '?'.
109const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
110
111// **INTERNAL** Not intended for outside usage
112// join logically computes regexps.join(separator), but fixes the
113// backreferences so they continue to match.
114// it also places each individual regular expression into it's own
115// match group, keeping track of the sequencing of those match groups
116// is currently an exercise for the caller. :-)
117/**
118 * @param {(string | RegExp)[]} regexps
119 * @param {{joinWith: string}} opts
120 * @returns {string}
121 */
122function _rewriteBackreferences(regexps, { joinWith }) {
123 let numCaptures = 0;
124
125 return regexps.map((regex) => {
126 numCaptures += 1;
127 const offset = numCaptures;
128 let re = source(regex);
129 let out = '';
130
131 while (re.length > 0) {
132 const match = BACKREF_RE.exec(re);
133 if (!match) {
134 out += re;
135 break;
136 }
137 out += re.substring(0, match.index);
138 re = re.substring(match.index + match[0].length);
139 if (match[0][0] === '\\' && match[1]) {
140 // Adjust the backreference.
141 out += '\\' + String(Number(match[1]) + offset);
142 } else {
143 out += match[0];
144 if (match[0] === '(') {
145 numCaptures++;
146 }
147 }
148 }
149 return out;
150 }).map(re => `(${re})`).join(joinWith);
151}
152
153export { _rewriteBackreferences, anyNumberOfTimes, concat, countMatchGroups, either, escape, lookahead, optional, source, startsWith };