Source: verbalexpressions.js

/*!
 * VerbalExpressions JavaScript Library v0.3.0
 * https://github.com/VerbalExpressions/JSVerbalExpressions
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 */

/**
* Define the VerbalExpression class.
* @class
*/
(function verbalExpressionIIFE(root) {
    // Constants
    var MODULE_NAME = 'VerEx';

    /**
    * I am the constructor function.
    * @constructor
    * @alias VerEx
    * @return {RegExp} A new instance of RegExp with injected methods
    */
    function VerbalExpression() {
        var verbalExpression = new RegExp();

        // Add all the class methods
        VerbalExpression.injectClassMethods(verbalExpression);

        // Return the new object.
        return verbalExpression;
    }

    /**
    * @param {RegExp} verbalExpression An instance of RegExp on which to add VerbalExpressions methods
    * @return {RegExp} A new instance of RegExp with injected methods
    */
    VerbalExpression.injectClassMethods = function injectClassMethods(verbalExpression) {
        var method;
        // Loop over all the prototype methods
        for (method in VerbalExpression.prototype) {
            // Make sure this is a local method.
            if (VerbalExpression.prototype.hasOwnProperty(method)) {
                // Add the method
                verbalExpression[method] = VerbalExpression.prototype[method];
            }
        }

        return verbalExpression;
    };

    /**
    * Define the class methods.
    */
    VerbalExpression.prototype = {
        // Variables to hold the whole
        // expression construction in order
        _prefixes: '',
        _source: '',
        _suffixes: '',
        _modifiers: 'gm', // default to global multiline matching

        /**
        * Sanitation function for adding anything safely to the expression
        * @param {String} value string to sanitize
        * @return {String} sanitized value
        */
        sanitize: function sanitize(value) {
            var reRegExpEscape;

            if (value.source) {
                return value.source;
            }

            if (typeof value === 'number') {
                return value;
            }

            // Regular expression meta characters, URL: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp
            reRegExpEscape = /([\].|*?+(){}^$\\:=[])/g;

            // Escape RegExp special characters only
            // $& => Last match, URL: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastMatch
            return value.replace(reRegExpEscape, '\\$&');
        },

        /**
        * Function to add stuff to the expression. Also compiles the new expression so it's ready to be used.
        * @param {string} value literal expression, not sanitized
        * @return {VerbalExpression} Freshly recompiled instance of VerbalExpression
        */
        add: function add(value) {
            this._source += value || '';
            this.compile(this._prefixes + this._source + this._suffixes, this._modifiers);

            return this;
        },

        /**
        * Control start-of-line matching
        * @param {Boolean} enable Control start-of-line matching
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        startOfLine: function startOfLine(enable) {
            enable = (enable !== false);
            this._prefixes = enable ? '^' : '';
            return this.add();
        },

        /**
        * Control end-of-line matching
        * @param {Boolean} enable Control end-of-line matching
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        endOfLine: function endOfLine(enable) {
            enable = (enable !== false);
            this._suffixes = enable ? '$' : '';
            return this.add();
        },

        /**
        * We try to keep the syntax as user-friendly as possible. So we can use the "normal" behaviour to split the "sentences" naturally.
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        then: function then(value) {
            value = this.sanitize(value);
            return this.add('(?:' + value + ')');
        },

        /**
        * And because we can't start with "then" function, we create an alias to be used as the first function of the chain.
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        find: function find(value) {
            return this.then(value);
        },

        /*
        * Maybe is used to add values with ?
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        maybe: function maybe(value) {
            value = this.sanitize(value);
            return this.add('(?:' + value + ')?');
        },

        /**
        * Any character any number of times
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        anything: function anything() {
            return this.add('(?:.*)');
        },

        /**
        * Anything but these characters
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        anythingBut: function anythingBut(value) {
            value = this.sanitize(value);
            return this.add('(?:[^' + value + ']*)');
        },

        /**
        * Any character at least one time
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        something: function something() {
            return this.add('(?:.+)');
        },

        /**
        * Any character at least one time except for these characters
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        somethingBut: function somethingBut(value) {
            value = this.sanitize(value);
            return this.add('(?:[^' + value + ']+)');
        },

        /**
        * Shorthand function for the String.replace function to give more logical flow if, for example, we're doing multiple replacements on one regexp.
        * @param {String} source string to search for
        * @param {String} value value to replace with
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        replace: function replace(source, value) {
            source = source.toString();
            return source.replace(this, value);
        },

        /// Add regular expression special ///
        /// characters                     ///

        /**
        * Line break
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        lineBreak: function lineBreak() {
            return this.add('(?:\\r\\n|\\r|\\n)'); // Unix + Windows CRLF
        },

        /**
        * And a shorthand for html-minded
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        br: function br() {
            return this.lineBreak();
        },

        /**
        * Tab (duh?)
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        tab: function tab() {
            return this.add('\\t');
        },

        /**
        * Any alphanumeric
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        word: function word() {
            return this.add('\\w+');
        },

        /**
        * Any digit
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        digit: function digit() {
            this.add('\\d');
            return this;
        },

        /**
        * Any whitespace
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        whitespace: function whitespace() {
            return this.add('\\s');
        },

        /**
        * Any given character
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        anyOf: function anyOf(value) {
            value = this.sanitize(value);
            return this.add('[' + value + ']');
        },

        /**
        * Shorthand
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        any: function any(value) {
            return this.anyOf(value);
        },

        /**
        * Usage: .range( from, to [, from, to ... ] )
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        range: function range() {
            var length = arguments.length;

            // Create a string buffer instead of concatenating on iteration
            var buffer = new Array(length / 2);
            var index = 0;
            var i = 0;
            var from;
            var to;

            buffer[index++] = '[';

            while (i < length) {
                from = this.sanitize(arguments[i++]);
                to = this.sanitize(arguments[i++]);
                buffer[index++] = from + '-' + to;
            }

            buffer[index++] = ']';

            return this.add(buffer.join(''));
        },

        /// Modifiers      ///

        /**
        * Modifier abstraction
        * @param {String} modifier modifier to add
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        addModifier: function addModifier(modifier) {
            if (this._modifiers.indexOf(modifier) === -1) {
                this._modifiers += modifier;
            }

            return this.add();
        },

        /**
        * Remove modifier
        * @param {String} modifier modifier to remove
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        removeModifier: function removeModifier(modifier) {
            this._modifiers = this._modifiers.replace(modifier, '');
            return this.add();
        },

        /**
        * Case-insensitivity modifier
        * @param {Boolean} enable Control case-insensitive matching
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        withAnyCase: function withAnyCase(enable) {
            return enable !== false ? this.addModifier('i') : this.removeModifier('i');
        },

        /**
        * Default behaviour is with "g" modifier, so we can turn this another way around than other modifiers
        * @param {Boolean} enable Control global matching
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        stopAtFirst: function stopAtFirst(enable) {
            return enable !== false ? this.removeModifier('g') : this.addModifier('g');
        },

        /**
        * Multiline, also reversed
        * @param {Boolean} enable Control multi-line matching
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        searchOneLine: function searchOneLine(enable) {
            return enable !== false ? this.removeModifier('m') : this.addModifier('m');
        },

        /**
        * Repeats the previous item exactly n times or between n and m times.
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        repeatPrevious: function repeatPrevious() {
            var value;
            var reIsInteger = /\d+/;
            var length = arguments.length;
            var values = new Array(length);
            var i = 0;
            var j = 0;
            for (i = 0; i < length; i++) {
                if (reIsInteger.test(arguments[i])) {
                    values[j++] = arguments[i];
                }
            }

            if (j > 0) {
                // Set the new length of the array, thus reducing to the elements that have content
                values.length = j;
                value = '{' + values.join(',') + '}';
            }


            return this.add(value);
        },

        /**
        * Repeats the previous at least once
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        oneOrMore: function oneOrMore() {
            return this.add('+');
        },

        /// Loops  ///

        /**
        * Matches the value zero or more times
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        multiple: function multiple(value) {
            // Use expression or string
            value = value.source || this.sanitize(value);
            if (arguments.length === 1) {
                this.add('(?:' + value + ')*');
            }

            if (arguments.length > 1) {
                this.add('(?:' + value + ')');
                this.add('{' + arguments[1] + '}');
            }

            return this;
        },

        /**
        * Adds alternative expressions
        * @param {String} value value to find
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        or: function or(value) {
            this._prefixes += '(?:';
            this._suffixes = ')' + this._suffixes;

            this.add(')|(?:');
            if (value) {
                this.then(value);
            }

            return this;
        },

        /**
        * Starts a capturing group
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        beginCapture: function beginCapture() {
            // Add the end of the capture group to the suffixes for now so compilation continues to work
            this._suffixes += ')';
            return this.add('(');
        },

        /**
        * Ends a capturing group
        * @return {VerbalExpression} Same instance of VerbalExpression to allow method chaining
        */
        endCapture: function endCapture() {
            // Remove the last parentheses from the _suffixes and add to the regex itself
            this._suffixes = this._suffixes.substring(0, this._suffixes.length - 1);
            return this.add(')');
        },

        /**
        * Convert to RegExp object
        * @return {RegExp} Converted RegExp instance
        */
        toRegExp: function toRegExp() {
            var array = this.toString().match(/\/(.*)\/([gimuy]+)?/);
            return new RegExp(array[1], array[2]);
        }
    };

    /**
    * @return {VerbalExpression} Returns a new instance of VerbalExpressions
    */
    function createVerbalExpression() {
        return new VerbalExpression();
    }

    // UMD (Universal Module Definition), URL: https://github.com/umdjs/umd
    // Supports AMD, CommonJS and the browser
    if (typeof module !== 'undefined' && module.exports) {
        // Node.js Module
        module.exports = createVerbalExpression;
    } else if (typeof define === 'function' && define.amd) {
        // AMD Module
        define(MODULE_NAME, [], function define() {
            return VerbalExpression;
        });
    } else {
        // Browser
        root[MODULE_NAME] = createVerbalExpression;
    }
}(this));