UNPKG

incr-regex-package

Version:

An incremental regular expression parser in JavaScript; useful for input validation, RegExp

718 lines (646 loc) 25.5 kB
/** * Copyright (c) 2016, Nurul Choudhury * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ // // Modified from https://github.com/insin/inputmask-core // This was originally written by insin - on GIT hub // The code worked fine for fixed formatted input mask, but is not so useful for // varible mask based on regular expression (RegExp) // That capability regires this implementation of Regexp, and provides incremental processing of regular expression // Amost the entire original code has been replaces but the original interfaces remain // "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RXInputMask = undefined; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.trimHolder = trimHolder; exports._after = _after; var _utils = require("../utils"); var _incrRegexV = require("../incr-regex-v3"); var _regexUtils = require("../regex-utils"); var _RxMatcher = require("../RxMatcher"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } //import {DONE,MORE,MAYBE,FAILED} from '../rxtree'; //import {printExpr,printExprN,printExprQ} from "../rxprint"; function newSel(s, e) { e = e || s;return { start: Math.min(s, e), end: Math.max(s, e) }; } function selPlus(sel, x, y) { return clip(newSel(sel.start + x, sel.end + (y === undefined ? x : y))); } function clip(sel, range, clipFnAfter, clipFnBefore) { clipFnAfter = clipFnAfter || function (x) { return x; }; clipFnBefore = clipFnBefore || clipFnAfter; var clp = function clp(x, r, clipFn) { return clipFn(Math.max(Math.min(x, r.end), r.start)); }; return !range ? sel : { start: clp(sel.start, range, clipFnBefore), end: clp(sel.end, range, clipFnAfter) }; } function zero(x) { return !(x || 0); } function selRange(sel) { return sel ? sel.end - sel.start : 0; } function zeroRange(sel) { return zero(selRange(sel)); } function backward(oldSelV, newSelV) { return oldSelV.start > newSelV.start; } var RXInputMask = exports.RXInputMask = function () { function RXInputMask(options) { _classCallCheck(this, RXInputMask); options = (0, _utils.assign)({ pattern: null, selection: { start: 0, end: 0 }, value: '', history: { data: [], index: null, lastOp: null } }, options); if (options.pattern === null) { throw new Error('RXInputMask: you must provide a pattern.'); } this.setPattern(options.pattern, { value: options.value, selection: options.selection, history: options.history }); } /** * Get the state of object * @returns { pattern: RegExp, selection: { start: int, end: int}, value: String} */ _createClass(RXInputMask, [{ key: "getState", value: function getState() { var options = { pattern: this.pattern.clone(), selection: selPlus(this.selection, 0), value: this._getValue(), history: { data: [], index: null, lastOp: null } }; return options; } /** * Applies a single character of input based on the current selection. * @param {string} char * @return {boolean} true if a change has been made to value or selection as a * result of the input, false otherwise. */ }, { key: "input", value: function input(char) { // Ignore additional input if the cursor's at the end of the pattern if (zeroRange(this.selection) && this.pattern.isDone(this.selection.start)) { // to do find out if we are at the end return false; } var _input2 = this._input(char, this.selection, this.pattern), _input3 = _slicedToArray(_input2, 3), result = _input3[0], newSel = _input3[1], newPat = _input3[2]; // returns [status:boolean, newSelection, newPattern] if (!result) return false; var _ref = [this._getValue(), this.selection, this.pattern], valueBefore = _ref[0], selectionBefore = _ref[1], patternBefore = _ref[2]; this._lastOp = 'input'; this.selection = newSel; this.pattern = newPat; this.value = this.getValue(); // History if (this._historyIndex !== null) { // Took more input after undoing, so blow any subsequent history away //console.log('splice(', this._historyIndex, this._history.length - this._historyIndex, ')'); this._history.splice(this._historyIndex, this._history.length - this._historyIndex); this._historyIndex = null; } if (this._lastOp !== 'input' || !zeroRange(selectionBefore) || this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) { this._history.push({ value: valueBefore, selection: selectionBefore, lastOp: this._lastOp, pattern: patternBefore }); } this._lastSelection = selectionBefore; return true; } /** * Internal method to add a character at the current position * move all the characters. When inserting subsequent characters * the syatem tries to take care of the fixed characters * this is the same as the public input() method but it does not update history * * returns: [status:boolean, newSelection:Selection, newPattern: RegExp] * */ }, { key: "_input", value: function _input(ch, selection, aPattern) { // check if we are under an empty slot, then we can set the value there without moving anything if (zeroRange(selection) && aPattern.emptyAt(selection.start)) { selection = selPlus(selection, 0, 1); ////newPattern.getFirstEditableAtOrAfter(selection.end+1); } var newPattern = aPattern.clone(); // copy the current var _selection = selection, startPos = _selection.start, endPos = _selection.end; selection = this._updateSelection(selection, newPattern.getFirstEditableAtOrAfter(selection.start)); // start from the first editable position endPos = newPattern.getFirstEditableAtOrAfter(selection.end); newPattern.updateFixed(startPos, endPos); // make sure all the fixed values are set var textAfterSelection = _after(newPattern, false, endPos); // get the raw value var inputIndex = selection.start; newPattern.setPos(inputIndex); newPattern.fixTracker(); if (ch !== undefined && !_skipAndMatch(newPattern, ch)) { // but first make sure we did not enter a fixed character return [false, selection, newPattern]; } selection = this._updateSelection(selection, newPattern.getInputLength()); endPos = Math.max(endPos, selection.end); var newPos = newPattern.getFirstEditableAtOrAfter(newPattern.getInputLength()); // Put back the remainder // var resultPattern = this._insertRest(textAfterSelection, newPattern, inputIndex, endPos + 1); _fillInFixedValuesAtEnd(resultPattern || newPattern); resultPattern.fixTracker(); // Advance the cursor to the next character return [true, newSel(newPos, newPos), resultPattern || newPattern]; } }, { key: "_insertRest", value: function _insertRest(textToAdd, aPattern, inputIndex, endIndex) { function _ins(textPos, textToAdd, aPattern, inputIndex, endIndex) { if (textPos >= textToAdd.length) return aPattern; if (inputIndex > endIndex || aPattern.isDone()) return aPattern; var alt = aPattern.clone(); if (_skipAndMatch(alt, textToAdd.charAt(textPos))) { var res = _ins(textPos + 1, textToAdd, alt, alt.getInputLength() - 1, endIndex); if (res !== undefined) return res; } //console.log("rx", aPattern); while (_skipFixed(aPattern, false)) {} aPattern.match(undefined); return _ins(textPos, textToAdd, aPattern, inputIndex + 1, endIndex); } //textToAdd = trimHolder(textToAdd); var retV = _ins(0, textToAdd, aPattern, inputIndex, endIndex + textToAdd.length); //while(aPattern.skipFixed(true)); return retV; } /** * Attempts to delete from the value based on the current cursor position or * selection. * @return {boolean} true if the value or selection changed as the result of * backspacing, false otherwise. */ }, { key: "backspace", value: function backspace() { // If the cursor is at the start there's nothing to do var firstIx = this.pattern.getFirstEditableAtOrAfter(0); if (this.selection.start < firstIx || this.selection.end < firstIx) { return false; } var selectionBefore = (0, _utils.copy)(this.selection); var valueBefore = this._getValue(); var _selection2 = this.selection, start = _selection2.start, end = _selection2.end; // No range selected - work on the character preceding the cursor if (start === end) { start = this.pattern.getFirstEditableAtOrBefore(start - 1); end = start + 1; if (start < firstIx) return; } // Range selected - delete characters and leave the cursor at the start of the selection else { //end = this.pattern.getFirstEditableAtOrBefore(end); start = this.pattern.getFirstEditableAtOrBefore(end < start ? end : start); if (end === start) { end++; } if (end <= firstIx) return; } var result = this._input(undefined, newSel(start, end), this.pattern); var patternBefore = this.pattern; if (!result[0]) return false; this.pattern = result[2]; this.selection.start = this.selection.end = start; //console.log("Before:", selectionBefore, " After:",this.selection); this.pattern.fixTracker(); // History if (this._historyIndex !== null) { // Took more input after undoing, so blow any subsequent history away this._history.splice(this._historyIndex, this._history.length - this._historyIndex); } if (this._lastOp !== 'backspace' || selectionBefore.start !== selectionBefore.end || this._lastSelection !== null && selectionBefore.start !== this._lastSelection.start) { this._history.push({ value: valueBefore, selection: selectionBefore, lastOp: this._lastOp, pattern: patternBefore }); } this._lastOp = 'backspace'; this._lastSelection = (0, _utils.copy)(selectionBefore); return true; } }, { key: "del", value: function del() { if (zeroRange(this.selection)) { this.right(this.selection); return this.backspace(); } else return this.backspace(); } /** * Attempts to paste a string of input at the current cursor position or over * the top of the current selection. * Invalid content at any position will cause the paste to be rejected, and it * may contain static parts of the mask's pattern. * @param {string} input * @return {boolean} true if the paste was successful, false otherwise. */ }, { key: "paste", value: function paste(input) { // This is necessary because we're just calling input() with each character // and rolling back if any were invalid, rather than checking up-front. var initialState = { value: this.value.slice(), selection: (0, _utils.copy)(this.selection), _lastOp: this._lastOp, _history: this._history.slice(), _historyIndex: this._historyIndex, _lastSelection: (0, _utils.copy)(this._lastSelection), pattern: this.pattern.clone() }; // If there are static characters at the start of the pattern and the cursor // or selection is within them, the static characters must match for a valid // paste. var rest = this.getRawValueAt(this.selection.end); // get raw value from the pattern this.pattern.setPos(this.selection.start); var insVal = this._setValueFrom(this.selection.start, input); this.selection.end = this.pattern.getInputLength(); if (!insVal || !this._setValueFrom(this.selection.end, rest)) { (0, _utils.assign)(this, initialState); return false; } return true; } // History }, { key: "undo", value: function undo() { // If there is no history, or nothing more on the history stack, we can't undo if (this._history.length === 0 || this._historyIndex === 0) { return false; } var historyItem; if (this._historyIndex === null) { // Not currently undoing, set up the initial history index this._historyIndex = this._history.length - 1; historyItem = this._history[this._historyIndex]; // Add a new history entry if anything has changed since the last one, so we // can redo back to the initial state we started undoing from. var value = this._getValue(); if (historyItem.value !== value || historyItem.selection.start !== this.selection.start || historyItem.selection.end !== this.selection.end) { this._history.push({ value: value, selection: (0, _utils.copy)(this.selection), lastOp: this._lastOp, startUndo: true, pattern: this.pattern.clone() }); } } else { historyItem = this._history[--this._historyIndex]; } this.pattern = historyItem.pattern; this.setValue(historyItem.value); this.selection = historyItem.selection; this._lastOp = historyItem.lastOp; return true; } }, { key: "redo", value: function redo() { if (this._history.length === 0 || this._historyIndex === null) { return false; } var historyItem = this._history[++this._historyIndex]; // If this is the last history item, we're done redoing if (this._historyIndex === this._history.length - 1) { this._historyIndex = null; // If the last history item was only added to start undoing, remove it if (historyItem.startUndo) { this._history.pop(); } } this.pattern = historyItem.pattern.clone(); this.setValue(historyItem.value); this.selection = historyItem.selection; this._lastOp = historyItem.lastOp; return true; } }, { key: "left", value: function left(selection) { var sel = (0, _utils.copy)(selection); if (sel && zeroRange(sel)) { sel.start = sel.end = selection.start - 1; this.setSelection(sel); } return this; } }, { key: "right", value: function right(selection) { var sel = (0, _utils.copy)(selection); if (sel && sel.start === sel.end) { sel.start = sel.end = selection.start + 1; this.setSelection(sel); } return this; } // Getters & setters }, { key: "setPattern", value: function setPattern(pattern, options) { options = (0, _utils.assign)({ // selection: {start: 0, end: 0}, value: '' }, options); this.pattern = new _RxMatcher.RxMatcher((0, _incrRegexV.incrRegEx)(pattern)); this.setValue(options.value); this.emptyValue = this.pattern.minChars(); this.setSelection(options.selection); while (this.skipFixed(true)) {} if (zeroRange(this.selection) && this.pattern.getInputTracker().length !== 0) { var ss = this._getValue(); this.setValue(ss); this.selection.start = this.selection.end = ss.length; } this._resetHistory(); return this; } }, { key: "select", value: function select(low, high) { this.selection = high < low ? newSel(high, low) : newSel(low, high); return this; } }, { key: "setSelection", value: function setSelection(selection) { var _this = this; var sel = selection === this.selection ? this.selection : (0, _utils.copy)(selection); var old = this.selection || sel; var fea = function fea(x) { return _this.pattern.getFirstEditableAtOrAfter(x); }; var feb = function feb(x) { return _this.pattern.getFirstEditableAtOrBefore(x); }; this.selection = old; var firstEditableIndex = fea(0); // first editable after var lastEditableIndex = this.pattern.lastEditableIndex(); var range = newSel(firstEditableIndex, lastEditableIndex); if (zeroRange(sel)) { this.selection = clip(sel, range, backward(this.selection, sel) ? feb : fea); } else { this.selection = clip(sel, range, fea, feb); } return this; } }, { key: "_adjustSelection", value: function _adjustSelection(sel, forward) { if (zeroRange(sel)) { return newSel(forward ? this.pattern.getFirstEditableAtOrAfter(sel.start) : this.pattern.getFirstEditableAtOrBefore(sel.start)); } return newSel(this.pattern.getFirstEditableAtOrBefore(sel.start), this.pattern.getFirstEditableAtOrAfter(sel.end)); } }, { key: "_setValueFrom", value: function _setValueFrom(ix, str) { var newPattern = this.pattern.clone(); var success = true; if (ix !== undefined) newPattern.setPos(ix); for (var i = ix, j = 0; j < str.length && success; i++, j++) { var c = str.charAt(j); success &= _skipAndMatch(newPattern, c); } if (success) { _fillInFixedValuesAtEnd(newPattern); this.pattern = newPattern; } return success; } }, { key: "setValue", value: function setValue(value) { var lg = new Logger("RXInputMask:"); if (this.getValue() === value) { // lg.println("no change to:",value ).flush(); return true; } var workingPattern = this.pattern.clone(); if (value === null) { value = ''; } workingPattern.reset(); lg.println("iterate over", value, "length:", value.length); for (var i = 0; i < value.length; i++) { var c = value.charAt(i); lg.print("index: ", i, "char:", c, "minChars:", workingPattern.minChars(), "sameAsRest", sameAsRest(value.substring(i), workingPattern.minChars()), "---"); if (sameAsRest(value.substring(i), workingPattern.minChars())) break; if ((0, _regexUtils.isHolder)(c)) c = undefined; if (!_skipAndMatch(workingPattern, c)) { return false; } } //_fillInFixedValuesAtEnd(this.pattern); this.pattern = workingPattern; this.value = this.getValue(); return true; } }, { key: "minCharsList", value: function minCharsList(flag) { //if( !flag ) throw new Error("flag should be true"); return this.pattern.minCharsList(flag); } }, { key: "getSelection", value: function getSelection() { return (0, _utils.copy)(this.selection); } }, { key: "_getValue", value: function _getValue() { return _after(this.pattern, true, 0); } }, { key: "getValue", value: function getValue() { return _after(this.pattern, true, 0) + this.pattern.minChars(); //.valueWithMask(); //return this.pattern.rawValue(0); //return this._getValue() } }, { key: "getRawValue", value: function getRawValue() { return _after(this.pattern, false, 0); } }, { key: "getRawValueAt", value: function getRawValueAt(ix) { return _after(this.pattern, false, ix); } }, { key: "reset", value: function reset() { this.pattern.reset(); this._resetHistory(); this.value = this.getValue(); this.selection.start = this.selection.end = 0; this.setSelection(this.selection); return this; } }, { key: "_resetHistory", value: function _resetHistory() { this._history = []; this._historyIndex = null; this._lastOp = null; this._lastSelection = (0, _utils.copy)(this.selection); return this; } }, { key: "_updateSelection", value: function _updateSelection(aSelection, start) { var res = (0, _utils.copy)(aSelection); res.start = start; if (start > res.end) res.end = start; return res; } }, { key: "skipFixed", value: function skipFixed(flag) { return _skipFixed(this.pattern, flag); } }, { key: "isDone", value: function isDone() { var pattern = this.pattern.clone(); var value = this.getValue(); var list = value.split(''); //console.log("isDone: ", value); if ((0, _utils.arr_find)(function (e) { return (0, _regexUtils.isHolder)(e); }, list)) return "MORE"; pattern.reset(); for (var i = 0; i < list.length; i++) { if ((0, _regexUtils.isMeta)(list[i])) continue; if (!pattern.match(list[i])) return "MORE"; } //console.log("isDone: state", pattern.stateStr()); return pattern.stateStr(); } }]); return RXInputMask; }(); // *pattern Helpers function sameAsRest(str, rest) { if (str === rest) return true; return false; //( isMeta(rest[0]) && str === rest.substring(1,rest.length) ) ; } function _fillInFixedValuesAtEnd(pattern) { var s = pattern.minChars(); var i = 0; for (; s.length > i && !(0, _regexUtils.isMeta)(s.charAt(0)); i++) { if (!pattern.match(s.charAt(0))) return i > 0; s = pattern.minChars(); } return i > 0; } function _skipFixed(aPattern, onlyFixed) { var s = aPattern.minChars(); onlyFixed = !!onlyFixed; if (onlyFixed !== true && s.length > 1 && (0, _regexUtils.isOptional)(s.charAt(0)) && !(0, _regexUtils.isMeta)(s.charAt(1))) { if (aPattern.match(s.charAt(1))) return true; } else if ( /* onlyFixed === true && */s.length > 0 && !(0, _regexUtils.isMeta)(s.charAt(0))) return aPattern.match(s.charAt(0)); return false; } function _skipAndMatch(aPattern, ch) { if (aPattern.match(ch)) return true; var backup = aPattern.clone(); while (_skipFixed(aPattern, false)) { if (aPattern.match(ch)) return true; } aPattern.reset(); aPattern.matchStr(_after(backup, true, 0)); return false; } function trimHolder(textToAdd) { var i = textToAdd.length - 1; for (; i >= 0 && (0, _regexUtils.isHolder)(textToAdd.charAt(i)); i--) {} return textToAdd.substring(0, i + 1); } function _after(aPattern, all, ix) { /* public */ // get the input matched so far after ix. var tracker = aPattern.getInputTracker(); if (!ix) { var al = all ? tracker : tracker.filter(function (e) { return e[1] === undefined; }); return al.map(function (e) { return e[0]; }).join(''); } else { var _al = tracker.filter(function (e, i) { return i >= ix && (all || e[1] === undefined); }); return _al.map(function (e) { return e[0]; }).join(''); } } var Logger = function () { function Logger(X) { _classCallCheck(this, Logger); this.content = X || ""; } _createClass(Logger, [{ key: "print", value: function print() { for (var _len = arguments.length, s = Array(_len), _key = 0; _key < _len; _key++) { s[_key] = arguments[_key]; } this.content += "," + (s || []).map(function (a) { return JSON.stringify(a); }).join(" ");return this; } }, { key: "println", value: function println() { for (var _len2 = arguments.length, s = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { s[_key2] = arguments[_key2]; } this.print.apply(this, s);this.content += "\n";return this; } }, { key: "flush", value: function flush() { console.log(this.content);this.content = ""; } }]); return Logger; }();