UNPKG

string-mask

Version:

A string formatter and validator based on masks

252 lines (228 loc) 10.3 kB
(function(root, factory) { /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.StringMask = factory(); } }(this, function() { var tokens = { '0': {pattern: /\d/, _default: '0'}, '9': {pattern: /\d/, optional: true}, '#': {pattern: /\d/, optional: true, recursive: true}, 'A': {pattern: /[a-zA-Z0-9]/}, 'S': {pattern: /[a-zA-Z]/}, 'U': {pattern: /[a-zA-Z]/, transform: function(c) { return c.toLocaleUpperCase(); }}, 'L': {pattern: /[a-zA-Z]/, transform: function(c) { return c.toLocaleLowerCase(); }}, '$': {escape: true} }; function isEscaped(pattern, pos) { var count = 0; var i = pos - 1; var token = {escape: true}; while (i >= 0 && token && token.escape) { token = tokens[pattern.charAt(i)]; count += token && token.escape ? 1 : 0; i--; } return count > 0 && count % 2 === 1; } function calcOptionalNumbersToUse(pattern, value) { var numbersInP = pattern.replace(/[^0]/g,'').length; var numbersInV = value.replace(/[^\d]/g,'').length; return numbersInV - numbersInP; } function concatChar(text, character, options, token) { if (token && typeof token.transform === 'function') { character = token.transform(character); } if (options.reverse) { return character + text; } return text + character; } function hasMoreTokens(pattern, pos, inc) { var pc = pattern.charAt(pos); var token = tokens[pc]; if (pc === '') { return false; } return token && !token.escape ? true : hasMoreTokens(pattern, pos + inc, inc); } function hasMoreRecursiveTokens(pattern, pos, inc) { var pc = pattern.charAt(pos); var token = tokens[pc]; if (pc === '') { return false; } return token && token.recursive ? true : hasMoreRecursiveTokens(pattern, pos + inc, inc); } function insertChar(text, char, position) { var t = text.split(''); t.splice(position, 0, char); return t.join(''); } function StringMask(pattern, opt) { this.options = opt || {}; this.options = { reverse: this.options.reverse || false, usedefaults: this.options.usedefaults || this.options.reverse }; this.pattern = pattern; } StringMask.prototype.process = function proccess(value) { if (!value) { return {result: '', valid: false}; } value = value + ''; var pattern2 = this.pattern; var valid = true; var formatted = ''; var valuePos = this.options.reverse ? value.length - 1 : 0; var patternPos = 0; var optionalNumbersToUse = calcOptionalNumbersToUse(pattern2, value); var escapeNext = false; var recursive = []; var inRecursiveMode = false; var steps = { start: this.options.reverse ? pattern2.length - 1 : 0, end: this.options.reverse ? -1 : pattern2.length, inc: this.options.reverse ? -1 : 1 }; function continueCondition(options) { if (!inRecursiveMode && !recursive.length && hasMoreTokens(pattern2, patternPos, steps.inc)) { // continue in the normal iteration return true; } else if (!inRecursiveMode && recursive.length && hasMoreRecursiveTokens(pattern2, patternPos, steps.inc)) { // continue looking for the recursive tokens // Note: all chars in the patterns after the recursive portion will be handled as static string return true; } else if (!inRecursiveMode) { // start to handle the recursive portion of the pattern inRecursiveMode = recursive.length > 0; } if (inRecursiveMode) { var pc = recursive.shift(); recursive.push(pc); if (options.reverse && valuePos >= 0) { patternPos++; pattern2 = insertChar(pattern2, pc, patternPos); return true; } else if (!options.reverse && valuePos < value.length) { pattern2 = insertChar(pattern2, pc, patternPos); return true; } } return patternPos < pattern2.length && patternPos >= 0; } /** * Iterate over the pattern's chars parsing/matching the input value chars * until the end of the pattern. If the pattern ends with recursive chars * the iteration will continue until the end of the input value. * * Note: The iteration must stop if an invalid char is found. */ for (patternPos = steps.start; continueCondition(this.options); patternPos = patternPos + steps.inc) { // Value char var vc = value.charAt(valuePos); // Pattern char to match with the value char var pc = pattern2.charAt(patternPos); var token = tokens[pc]; if (recursive.length && token && !token.recursive) { // In the recursive portion of the pattern: tokens not recursive must be seen as static chars token = null; } // 1. Handle escape tokens in pattern // go to next iteration: if the pattern char is a escape char or was escaped if (!inRecursiveMode || vc) { if (this.options.reverse && isEscaped(pattern2, patternPos)) { // pattern char is escaped, just add it and move on formatted = concatChar(formatted, pc, this.options, token); // skip escape token patternPos = patternPos + steps.inc; continue; } else if (!this.options.reverse && escapeNext) { // pattern char is escaped, just add it and move on formatted = concatChar(formatted, pc, this.options, token); escapeNext = false; continue; } else if (!this.options.reverse && token && token.escape) { // mark to escape the next pattern char escapeNext = true; continue; } } // 2. Handle recursive tokens in pattern // go to next iteration: if the value str is finished or // if there is a normal token in the recursive portion of the pattern if (!inRecursiveMode && token && token.recursive) { // save it to repeat in the end of the pattern and handle the value char now recursive.push(pc); } else if (inRecursiveMode && !vc) { // in recursive mode but value is finished. Add the pattern char if it is not a recursive token formatted = concatChar(formatted, pc, this.options, token); continue; } else if (!inRecursiveMode && recursive.length > 0 && !vc) { // recursiveMode not started but already in the recursive portion of the pattern continue; } // 3. Handle the value // break iterations: if value is invalid for the given pattern if (!token) { // add char of the pattern formatted = concatChar(formatted, pc, this.options, token); if (!inRecursiveMode && recursive.length) { // save it to repeat in the end of the pattern recursive.push(pc); } } else if (token.optional) { // if token is optional, only add the value char if it matchs the token pattern // if not, move on to the next pattern char if (token.pattern.test(vc) && optionalNumbersToUse) { formatted = concatChar(formatted, vc, this.options, token); valuePos = valuePos + steps.inc; optionalNumbersToUse--; } else if (recursive.length > 0 && vc) { valid = false; break; } } else if (token.pattern.test(vc)) { // if token isn't optional the value char must match the token pattern formatted = concatChar(formatted, vc, this.options, token); valuePos = valuePos + steps.inc; } else if (!vc && token._default && this.options.usedefaults) { // if the token isn't optional and has a default value, use it if the value is finished formatted = concatChar(formatted, token._default, this.options, token); } else { // the string value don't match the given pattern valid = false; break; } } return {result: formatted, valid: valid}; }; StringMask.prototype.apply = function(value) { return this.process(value).result; }; StringMask.prototype.validate = function(value) { return this.process(value).valid; }; StringMask.process = function(value, pattern, options) { return new StringMask(pattern, options).process(value); }; StringMask.apply = function(value, pattern, options) { return new StringMask(pattern, options).apply(value); }; StringMask.validate = function(value, pattern, options) { return new StringMask(pattern, options).validate(value); }; return StringMask; }));