UNPKG

luhn-generator

Version:

A generator of numbers that passes the validation of Luhn algorithm or Luhn formula, also known as the 'modulus 10' or 'mod 10' algorithm

534 lines (438 loc) 11.1 kB
'use strict'; var use = require('use'); var util = require('util'); var Cache = require('map-cache'); var define = require('define-property'); var debug = require('debug')('snapdragon:parser'); var Position = require('./position'); var utils = require('./utils'); /** * Create a new `Parser` with the given `input` and `options`. * @param {String} `input` * @param {Object} `options` * @api public */ function Parser(options) { debug('initializing', __filename); this.options = utils.extend({source: 'string'}, options); this.init(this.options); use(this); } /** * Prototype methods */ Parser.prototype = { constructor: Parser, init: function(options) { this.orig = ''; this.input = ''; this.parsed = ''; this.column = 1; this.line = 1; this.regex = new Cache(); this.errors = this.errors || []; this.parsers = this.parsers || {}; this.types = this.types || []; this.sets = this.sets || {}; this.fns = this.fns || []; this.currentType = 'root'; var pos = this.position(); this.bos = pos({type: 'bos', val: ''}); this.ast = { type: 'root', errors: this.errors, nodes: [this.bos] }; define(this.bos, 'parent', this.ast); this.nodes = [this.ast]; this.count = 0; this.setCount = 0; this.stack = []; }, /** * Throw a formatted error with the cursor column and `msg`. * @param {String} `msg` Message to use in the Error. */ error: function(msg, node) { var pos = node.position || {start: {column: 0, line: 0}}; var line = pos.start.line; var column = pos.start.column; var source = this.options.source; var message = source + ' <line:' + line + ' column:' + column + '>: ' + msg; var err = new Error(message); err.source = source; err.reason = msg; err.pos = pos; if (this.options.silent) { this.errors.push(err); } else { throw err; } }, /** * Define a non-enumberable property on the `Parser` instance. * * ```js * parser.define('foo', 'bar'); * ``` * @name .define * @param {String} `key` propery name * @param {any} `val` property value * @return {Object} Returns the Parser instance for chaining. * @api public */ define: function(key, val) { define(this, key, val); return this; }, /** * Mark position and patch `node.position`. */ position: function() { var start = { line: this.line, column: this.column }; var self = this; return function(node) { define(node, 'position', new Position(start, self)); return node; }; }, /** * Set parser `name` with the given `fn` * @param {String} `name` * @param {Function} `fn` * @api public */ set: function(type, fn) { if (this.types.indexOf(type) === -1) { this.types.push(type); } this.parsers[type] = fn.bind(this); return this; }, /** * Get parser `name` * @param {String} `name` * @api public */ get: function(name) { return this.parsers[name]; }, /** * Push a `token` onto the `type` stack. * * @param {String} `type` * @return {Object} `token` * @api public */ push: function(type, token) { this.sets[type] = this.sets[type] || []; this.count++; this.stack.push(token); return this.sets[type].push(token); }, /** * Pop a token off of the `type` stack * @param {String} `type` * @returns {Object} Returns a token * @api public */ pop: function(type) { this.sets[type] = this.sets[type] || []; this.count--; this.stack.pop(); return this.sets[type].pop(); }, /** * Return true if inside a `stack` node. Types are `braces`, `parens` or `brackets`. * * @param {String} `type` * @return {Boolean} * @api public */ isInside: function(type) { this.sets[type] = this.sets[type] || []; return this.sets[type].length > 0; }, /** * Return true if `node` is the given `type`. * * ```js * parser.isType(node, 'brace'); * ``` * @param {Object} `node` * @param {String} `type` * @return {Boolean} * @api public */ isType: function(node, type) { return node && node.type === type; }, /** * Get the previous AST node * @return {Object} */ prev: function(n) { return this.stack.length > 0 ? utils.last(this.stack, n) : utils.last(this.nodes, n); }, /** * Update line and column based on `str`. */ consume: function(len) { this.input = this.input.substr(len); }, /** * Update column based on `str`. */ updatePosition: function(str, len) { var lines = str.match(/\n/g); if (lines) this.line += lines.length; var i = str.lastIndexOf('\n'); this.column = ~i ? len - i : this.column + len; this.parsed += str; this.consume(len); }, /** * Match `regex`, return captures, and update the cursor position by `match[0]` length. * @param {RegExp} `regex` * @return {Object} */ match: function(regex) { var m = regex.exec(this.input); if (m) { this.updatePosition(m[0], m[0].length); return m; } }, /** * Capture `type` with the given regex. * @param {String} `type` * @param {RegExp} `regex` * @return {Function} */ capture: function(type, regex) { if (typeof regex === 'function') { return this.set.apply(this, arguments); } this.regex.set(type, regex); this.set(type, function() { var parsed = this.parsed; var pos = this.position(); var m = this.match(regex); if (!m || !m[0]) return; var prev = this.prev(); var node = pos({ type: type, val: m[0], parsed: parsed, rest: this.input }); if (m[1]) { node.inner = m[1]; } define(node, 'inside', this.stack.length > 0); define(node, 'parent', prev); prev.nodes.push(node); }.bind(this)); return this; }, /** * Create a parser with open and close for parens, * brackets or braces */ capturePair: function(type, openRegex, closeRegex, fn) { this.sets[type] = this.sets[type] || []; /** * Open */ this.set(type + '.open', function() { var parsed = this.parsed; var pos = this.position(); var m = this.match(openRegex); if (!m || !m[0]) return; var val = m[0]; this.setCount++; this.specialChars = true; var open = pos({ type: type + '.open', val: val, rest: this.input }); if (typeof m[1] !== 'undefined') { open.inner = m[1]; } var prev = this.prev(); var node = pos({ type: type, nodes: [open] }); define(node, 'rest', this.input); define(node, 'parsed', parsed); define(node, 'prefix', m[1]); define(node, 'parent', prev); define(open, 'parent', node); if (typeof fn === 'function') { fn.call(this, open, node); } this.push(type, node); prev.nodes.push(node); }); /** * Close */ this.set(type + '.close', function() { var pos = this.position(); var m = this.match(closeRegex); if (!m || !m[0]) return; var parent = this.pop(type); var node = pos({ type: type + '.close', rest: this.input, suffix: m[1], val: m[0] }); if (!this.isType(parent, type)) { if (this.options.strict) { throw new Error('missing opening "' + type + '"'); } this.setCount--; node.escaped = true; return node; } if (node.suffix === '\\') { parent.escaped = true; node.escaped = true; } parent.nodes.push(node); define(node, 'parent', parent); }); return this; }, /** * Capture end-of-string */ eos: function() { var pos = this.position(); if (this.input) return; var prev = this.prev(); while (prev.type !== 'root' && !prev.visited) { if (this.options.strict === true) { throw new SyntaxError('invalid syntax:' + util.inspect(prev, null, 2)); } if (!hasDelims(prev)) { prev.parent.escaped = true; prev.escaped = true; } visit(prev, function(node) { if (!hasDelims(node.parent)) { node.parent.escaped = true; node.escaped = true; } }); prev = prev.parent; } var tok = pos({ type: 'eos', val: this.append || '' }); define(tok, 'parent', this.ast); return tok; }, /** * Run parsers to advance the cursor position */ next: function() { var parsed = this.parsed; var len = this.types.length; var idx = -1; var tok; while (++idx < len) { if ((tok = this.parsers[this.types[idx]].call(this))) { define(tok, 'rest', this.input); define(tok, 'parsed', parsed); this.last = tok; return tok; } } }, /** * Parse the given string. * @return {Array} */ parse: function(input) { if (typeof input !== 'string') { throw new TypeError('expected a string'); } this.init(this.options); this.orig = input; this.input = input; var self = this; function parse() { // check input before calling `.next()` input = self.input; // get the next AST ndoe var node = self.next(); if (node) { var prev = self.prev(); if (prev) { define(node, 'parent', prev); if (prev.nodes) { prev.nodes.push(node); } } if (self.sets.hasOwnProperty(prev.type)) { self.currentType = prev.type; } } // if we got here but input is not changed, throw an error if (self.input && input === self.input) { throw new Error('no parsers registered for: "' + self.input.slice(0, 5) + '"'); } } while (this.input) parse(); if (this.stack.length && this.options.strict) { var node = this.stack.pop(); throw this.error('missing opening ' + node.type + ': "' + this.orig + '"'); } var eos = this.eos(); var tok = this.prev(); if (tok.type !== 'eos') { this.ast.nodes.push(eos); } return this.ast; } }; /** * Visit `node` with the given `fn` */ function visit(node, fn) { if (!node.visited) { define(node, 'visited', true); return node.nodes ? mapVisit(node.nodes, fn) : fn(node); } return node; } /** * Map visit over array of `nodes`. */ function mapVisit(nodes, fn) { var len = nodes.length; var idx = -1; while (++idx < len) { visit(nodes[idx], fn); } } function hasOpen(node) { return node.nodes && node.nodes[0].type === (node.type + '.open'); } function hasClose(node) { return node.nodes && utils.last(node.nodes).type === (node.type + '.close'); } function hasDelims(node) { return hasOpen(node) && hasClose(node); } /** * Expose `Parser` */ module.exports = Parser;