strong-tie/inbound-calls
0
1"use strict";2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {3 if (k2 === undefined) k2 = k;4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });5}) : (function(o, m, k, k2) {6 if (k2 === undefined) k2 = k;7 o[k2] = m[k];8}));9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {10 Object.defineProperty(o, "default", { enumerable: true, value: v });11}) : function(o, v) {12 o["default"] = v;13});14var __importStar = (this && this.__importStar) || function (mod) {15 if (mod && mod.__esModule) return mod;16 var result = {};17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);18 __setModuleDefault(result, mod);19 return result;20};21Object.defineProperty(exports, "__esModule", { value: true });22exports.tokenizer = void 0;23const util = __importStar(require("./util"));24const types_1 = require("./types");25const sets = __importStar(require("./sets"));26/**27 * Valid opening characters for capture group names.28 */29const captureGroupFirstChar = /^[a-zA-Z_$]$/i;30/**31 * Valid characters for capture group names.32 */33const captureGroupChars = /^[a-zA-Z0-9_$]$/i;34const digit = /\d/;35/**36 * Tokenizes a regular expression (that is currently a string)37 * @param {string} regexpStr String of regular expression to be tokenized38 *39 * @returns {Root}40 */41exports.tokenizer = (regexpStr) => {42 let i = 0, c;43 let start = { type: types_1.types.ROOT, stack: [] };44 // Keep track of last clause/group and stack.45 let lastGroup = start;46 let last = start.stack;47 let groupStack = [];48 let referenceQueue = [];49 let groupCount = 0;50 const repeatErr = (col) => {51 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Nothing to repeat at column ${col - 1}`);52 };53 // Decode a few escaped characters.54 let str = util.strToChars(regexpStr);55 // Iterate through each character in string.56 while (i < str.length) {57 switch (c = str[i++]) {58 // Handle escaped characters, inclues a few sets.59 case '\\':60 if (i === str.length) {61 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: \\ at end of pattern`);62 }63 switch (c = str[i++]) {64 case 'b':65 last.push({ type: types_1.types.POSITION, value: 'b' });66 break;67 case 'B':68 last.push({ type: types_1.types.POSITION, value: 'B' });69 break;70 case 'w':71 last.push(sets.words());72 break;73 case 'W':74 last.push(sets.notWords());75 break;76 case 'd':77 last.push(sets.ints());78 break;79 case 'D':80 last.push(sets.notInts());81 break;82 case 's':83 last.push(sets.whitespace());84 break;85 case 'S':86 last.push(sets.notWhitespace());87 break;88 default:89 // Check if c is integer.90 // In which case it's a reference.91 if (digit.test(c)) {92 let digits = c;93 while (i < str.length && digit.test(str[i])) {94 digits += str[i++];95 }96 let value = parseInt(digits, 10);97 const reference = { type: types_1.types.REFERENCE, value };98 last.push(reference);99 referenceQueue.push({ reference, stack: last, index: last.length - 1 });100 // Escaped character.101 }102 else {103 last.push({ type: types_1.types.CHAR, value: c.charCodeAt(0) });104 }105 }106 break;107 // Positionals.108 case '^':109 last.push({ type: types_1.types.POSITION, value: '^' });110 break;111 case '$':112 last.push({ type: types_1.types.POSITION, value: '$' });113 break;114 // Handle custom sets.115 case '[': {116 // Check if this class is 'anti' i.e. [^abc].117 let not;118 if (str[i] === '^') {119 not = true;120 i++;121 }122 else {123 not = false;124 }125 // Get all the characters in class.126 let classTokens = util.tokenizeClass(str.slice(i), regexpStr);127 // Increase index by length of class.128 i += classTokens[1];129 last.push({130 type: types_1.types.SET,131 set: classTokens[0],132 not,133 });134 break;135 }136 // Class of any character except \n.137 case '.':138 last.push(sets.anyChar());139 break;140 // Push group onto stack.141 case '(': {142 // Create group.143 let group = {144 type: types_1.types.GROUP,145 stack: [],146 remember: true,147 };148 // If this is a special kind of group.149 if (str[i] === '?') {150 c = str[i + 1];151 i += 2;152 // Match if followed by.153 if (c === '=') {154 group.followedBy = true;155 group.remember = false;156 // Match if not followed by.157 }158 else if (c === '!') {159 group.notFollowedBy = true;160 group.remember = false;161 }162 else if (c === '<') {163 let name = '';164 if (captureGroupFirstChar.test(str[i])) {165 name += str[i];166 i++;167 }168 else {169 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Invalid capture group name, character '${str[i]}'` +170 ` after '<' at column ${i + 1}`);171 }172 while (i < str.length && captureGroupChars.test(str[i])) {173 name += str[i];174 i++;175 }176 if (!name) {177 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Invalid capture group name, character '${str[i]}'` +178 ` after '<' at column ${i + 1}`);179 }180 if (str[i] !== '>') {181 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Unclosed capture group name, expected '>', found` +182 ` '${str[i]}' at column ${i + 1}`);183 }184 group.name = name;185 i++;186 }187 else if (c === ':') {188 group.remember = false;189 }190 else {191 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Invalid group, character '${c}'` +192 ` after '?' at column ${i - 1}`);193 }194 }195 else {196 groupCount += 1;197 }198 // Insert subgroup into current group stack.199 last.push(group);200 // Remember the current group for when the group closes.201 groupStack.push(lastGroup);202 // Make this new group the current group.203 lastGroup = group;204 last = group.stack;205 break;206 }207 // Pop group out of stack.208 case ')':209 if (groupStack.length === 0) {210 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Unmatched ) at column ${i - 1}`);211 }212 lastGroup = groupStack.pop();213 // Check if this group has a PIPE.214 // To get back the correct last stack.215 last = lastGroup.options ?216 lastGroup.options[lastGroup.options.length - 1] :217 lastGroup.stack;218 break;219 // Use pipe character to give more choices.220 case '|': {221 // Create array where options are if this is the first PIPE222 // in this clause.223 if (!lastGroup.options) {224 lastGroup.options = [lastGroup.stack];225 delete lastGroup.stack;226 }227 // Create a new stack and add to options for rest of clause.228 let stack = [];229 lastGroup.options.push(stack);230 last = stack;231 break;232 }233 // Repetition.234 // For every repetition, remove last element from last stack235 // then insert back a RANGE object.236 // This design is chosen because there could be more than237 // one repetition symbols in a regex i.e. `a?+{2,3}`.238 case '{': {239 let rs = /^(\d+)(,(\d+)?)?\}/.exec(str.slice(i)), min, max;240 if (rs !== null) {241 if (last.length === 0) {242 repeatErr(i);243 }244 min = parseInt(rs[1], 10);245 max = rs[2] ? rs[3] ? parseInt(rs[3], 10) : Infinity : min;246 i += rs[0].length;247 last.push({248 type: types_1.types.REPETITION,249 min,250 max,251 value: last.pop(),252 });253 }254 else {255 last.push({256 type: types_1.types.CHAR,257 value: 123,258 });259 }260 break;261 }262 case '?':263 if (last.length === 0) {264 repeatErr(i);265 }266 last.push({267 type: types_1.types.REPETITION,268 min: 0,269 max: 1,270 value: last.pop(),271 });272 break;273 case '+':274 if (last.length === 0) {275 repeatErr(i);276 }277 last.push({278 type: types_1.types.REPETITION,279 min: 1,280 max: Infinity,281 value: last.pop(),282 });283 break;284 case '*':285 if (last.length === 0) {286 repeatErr(i);287 }288 last.push({289 type: types_1.types.REPETITION,290 min: 0,291 max: Infinity,292 value: last.pop(),293 });294 break;295 // Default is a character that is not `\[](){}?+*^$`.296 default:297 last.push({298 type: types_1.types.CHAR,299 value: c.charCodeAt(0),300 });301 }302 }303 // Check if any groups have not been closed.304 if (groupStack.length !== 0) {305 throw new SyntaxError(`Invalid regular expression: /${regexpStr}/: Unterminated group`);306 }307 updateReferences(referenceQueue, groupCount);308 return start;309};310/**311 * This is a side effecting function that changes references to chars312 * if there are not enough capturing groups to reference313 * See: https://github.com/fent/ret.js/pull/39#issuecomment-1006475703314 * See: https://github.com/fent/ret.js/issues/38315 * @param {(Reference | Char)[]} referenceQueue316 * @param {number} groupCount317 * @returns {void}318 */319function updateReferences(referenceQueue, groupCount) {320 // Note: We go through the queue in reverse order so321 // that index we use is correct even if we have to add322 // multiple tokens to one stack323 for (const elem of referenceQueue.reverse()) {324 if (groupCount < elem.reference.value) {325 // If there is nothing to reference then turn this into a char token326 elem.reference.type = types_1.types.CHAR;327 const valueString = elem.reference.value.toString();328 elem.reference.value = parseInt(valueString, 8);329 // If the number is not octal then we need to create multiple tokens330 // https://github.com/fent/ret.js/pull/39#issuecomment-1008229226331 if (!/^[0-7]+$/.test(valueString)) {332 let i = 0;333 while (valueString[i] !== '8' && valueString[i] !== '9') {334 i += 1;335 }336 if (i === 0) {337 // Handling case when escaped number starts with 8 or 9338 elem.reference.value = valueString.charCodeAt(0);339 i += 1;340 }341 else {342 // If the escaped number does not start with 8 or 9, then all343 // 0-7 digits before the first 8/9 form the first character code344 // see: https://github.com/fent/ret.js/pull/39#discussion_r780747085345 elem.reference.value = parseInt(valueString.slice(0, i), 8);346 }347 if (valueString.length > i) {348 const tail = elem.stack.splice(elem.index + 1);349 for (const char of valueString.slice(i)) {350 elem.stack.push({351 type: types_1.types.CHAR,352 value: char.charCodeAt(0),353 });354 }355 elem.stack.push(...tail);356 }357 }358 }359 }360}361//# sourceMappingURL=tokenizer.js.map