From 08f3adb990488ae5252a3c43a921f88ea3d86380 Mon Sep 17 00:00:00 2001 From: amarcruz Date: Tue, 29 Dec 2015 11:55:14 -0600 Subject: [PATCH 1/3] v0.2.7 --- .eslintrc | 18 +-- lib/ccparser.js | 355 ++++++++++++++++++++++++++--------------------- lib/compactor.js | 40 +++--- lib/evalexpr.js | 61 ++++---- lib/options.js | 8 +- lib/preproc.js | 2 - lib/procbuf.js | 88 ++++++------ lib/regexes.js | 31 ++--- package.json | 4 +- test.txt | 3 + 10 files changed, 322 insertions(+), 288 deletions(-) create mode 100644 test.txt diff --git a/.eslintrc b/.eslintrc index 54e1c53..9e135f0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -65,7 +65,7 @@ rules: no-sparse-arrays: 2 # disallow sparse arrays no-unreachable: 2 # disallow unreachable statements after a return, throw, continue, or break statement use-isnan: 2 # disallow comparisons with the value NaN - valid-jsdoc: [2, { requireReturn: false }] # ensure JSDoc comments are valid, returns optional in void functions + valid-jsdoc: [2, { "requireReturn": false }] # ensure JSDoc comments are valid, returns optional in void functions valid-typeof: 2 # ensure that the results of typeof are compared against a valid string no-unexpected-multiline: 2 # Avoid code that looks like two expressions but is actually one @@ -78,12 +78,12 @@ rules: ########################################################################### block-scoped-var: 2 # treat var statements as if they were block scoped - complexity: [1, 9] # specify the maximum cyclomatic complexity allowed in a program + complexity: [1, 9] # specify the maximum cyclomatic complexity allowed in a program consistent-return: 1 # require return statements to either always or never specify values curly: 0 # specify curly brace conventions for all control statements default-case: 1 # require default case in switch statements dot-notation: 1 # encourages use of dot notation whenever possible - eqeqeq: [2, 'smart'] # require the use of === and !== + eqeqeq: [2, "smart"] # require the use of === and !== guard-for-in: 1 # make sure for-in loops have an if statement no-alert: 2 # disallow the use of alert, confirm, and prompt no-caller: 2 # disallow use of arguments.caller or arguments.callee @@ -142,7 +142,7 @@ rules: no-delete-var: 2 # disallow deletion of variables no-label-var: 2 # disallow labels that share a name with a variable no-shadow: 2 # disallow declaration of variables already declared in the outer scope - no-shadow-restricted-names: 2 # disallow shadowing of names such as arguments + no-shadow-restricted-names: 2 # disallow shadowing of names such as arguments no-undef: 2 # disallow use of undeclared variables unless mentioned in a /*global */ block no-undef-init: 1 # disallow use of undefined when initializing variables no-undefined: 2 # disallow use of undefined variable @@ -173,9 +173,9 @@ rules: linebreak-style: [2, "unix"] - indent: [2, 2, { SwitchCase: 1, VariableDeclarator: 2 }] # Set a specific tab width - brace-style: [1, "stroustrup", { allowSingleLine: true }] # enforce stroustrup brace style - camelcase: [2, properties: "always" ] # require camel case names + indent: [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }] # Set a specific tab width + brace-style: [1, "stroustrup", { "allowSingleLine": true }] # enforce stroustrup brace style + camelcase: [2, { "properties": "always" } ] # require camel case names comma-spacing: 2 # enforce spacing before and after comma comma-style: 2 # enforce one true comma style consistent-this: 1 # enforces consistent naming when capturing the current execution context @@ -198,10 +198,10 @@ rules: quote-props: [2, "as-needed"] # require quotes around object literal property names only if needed quotes: [1, "single", "avoid-escape"] # specify whether double or single quotes should be used semi: [2, "never"] # require or disallow use of semicolons instead of ASI - #semi-spacing: [2, { before: false, after: true }] # disallow space before semicolon, require after + #semi-spacing: [2, { "before": false, "after": true }] # disallow space before semicolon, require after space-after-keywords: [2, "always"] # require a space after certain keywords space-before-blocks: 2 # require or disallow space before blocks - space-before-function-paren: [2, { anonymous: "always", named: "never" }] + space-before-function-paren: [2, { "anonymous": "always", "named": "never" }] #space-in-brackets: 0 # require or disallow spaces inside brackets #space-in-parens: 0 # require or disallow spaces inside parentheses space-infix-ops: 1 # require spaces around operators diff --git a/lib/ccparser.js b/lib/ccparser.js index ffcae5a..a203ead 100644 --- a/lib/ccparser.js +++ b/lib/ccparser.js @@ -4,6 +4,7 @@ 'use strict' var evalExpr = require('./evalexpr'), + repVars = require('./regexes').repVars, path = require('path') //const @@ -15,192 +16,226 @@ var NONE = 0, TESTING = 1, ENDING = 2, - INCLUDE = /^\s*("[^"]+"|'[^']+'|\S+)/ - -var options = {}, - varset = {} - -/** - * Parses conditional comments. - * The key and value parameters was obtained with the `CC` regex. - * - * @param {string } key - Key of the conditional comment - * @param {string } expr - Value, can be empty - * @param {Object } cc - Private configuration - * @returns {boolean} Output state, `false` for hide the output - */ -function ccp(key, expr, cc) { // eslint-disable-line complexity + CCLINE1 = /^[ \t]*\/([*\/]#[ \t]*[iedus][^\n]+)\n?/g, + CCLINE2 = /^([*\/])#[ \t]*(if|ifn?def|el(?:if|se)|endif|define|undef|set|unset|include(?:_once)?|indent)(?=[ \t\n\(]|$)(.*)/, + INCNAME = /^\s*("[^"]+"|'[^']+'|\S+)/ + +/* + CC Parser +*/ +function CCParser(options) { var - last = cc.state.length - 1, - state = cc.state[last] + result = {output: true}, + queue = [], + cc = null + + function emitError(str) { + if (typeof str === 'string') + str = str.replace('@', cc && cc.fname || 'the input') + options.emitError(str) + } - cc.insert = false + // Expression evaluation. + // Intercepts the `#ifdef/ifndef` shorthands, call `evalExpr` for `#if` statements. + function getValue(ckey, expr) { - if (key.slice(0, 3) !== 'inc') { + if (ckey !== 'if') { + var yes = expr in cc.varset + return ckey === 'ifdef' ? yes : !yes + } - expr = expr.replace(/\/\/.*/, '').trim() + // returns the raw value of the expression + return evalExpr(expr, cc.varset, emitError) + } - // All keywords, except `#else/#endif`, must have an expression - if (!expr && key !== 'else' && key !== 'endif') { - emitError('Expected expression for #' + key + ' in ' + cc.fname) - return false + // Prepares `cc` for file insertion setting the `insert` and `once` properties of `cc`. + // Accepts quoted or unquoted filenames + function include(ckey, file) { + var + match = file.match(INCNAME) + + file = match && match[1] + if (file) { + var ch = file[0] + if ((ch === '"' || ch === "'") && ch === file.slice(-1)) + file = file.slice(1, -1).trim() } + if (file) { + result.insert = file + result.once = !!ckey[8] + } + else + emitError('Expected filename for #' + ckey + ' in @') } - switch (key) { - // Conditional blocks. `#if-ifdef-ifndef` pushes the state and `#endif` pop it - case 'if': - case 'ifdef': - case 'ifndef': - ++last - cc.block[last] = IF - cc.state[last] = state === ENDING ? ENDING : getValue(expr, key) ? WORKING : TESTING - break - - case 'elif': - if (checkInBlock(IF)) { - if (state === TESTING && getValue(expr, 'if')) - cc.state[last] = WORKING - else if (state === WORKING) - cc.state[last] = ENDING - } - break - - case 'else': - if (checkInBlock(IF)) { - cc.block[last] = ELSE - cc.state[last] = state === TESTING ? WORKING : ENDING - } - break - - case 'endif': - if (checkInBlock(IF | ELSE)) { - cc.block.pop() - cc.state.pop() - --last - } - break - - default: - // Defines and includes, processed only for working blocks - if (state === WORKING) { - switch (key) { // eslint-disable-line default-case - case 'define': - case 'set': - options.def(expr) - break - case 'undef': - case 'unset': - options.undef(expr) - break - case 'include': - case 'include_once': - include(expr, key, cc) - break - case 'indent': - options.merge({indent: expr}) - break - } - } - break + // Removes any one-line comment and checks if expression is present + function normalize(key, expr) { + + if (key.slice(0, 4) !== 'incl') { + expr = expr.replace(/\/\/.*/, '').trim() + + // all keywords must have an expression, except `#else/#endif` + if (!expr && key !== 'else' && key !== 'endif') + emitError('Expected expression for #' + key + ' in @') + } + return expr } - cc.output = cc.state[last] === WORKING + /** + * Parses conditional comments. + * + * @param {Object } data - Directive created by the getData method + * @returns {boolean} Output state, `false` for hide the output + */ + this.parse = function _parse(data) { // eslint-disable-line complexity + var + state = cc.states[cc.states.length - 1], + step = state.step, + key = data.key, + expr = normalize(key, data.expr) + + result.insert = false + + switch (key) { + // Conditional blocks -- `#if-ifdef-ifndef` pushes the state and `#endif` pop it + case 'if': + case 'ifdef': + case 'ifndef': + step = step === ENDING ? ENDING : getValue(key, expr) ? WORKING : TESTING + cc.states.push({block: IF, step: step}) + break + + case 'elif': + if (checkInBlock(IF)) { + if (step === WORKING) + step = state.step = ENDING + else if (step === TESTING && getValue('if', expr)) + step = state.step = WORKING + } + break - return cc.output + case 'else': + if (checkInBlock(IF)) { + state.block = ELSE + state.step = step = step === TESTING ? WORKING : ENDING + } + break - // Inner helper - throws if the current block is not of the expected type - function checkInBlock(mask) { - var block = cc.block[last] - if (block && block === (block & mask)) - return true - emitError('Unexpected #' + key + ' in ' + cc.fname) - return false - } -} + case 'endif': + if (checkInBlock(IF | ELSE)) { + cc.states.pop() + state = cc.states[cc.states.length - 1] + } + break + + default: + // Defines and includes -- processed only for working blocks + if (step === WORKING) { + switch (key) { + case 'define': + case 'set': + options.def(expr) + break + case 'undef': + case 'unset': + options.undef(expr) + break + case 'indent': + options.merge({indent: expr}) + break + case 'include': + case 'include_once': + include(key, expr) + break + default: + emitError('Unknown directive #' + key + ' in @') + break + } + } + break + } -function emitError(str) { - options.emitError(str) -} + result.output = step === WORKING -// Prepares `cc` for file insertion setting the `insert` and `once` properties of `cc`. -// Accepts quoted or unquoted filenames -function include(file, ckey, cc) { - var - match = file.match(INCLUDE) + return result - file = match && match[1] - if (file) { - var ch = file[0] - if ((ch === '"' || ch === "'") && ch === file.slice(-1)) - file = file.slice(1, -1).trim() - } - if (!file) - emitError('Expected filename for #' + ckey + ' in ' + cc.fname) - else { - cc.insert = file - cc.once = !!ckey[8] + // Inner helper - throws if the current block is not of the expected type + function checkInBlock(mask) { + var block = state.block + if (block && block === (block & mask)) + return true + emitError('Unexpected #' + key + ' in @') + return false + } } -} - -// Expression evaluation. -// Intercepts the `#ifdef/ifndef` shorthands, call `evalExpr` for `#if` statements. -function getValue(expr, ckey) { - if (ckey !== 'if') { - var yes = ckey === 'ifdef' ? 1 : 0 - return expr in varset ? yes : yes ^ 1 + /** + * Check if the line is a conditional comment + * + * @param {string} line - line starting with "//#" or "/*#" + * @returns {object} pair key-expr if the line contains a conditional comment + */ + this.getData = function _getData(line) { + var + match = line.match(CCLINE2) + + if (match) { + var k = match[2], + v = match[3] + if (match[1] !== '*' || !/^(?:if|ifn?def|el(?:if|se))\b/.test(v)) + return {key: k, expr: v} + } + return false } - // returns the raw value of the expression - return evalExpr(expr, varset, options.emitError) -} + /** + * Creates the configuration object for the given file. + * + * @param {string} file - Filename of current processing file + * @param {number} level - Nested level of processing file + * @param {Object} opts - User options + * @returns {Object} A new configuration object + */ + this.start = function _start(file, level) { + + if (cc) queue.push(cc) + + cc = { + states: [{block: NONE, step: WORKING}], + ffile: file || '', + fname: file ? path.relative('.', file).replace(/\\/g, '/') : '', + varset: options.getVars() + } -/** - * Creates the configuration object for the given file. - * - * @param {string} file - Filename of current processing file - * @param {number} level - Nested level of processing file - * @param {Object} opts - User options - * @returns {Object} A new configuration object - */ -ccp.CC = function (file, level, opts) { + options.setFile(cc.fname) // relative path - this.output = true - this.state = [WORKING] - this.block = [NONE] - this.fname = file ? path.relative('.', file).replace(/\\/g, '/') : '' + // Read and evaluate the header for this file + var hdr = options[level > 0 ? 'headers' : 'header1'] + cc.header = result.header = hdr ? repVars(hdr, cc.varset) : '' - // Keep local reference to the global options & varset objects - options = opts - varset = opts.getVars() + return result + } - options.setFile(this.fname) // relative path + /** + * Check blocks on old cc before vanish it, restore the predefined symbol `__FILE`. + * Changing __FILE here avoids setting the value in each call to `evalExpr`. + * + * @returns {Object} The current configuration + */ + this.reset = function reset() { + + if (cc && cc.states.length > 1) + emitError('Unclosed conditional block in @') + cc = queue.pop() + result.header = cc ? cc.header : '' + options.setFile(cc ? cc.fname : '') - // Read and evaluate the header for this file - var hdr = opts[level > 0 ? 'headers' : 'header1'] - if (hdr) - this.header = evalExpr.repVars(hdr, varset) + return cc + } return this } -/** - * Check blocks on old cc before vanish, restore the predefined symbol `__FILE`. - * Changing __FILE here avoids setting the value in each call to `evalExpr`. - * - * @param {Object} old - The configuration object to close - * @param {Object} cc - The configuration object to reopen - * @returns {Object} The same configuration object, with `__FILE` updated - */ -ccp.reset = function reset(old, cc) { - - if (old.block[1]) - emitError('Unclosed conditional block in ' + (old.fname || 'the input')) - else - options.setFile(cc ? cc.fname : '') - - return cc -} +CCParser.CCLINE = CCLINE1 -module.exports = ccp +module.exports = CCParser diff --git a/lib/compactor.js b/lib/compactor.js index 3110c4c..ff790f5 100644 --- a/lib/compactor.js +++ b/lib/compactor.js @@ -1,6 +1,6 @@ // Buffer compactation // ------------------- -// Removes duplicate empty lines and trailing whitespace, converts end of lines +// Removes duplicate empty lines and trailing whitespace, normalizes end of lines 'use strict' // eslint-disable-line //const @@ -8,13 +8,13 @@ var eolstr = {unix: '\n', win: '\r\n', mac: '\r'} var events = require('events') /* - class Compact + class Compactor */ -module.exports = function (options, output) { +module.exports = function Compactor(options, output) { var _to = eolstr[options.eolType], - _nn = options.emptyLines, - _re = new RegExp('(\n{' + (_nn + 1) + '})\n+', 'g'), + _elines = options.emptyLines, + _re = RegExp('(\n{' + (_elines + 1) + '})\n+', 'g'), _lastch = null, _indent = '', _cache = '' @@ -27,7 +27,8 @@ module.exports = function (options, output) { return this - // On file change, reset indentation. + // On file change, reset indentation + function indent(level) { var val = options.indent _indent = '' @@ -49,26 +50,27 @@ module.exports = function (options, output) { } } - // On new data, trim trailing ws and transform eols before write + // On new data, trim trailing whitespace and normalize eols + function write(buffer) { - // first we need trim trailing whitespace for fast searching + // first, trim trailing whitespace for fast searching buffer = buffer.replace(/[ \t]+$/gm, '') // compact lines if emptyLines != -1 - if (_nn >= 0) + if (_elines >= 0) buffer = trimBuffer(buffer) if (!buffer) return - // finish with surrounding lines, now the inners - if (!_nn) { + // finished the surrounding lines, now the inners + if (!_elines) { // remove empty lines and change eols in one unique operation buffer = buffer.replace(/\n{2,}/g, _to) } else { // keep max n empty lines (n+1 sucesive eols), -1 keep all - if (_nn > 0) + if (_elines > 0) buffer = buffer.replace(_re, '$1') // change line terminator if not unix @@ -90,8 +92,8 @@ module.exports = function (options, output) { if (err) output.emit('error', err) else { - if (_nn > 0 && _cache) - output.emit('data', _cache.slice(0, _nn).replace(/\n/g, _to)) + if (_elines > 0 && _cache) + output.emit('data', _cache.slice(0, _elines).replace(/\n/g, _to)) output.emit('end') } } @@ -119,15 +121,15 @@ module.exports = function (options, output) { } // at the point we have non-eol chars and ... - // _nn >= 0 + // _elines >= 0 // pos >= 0 - if (_nn < pos) { + if (_elines < pos) { // remove excess of empty lines - buffer = buffer.slice(pos - _nn) + buffer = buffer.slice(pos - _elines) } - else if (_nn > pos && _cache) { + else if (_elines > pos && _cache) { // add cached empty lines to complete `emptyLines` value - buffer = _cache.slice(0, _nn - pos) + buffer + buffer = _cache.slice(0, _elines - pos) + buffer } // top lines done, now leave only one eol at end, save others in _cache diff --git a/lib/evalexpr.js b/lib/evalexpr.js index 29cafe3..5d73c76 100644 --- a/lib/evalexpr.js +++ b/lib/evalexpr.js @@ -4,42 +4,44 @@ var RE = require('./regexes') -module.exports = function evalExpr(str, varset, errhan) { - - // Uses Function ctor, obtains values from the given options instance - 'use strict' // eslint-disable-line - - // For replacing of jspreproc variables - $1-$2: slash, $4: the cc var - // This is multiline global, for `string.replace()` - var REPVARS = new RegExp( +// For replacing of jspreproc variables -- $1-$2: slash, $3: var prefix, $4: the cc var +var DEFINED = RE.DEFINED, + REPVARS = RegExp( RE.STRINGS.source + '|' + RE.DIVISOR.source + '|' + RE.REGEXES.source + '|(^|[^$\\w])([$_A-Z][_0-9A-Z]+)\\b', 'g') - function _repVars(s) { - var match - - REPVARS.lastIndex = 0 - - while (match = REPVARS.exec(s)) { //eslint-disable-line no-cond-assign - var v = match[4] +/** + * Performs the evaluation of the received string using a function instantiated dynamically. + * + * @param {string} str - String to evaluate, can include other defined vars + * @param {object} varset - Set of variable definitions + * @param {function} errhan - Function to handle evaluation errors + * @returns {*} The result can have any type + */ +module.exports = function evalExpr(str, varset, errhan) { - if (v) { - v = match[3] + (v in varset ? varset[v] : '0') - s = RegExp.leftContext + v + RegExp.rightContext - REPVARS.lastIndex = match.index + v.length - } - } - return s - } + // Uses Function ctor, obtains values from the given options instance + 'use strict' // eslint-disable-line function _repDefs(_, v) { return v in varset ? '1' : '0' } + function _repVars(m, _1, _2, p, v) { + return v ? p + (v in varset ? varset[v] : '0') : m + } - var expr = _repVars(str.replace(RE.DEFINED, _repDefs)) - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') + if (typeof str !== 'string') { + var s1 = 'evalExpr(str) with type ' + typeof str + console.log(s1) + throw new Error(s1) + } + + var expr = str + .replace(DEFINED, _repDefs) + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(REPVARS, _repVars) try { var fn = new Function('', 'return (' + expr + ');') // eslint-disable-line no-new-func @@ -50,9 +52,8 @@ module.exports = function evalExpr(str, varset, errhan) { expr = 0 } - // Division by zero returns Infinity, catch here because Infinity is a trueish - // value, so a test with #if / #elif is likely to expect false for this. + // Division by zero returns Infinity, catch it here because Infinity is a trueish + // value, and a test with #if / #elif is likely to expect false for this ??? + // JSON.stringify convert this to null. return expr === Infinity ? 0 : expr } - -module.exports.repVars = RE.repVars diff --git a/lib/options.js b/lib/options.js index 7290b49..3fe84a2 100644 --- a/lib/options.js +++ b/lib/options.js @@ -51,12 +51,12 @@ function procError(str) { // http://stackoverflow.com/questions/16686687/json-stringify-and-u2028-u2029-check if (JSON.stringify(['\u2028\u2029']) === '["\u2028\u2029"]') { JSON.stringify = (function (fn) { - var re = /[\u2028\u2029]/g - + function repPara(c) { + return c === '\u2028' ? '\\u2028' : '\\u2029' + } return function (s, e, f) { s = fn.call(this, s, e, f) - return s && - s.replace(re, function (c) { return c === '\u2028' ? '\\u2028' : '\\u2029' }) + return s && s.replace(/[\u2028\u2029]/g, repPara) } })(JSON.stringify) } diff --git a/lib/preproc.js b/lib/preproc.js index 61c8f19..510f5c5 100644 --- a/lib/preproc.js +++ b/lib/preproc.js @@ -35,8 +35,6 @@ module.exports = function jspp(file, opts) { if (!file) file = process.stdin } - //if (opts._back) return null - return procbuf(data, file, options) } diff --git a/lib/procbuf.js b/lib/procbuf.js index feeae7a..9978677 100644 --- a/lib/procbuf.js +++ b/lib/procbuf.js @@ -5,22 +5,32 @@ var RE = require('./regexes'), Compactor = require('./compactor'), - ccparser = require('./ccparser'), + CCParser = require('./ccparser'), stream = require('stream'), path = require('path'), fs = require('fs') // Matches comments, strings, and preprocessor directives - $1: cc directive // This is multiline only, for `string.match()` -var RE_BLOCKS = new RegExp(RE.CCCOMMS.source + '|' + RE.S_QBLOCKS, 'm') +var RE_BLOCKS = RegExp(CCParser.CCLINE.source + '|' + RE.S_QBLOCKS, 'm') var _id = 0 +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +function fullPath(file, base) { + if (!path.extname(file)) file += '.js' + base = base ? path.dirname(base) : process.cwd() + return path.resolve(base, file) +} + /** * Take the input and options, and returns a readable stream with the results. * * Inyect data to eventEmitter with procData. - * procData calls to ccparser, and emit a 'data' event. + * procData calls to _parser, and emit a 'data' event. * compact handle 'data' event and writes to _output. * * @param {string} strbuf - String buffer to process directly. @@ -35,6 +45,7 @@ module.exports = function procbuf(strbuf, fname, options) { // var _output = new stream.PassThrough({encoding: 'utf8', decodeStrings: false}), _emitter = new Compactor(options, _output).emitter, + _parser = new CCParser(options), _level = 0, _files = {}, _queue = [] @@ -113,12 +124,11 @@ module.exports = function procbuf(strbuf, fname, options) { function procData(data, file) { // eslint-disable-line complexity if (!file) file = '' - var cc = new ccparser.CC(file, _level, options), + var cc = _parser.start(file, _level), cache = [], match, q - cc.file_ = file - + console.log('- PROCBUF entry: "' + file + '", ' + _level) // normalize eols here if (~data.indexOf('\r')) data = data.replace(/\r\n?/g, '\n') @@ -139,15 +149,16 @@ module.exports = function procbuf(strbuf, fname, options) { data = RegExp.rightContext pushData(RegExp.leftContext) - if (match[1]) { + q = match[1] && _parser.getData(match[1]) + if (q) { flush() - ccparser(match[1], match[2], cc) + cc = _parser.parse(q) if (cc.insert && insert(cc.insert, file, cc.once)) return } else if (cc.output) { q = match[0] - if (match[3] || match[4] || q[0] !== '/') + if (match[2] || match[3] || q[0] !== '/') pushData(q) // string, div, or regex else pushComment(q) // regular comment @@ -158,50 +169,57 @@ module.exports = function procbuf(strbuf, fname, options) { pushData(data) flush() + cc = _parser.reset() // allows detect unclosed block + // now, check if there's something of a previous file - if (!_queue.length) { - ccparser.reset(cc) // allows detect unclosed block + if (!_queue.length) break - } - q = _queue.pop() + q = _queue.pop() + file = q[0] data = q[1] - cc = ccparser.reset(cc, q[0]) - file = cc.file_ --_level + console.log('- PROCBUF pop() "' + file + '", ' + _level) } _emitter.emit('end') // Helpers ----- + /** + * Pushes the current filename and remaining data and inserts a new file. + * Skips the insertion if the file is in the queue, avoiding recursion, + * or if it's an include_once directive and the file was already inserted + * anywhere in the process. + * + * @param {string} name - The file's name being included, relative to base + * @param {string} base - The file's name being processed + * @param {boolean} once - `true` for unique inclusion + * @returns {boolean} - `true` if the file can be inserted + */ function insert(name, base, once) { name = fullPath(name, base) + // _files is an object with keys of all the processed files var f = _files[name] | 0 - if (f > 1) return false // 2 or 3 is include_once + if (f > 1) return false // 2: already include_once if (once) { - _files[name] = 2 // 2 - if (f) return false + _files[name] = 2 // mark as include_once + if (f) return false // 1: already included } else - _files[name] = f | 1 // 1 or 3 + _files[name] = 1 // mark as normal include - if (isInQueue()) { - cache.push('// ignored ' + path.relative('.', name) + '\n') - return false + // abort if the file is an ancestor (is in the queue) + f = _queue.length + while (--f >= 0) { + if (_queue[f][0] === name) return false } - _queue.push([cc, data]) + _queue.push([base, data]) readImport(name, ++_level) return true - - function isInQueue() { - var i = _queue.length - while (--i >= 0 && _queue[i][0].file_ !== name); - return ~i - } } function pushData(str) { @@ -226,15 +244,3 @@ module.exports = function procbuf(strbuf, fname, options) { } } } - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -function fullPath(file, base) { - - if (!path.extname(file)) file += '.js' - - base = base ? path.dirname(base) : process.cwd() - return path.resolve(base, file) -} diff --git a/lib/regexes.js b/lib/regexes.js index e2d8b68..231a48a 100644 --- a/lib/regexes.js +++ b/lib/regexes.js @@ -2,17 +2,15 @@ Shared regexes */ var RE = module.exports = { - // Matches key & value of a conditional comment. $1: key, $2: value - CCCOMMS: /^[ \t]*\/\/#[ \t]*(if(?:n?def)?|el(?:if|se)|endif|define|undef|set|unset|include(?:_once)?|indent)(?=[ \t\n\(]|$)([^\n]*)\n?/g, // Multi-line comment MLCOMMS: /\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//g, // Single-line comment - SLCOMMS: /\/\/[^\n]*$/gm, + SLCOMMS: /\/\/.*$/g, // Quoted strings, take care about embedded eols - STRINGS: /"(?:[^"\r\n\\]*|\\[^])*"|'(?:[^'\r\n\\]*|\\[^])*'/g, - // Allows skip division operators to detect non-regex slash $2: the slash + STRINGS: /"(?:[^"\n\\]*|\\[\S\s])*"|'(?:[^'\n\\]*|\\[\S\s])*'/g, + // Allows skip division operators to detect non-regex slash -- $1: the slash DIVISOR: /(?:\breturn\s+|(?:[$\w\)\]]|\+\+|--)\s*(\/)(?![*\/]))/g, - // Matches regexes - $1 last slash of the regex + // Matches regexes -- $1 last slash of the regex REGEXES: /\/(?=[^*\/])[^[/\\]*(?:(?:\[(?:\\.|[^\]\\]*)*\]|\\.)[^[/\\]*)*?(\/)[gim]*/g, // Matches the `defined()` function DEFINED: /\bdefined[ \t]*\([ \t]*([$_A-Z][_0-9A-Z]+)[ \t]*\)/g @@ -32,6 +30,7 @@ var REPVARS = RegExp(RE.S_QBLOCKS + '|([^$\\w])(\\$_[_0-9A-Z]+)\\b', 'gm') /** * Replaces jspreproc variables that begins with '$_' through all the code. + * * @param {string} str - Partial code to replace, with CC already preprocessed * @param {object} varset - Contains the variables * @returns {string} Processed code, with varset replaced with their literal values @@ -41,23 +40,13 @@ RE.repVars = function repVars(str, varset) { if (str[0] === '$') str = str.replace(/^\$_[_0-9A-Z]+\b/, function (v) { - return v && v in varset ? varset[v] : v + return v in varset ? varset[v] : v }) - if (~str.indexOf('$_')) { - var match, - re = RegExp(REPVARS) - - while (match = re.exec(str)) { //eslint-disable-line no-cond-assign - var v = match[4] - - if (v && v in varset) { // Don't replace undefined names - v = match[3] + varset[v] - re.lastIndex = match.index + v.length - str = RegExp.leftContext + v + RegExp.rightContext - } - } - } + if (~str.indexOf('$_')) + str = str.replace(REPVARS, function (m, _1, _2, p, v) { + return v && v in varset ? p + varset[v] : m + }) return /__FILE/.test(str) ? str.replace(/\b__FILE\b/g, varset.__FILE) : str } diff --git a/package.json b/package.json index 9b94b2c..22c7097 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jspreproc", - "version": "0.2.6", - "description": "C-Style source file preprocessor and comments remover for JavaScript in JavaScript", + "version": "0.2.7", + "description": "C-Style source file preprocessor and comments remover for JavaScript", "license": "MIT", "main": "lib/preproc.js", "bin": { diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..202bfe6 --- /dev/null +++ b/test.txt @@ -0,0 +1,3 @@ +//#include spec/fixtures/noeol.txt +//#include spec/fixtures/noeol.txt +ok \ No newline at end of file From c1e20123aed42ab3be93d2e5cef650a4de43e313 Mon Sep 17 00:00:00 2001 From: amarcruz Date: Tue, 5 Jan 2016 03:29:48 -0600 Subject: [PATCH 2/3] v0.2.7 --- .eslintignore | 6 +-- .travis.yml | 1 + CHANGELOG.md | 4 ++ doc/syntax.md | 29 ++++++++++++-- lib/ccparser.js | 79 +++++++++++++++++++------------------- lib/evalexpr.js | 8 +--- lib/options.js | 5 ++- lib/procbuf.js | 15 ++++---- package.json | 1 - spec/.eslintrc | 6 --- spec/app-spec.js | 61 ++++++++++++++++++++++++++--- spec/helpers/SpecHelper.js | 2 +- test.txt | 3 -- 13 files changed, 142 insertions(+), 78 deletions(-) delete mode 100644 spec/.eslintrc delete mode 100644 test.txt diff --git a/.eslintignore b/.eslintignore index cfc1b98..72db58d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ **/coverage/** -**/dist/** -**/spec/** -!**/spec/*.js +spec/expect/*.js +spec/fixtures/*.js +**.md diff --git a/.travis.yml b/.travis.yml index 55398a1..2370e72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: node_js sudo: false node_js: + - "5" - "4.2" - "4.1" - "4.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b0263..1f92b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog for jspreproc +#### Version 0.2.7 - 2016-01-05 +- Rewrite of the conditional comments' parser. +- Enhancement: Multiline comment with `if/ifdef/ifndef/elif/else` allows hide conditional blocks. See [doc/syntax.md](doc/syntax.md) + #### Version 0.2.6 - 2015-12-03 - Fix errors when including unicode line and paragraph characters. - Enhancement: New `sources` filer. It supports `sourceMappingURL` and `sourceMappingURL` directives. diff --git a/doc/syntax.md b/doc/syntax.md index bb7b1ca..f8d1d01 100644 --- a/doc/syntax.md +++ b/doc/syntax.md @@ -7,10 +7,10 @@ However, there are some differences. Differences from the C preprocessor: -- The `#define` keyword does not works in jspreproc as in the C preprocessor and is **deprecated** in 0.2.3 -- Use the `#set` keyword instead +- The `#define` keyword does not works in jspreproc as in the C preprocessor and is **deprecated** in 0.2.3 -- Use the `#set` keyword instead - jspreproc does not supports function-like macros or macro-substitution on the source files, out of expressions(1) -- Escaped characters remains literals, e.g. the `\n` sequence not generate end of line, it is written to the output as the string "\n" -- The evaluation of expressions by `#set/#if/#endif` is through a Function instance, so the result is the same from a JavaScript expression, in the global scope. +- Escaped characters remains literals, e.g. `\n` does not generate an end of line, it is written to the output as the string "\n" +- The evaluation of expressions by `#set/#if/#elif` is done through a Function instance, so the result is the same as for a JavaScript expression in the global scope. (1) See [Variables Symbols](#variable-symbols) for some exceptions. @@ -85,6 +85,29 @@ These are shorthands for `#if defined(SYMBOL)` and `#if !defined(SYMBOL)`. These are the default block and closing statement keywords. +### Hiding blocks + +From v0.2.7 you can hide conditional blocks using multiline comments. This syntax is valid for the `if/ifdef/ifndef/elif/else` keywords only. The opening sequence (`/*`) must comply with the same rules as the one-line conditional comments, the closing sequence (`*/`) must follow another regular conditional comment: +```js +/*#if EXPRESSION +... +//#endif */ +``` + +Note the `//#endif`, this is a regular conditional comment, but now the parser will ignore anything in the line starting from `*/`. Tools such as minifiers or syntax highlighters will see these blocks as a regular multiline comment, which allows you to hide the block, as in this example: +```js +//#set FOO = 1 + +/*#if FOO == 1 +var x = one() +//#elif FOO == 2 +var x = two() +//#else*/ +var x = other() +//#endif +``` + + ### Includes **`//#include filename`** diff --git a/lib/ccparser.js b/lib/ccparser.js index a203ead..8cb02f5 100644 --- a/lib/ccparser.js +++ b/lib/ccparser.js @@ -16,8 +16,8 @@ var NONE = 0, TESTING = 1, ENDING = 2, - CCLINE1 = /^[ \t]*\/([*\/]#[ \t]*[iedus][^\n]+)\n?/g, - CCLINE2 = /^([*\/])#[ \t]*(if|ifn?def|el(?:if|se)|endif|define|undef|set|unset|include(?:_once)?|indent)(?=[ \t\n\(]|$)(.*)/, + CCLINE1 = /^[ \t]*\/\/#[ \t]*(if|ifn?def|el(?:if|se)|endif|define|undef|set|unset|include(?:_once)?|indent)(?=[ \t\n\(/\*]|$)(.*)\n?/, + CCLINE2 = /^[ \t]*\/\*#[ \t]*(if|ifn?def|el(?:if|se))(?=[ \t\n\(/\*]|$)(.*)\n?/, INCNAME = /^\s*("[^"]+"|'[^']+'|\S+)/ /* @@ -40,8 +40,8 @@ function CCParser(options) { function getValue(ckey, expr) { if (ckey !== 'if') { - var yes = expr in cc.varset - return ckey === 'ifdef' ? yes : !yes + var yes = expr in cc.varset ? 1 : 0 + return ckey === 'ifdef' ? yes : yes ^ 1 } // returns the raw value of the expression @@ -72,11 +72,13 @@ function CCParser(options) { function normalize(key, expr) { if (key.slice(0, 4) !== 'incl') { - expr = expr.replace(/\/\/.*/, '').trim() + expr = expr.replace(/[/\*]\/.*/, '').trim() // all keywords must have an expression, except `#else/#endif` - if (!expr && key !== 'else' && key !== 'endif') + if (!expr && key !== 'else' && key !== 'endif') { emitError('Expected expression for #' + key + ' in @') + return false + } } return expr } @@ -89,48 +91,52 @@ function CCParser(options) { */ this.parse = function _parse(data) { // eslint-disable-line complexity var - state = cc.states[cc.states.length - 1], - step = state.step, + last = cc.state.length - 1, + state = cc.state[last], key = data.key, expr = normalize(key, data.expr) result.insert = false + if (expr === false) return expr + switch (key) { // Conditional blocks -- `#if-ifdef-ifndef` pushes the state and `#endif` pop it case 'if': case 'ifdef': case 'ifndef': - step = step === ENDING ? ENDING : getValue(key, expr) ? WORKING : TESTING - cc.states.push({block: IF, step: step}) + ++last + cc.block[last] = IF + cc.state[last] = state === ENDING ? ENDING : getValue(key, expr) ? WORKING : TESTING break case 'elif': if (checkInBlock(IF)) { - if (step === WORKING) - step = state.step = ENDING - else if (step === TESTING && getValue('if', expr)) - step = state.step = WORKING + if (state === TESTING && getValue('if', expr)) + cc.state[last] = WORKING + else if (state === WORKING) + cc.state[last] = ENDING } break case 'else': if (checkInBlock(IF)) { - state.block = ELSE - state.step = step = step === TESTING ? WORKING : ENDING + cc.block[last] = ELSE + cc.state[last] = state === TESTING ? WORKING : ENDING } break case 'endif': if (checkInBlock(IF | ELSE)) { - cc.states.pop() - state = cc.states[cc.states.length - 1] + cc.block.pop() + cc.state.pop() + --last } break default: // Defines and includes -- processed only for working blocks - if (step === WORKING) { + if (state === WORKING) { switch (key) { case 'define': case 'set': @@ -147,6 +153,7 @@ function CCParser(options) { case 'include_once': include(key, expr) break + // istanbul ignore next: just in case default: emitError('Unknown directive #' + key + ' in @') break @@ -155,13 +162,13 @@ function CCParser(options) { break } - result.output = step === WORKING + result.output = cc.state[last] === WORKING return result // Inner helper - throws if the current block is not of the expected type function checkInBlock(mask) { - var block = state.block + var block = cc.block[last] if (block && block === (block & mask)) return true emitError('Unexpected #' + key + ' in @') @@ -172,19 +179,13 @@ function CCParser(options) { /** * Check if the line is a conditional comment * - * @param {string} line - line starting with "//#" or "/*#" + * @param {Array} match - line starting with "//#" or "/*#" * @returns {object} pair key-expr if the line contains a conditional comment */ - this.getData = function _getData(line) { - var - match = line.match(CCLINE2) + this.getData = function _getData(match) { - if (match) { - var k = match[2], - v = match[3] - if (match[1] !== '*' || !/^(?:if|ifn?def|el(?:if|se))\b/.test(v)) - return {key: k, expr: v} - } + if (match[1]) return {key: match[1], expr: match[2]} + if (match[3]) return {key: match[3], expr: match[4]} return false } @@ -193,7 +194,6 @@ function CCParser(options) { * * @param {string} file - Filename of current processing file * @param {number} level - Nested level of processing file - * @param {Object} opts - User options * @returns {Object} A new configuration object */ this.start = function _start(file, level) { @@ -201,7 +201,8 @@ function CCParser(options) { if (cc) queue.push(cc) cc = { - states: [{block: NONE, step: WORKING}], + state: [WORKING], + block: [NONE], ffile: file || '', fname: file ? path.relative('.', file).replace(/\\/g, '/') : '', varset: options.getVars() @@ -211,31 +212,31 @@ function CCParser(options) { // Read and evaluate the header for this file var hdr = options[level > 0 ? 'headers' : 'header1'] - cc.header = result.header = hdr ? repVars(hdr, cc.varset) : '' + cc.header = result.header = hdr && repVars(hdr, cc.varset) || '' return result } /** - * Check blocks on old cc before vanish it, restore the predefined symbol `__FILE`. + * Check unclosed blocks on old cc before vanish it and restore the previous state. * Changing __FILE here avoids setting the value in each call to `evalExpr`. * * @returns {Object} The current configuration */ this.reset = function reset() { - if (cc && cc.states.length > 1) - emitError('Unclosed conditional block in @') + if (cc && cc.block.length > 1) emitError('Unclosed conditional block in @') + cc = queue.pop() result.header = cc ? cc.header : '' options.setFile(cc ? cc.fname : '') - return cc + return result } return this } -CCParser.CCLINE = CCLINE1 +CCParser.S_CCLINE = CCLINE1.source + '|' + CCLINE2.source module.exports = CCParser diff --git a/lib/evalexpr.js b/lib/evalexpr.js index 5d73c76..e6bc8ff 100644 --- a/lib/evalexpr.js +++ b/lib/evalexpr.js @@ -31,12 +31,6 @@ module.exports = function evalExpr(str, varset, errhan) { return v ? p + (v in varset ? varset[v] : '0') : m } - if (typeof str !== 'string') { - var s1 = 'evalExpr(str) with type ' + typeof str - console.log(s1) - throw new Error(s1) - } - var expr = str .replace(DEFINED, _repDefs) .replace(/\n/g, '\\n') @@ -48,7 +42,7 @@ module.exports = function evalExpr(str, varset, errhan) { expr = fn.call(null) } catch (e) { - errhan(new Error('Can\'t evaluate `' + str + '` (`' + expr + '`)')) + errhan(new Error('Can\'t evaluate `' + str + '` (`' + expr + '`) : ' + e)) expr = 0 } diff --git a/lib/options.js b/lib/options.js index 3fe84a2..4219173 100644 --- a/lib/options.js +++ b/lib/options.js @@ -49,14 +49,15 @@ function procError(str) { // Fix errors when including unicode line and paragraph characters // http://stackoverflow.com/questions/16686687/json-stringify-and-u2028-u2029-check +// istanbul ignore else: seems that node always needs this hack if (JSON.stringify(['\u2028\u2029']) === '["\u2028\u2029"]') { JSON.stringify = (function (fn) { - function repPara(c) { + function _rep(c) { return c === '\u2028' ? '\\u2028' : '\\u2029' } return function (s, e, f) { s = fn.call(this, s, e, f) - return s && s.replace(/[\u2028\u2029]/g, repPara) + return s && s.replace(/[\u2028\u2029]/g, _rep) } })(JSON.stringify) } diff --git a/lib/procbuf.js b/lib/procbuf.js index 9978677..1e03161 100644 --- a/lib/procbuf.js +++ b/lib/procbuf.js @@ -12,7 +12,7 @@ var RE = require('./regexes'), // Matches comments, strings, and preprocessor directives - $1: cc directive // This is multiline only, for `string.match()` -var RE_BLOCKS = RegExp(CCParser.CCLINE.source + '|' + RE.S_QBLOCKS, 'm') +var RE_BLOCKS = RegExp(CCParser.S_CCLINE + '|' + RE.S_QBLOCKS, 'm') var _id = 0 @@ -112,7 +112,7 @@ module.exports = function procbuf(strbuf, fname, options) { function readImport(file) { file = fullPath(file) - fs.readFile(file, {encoding: 'utf8'}, function (err, data) { + fs.readFile(file, 'utf8', function (err, data) { // istanbul ignore next if (err) errHandler(err) @@ -128,7 +128,7 @@ module.exports = function procbuf(strbuf, fname, options) { cache = [], match, q - console.log('- PROCBUF entry: "' + file + '", ' + _level) + // normalize eols here if (~data.indexOf('\r')) data = data.replace(/\r\n?/g, '\n') @@ -149,7 +149,7 @@ module.exports = function procbuf(strbuf, fname, options) { data = RegExp.rightContext pushData(RegExp.leftContext) - q = match[1] && _parser.getData(match[1]) + q = _parser.getData(match) if (q) { flush() cc = _parser.parse(q) @@ -158,10 +158,10 @@ module.exports = function procbuf(strbuf, fname, options) { } else if (cc.output) { q = match[0] - if (match[2] || match[3] || q[0] !== '/') - pushData(q) // string, div, or regex - else + if (/^\/[/\*]/.test(q)) pushComment(q) // regular comment + else + pushData(q) // string, div, or regex } } @@ -178,7 +178,6 @@ module.exports = function procbuf(strbuf, fname, options) { file = q[0] data = q[1] --_level - console.log('- PROCBUF pop() "' + file + '", ' + _level) } _emitter.emit('end') diff --git a/package.json b/package.json index 22c7097..8f82a3a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ }, "files": [ "index.js", - "jspp.cmd", "bin/jspp.js", "lib", "doc", diff --git a/spec/.eslintrc b/spec/.eslintrc deleted file mode 100644 index 2d8e477..0000000 --- a/spec/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -env: - node: true - jasmine: true - -rules: - no-unused-expressions: 0 diff --git a/spec/app-spec.js b/spec/app-spec.js index 4ee541d..e7c432e 100644 --- a/spec/app-spec.js +++ b/spec/app-spec.js @@ -1,6 +1,5 @@ -/* - Tests for jspreproc using jasmine -*/ +/*eslint-env node, jasmine */ +/*eslint no-unused-expressions: 0 */ 'use strict' var jspp = require('../lib/preproc'), @@ -786,6 +785,57 @@ describe('Conditionals Blocks', function () { }) }) + it('if/ifdef/ifndef/elif/else can start with `/*#` (v0.2.7)', function (done) { + var text = [ + '/*#if 1', + 'var x = 1', + '//#else*/', + 'var x = 2', + '//#endif' + ].join('\n') + + testStr(text, {comments: 'none'}, function (result) { + expect(result.trim()).toBe('var x = 1') + done() + }) + }) + + it('starting with `/*#` can include another conditionals', function (done) { + var text = [ + '//#set FOO=2', + '/*#if FOO==1', + 'var x = 1', + '//#elif FOO==2', + 'var x = 2', + '//#else*/', + 'var x = 3', + '//#endif' + ].join('\n') + + testStr(text, {comments: 'none'}, function (result) { + expect(result.trim()).toBe('var x = 2') + done() + }) + }) + + it('not recognized keywords following `/*#` are comments', function (done) { + var text = [ + '/*#if- 0', + 'var x = 1', + '//#else', + 'var x = 2', + '//#endif */', + '/*#set $_FOO = 1', + 'var x = $_FOO', + '//#unset $_FOO*/' + ].join('\n') + + testStr(text, {comments: 'none'}, function (result) { + expect(result.trim()).toBe('') + done() + }) + }) + }) @@ -1145,14 +1195,15 @@ describe('fixes', function () { }) }) - it('unicode line and paragraph characters break strings', function (done) { + it('Unicode line and paragraph characters doesn\'t break strings', function (done) { var text = [ '//#set $_STR = "\\u2028\\u2029"', + '//#set $_STR = $_STR[0]', 'var s = $_STR' ].join('\n') testStr(text, {}, function (result) { - expect(result).toBe('var s = "\\u2028\\u2029"') + expect(result).toBe('var s = "\\u2028"') done() }) }) diff --git a/spec/helpers/SpecHelper.js b/spec/helpers/SpecHelper.js index f04c3c7..ac18e6b 100644 --- a/spec/helpers/SpecHelper.js +++ b/spec/helpers/SpecHelper.js @@ -21,7 +21,7 @@ beforeEach(function () { }, // actual has to be an Error instance, and contain the expected string - toErrorContain: function (util) { + toErrorContain: function (/*util*/) { function compare(actual, expected) { return { diff --git a/test.txt b/test.txt deleted file mode 100644 index 202bfe6..0000000 --- a/test.txt +++ /dev/null @@ -1,3 +0,0 @@ -//#include spec/fixtures/noeol.txt -//#include spec/fixtures/noeol.txt -ok \ No newline at end of file From f196adc9ee5cd4ce8f56cfcc52d4dbc4512559d0 Mon Sep 17 00:00:00 2001 From: amarcruz Date: Tue, 5 Jan 2016 19:48:33 -0600 Subject: [PATCH 3/3] v0.2.7 --- .eslintignore | 6 +- CHANGELOG.md | 1 + Makefile | 3 + bin/jspp.js | 0 spec/uglify/core.js | 712 ++++ spec/uglify/riot.js | 3191 +++++++++++++++ spec/uglify/vue.js | 9434 +++++++++++++++++++++++++++++++++++++++++++ spec/ut.js | 50 + 8 files changed, 13394 insertions(+), 3 deletions(-) mode change 100644 => 100755 bin/jspp.js create mode 100644 spec/uglify/core.js create mode 100644 spec/uglify/riot.js create mode 100644 spec/uglify/vue.js create mode 100644 spec/ut.js diff --git a/.eslintignore b/.eslintignore index 72db58d..4bda2d5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -**/coverage/** -spec/expect/*.js -spec/fixtures/*.js +**/coverage/ +**/spec/ +!**/spec/*.js **.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f92b68..d656e51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Version 0.2.7 - 2016-01-05 - Rewrite of the conditional comments' parser. - Enhancement: Multiline comment with `if/ifdef/ifndef/elif/else` allows hide conditional blocks. See [doc/syntax.md](doc/syntax.md) +- Added test with uglify-js #### Version 0.2.6 - 2015-12-03 - Fix errors when including unicode line and paragraph characters. diff --git a/Makefile b/Makefile index dec0c43..13d5df1 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,7 @@ lint: @ eslint **/*.js # Done. +utest: + @ node spec/ut.js + .PHONY: sendc cover lint debug diff --git a/bin/jspp.js b/bin/jspp.js old mode 100644 new mode 100755 diff --git a/spec/uglify/core.js b/spec/uglify/core.js new file mode 100644 index 0000000..f5a2e12 --- /dev/null +++ b/spec/uglify/core.js @@ -0,0 +1,712 @@ + +function _regEx(str, opt) { return new RegExp(str, opt) } + +// Looks like, in [jsperf tests](http://jsperf.com/riot-regexp-test-vs-array-indexof) +// RegExp is faster in most browsers, except for very shorty arrays. +var + // Boolean attributes, prefixed with `__` in the riot tag definition. + // See ../doc/attributes.md + // + BOOL_ATTRS = _regEx( + '^(?:disabled|checked|readonly|required|allowfullscreen|auto(?:focus|play)|' + + 'compact|controls|default|formnovalidate|hidden|ismap|itemscope|loop|' + + 'multiple|muted|no(?:resize|shade|validate|wrap)?|open|reversed|seamless|' + + 'selected|sortable|truespeed|typemustmatch)$'), + + // The following attributes give error when parsed on browser with `{ exrp_value }` + // See ../doc/attributes.md + RIOT_ATTRS = ['style', 'src', 'd'], + + // HTML5 void elements that cannot be auto-closed. + // See: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements + // http://www.w3.org/TR/html5/syntax.html#void-elements + VOID_TAGS = /^(?:input|img|br|wbr|hr|area|base|col|embed|keygen|link|meta|param|source|track)$/, + + // Matches attributes. Names can contain almost all iso-8859-1 character set. + HTML_ATTR = /\s*([-\w:\xA0-\xFF]+)\s*(?:=\s*('[^']+'|"[^"]+"|\S+))?/g, + SPEC_TYPES = /^"(?:number|date(?:time)?|time|month|email|color)\b/i, + TRIM_TRAIL = /[ \t]+$/gm, + S_STRINGS = brackets.R_STRINGS.source + +//#if NODE +var path = require('path') +//#endif + +//#set $_RIX_TESTX = 4 +//#set $_RIX_ESCX = 5 +//#set $_RIX_OPENX = 6 +//#set $_RIX_CLOSEX= 7 +//#set $_RIX_PAIRX = 8 +//#ifndef $_RIX_TEST +var + $_RIX_TEST = 4, // DONT'T FORGET SYNC THE #set BLOCK!!! + $_RIX_ESC = 5, + $_RIX_OPEN = 6, + $_RIX_CLOSE = 7, + $_RIX_PAIR = 8 +//#endif + +// Escape backslashes and inner single quotes, and enclose s in single quotes +function q(s) { + return "'" + (s ? s + .replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r') : + '') + "'" +} + +// Generates the `riot.tag2` call with the processed parts. +function mktag(name, html, css, attrs, js, pcex) { + var + c = ', ', + s = '}' + (pcex.length ? ', ' + q(pcex._bp[8]) : '') + ');' + + // give more consistency to the output + if (js && js.slice(-1) !== '\n') s = '\n' + s + + return 'riot.tag2(\'' + name + "'" + c + q(html) + c + q(css) + c + q(attrs) + + ', function(opts) {\n' + js + s +} + +/** + * Merge two javascript object extending the properties of the first one with + * the second + * + * @param {object} obj - source object + * @param {object} props - extra properties + * @returns {object} source object containing the new properties + */ +function extend(obj, props) { + if (props) { + for (var prop in props) { + /* istanbul ignore next */ + if (props.hasOwnProperty(prop)) { + obj[prop] = props[prop] + } + } + } + return obj +} + +/** + * Parses and format attributes. + * + * @param {string} str - Attributes, with expressions replaced by their hash + * @param {Array} pcex - Has a _bp property with info about brackets + * @returns {string} Formated attributes + */ +function parseAttrs(str, pcex) { + var + list = [], + match, + k, v, t, e, + DQ = '"' + + HTML_ATTR.lastIndex = 0 + + str = str.replace(/\s+/g, ' ') + + while (match = HTML_ATTR.exec(str)) { + + // all attribute names are converted to lower case + k = match[1].toLowerCase() + v = match[2] + + if (!v) { + list.push(k) // boolean attribute without explicit value + } + else { + // attribute values must be enclosed in double quotes + if (v[0] !== DQ) + v = DQ + (v[0] === "'" ? v.slice(1, -1) : v) + DQ + + if (k === 'type' && SPEC_TYPES.test(v)) { + t = v + } + else { + if (/\u0001\d/.test(v)) { + // renames special attributes with expressiones in their value. + if (k === 'value') e = 1 + else if (BOOL_ATTRS.test(k)) k = '__' + k + else if (~RIOT_ATTRS.indexOf(k)) k = 'riot-' + k + } + // join the key-value pair, with no spaces between the parts + list.push(k + '=' + v) + } + } + } + // update() will evaluate `type` after the value, avoiding warnings + if (t) { + if (e) t = DQ + pcex._bp[0] + "'" + v.slice(1, -1) + "'" + pcex._bp[1] + DQ + list.push('type=' + t) + } + return list.join(' ') // returns the attribute list +} + +// Replaces expressions in the HTML with a marker, and runs expressions +// through the parser, except those beginning with `{^`. +function splitHtml(html, opts, pcex) { + var _bp = pcex._bp + + // `brackets.split` is a heavy function, so don't call it if not necessary + if (html && _bp[4].test(html)) { + var + jsfn = opts.expr && (opts.parser || opts.type) ? _compileJS : 0, + list = brackets.split(html, 0, _bp), + expr + + for (var i = 1; i < list.length; i += 2) { + expr = list[i] + if (expr[0] === '^') + expr = expr.slice(1) + else if (jsfn) { + var israw = expr[0] === '=' + expr = jsfn(israw ? expr.slice(1) : expr, opts).trim() + if (expr.slice(-1) === ';') expr = expr.slice(0, -1) + if (israw) expr = '=' + expr + } + list[i] = '\u0001' + (pcex.push(expr.replace(/[\r\n]+/g, ' ').trim()) - 1) + _bp[1] + } + html = list.join('') + } + return html +} + +// Restores expressions hidden by splitHtml and escape literal internal brackets +function restoreExpr(html, pcex) { + if (pcex.length) { + html = html + .replace(/\u0001(\d+)/g, function (_, d) { + var expr = pcex[d] + if (expr[0] === '=') { + expr = expr.replace(brackets.R_STRINGS, function (qs) { + return qs //.replace(/&/g, '&') // I don't know if this make sense + .replace(//g, '>') + }) + } + return pcex._bp[0] + expr.replace(/"/g, '\u2057') + }) + } + return html +} + + +//## HTML Compilation +//------------------- + +// `HTML_TAGS` matches only start and self-closing tags, not the content. +var + HTML_COMMENT = _regEx(//.source + '|' + S_STRINGS, 'g'), + HTML_TAGS = /<([-\w]+)\s*([^"'\/>]*(?:(?:"[^"]*"|'[^']*'|\/[^>])[^'"\/>]*)*)(\/?)>/g, + PRE_TAG = _regEx( + /]+(?:(?:@Q)|[^>]*)*|\s*)?>([\S\s]*?)<\/pre\s*>/.source.replace('@Q', S_STRINGS), 'gi') + +function _compileHTML(html, opts, pcex) { + + // separate the expressions, then parse the tags and their attributes + html = splitHtml(html, opts, pcex) + .replace(HTML_TAGS, function (_, name, attr, ends) { + // force all tag names to lowercase + name = name.toLowerCase() + // close self-closing tag, except if this is a html5 void tag + ends = ends && !VOID_TAGS.test(name) ? '>' + }) + + // tags parsed, now compact whitespace if `opts.whitespace` is not set + if (!opts.whitespace) { + if (/]/.test(html)) { + var p = [] + html = html.replace(PRE_TAG, function (q) + { return p.push(q) && '\u0002' }).trim().replace(/\s+/g, ' ') + // istanbul ignore else + if (p.length) + html = html.replace(/\u0002/g, function (_) { return p.shift() }) + } + else + html = html.trim().replace(/\s+/g, ' ') + } + + // for `opts.compact`, remove whitespace between tags + if (opts.compact) html = html.replace(/> <([-\w\/])/g, '><$1') + + return restoreExpr(html, pcex) +} + +/** + * Parses and formats the HTML text. + * + * @param {string} html - Can contain embedded HTML comments and literal whitespace + * @param {Object} opts - Collected user options. Includes the brackets array in `_bp` + * @param {Array} [pcex] - Keeps precompiled expressions + * @returns {string} The parsed HTML text + * @see http://www.w3.org/TR/html5/syntax.html + */ +// istanbul ignore next +function compileHTML(html, opts, pcex) { + if (Array.isArray(opts)) { + pcex = opts + opts = {} + } + else { + if (!pcex) pcex = [] + if (!opts) opts = {} + } + + html = html.replace(/\r\n?/g, '\n').replace(HTML_COMMENT, + function (s) { return s[0] === '<' ? '' : s }).replace(TRIM_TRAIL, '') + + // `_bp` is undefined when `compileHTML` is not called by compile + if (!pcex._bp) pcex._bp = brackets.array(opts.brackets) + + return _compileHTML(html, opts, pcex) +} + + +// JavaScript Compilation +// ---------------------- + +// JS_RMCOMMS prepares regexp for remotion of multiline and single-line comments +// JS_ES6SIGN matches es6 methods across multiple lines up to their first curly brace +var + JS_RMCOMMS = _regEx('(' + brackets.S_QBLOCKS + ')|' + brackets.R_MLCOMMS.source + '|//[^\r\n]*', 'g'), + JS_ES6SIGN = /^([ \t]*)([$_A-Za-z][$\w]*)\s*(\([^()]*\)\s*{)/m + +// Default parser for JavaScript code +function riotjs(js) { + var + match, + toes5, + parts = [], // parsed code + pos + + // remove comments + js = js.replace(JS_RMCOMMS, function (m, q) { return q ? m : ' ' }) + + // $1: indentation, + // $2: method name, + // $3: parameters + while (match = js.match(JS_ES6SIGN)) { + + // save remaining part now -- IE9 changes `rightContext` in `RegExp.test` + parts.push(RegExp.leftContext) + js = RegExp.rightContext + pos = skipBlock(js) // find the closing bracket + + // convert ES6 method signature to ES5 function + toes5 = !/^(?:if|while|for|switch|catch|function)$/.test(match[2]) + if (toes5) + match[0] = match[1] + 'this.' + match[2] + ' = function' + match[3] + + parts.push(match[0], js.slice(0, pos)) + js = js.slice(pos) + if (toes5 && !/^\s*.\s*bind\b/.test(js)) parts.push('.bind(this)') + } + + return parts.length ? parts.join('') + js : js + + // Inner helper - find the position following the closing bracket for the current block + function skipBlock(str) { + var + re = _regEx('([{}])|' + brackets.S_QBLOCKS, 'g'), + level = 1, + match + + while (level && (match = re.exec(str))) { + if (match[1]) + match[1] === '{' ? ++level : --level + } + return level ? str.length : re.lastIndex + } +} + +function _compileJS(js, opts, type, parserOpts, url) { + if (!js) return '' + if (!type) type = opts.type + + var parser = opts.parser || (type ? parsers.js[type] : riotjs) + if (!parser) + throw new Error('JS parser not found: "' + type + '"') + + return parser(js, parserOpts, url).replace(TRIM_TRAIL, '') +} + +/** + * Runs the parser for the JavaScript code, defaults to `riotjs` + * + * @param {string} js - Buffer with the javascript code + * @param {Object} [opts] - Compiler options, can include a custom parser function + * @param {string} [type] - Optional type for parser selection + * @param {Object} [extra] - User options for the parser + * @returns {string} The parsed JavaScript code + */ +// istanbul ignore next +function compileJS(js, opts, type, extra) { + if (typeof opts === 'string') { + extra = type + type = opts + opts = {} + } + if (typeof type === 'object') { + extra = type + type = '' + } + else if (!extra) extra = {} + + return _compileJS(js, opts, type, extra.parserOptions, extra.url) +} + + +// CSS Compilation +// ---------------- +// See http://www.w3.org/TR/CSS21/ + +// Prepare regex to match CSS selectors, excluding those beginning with '@'. +var CSS_SELECTOR = _regEx('(}|{|^)[ ;]*([^@ ;{}][^{}]*)(?={)|' + S_STRINGS, 'g') + +// Parses styles enclosed in a "scoped" tag (`scoped` is deprecated in HTML5). +// The "style" string is received without comments or surrounding spaces. +function scopedCSS(tag, style) { + var scope = ':scope' + + return style.replace(CSS_SELECTOR, function (m, p1, p2) { + // skip quoted strings + if (!p2) return m + + // we have a selector list, parse each individually + p2 = p2.replace(/[^,]+/g, function (sel) { + var s = sel.trim() + // skips the keywords and percents of css animations + if (s && s !== 'from' && s !== 'to' && s.slice(-1) !== '%') { + // replace the `:scope` pseudo-selector, where it is, with the root tag name; + // if `:scope` was not included, add the tag name as prefix, and mirror all + // to `[riot-tag]` + if (s.indexOf(scope) < 0) s = scope + ' ' + s + s = s.replace(scope, tag) + ',' + + s.replace(scope, '[riot-tag="' + tag + '"]') + } + return sel.slice(-1) === ' ' ? s + ' ' : s // respect (a little) the user style + }) + // add the danling bracket char and return the processed selector list + return p1 ? p1 + ' ' + p2 : p2 + }) +} + +function _compileCSS(style, tag, type, opts) { + var scoped = (opts || (opts = {})).scoped + + if (type) { + if (type === 'scoped-css') { // DEPRECATED + scoped = true + } + else if (parsers.css[type]) { + style = parsers.css[type](tag, style, opts.parserOpts || {}, opts.url) + } + else if (type !== 'css') { + throw new Error('CSS parser not found: "' + type + '"') + } + } + + // remove comments, compact and trim whitespace + style = style.replace(brackets.R_MLCOMMS, '').replace(/\s+/g, ' ').trim() + + // translate scoped rules if nedded + if (scoped) { + // istanbul ignore next + if (!tag) + throw new Error('Can not parse scoped CSS without a tagName') + style = scopedCSS(tag, style) + } + return style +} + +/** + * Runs the parser for style blocks. + * Simple API to the compileCSS function. + * + * @param {string} style - Raw style block + * @param {string} [parser] - Must be one of `parsers.css`, can be omited. + * @param {object} [opts] - passed to the given parser, can be omited. + * @returns {string} The processed style block + */ +// istanbul ignore next +function compileCSS(style, parser, opts) { + if (typeof parser === 'object') { + opts = parser + parser = '' + } + return _compileCSS(style, opts.tagName, parser, opts) +} + +// The main compiler +// ----------------- + +// TYPE_ATTR matches the 'type' attribute, for script and style tags +var + TYPE_ATTR = /\stype\s*=\s*(?:(['"])(.+?)\1|(\S+))/i, // don't handle escaped quotes :( + MISC_ATTR = /\s*=\s*("(?:\\[\S\s]|[^"\\]*)*"|'(?:\\[\S\s]|[^'\\]*)*'|\{[^}]+}|\S+)/.source + +// Returns the value of the 'type' attribute, with the prefix "text/" removed. +function getType(str) { + + if (str) { + var match = str.match(TYPE_ATTR) + str = match && (match[2] || match[3]) + } + return str ? str.replace('text/', '') : '' +} + +// Returns the value of any attribute, or the empty string for missing attribute. +function getAttr(str, name) { + + if (str) { + var + re = _regEx('\\s' + name + MISC_ATTR, 'i'), + match = str.match(re) + str = match && match[1] + if (str) + return (/^['"]/).test(str) ? str.slice(1, -1) : str + } + return '' +} + +// get the parser options from the options attribute +function getParserOptions(attrs) { + var opts = getAttr(attrs, 'options') + // convert the string into a valid js object + if (opts) opts = JSON.parse(opts) + return opts +} + +// Runs the custom or default parser on the received JavaScript code. +// The CLI version can read code from the file system (experimental) +function getCode(code, opts, attrs, url) { + var type = getType(attrs), + parserOpts = getParserOptions(attrs) + + //#if NODE + // istanbul ignore else + if (url) { + var src = getAttr(attrs, 'src') + if (src) { + var + charset = getAttr(attrs, 'charset'), + file = path.resolve(path.dirname(url), src) + code = require('fs').readFileSync(file, charset || 'utf8') + } + } + //#endif + return _compileJS(code, opts, type, parserOpts, url) +} + +function cssCode(code, opts, attrs, url, tag) { + var extraOpts = { + parserOpts: getParserOptions(attrs), + scoped: attrs && /\sscoped(\s|=|$)/i.test(attrs), + url: url + } + return _compileCSS(code, tag, getType(attrs) || opts.style, extraOpts) +} + +// Matches HTML tag ending a line. This regex still can be fooled by code as: +// ```js +// x +// z +// ``` +var END_TAGS = /\/>\n|^<(?:\/[\w\-]+\s*|[\w\-]+(?:\s+(?:[-\w:\xA0-\xFF][\S\s]*?)?)?)>\n/ + +function splitBlocks(str) { + var k, m + + /* istanbul ignore next: this if() can't be true, but just in case... */ + if (str[str.length - 1] === '>') return [str, ''] + + k = str.lastIndexOf('<') // first probable open tag + while (~k) { + if (m = str.slice(k).match(END_TAGS)) { + k += m.index + m[0].length + return [str.slice(0, k), str.slice(k)] + } + k = str.lastIndexOf('<', k -1) + } + return ['', str] +} + +/** + * Runs the external HTML parser for the entire tag file + * + * @param {string} html - Entire, untouched html received for the compiler + * @param {string} url - The source url or file name + * @param {string} lang - Name of the parser, one of `parsers.html` + * @param {object} opts - Extra option passed to the parser + * @returns {string} parsed html + */ +function compileTemplate(html, url, lang, opts) { + var parser = parsers.html[lang] + + if (!parser) + throw new Error('Template parser not found: "' + lang + '"') + + return parser(html, opts, url) +} + +/* + CUST_TAG regex don't allow unquoted expressions containing the `>` operator. + STYLES and SCRIPT disallows the operator `>` at all. + + The beta.4 CUST_TAG regex is fast, with RegexBuddy I get 76 steps and 14 backtracks on + the test/specs/fixtures/treeview.tag :) but fails with nested tags of the same name :( + With a greedy * operator, we have ~500 and 200bt, it is acceptable. So let's fix this. + */ +// TODO: CUST_TAG fails with unescaped regex in the root attributes having '>' characters. +// We need brackets.split here? +var + CUST_TAG = _regEx( + /^([ \t]*)<([-\w]+)(?:\s+([^'"\/>]+(?:(?:@Q|\/[^>])[^'"\/>]*)*)|\s*)?(?:\/>|>[ \t]*\n?([\S\s]*)^\1<\/\2\s*>|>(.*)<\/\2\s*>)/ + .source.replace('@Q', S_STRINGS), 'gim'), + SRC_TAGS = /]*)?>\n?([^<]*(?:<(?!\/style\s*>)[^<]*)*)<\/style\s*>/.source + '|' + S_STRINGS, + STYLES = _regEx(SRC_TAGS, 'gi'), + SCRIPT = _regEx(SRC_TAGS.replace(/style/g, 'script'), 'gi') + +/** + * The main compiler processes all custom tags, one by one. + * + * In .tag files, a custom tag can span multiple lines, but there should be no other + * external element sharing the starting and ending lines of the tag (HTML comments + * inclusive) and should not be indented. + * Custom tags in HTML files don't have this restriction. + * + * @param {string} src - String with zero or more custom riot tags. + * @param {Object} [opts] - User options. + * @param {string} [url] - Filename of the riot tag, prepended to the generated code. + * @returns {string} JavaScript code to build the tag later, through riot.tag2 function. + */ +function compile(src, opts, url) { + var + parts = [], + exclude + + if (!opts) opts = {} + + //#if NODE + // let's be sure that an url is always defined at least on node + // this will allow the babeljs users using the .babelrc file to configure + // their transpiler setup + if (!url) url = process.cwd() + '/.' // getCode expect a file + /*#else + if (!url) url = '' + //#endif*/ + + exclude = opts.exclude || false + function included(s) { return !(exclude && ~exclude.indexOf(s)) } + + // get a static brackets array for use on the entire source + var _bp = brackets.array(opts.brackets) + + // run any custom html parser before the compilation + if (opts.template) + src = compileTemplate(src, url, opts.template, opts.templateOptions) + + // normalize eols and start processing the tags + src = src + .replace(/\r\n?/g, '\n') + .replace(CUST_TAG, function (_, indent, tagName, attribs, body, body2) { + + // content can have attributes first, then html markup with zero or more script or + // style tags of different types, and finish with an untagged block of javascript code. + var + jscode = '', + styles = '', + html = '', + pcex = [] + + pcex._bp = _bp // local copy, in preparation for async compilation + + tagName = tagName.toLowerCase() + + // process the attributes, including their expressions + attribs = attribs && included('attribs') ? + restoreExpr(parseAttrs(splitHtml(attribs, opts, pcex), pcex), pcex) : '' + + if (body2) body = body2 + + // remove comments and trim trailing whitespace + if (body && (body = body.replace(HTML_COMMENT, + function (s) { return s[0] === '<' ? '' : s })) && /\S/.test(body)) { + + if (body2) { + /* istanbul ignore next */ + html = included('html') ? _compileHTML(body2, opts, pcex) : '' + } + else { + body = body.replace(_regEx('^' + indent, 'gm'), '') + + // get and process the style blocks + + body = body.replace(STYLES, function (_m, _attrs, _style) { + if (_m[0] !== '<') return _m + if (included('css')) + styles += (styles ? ' ' : '') + cssCode(_style, opts, _attrs, url, tagName) + return '' + }) + + // now the script blocks + + body = body.replace(SCRIPT, function (_m, _attrs, _script) { + if (_m[0] !== '<') return _m + if (included('js')) + jscode += (jscode ? '\n' : '') + getCode(_script, opts, _attrs, url) + return '' + }) + + // separate the untagged javascript block from the html markup + + var blocks = splitBlocks(body.replace(TRIM_TRAIL, '')) + + if (included('html')) { + body = blocks[0] + if (body) + html = _compileHTML(body, opts, pcex) + } + + if (included('js')) { + body = blocks[1] + if (/\S/.test(body)) + jscode += (jscode ? '\n' : '') + _compileJS(body, opts, null, null, url) + } + } + } + + // give more consistency to the output + jscode = /\S/.test(jscode) ? jscode.replace(/\n{3,}/g, '\n\n') : '' + + // replace the tag with a call to the riot.tag2 function and we are done + if (opts.entities) { + parts.push({ + tagName: tagName, + html: html, + css: styles, + attribs: attribs, + js: jscode + }) + return '' + } + + // replace the tag with a call to the riot.tag2 function and we are done + return mktag(tagName, html, styles, attribs, jscode, pcex) + }) + + if (opts.entities) return parts + + //#if NODE + // Note: isAbsolute does not exists in node 10.x + if (url && opts.debug) { + /* istanbul ignore if */ + if (path.isAbsolute(url)) url = path.relative('.', url) + src = '//src: ' + url.replace(/\\/g, '/') + '\n' + src + } + //#endif + return src +} diff --git a/spec/uglify/riot.js b/spec/uglify/riot.js new file mode 100644 index 0000000..0e01fdb --- /dev/null +++ b/spec/uglify/riot.js @@ -0,0 +1,3191 @@ +/* Riot WIP, @license MIT, (c) 2015 Muut Inc. + contributors */ + +;(function(window, undefined) { + 'use strict'; +var riot = { version: 'WIP', settings: {} }, + // be aware, internal usage + // ATTENTION: prefix the global dynamic variables with `__` + + // counter to give a unique id to all the Tag instances + __uid = 0, + // tags instances cache + __virtualDom = [], + // tags implementation cache + __tagImpl = {}, + + /** + * Const + */ + // riot specific prefixes + RIOT_PREFIX = 'riot-', + RIOT_TAG = RIOT_PREFIX + 'tag', + + // for typeof == '' comparisons + T_STRING = 'string', + T_OBJECT = 'object', + T_UNDEF = 'undefined', + T_FUNCTION = 'function', + // special native tags that cannot be treated like the others + SPECIAL_TAGS_REGEX = /^(?:opt(ion|group)|tbody|col|t[rhd])$/, + RESERVED_WORDS_BLACKLIST = ['_item', '_id', '_parent', 'update', 'root', 'mount', 'unmount', 'mixin', 'isMounted', 'isLoop', 'tags', 'parent', 'opts', 'trigger', 'on', 'off', 'one'], + + // version# for IE 8-11, 0 for others + IE_VERSION = (window && window.document || {}).documentMode | 0 +/* istanbul ignore next */ +riot.observable = function(el) { + + /** + * Extend the original object or create a new empty one + * @type { Object } + */ + + el = el || {} + + /** + * Private variables and methods + */ + + var callbacks = {}, + onEachEvent = function(e, fn) { e.replace(/\S+/g, fn) }, + defineProperty = function (key, value) { + Object.defineProperty(el, key, { + value: value, + enumerable: false, + writable: false, + configurable: false + }) + } + + /** + * Listen to the given space separated list of `events` and execute the `callback` each time an event is triggered. + * @param { String } events - events ids + * @param { Function } fn - callback function + * @returns { Object } el + */ + + defineProperty('on', function(events, fn) { + if (typeof fn != 'function') return el + + onEachEvent(events, function(name, pos) { + (callbacks[name] = callbacks[name] || []).push(fn) + fn.typed = pos > 0 + }) + + return el + }) + + /** + * Removes the given space separated list of `events` listeners + * @param { String } events - events ids + * @param { Function } fn - callback function + * @returns { Object } el + */ + + defineProperty('off', function(events, fn) { + if (events == '*') callbacks = {} + else { + onEachEvent(events, function(name) { + if (fn) { + var arr = callbacks[name] + for (var i = 0, cb; cb = arr && arr[i]; ++i) { + if (cb == fn) arr.splice(i--, 1) + } + } else delete callbacks[name] + }) + } + return el + }) + + /** + * Listen to the given space separated list of `events` and execute the `callback` at most once + * @param { String } events - events ids + * @param { Function } fn - callback function + * @returns { Object } el + */ + + defineProperty('one', function(events, fn) { + function on() { + el.off(events, on) + fn.apply(el, arguments) + } + return el.on(events, on) + }) + + /** + * Execute all callback functions that listen to the given space separated list of `events` + * @param { String } events - events ids + * @returns { Object } el + */ + + defineProperty('trigger', function(events) { + + // getting the arguments + // skipping the first one + var arglen = arguments.length - 1, + args = new Array(arglen) + for (var i = 0; i < arglen; i++) { + args[i] = arguments[i + 1] + } + + onEachEvent(events, function(name) { + + var fns = (callbacks[name] || []).slice(0) + + for (var i = 0, fn; fn = fns[i]; ++i) { + if (fn.busy) return + fn.busy = 1 + + try { + fn.apply(el, fn.typed ? [name].concat(args) : args) + } catch (e) { el.trigger('error', e) } + if (fns[i] !== fn) { i-- } + fn.busy = 0 + } + + if (callbacks.all && name != 'all') + el.trigger.apply(el, ['all', name].concat(args)) + + }) + + return el + }) + + return el + +} +/* istanbul ignore next */ +;(function(riot) { if (!window) return; + +/** + * Simple client-side router + * @module riot-route + */ + + +var RE_ORIGIN = /^.+?\/+[^\/]+/, + EVENT_LISTENER = 'EventListener', + REMOVE_EVENT_LISTENER = 'remove' + EVENT_LISTENER, + ADD_EVENT_LISTENER = 'add' + EVENT_LISTENER, + HAS_ATTRIBUTE = 'hasAttribute', + REPLACE = 'replace', + POPSTATE = 'popstate', + HASHCHANGE = 'hashchange', + TRIGGER = 'trigger', + MAX_EMIT_STACK_LEVEL = 3, + win = window, + doc = document, + loc = win.history.location || win.location, // see html5-history-api + prot = Router.prototype, // to minify more + clickEvent = doc && doc.ontouchstart ? 'touchstart' : 'click', + started = false, + central = riot.observable(), + routeFound = false, + debouncedEmit, + base, current, parser, secondParser, emitStack = [], emitStackLevel = 0 + +/** + * Default parser. You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @returns {array} array + */ +function DEFAULT_PARSER(path) { + return path.split(/[/?#]/) +} + +/** + * Default parser (second). You can replace it via router.parser method. + * @param {string} path - current path (normalized) + * @param {string} filter - filter string (normalized) + * @returns {array} array + */ +function DEFAULT_SECOND_PARSER(path, filter) { + var re = new RegExp('^' + filter[REPLACE](/\*/g, '([^/?#]+?)')[REPLACE](/\.\./, '.*') + '$'), + args = path.match(re) + + if (args) return args.slice(1) +} + +/** + * Simple/cheap debounce implementation + * @param {function} fn - callback + * @param {number} delay - delay in seconds + * @returns {function} debounced function + */ +function debounce(fn, delay) { + var t + return function () { + clearTimeout(t) + t = setTimeout(fn, delay) + } +} + +/** + * Set the window listeners to trigger the routes + * @param {boolean} autoExec - see route.start + */ +function start(autoExec) { + debouncedEmit = debounce(emit, 1) + win[ADD_EVENT_LISTENER](POPSTATE, debouncedEmit) + win[ADD_EVENT_LISTENER](HASHCHANGE, debouncedEmit) + doc[ADD_EVENT_LISTENER](clickEvent, click) + if (autoExec) emit(true) +} + +/** + * Router class + */ +function Router() { + this.$ = [] + riot.observable(this) // make it observable + central.on('stop', this.s.bind(this)) + central.on('emit', this.e.bind(this)) +} + +function normalize(path) { + return path[REPLACE](/^\/|\/$/, '') +} + +function isString(str) { + return typeof str == 'string' +} + +/** + * Get the part after domain name + * @param {string} href - fullpath + * @returns {string} path from root + */ +function getPathFromRoot(href) { + return (href || loc.href)[REPLACE](RE_ORIGIN, '') +} + +/** + * Get the part after base + * @param {string} href - fullpath + * @returns {string} path from base + */ +function getPathFromBase(href) { + return base[0] == '#' + ? (href || loc.href).split(base)[1] || '' + : getPathFromRoot(href)[REPLACE](base, '') +} + +function emit(force) { + // the stack is needed for redirections + var isRoot = emitStackLevel == 0 + if (MAX_EMIT_STACK_LEVEL <= emitStackLevel) return + + emitStackLevel++ + emitStack.push(function() { + var path = getPathFromBase() + if (force || path != current) { + central[TRIGGER]('emit', path) + current = path + } + }) + if (isRoot) { + while (emitStack.length) { + emitStack[0]() + emitStack.shift() + } + emitStackLevel = 0 + } +} + +function click(e) { + if ( + e.which != 1 // not left click + || e.metaKey || e.ctrlKey || e.shiftKey // or meta keys + || e.defaultPrevented // or default prevented + ) return + + var el = e.target + while (el && el.nodeName != 'A') el = el.parentNode + if ( + !el || el.nodeName != 'A' // not A tag + || el[HAS_ATTRIBUTE]('download') // has download attr + || !el[HAS_ATTRIBUTE]('href') // has no href attr + || el.target && el.target != '_self' // another window or frame + || el.href.indexOf(loc.href.match(RE_ORIGIN)[0]) == -1 // cross origin + ) return + + if (el.href != loc.href) { + if ( + el.href.split('#')[0] == loc.href.split('#')[0] // internal jump + || base != '#' && getPathFromRoot(el.href).indexOf(base) !== 0 // outside of base + || !go(getPathFromBase(el.href), el.title || doc.title) // route not found + ) return + } + + e.preventDefault() +} + +/** + * Go to the path + * @param {string} path - destination path + * @param {string} title - page title + * @returns {boolean} - route not found flag + */ +function go(path, title) { + title = title || doc.title + // browsers ignores the second parameter `title` + history.pushState(null, title, base + normalize(path)) + // so we need to set it manually + doc.title = title + routeFound = false + emit() + return routeFound +} + +/** + * Go to path or set action + * a single string: go there + * two strings: go there with setting a title + * a single function: set an action on the default route + * a string/RegExp and a function: set an action on the route + * @param {(string|function)} first - path / action / filter + * @param {(string|RegExp|function)} second - title / action + */ +prot.m = function(first, second) { + if (isString(first) && (!second || isString(second))) go(first, second) + else if (second) this.r(first, second) + else this.r('@', first) +} + +/** + * Stop routing + */ +prot.s = function() { + this.off('*') + this.$ = [] +} + +/** + * Emit + * @param {string} path - path + */ +prot.e = function(path) { + this.$.concat('@').some(function(filter) { + var args = (filter == '@' ? parser : secondParser)(normalize(path), normalize(filter)) + if (typeof args != 'undefined') { + this[TRIGGER].apply(null, [filter].concat(args)) + return routeFound = true // exit from loop + } + }, this) +} + +/** + * Register route + * @param {string} filter - filter for matching to url + * @param {function} action - action to register + */ +prot.r = function(filter, action) { + if (filter != '@') { + filter = '/' + normalize(filter) + this.$.push(filter) + } + this.on(filter, action) +} + +var mainRouter = new Router() +var route = mainRouter.m.bind(mainRouter) + +/** + * Create a sub router + * @returns {function} the method of a new Router object + */ +route.create = function() { + var newSubRouter = new Router() + // stop only this sub-router + newSubRouter.m.stop = newSubRouter.s.bind(newSubRouter) + // return sub-router's main method + return newSubRouter.m.bind(newSubRouter) +} + +/** + * Set the base of url + * @param {(str|RegExp)} arg - a new base or '#' or '#!' + */ +route.base = function(arg) { + base = arg || '#' + current = getPathFromBase() // recalculate current path +} + +/** Exec routing right now **/ +route.exec = function() { + emit(true) +} + +/** + * Replace the default router to yours + * @param {function} fn - your parser function + * @param {function} fn2 - your secondParser function + */ +route.parser = function(fn, fn2) { + if (!fn && !fn2) { + // reset parser for testing... + parser = DEFAULT_PARSER + secondParser = DEFAULT_SECOND_PARSER + } + if (fn) parser = fn + if (fn2) secondParser = fn2 +} + +/** + * Helper function to get url query as an object + * @returns {object} parsed query + */ +route.query = function() { + var q = {} + loc.href[REPLACE](/[?&](.+?)=([^&]*)/g, function(_, k, v) { q[k] = v }) + return q +} + +/** Stop routing **/ +route.stop = function () { + if (started) { + win[REMOVE_EVENT_LISTENER](POPSTATE, debouncedEmit) + win[REMOVE_EVENT_LISTENER](HASHCHANGE, debouncedEmit) + doc[REMOVE_EVENT_LISTENER](clickEvent, click) + central[TRIGGER]('stop') + started = false + } +} + +/** + * Start routing + * @param {boolean} autoExec - automatically exec after starting if true + */ +route.start = function (autoExec) { + if (!started) { + if (document.readyState == 'complete') start(autoExec) + // the timeout is needed to solve + // a weird safari bug https://github.com/riot/route/issues/33 + else win[ADD_EVENT_LISTENER]('load', function() { + setTimeout(function() { start(autoExec) }, 1) + }) + started = true + } +} + +/** Prepare the router **/ +route.base() +route.parser() + +riot.route = route +})(riot) +/* istanbul ignore next */ + +/** + * The riot template engine + * @version WIP + */ + +/** + * @module brackets + * + * `brackets ` Returns a string or regex based on its parameter + * `brackets.settings` Mirrors the `riot.settings` object (use brackets.set in new code) + * `brackets.set ` Change the current riot brackets + */ + +var brackets = (function (UNDEF) { + + var + REGLOB = 'g', + + MLCOMMS = /\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//g, + STRINGS = /"[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'/g, + + S_QBSRC = STRINGS.source + '|' + + /(?:\breturn\s+|(?:[$\w\)\]]|\+\+|--)\s*(\/)(?![*\/]))/.source + '|' + + /\/(?=[^*\/])[^[\/\\]*(?:(?:\[(?:\\.|[^\]\\]*)*\]|\\.)[^[\/\\]*)*?(\/)[gim]*/.source, + + DEFAULT = '{ }', + + FINDBRACES = { + '(': RegExp('([()])|' + S_QBSRC, REGLOB), + '[': RegExp('([[\\]])|' + S_QBSRC, REGLOB), + '{': RegExp('([{}])|' + S_QBSRC, REGLOB) + } + + var + cachedBrackets = UNDEF, + _regex, + _pairs = [] + + function _loopback(re) { return re } + + function _rewrite(re, bp) { + if (!bp) bp = _pairs + return new RegExp( + re.source.replace(/{/g, bp[2]).replace(/}/g, bp[3]), re.global ? REGLOB : '' + ) + } + + function _create(pair) { + var + cvt, + arr = pair.split(' ') + + if (pair === DEFAULT) { + arr[2] = arr[0] + arr[3] = arr[1] + cvt = _loopback + } + else { + if (arr.length !== 2 || /[\x00-\x1F<>a-zA-Z0-9'",;\\]/.test(pair)) { + throw new Error('Unsupported brackets "' + pair + '"') + } + arr = arr.concat(pair.replace(/(?=[[\]()*+?.^$|])/g, '\\').split(' ')) + cvt = _rewrite + } + arr[4] = cvt(arr[1].length > 1 ? /{[\S\s]*?}/ : /{[^}]*}/, arr) + arr[5] = cvt(/\\({|})/g, arr) + arr[6] = cvt(/(\\?)({)/g, arr) + arr[7] = RegExp('(\\\\?)(?:([[({])|(' + arr[3] + '))|' + S_QBSRC, REGLOB) + arr[8] = pair + return arr + } + + function _reset(pair) { + if (!pair) pair = DEFAULT + + if (pair !== _pairs[8]) { + _pairs = _create(pair) + _regex = pair === DEFAULT ? _loopback : _rewrite + _pairs[9] = _regex(/^\s*{\^?\s*([$\w]+)(?:\s*,\s*(\S+))?\s+in\s+(\S.*)\s*}/) + _pairs[10] = _regex(/(^|[^\\]){=[\S\s]*?}/) + _brackets._rawOffset = _pairs[0].length + } + cachedBrackets = pair + } + + function _brackets(reOrIdx) { + return reOrIdx instanceof RegExp ? _regex(reOrIdx) : _pairs[reOrIdx] + } + + _brackets.split = function split(str, tmpl, _bp) { + // istanbul ignore next: _bp is for the compiler + if (!_bp) _bp = _pairs + + var + parts = [], + match, + isexpr, + start, + pos, + re = _bp[6] + + isexpr = start = re.lastIndex = 0 + + while (match = re.exec(str)) { + + pos = match.index + + if (isexpr) { + + if (match[2]) { + re.lastIndex = skipBraces(match[2], re.lastIndex) + continue + } + + if (!match[3]) + continue + } + + if (!match[1]) { + unescapeStr(str.slice(start, pos)) + start = re.lastIndex + re = _bp[6 + (isexpr ^= 1)] + re.lastIndex = start + } + } + + if (str && start < str.length) { + unescapeStr(str.slice(start)) + } + + return parts + + function unescapeStr(str) { + if (tmpl || isexpr) + parts.push(str && str.replace(_bp[5], '$1')) + else + parts.push(str) + } + + function skipBraces(ch, pos) { + var + match, + recch = FINDBRACES[ch], + level = 1 + recch.lastIndex = pos + + while (match = recch.exec(str)) { + if (match[1] && + !(match[1] === ch ? ++level : --level)) break + } + return match ? recch.lastIndex : str.length + } + } + + _brackets.hasExpr = function hasExpr(str) { + return _brackets(4).test(str) + } + + _brackets.loopKeys = function loopKeys(expr) { + var m = expr.match(_brackets(9)) + return m ? + { key: m[1], pos: m[2], val: _pairs[0] + m[3].trim() + _pairs[1] } : { val: expr.trim() } + } + + _brackets.array = function array(pair) { + return _create(pair || cachedBrackets) + } + + var _settings + function _setSettings(o) { + var b + o = o || {} + b = o.brackets + Object.defineProperty(o, 'brackets', { + set: _reset, + get: function () { return cachedBrackets }, + enumerable: true + }) + _settings = o + _reset(b) + } + Object.defineProperty(_brackets, 'settings', { + set: _setSettings, + get: function () { return _settings } + }) + + /* istanbul ignore next: in the node version riot is not in the scope */ + _brackets.settings = typeof riot !== 'undefined' && riot.settings || {} + _brackets.set = _reset + + _brackets.R_STRINGS = STRINGS + _brackets.R_MLCOMMS = MLCOMMS + _brackets.S_QBLOCKS = S_QBSRC + + return _brackets + +})() + +/** + * @module tmpl + * + * tmpl - Root function, returns the template value, render with data + * tmpl.hasExpr - Test the existence of a expression inside a string + * tmpl.loopKeys - Get the keys for an 'each' loop (used by `_each`) + */ + +var tmpl = (function () { + + var _cache = {} + + function _tmpl(str, data) { + if (!str) return str + + return (_cache[str] || (_cache[str] = _create(str))).call(data, _logErr) + } + + _tmpl.isRaw = function (expr) { + return expr[brackets._rawOffset] === "=" + } + + _tmpl.haveRaw = function (src) { + return brackets(10).test(src) + } + + _tmpl.hasExpr = brackets.hasExpr + + _tmpl.loopKeys = brackets.loopKeys + + _tmpl.errorHandler = null + + function _logErr(err, ctx) { + + if (_tmpl.errorHandler) { + + err.riotData = { + tagName: ctx && ctx.root && ctx.root.tagName, + _riot_id: ctx && ctx._riot_id //eslint-disable-line camelcase + } + _tmpl.errorHandler(err) + } + } + + function _create(str) { + + var expr = _getTmpl(str) + if (expr.slice(0, 11) !== 'try{return ') expr = 'return ' + expr + + return new Function('E', expr + ';') + } + + var + RE_QBLOCK = RegExp(brackets.S_QBLOCKS, 'g'), + RE_QBMARK = /\x01(\d+)~/g + + function _getTmpl(str) { + var + qstr = [], + expr, + parts = brackets.split(str.replace(/\u2057/g, '"'), 1) + + if (parts.length > 2 || parts[0]) { + var i, j, list = [] + + for (i = j = 0; i < parts.length; ++i) { + + expr = parts[i] + + if (expr && (expr = i & 1 ? + + _parseExpr(expr, 1, qstr) : + + '"' + expr + .replace(/\\/g, '\\\\') + .replace(/\r\n?|\n/g, '\\n') + .replace(/"/g, '\\"') + + '"' + + )) list[j++] = expr + + } + + expr = j < 2 ? list[0] : + '[' + list.join(',') + '].join("")' + } + else { + + expr = _parseExpr(parts[1], 0, qstr) + } + + if (qstr[0]) + expr = expr.replace(RE_QBMARK, function (_, pos) { + return qstr[pos] + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + }) + + return expr + } + + var + CS_IDENT = /^(?:(-?[_A-Za-z\xA0-\xFF][-\w\xA0-\xFF]*)|\x01(\d+)~):/, + RE_BRACE = /,|([[{(])|$/g + + function _parseExpr(expr, asText, qstr) { + + if (expr[0] === "=") expr = expr.slice(1) + + expr = expr + .replace(RE_QBLOCK, function (s, div) { + return s.length > 2 && !div ? '\x01' + (qstr.push(s) - 1) + '~' : s + }) + .replace(/\s+/g, ' ').trim() + .replace(/\ ?([[\({},?\.:])\ ?/g, '$1') + + if (expr) { + var + list = [], + cnt = 0, + match + + while (expr && + (match = expr.match(CS_IDENT)) && + !match.index + ) { + var + key, + jsb, + re = /,|([[{(])|$/g + + expr = RegExp.rightContext + key = match[2] ? qstr[match[2]].slice(1, -1).trim().replace(/\s+/g, ' ') : match[1] + + while (jsb = (match = re.exec(expr))[1]) skipBraces(jsb, re) + + jsb = expr.slice(0, match.index) + expr = RegExp.rightContext + + list[cnt++] = _wrapExpr(jsb, 1, key) + } + + expr = !cnt ? _wrapExpr(expr, asText) : + cnt > 1 ? '[' + list.join(',') + '].join(" ").trim()' : list[0] + } + return expr + + function skipBraces(jsb, re) { + var + match, + lv = 1, + ir = jsb === '(' ? /[()]/g : jsb === '[' ? /[[\]]/g : /[{}]/g + + ir.lastIndex = re.lastIndex + while (match = ir.exec(expr)) { + if (match[0] === jsb) ++lv + else if (!--lv) break + } + re.lastIndex = lv ? expr.length : ir.lastIndex + } + } + + // istanbul ignore next: not both + var JS_CONTEXT = '"in this?this:' + (typeof window !== 'object' ? 'global' : 'window') + ').' + var JS_VARNAME = /[,{][$\w]+:|(^ *|[^$\w\.])(?!(?:typeof|true|false|null|undefined|in|instanceof|is(?:Finite|NaN)|void|NaN|new|Date|RegExp|Math)(?![$\w]))([$_A-Za-z][$\w]*)/g + + function _wrapExpr(expr, asText, key) { + var tb + + expr = expr.replace(JS_VARNAME, function (match, p, mvar, pos, s) { + if (mvar) { + pos = tb ? 0 : pos + match.length + + if (mvar !== 'this' && mvar !== 'global' && mvar !== 'window') { + match = p + '("' + mvar + JS_CONTEXT + mvar + if (pos) tb = (s = s[pos]) === '.' || s === '(' || s === '[' + } + else if (pos) + tb = !/^(?=(\.[$\w]+))\1(?:[^.[(]|$)/.test(s.slice(pos)) + } + return match + }) + + if (tb) { + expr = 'try{return ' + expr + '}catch(e){E(e,this)}' + } + + if (key) { + + expr = (tb ? + 'function(){' + expr + '}.call(this)' : '(' + expr + ')' + ) + '?"' + key + '":""' + } + else if (asText) { + + expr = 'function(v){' + (tb ? + expr.replace('return ', 'v=') : 'v=(' + expr + ')' + ) + ';return v||v===0?v:""}.call(this)' + } + + return expr + } + + // istanbul ignore next: compatibility fix for beta versions + _tmpl.parse = function (s) { return s } + + return _tmpl + +})() + + tmpl.version = brackets.version = 'WIP' + + +/* + lib/browser/tag/mkdom.js + + Includes hacks needed for the Internet Explorer version 9 and below + +*/ +// http://kangax.github.io/compat-table/es5/#ie8 +// http://codeplanet.io/dropping-ie8/ + +var mkdom = (function (checkIE) { + + var rootEls = { + tr: 'tbody', + th: 'tr', + td: 'tr', + tbody: 'table', + col: 'colgroup' + }, + reToSrc = /([\S\s]+?)<\/yield\s*>/.source, + GENERIC = 'div' + + checkIE = checkIE && checkIE < 10 + + // creates any dom element in a div, table, or colgroup container + function _mkdom(templ, html) { + + var match = templ && templ.match(/^\s*<([-\w]+)/), + tagName = match && match[1].toLowerCase(), + rootTag = rootEls[tagName] || GENERIC, + el = mkEl(rootTag) + + el.stub = true + + // replace all the yield tags with the tag inner html + if (html) templ = replaceYield(templ, html) + + /* istanbul ignore next */ + if (checkIE && tagName && (match = tagName.match(SPECIAL_TAGS_REGEX))) + ie9elem(el, templ, tagName, !!match[1]) + else + el.innerHTML = templ + + return el + } + + // creates tr, th, td, option, optgroup element for IE8-9 + /* istanbul ignore next */ + function ie9elem(el, html, tagName, select) { + + var div = mkEl(GENERIC), + tag = select ? 'select>' : 'table>', + child + + div.innerHTML = '<' + tag + html + '|>\s*<\/yield\s*>)/ig, + function (str, ref) { + var m = html.match(RegExp(reToSrc.replace('@', ref), 'i')) + ++n + return m && m[2] || '' + }) + + // yield without any "from", replace yield in templ with the innerHTML + return n ? templ : templ.replace(/|>\s*<\/yield\s*>)/gi, html || '') + } + + return _mkdom + +})(IE_VERSION) + +/** + * Convert the item looped into an object used to extend the child tag properties + * @param { Object } expr - object containing the keys used to extend the children tags + * @param { * } key - value to assign to the new object returned + * @param { * } val - value containing the position of the item in the array + * @returns { Object } - new object containing the values of the original item + * + * The variables 'key' and 'val' are arbitrary. + * They depend on the collection type looped (Array, Object) + * and on the expression used on the each tag + * + */ +function mkitem(expr, key, val) { + var item = {} + item[expr.key] = key + if (expr.pos) item[expr.pos] = val + return item +} + +/** + * Unmount the redundant tags + * @param { Array } items - array containing the current items to loop + * @param { Array } tags - array containing all the children tags + */ +function unmountRedundant(items, tags) { + + var i = tags.length, + j = items.length, + t + + while (i > j) { + t = tags[--i] + tags.splice(i, 1) + t.unmount() + } +} + +/** + * Move the nested custom tags in non custom loop tags + * @param { Object } child - non custom loop tag + * @param { Number } i - current position of the loop tag + */ +function moveNestedTags(child, i) { + Object.keys(child.tags).forEach(function(tagName) { + var tag = child.tags[tagName] + if (isArray(tag)) + each(tag, function (t) { + moveChildTag(t, tagName, i) + }) + else + moveChildTag(tag, tagName, i) + }) +} + +/** + * Adds the elements for a virtual tag + * @param { Tag } tag - the tag whose root's children will be inserted or appended + * @param { Node } src - the node that will do the inserting or appending + * @param { Tag } target - only if inserting, insert before this tag's first child + */ +function addVirtual(tag, src, target) { + var el = tag._root, sib + tag._virts = [] + while (el) { + sib = el.nextSibling + if (target) + src.insertBefore(el, target._root) + else + src.appendChild(el) + + tag._virts.push(el) // hold for unmounting + el = sib + } +} + +/** + * Move virtual tag and all child nodes + * @param { Tag } tag - first child reference used to start move + * @param { Node } src - the node that will do the inserting + * @param { Tag } target - insert before this tag's first child + * @param { Number } len - how many child nodes to move + */ +function moveVirtual(tag, src, target, len) { + var el = tag._root, sib, i = 0 + for (; i < len; i++) { + sib = el.nextSibling + src.insertBefore(el, target._root) + el = sib + } +} + + +/** + * Manage tags having the 'each' + * @param { Object } dom - DOM node we need to loop + * @param { Tag } parent - parent tag instance where the dom node is contained + * @param { String } expr - string contained in the 'each' attribute + */ +function _each(dom, parent, expr) { + + // remove the each property from the original tag + remAttr(dom, 'each') + + var mustReorder = typeof getAttr(dom, 'no-reorder') !== T_STRING || remAttr(dom, 'no-reorder'), + tagName = getTagName(dom), + impl = __tagImpl[tagName] || { tmpl: dom.outerHTML }, + useRoot = SPECIAL_TAGS_REGEX.test(tagName), + root = dom.parentNode, + ref = document.createTextNode(''), + child = getTag(dom), + isOption = /option/gi.test(tagName), // the option tags must be treated differently + tags = [], + oldItems = [], + hasKeys, + isVirtual = dom.tagName == 'VIRTUAL' + + // parse the each expression + expr = tmpl.loopKeys(expr) + + // insert a marked where the loop tags will be injected + root.insertBefore(ref, dom) + + // clean template code + parent.one('before-mount', function () { + + // remove the original DOM node + dom.parentNode.removeChild(dom) + if (root.stub) root = parent.root + + }).on('update', function () { + // get the new items collection + var items = tmpl(expr.val, parent), + // create a fragment to hold the new DOM nodes to inject in the parent tag + frag = document.createDocumentFragment() + + + + // object loop. any changes cause full redraw + if (!isArray(items)) { + hasKeys = items || false + items = hasKeys ? + Object.keys(items).map(function (key) { + return mkitem(expr, key, items[key]) + }) : [] + } + + // loop all the new items + items.forEach(function(item, i) { + // reorder only if the items are objects + var _mustReorder = mustReorder && item instanceof Object, + oldPos = oldItems.indexOf(item), + pos = ~oldPos && _mustReorder ? oldPos : i, + // does a tag exist in this position? + tag = tags[pos] + + item = !hasKeys && expr.key ? mkitem(expr, item, i) : item + + // new tag + if ( + !_mustReorder && !tag // with no-reorder we just update the old tags + || + _mustReorder && !~oldPos || !tag // by default we always try to reorder the DOM elements + ) { + + tag = new Tag(impl, { + parent: parent, + isLoop: true, + hasImpl: !!__tagImpl[tagName], + root: useRoot ? root : dom.cloneNode(), + item: item + }, dom.innerHTML) + + tag.mount() + if (isVirtual) tag._root = tag.root.firstChild // save reference for further moves or inserts + // this tag must be appended + if (i == tags.length) { + if (isVirtual) + addVirtual(tag, frag) + else frag.appendChild(tag.root) + } + // this tag must be insert + else { + if (isVirtual) + addVirtual(tag, root, tags[i]) + else root.insertBefore(tag.root, tags[i].root) + oldItems.splice(i, 0, item) + } + + tags.splice(i, 0, tag) + pos = i // handled here so no move + } else tag.update(item) + + // reorder the tag if it's not located in its previous position + if (pos !== i && _mustReorder) { + // update the DOM + if (isVirtual) + moveVirtual(tag, root, tags[i], dom.childNodes.length) + else root.insertBefore(tag.root, tags[i].root) + // update the position attribute if it exists + if (expr.pos) + tag[expr.pos] = i + // move the old tag instance + tags.splice(i, 0, tags.splice(pos, 1)[0]) + // move the old item + oldItems.splice(i, 0, oldItems.splice(pos, 1)[0]) + // if the loop tags are not custom + // we need to move all their custom tags into the right position + if (!child) moveNestedTags(tag, i) + } + + // cache the original item to use it in the events bound to this node + // and its children + tag._item = item + // cache the real parent tag internally + defineProperty(tag, '_parent', parent) + + }, true) // allow null values + + // remove the redundant tags + unmountRedundant(items, tags) + + // insert the new nodes + if (isOption) root.appendChild(frag) + else root.insertBefore(frag, ref) + + // set the 'tags' property of the parent tag + // if child is 'undefined' it means that we don't need to set this property + // for example: + // we don't need store the `myTag.tags['div']` property if we are looping a div tag + // but we need to track the `myTag.tags['child']` property looping a custom child node named `child` + if (child) parent.tags[tagName] = tags + + // clone the items array + oldItems = items.slice() + + }) + +} +/** + * Object that will be used to inject and manage the css of every tag instance + */ +var styleManager = (function(_riot) { + + if (!window) return { // skip injection on the server + add: function () {}, + inject: function () {} + } + + var styleNode = (function () { + // create a new style element with the correct type + var newNode = mkEl('style') + setAttr(newNode, 'type', 'text/css') + + // replace any user node or insert the new one into the head + var userNode = $('style[type=riot]') + if (userNode) { + if (userNode.id) newNode.id = userNode.id + userNode.parentNode.replaceChild(newNode, userNode) + } + else document.getElementsByTagName('head')[0].appendChild(newNode) + + return newNode + })() + + // Create cache and shortcut to the correct property + var cssTextProp = styleNode.styleSheet, + stylesToInject = '' + + // Expose the style node in a non-modificable property + Object.defineProperty(_riot, 'styleNode', { + value: styleNode, + writable: true + }) + + /** + * Public api + */ + return { + /** + * Save a tag style to be later injected into DOM + * @param { String } css [description] + */ + add: function(css) { + stylesToInject += css + }, + /** + * Inject all previously saved tag styles into DOM + * innerHTML seems slow: http://jsperf.com/riot-insert-style + */ + inject: function() { + if (stylesToInject) { + if (cssTextProp) cssTextProp.cssText += stylesToInject + else styleNode.innerHTML += stylesToInject + stylesToInject = '' + } + } + } + +})(riot) + + +function parseNamedElements(root, tag, childTags, forceParsingNamed) { + + walk(root, function(dom) { + if (dom.nodeType == 1) { + dom.isLoop = dom.isLoop || + (dom.parentNode && dom.parentNode.isLoop || getAttr(dom, 'each')) + ? 1 : 0 + + // custom child tag + if (childTags) { + var child = getTag(dom) + + if (child && !dom.isLoop) + childTags.push(initChildTag(child, {root: dom, parent: tag}, dom.innerHTML, tag)) + } + + if (!dom.isLoop || forceParsingNamed) + setNamed(dom, tag, []) + } + + }) + +} + +function parseExpressions(root, tag, expressions) { + + function addExpr(dom, val, extra) { + if (tmpl.hasExpr(val)) { + expressions.push(extend({ dom: dom, expr: val }, extra)) + } + } + + walk(root, function(dom) { + var type = dom.nodeType, + attr + + // text node + if (type == 3 && dom.parentNode.tagName != 'STYLE') addExpr(dom, dom.nodeValue) + if (type != 1) return + + /* element */ + + // loop + attr = getAttr(dom, 'each') + + if (attr) { _each(dom, tag, attr); return false } + + // attribute expressions + each(dom.attributes, function(attr) { + var name = attr.name, + bool = name.split('__')[1] + + addExpr(dom, attr.value, { attr: bool || name, bool: bool }) + if (bool) { remAttr(dom, name); return false } + + }) + + // skip custom tags + if (getTag(dom)) return false + + }) + +} +function Tag(impl, conf, innerHTML) { + + var self = riot.observable(this), + opts = inherit(conf.opts) || {}, + parent = conf.parent, + isLoop = conf.isLoop, + hasImpl = conf.hasImpl, + item = cleanUpData(conf.item), + expressions = [], + childTags = [], + root = conf.root, + fn = impl.fn, + tagName = root.tagName.toLowerCase(), + attr = {}, + propsInSyncWithParent = [], + dom + + if (fn && root._tag) root._tag.unmount(true) + + // not yet mounted + this.isMounted = false + root.isLoop = isLoop + + // keep a reference to the tag just created + // so we will be able to mount this tag multiple times + root._tag = this + + // create a unique id to this tag + // it could be handy to use it also to improve the virtual dom rendering speed + defineProperty(this, '_riot_id', ++__uid) // base 1 allows test !t._riot_id + + extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item) + + // grab attributes + each(root.attributes, function(el) { + var val = el.value + // remember attributes with expressions only + if (tmpl.hasExpr(val)) attr[el.name] = val + }) + + dom = mkdom(impl.tmpl, innerHTML) + + // options + function updateOpts() { + var ctx = hasImpl && isLoop ? self : parent || self + + // update opts from current DOM attributes + each(root.attributes, function(el) { + var val = el.value + opts[toCamel(el.name)] = tmpl.hasExpr(val) ? tmpl(val, ctx) : val + }) + // recover those with expressions + each(Object.keys(attr), function(name) { + opts[toCamel(name)] = tmpl(attr[name], ctx) + }) + } + + function normalizeData(data) { + for (var key in item) { + if (typeof self[key] !== T_UNDEF && isWritable(self, key)) + self[key] = data[key] + } + } + + function inheritFromParent () { + if (!self.parent || !isLoop) return + each(Object.keys(self.parent), function(k) { + // some properties must be always in sync with the parent tag + var mustSync = !contains(RESERVED_WORDS_BLACKLIST, k) && contains(propsInSyncWithParent, k) + if (typeof self[k] === T_UNDEF || mustSync) { + // track the property to keep in sync + // so we can keep it updated + if (!mustSync) propsInSyncWithParent.push(k) + self[k] = self.parent[k] + } + }) + } + + defineProperty(this, 'update', function(data) { + + // make sure the data passed will not override + // the component core methods + data = cleanUpData(data) + // inherit properties from the parent + inheritFromParent() + // normalize the tag properties in case an item object was initially passed + if (data && typeof item === T_OBJECT) { + normalizeData(data) + item = data + } + extend(self, data) + updateOpts() + self.trigger('update', data) + update(expressions, self) + // the updated event will be triggered + // once the DOM will be ready and all the reflow are completed + // this is useful if you want to get the "real" root properties + // 4 ex: root.offsetWidth ... + rAF(function() { self.trigger('updated') }) + return this + }) + + defineProperty(this, 'mixin', function() { + each(arguments, function(mix) { + var instance + + mix = typeof mix === T_STRING ? riot.mixin(mix) : mix + + // check if the mixin is a function + if (isFunction(mix)) { + // create the new mixin instance + instance = new mix() + // save the prototype to loop it afterwards + mix = mix.prototype + } else instance = mix + + // loop the keys in the function prototype or the all object keys + each(Object.getOwnPropertyNames(mix), function(key) { + // bind methods to self + if (key != 'init') + self[key] = isFunction(instance[key]) ? + instance[key].bind(self) : + instance[key] + }) + + // init method will be called automatically + if (instance.init) instance.init.bind(self)() + }) + return this + }) + + defineProperty(this, 'mount', function() { + + updateOpts() + + // initialiation + if (fn) fn.call(self, opts) + + // parse layout after init. fn may calculate args for nested custom tags + parseExpressions(dom, self, expressions) + + // mount the child tags + toggle(true) + + // update the root adding custom attributes coming from the compiler + // it fixes also #1087 + if (impl.attrs || hasImpl) { + walkAttributes(impl.attrs, function (k, v) { setAttr(root, k, v) }) + parseExpressions(self.root, self, expressions) + } + + if (!self.parent || isLoop) self.update(item) + + // internal use only, fixes #403 + self.trigger('before-mount') + + if (isLoop && !hasImpl) { + // update the root attribute for the looped elements + self.root = root = dom.firstChild + + } else { + while (dom.firstChild) root.appendChild(dom.firstChild) + if (root.stub) self.root = root = parent.root + } + + // parse the named dom nodes in the looped child + // adding them to the parent as well + if (isLoop) + parseNamedElements(self.root, self.parent, null, true) + + // if it's not a child tag we can trigger its mount event + if (!self.parent || self.parent.isMounted) { + self.isMounted = true + self.trigger('mount') + } + // otherwise we need to wait that the parent event gets triggered + else self.parent.one('mount', function() { + // avoid to trigger the `mount` event for the tags + // not visible included in an if statement + if (!isInStub(self.root)) { + self.parent.isMounted = self.isMounted = true + self.trigger('mount') + } + }) + }) + + + defineProperty(this, 'unmount', function(keepRootTag) { + var el = root, + p = el.parentNode, + ptag + + self.trigger('before-unmount') + + // remove this tag instance from the global virtualDom variable + __virtualDom.splice(__virtualDom.indexOf(self), 1) + + if (this._virts) { + each(this._virts, function(v) { + v.parentNode.removeChild(v) + }) + } + + if (p) { + + if (parent) { + ptag = getImmediateCustomParentTag(parent) + // remove this tag from the parent tags object + // if there are multiple nested tags with same name.. + // remove this element form the array + if (isArray(ptag.tags[tagName])) + each(ptag.tags[tagName], function(tag, i) { + if (tag._riot_id == self._riot_id) + ptag.tags[tagName].splice(i, 1) + }) + else + // otherwise just delete the tag instance + ptag.tags[tagName] = undefined + } + + else + while (el.firstChild) el.removeChild(el.firstChild) + + if (!keepRootTag) + p.removeChild(el) + else + // the riot-tag attribute isn't needed anymore, remove it + remAttr(p, 'riot-tag') + } + + + self.trigger('unmount') + toggle() + self.off('*') + self.isMounted = false + delete root._tag + + }) + + function toggle(isMount) { + + // mount/unmount children + each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() }) + + // listen/unlisten parent (events flow one way from parent to children) + if (!parent) return + var evt = isMount ? 'on' : 'off' + + // the loop tags will be always in sync with the parent automatically + if (isLoop) + parent[evt]('unmount', self.unmount) + else + parent[evt]('update', self.update)[evt]('unmount', self.unmount) + } + + // named elements available for fn + parseNamedElements(dom, this, childTags) + +} +/** + * Attach an event to a DOM node + * @param { String } name - event name + * @param { Function } handler - event callback + * @param { Object } dom - dom node + * @param { Tag } tag - tag instance + */ +function setEventHandler(name, handler, dom, tag) { + + dom[name] = function(e) { + + var ptag = tag._parent, + item = tag._item, + el + + if (!item) + while (ptag && !item) { + item = ptag._item + ptag = ptag._parent + } + + // cross browser event fix + e = e || window.event + + // override the event properties + if (isWritable(e, 'currentTarget')) e.currentTarget = dom + if (isWritable(e, 'target')) e.target = e.srcElement + if (isWritable(e, 'which')) e.which = e.charCode || e.keyCode + + e.item = item + + // prevent default behaviour (by default) + if (handler.call(tag, e) !== true && !/radio|check/.test(dom.type)) { + if (e.preventDefault) e.preventDefault() + e.returnValue = false + } + + if (!e.preventUpdate) { + el = item ? getImmediateCustomParentTag(ptag) : tag + el.update() + } + + } + +} + + +/** + * Insert a DOM node replacing another one (used by if- attribute) + * @param { Object } root - parent node + * @param { Object } node - node replaced + * @param { Object } before - node added + */ +function insertTo(root, node, before) { + if (!root) return + root.insertBefore(before, node) + root.removeChild(node) +} + +/** + * Update the expressions in a Tag instance + * @param { Array } expressions - expression that must be re evaluated + * @param { Tag } tag - tag instance + */ +function update(expressions, tag) { + + each(expressions, function(expr, i) { + + var dom = expr.dom, + attrName = expr.attr, + value = tmpl(expr.expr, tag), + parent = expr.dom.parentNode + + if (expr.bool) + value = value ? attrName : false + else if (value == null) + value = '' + + // leave out riot- prefixes from strings inside textarea + // fix #815: any value -> string + if (parent && parent.tagName == 'TEXTAREA') { + value = ('' + value).replace(/riot-/g, '') + // change textarea's value + parent.value = value + } + + // no change + if (expr.value === value) return + expr.value = value + + // text node + if (!attrName) { + dom.nodeValue = '' + value // #815 related + return + } + + // remove original attribute + remAttr(dom, attrName) + // event handler + if (isFunction(value)) { + setEventHandler(attrName, value, dom, tag) + + // if- conditional + } else if (attrName == 'if') { + var stub = expr.stub, + add = function() { insertTo(stub.parentNode, stub, dom) }, + remove = function() { insertTo(dom.parentNode, dom, stub) } + + // add to DOM + if (value) { + if (stub) { + add() + dom.inStub = false + // avoid to trigger the mount event if the tags is not visible yet + // maybe we can optimize this avoiding to mount the tag at all + if (!isInStub(dom)) { + walk(dom, function(el) { + if (el._tag && !el._tag.isMounted) + el._tag.isMounted = !!el._tag.trigger('mount') + }) + } + } + // remove from DOM + } else { + stub = expr.stub = stub || document.createTextNode('') + // if the parentNode is defined we can easily replace the tag + if (dom.parentNode) + remove() + // otherwise we need to wait the updated event + else (tag.parent || tag).one('updated', remove) + + dom.inStub = true + } + // show / hide + } else if (/^(show|hide)$/.test(attrName)) { + if (attrName == 'hide') value = !value + dom.style.display = value ? '' : 'none' + + // field value + } else if (attrName == 'value') { + dom.value = value + + // + } else if (startsWith(attrName, RIOT_PREFIX) && attrName != RIOT_TAG) { + if (value) + setAttr(dom, attrName.slice(RIOT_PREFIX.length), value) + + } else { + if (expr.bool) { + dom[attrName] = value + if (!value) return + } + + if (value === 0 || value && typeof value !== T_OBJECT) + setAttr(dom, attrName, value) + + } + + }) + +} +/** + * Loops an array + * @param { Array } els - collection of items + * @param {Function} fn - callback function + * @returns { Array } the array looped + */ +function each(els, fn) { + for (var i = 0, len = (els || []).length, el; i < len; i++) { + el = els[i] + // return false -> remove current item during loop + if (el != null && fn(el, i) === false) i-- + } + return els +} + +/** + * Detect if the argument passed is a function + * @param { * } v - whatever you want to pass to this function + * @returns { Boolean } - + */ +function isFunction(v) { + return typeof v === T_FUNCTION || false // avoid IE problems +} + +/** + * Remove any DOM attribute from a node + * @param { Object } dom - DOM node we want to update + * @param { String } name - name of the property we want to remove + */ +function remAttr(dom, name) { + dom.removeAttribute(name) +} + +/** + * Convert a string containing dashes to camel case + * @param { String } string - input string + * @returns { String } my-string -> myString + */ +function toCamel(string) { + return string.replace(/-(\w)/g, function(_, c) { + return c.toUpperCase() + }) +} + +/** + * Get the value of any DOM attribute on a node + * @param { Object } dom - DOM node we want to parse + * @param { String } name - name of the attribute we want to get + * @returns { String | undefined } name of the node attribute whether it exists + */ +function getAttr(dom, name) { + return dom.getAttribute(name) +} + +/** + * Set any DOM attribute + * @param { Object } dom - DOM node we want to update + * @param { String } name - name of the property we want to set + * @param { String } val - value of the property we want to set + */ +function setAttr(dom, name, val) { + dom.setAttribute(name, val) +} + +/** + * Detect the tag implementation by a DOM node + * @param { Object } dom - DOM node we need to parse to get its tag implementation + * @returns { Object } it returns an object containing the implementation of a custom tag (template and boot function) + */ +function getTag(dom) { + return dom.tagName && __tagImpl[getAttr(dom, RIOT_TAG) || dom.tagName.toLowerCase()] +} +/** + * Add a child tag to its parent into the `tags` object + * @param { Object } tag - child tag instance + * @param { String } tagName - key where the new tag will be stored + * @param { Object } parent - tag instance where the new child tag will be included + */ +function addChildTag(tag, tagName, parent) { + var cachedTag = parent.tags[tagName] + + // if there are multiple children tags having the same name + if (cachedTag) { + // if the parent tags property is not yet an array + // create it adding the first cached tag + if (!isArray(cachedTag)) + // don't add the same tag twice + if (cachedTag !== tag) + parent.tags[tagName] = [cachedTag] + // add the new nested tag to the array + if (!contains(parent.tags[tagName], tag)) + parent.tags[tagName].push(tag) + } else { + parent.tags[tagName] = tag + } +} + +/** + * Move the position of a custom tag in its parent tag + * @param { Object } tag - child tag instance + * @param { String } tagName - key where the tag was stored + * @param { Number } newPos - index where the new tag will be stored + */ +function moveChildTag(tag, tagName, newPos) { + var parent = tag.parent, + tags + // no parent no move + if (!parent) return + + tags = parent.tags[tagName] + + if (isArray(tags)) + tags.splice(newPos, 0, tags.splice(tags.indexOf(tag), 1)[0]) + else addChildTag(tag, tagName, parent) +} + +/** + * Create a new child tag including it correctly into its parent + * @param { Object } child - child tag implementation + * @param { Object } opts - tag options containing the DOM node where the tag will be mounted + * @param { String } innerHTML - inner html of the child node + * @param { Object } parent - instance of the parent tag including the child custom tag + * @returns { Object } instance of the new child tag just created + */ +function initChildTag(child, opts, innerHTML, parent) { + var tag = new Tag(child, opts, innerHTML), + tagName = getTagName(opts.root), + ptag = getImmediateCustomParentTag(parent) + // fix for the parent attribute in the looped elements + tag.parent = ptag + // store the real parent tag + // in some cases this could be different from the custom parent tag + // for example in nested loops + tag._parent = parent + + // add this tag to the custom parent tag + addChildTag(tag, tagName, ptag) + // and also to the real parent tag + if (ptag !== parent) + addChildTag(tag, tagName, parent) + // empty the child node once we got its template + // to avoid that its children get compiled multiple times + opts.root.innerHTML = '' + + return tag +} + +/** + * Loop backward all the parents tree to detect the first custom parent tag + * @param { Object } tag - a Tag instance + * @returns { Object } the instance of the first custom parent tag found + */ +function getImmediateCustomParentTag(tag) { + var ptag = tag + while (!getTag(ptag.root)) { + if (!ptag.parent) break + ptag = ptag.parent + } + return ptag +} + +/** + * Helper function to set an immutable property + * @param { Object } el - object where the new property will be set + * @param { String } key - object key where the new property will be stored + * @param { * } value - value of the new property +* @param { Object } options - set the propery overriding the default options + * @returns { Object } - the initial object + */ +function defineProperty(el, key, value, options) { + Object.defineProperty(el, key, extend({ + value: value, + enumerable: false, + writable: false, + configurable: false + }, options)) + return el +} + +/** + * Get the tag name of any DOM node + * @param { Object } dom - DOM node we want to parse + * @returns { String } name to identify this dom node in riot + */ +function getTagName(dom) { + var child = getTag(dom), + namedTag = getAttr(dom, 'name'), + tagName = namedTag && !tmpl.hasExpr(namedTag) ? + namedTag : + child ? child.name : dom.tagName.toLowerCase() + + return tagName +} + +/** + * Extend any object with other properties + * @param { Object } src - source object + * @returns { Object } the resulting extended object + * + * var obj = { foo: 'baz' } + * extend(obj, {bar: 'bar', foo: 'bar'}) + * console.log(obj) => {bar: 'bar', foo: 'bar'} + * + */ +function extend(src) { + var obj, args = arguments + for (var i = 1; i < args.length; ++i) { + if (obj = args[i]) { + for (var key in obj) { + // check if this property of the source object could be overridden + if (isWritable(src, key)) + src[key] = obj[key] + } + } + } + return src +} + +/** + * Check whether an array contains an item + * @param { Array } arr - target array + * @param { * } item - item to test + * @returns { Boolean } Does 'arr' contain 'item'? + */ +function contains(arr, item) { + return ~arr.indexOf(item) +} + +/** + * Check whether an object is a kind of array + * @param { * } a - anything + * @returns {Boolean} is 'a' an array? + */ +function isArray(a) { return Array.isArray(a) || a instanceof Array } + +/** + * Detect whether a property of an object could be overridden + * @param { Object } obj - source object + * @param { String } key - object property + * @returns { Boolean } is this property writable? + */ +function isWritable(obj, key) { + var props = Object.getOwnPropertyDescriptor(obj, key) + return typeof obj[key] === T_UNDEF || props && props.writable +} + + +/** + * With this function we avoid that the internal Tag methods get overridden + * @param { Object } data - options we want to use to extend the tag instance + * @returns { Object } clean object without containing the riot internal reserved words + */ +function cleanUpData(data) { + if (!(data instanceof Tag) && !(data && typeof data.trigger == T_FUNCTION)) + return data + + var o = {} + for (var key in data) { + if (!contains(RESERVED_WORDS_BLACKLIST, key)) + o[key] = data[key] + } + return o +} + +/** + * Walk down recursively all the children tags starting dom node + * @param { Object } dom - starting node where we will start the recursion + * @param { Function } fn - callback to transform the child node just found + */ +function walk(dom, fn) { + if (dom) { + // stop the recursion + if (fn(dom) === false) return + else { + dom = dom.firstChild + + while (dom) { + walk(dom, fn) + dom = dom.nextSibling + } + } + } +} + +/** + * Minimize risk: only zero or one _space_ between attr & value + * @param { String } html - html string we want to parse + * @param { Function } fn - callback function to apply on any attribute found + */ +function walkAttributes(html, fn) { + var m, + re = /([-\w]+) ?= ?(?:"([^"]*)|'([^']*)|({[^}]*}))/g + + while (m = re.exec(html)) { + fn(m[1].toLowerCase(), m[2] || m[3] || m[4]) + } +} + +/** + * Check whether a DOM node is in stub mode, useful for the riot 'if' directive + * @param { Object } dom - DOM node we want to parse + * @returns { Boolean } - + */ +function isInStub(dom) { + while (dom) { + if (dom.inStub) return true + dom = dom.parentNode + } + return false +} + +/** + * Create a generic DOM node + * @param { String } name - name of the DOM node we want to create + * @returns { Object } DOM node just created + */ +function mkEl(name) { + return document.createElement(name) +} + +/** + * Shorter and fast way to select multiple nodes in the DOM + * @param { String } selector - DOM selector + * @param { Object } ctx - DOM node where the targets of our search will is located + * @returns { Object } dom nodes found + */ +function $$(selector, ctx) { + return (ctx || document).querySelectorAll(selector) +} + +/** + * Shorter and fast way to select a single node in the DOM + * @param { String } selector - unique dom selector + * @param { Object } ctx - DOM node where the target of our search will is located + * @returns { Object } dom node found + */ +function $(selector, ctx) { + return (ctx || document).querySelector(selector) +} + +/** + * Simple object prototypal inheritance + * @param { Object } parent - parent object + * @returns { Object } child instance + */ +function inherit(parent) { + function Child() {} + Child.prototype = parent + return new Child() +} + +/** + * Get the name property needed to identify a DOM node in riot + * @param { Object } dom - DOM node we need to parse + * @returns { String | undefined } give us back a string to identify this dom node + */ +function getNamedKey(dom) { + return getAttr(dom, 'id') || getAttr(dom, 'name') +} + +/** + * Set the named properties of a tag element + * @param { Object } dom - DOM node we need to parse + * @param { Object } parent - tag instance where the named dom element will be eventually added + * @param { Array } keys - list of all the tag instance properties + */ +function setNamed(dom, parent, keys) { + // get the key value we want to add to the tag instance + var key = getNamedKey(dom), + isArr, + // add the node detected to a tag instance using the named property + add = function(value) { + // avoid to override the tag properties already set + if (contains(keys, key)) return + // check whether this value is an array + isArr = isArray(value) + // if the key was never set + if (!value) + // set it once on the tag instance + parent[key] = dom + // if it was an array and not yet set + else if (!isArr || isArr && !contains(value, dom)) { + // add the dom node into the array + if (isArr) + value.push(dom) + else + parent[key] = [value, dom] + } + } + + // skip the elements with no named properties + if (!key) return + + // check whether this key has been already evaluated + if (tmpl.hasExpr(key)) + // wait the first updated event only once + parent.one('mount', function() { + key = getNamedKey(dom) + add(parent[key]) + }) + else + add(parent[key]) + +} + +/** + * Faster String startsWith alternative + * @param { String } src - source string + * @param { String } str - test string + * @returns { Boolean } - + */ +function startsWith(src, str) { + return src.slice(0, str.length) === str +} + +/** + * requestAnimationFrame function + * Adapted from https://gist.github.com/paulirish/1579671, license MIT + */ +var rAF = (function (w) { + var raf = w.requestAnimationFrame || + w.mozRequestAnimationFrame || w.webkitRequestAnimationFrame + + if (!raf || /iP(ad|hone|od).*OS 6/.test(w.navigator.userAgent)) { // buggy iOS6 + var lastTime = 0 + + raf = function (cb) { + var nowtime = Date.now(), timeout = Math.max(16 - (nowtime - lastTime), 0) + setTimeout(function () { cb(lastTime = nowtime + timeout) }, timeout) + } + } + return raf + +})(window || {}) + +/** + * Mount a tag creating new Tag instance + * @param { Object } root - dom node where the tag will be mounted + * @param { String } tagName - name of the riot tag we want to mount + * @param { Object } opts - options to pass to the Tag instance + * @returns { Tag } a new Tag instance + */ +function mountTo(root, tagName, opts) { + var tag = __tagImpl[tagName], + // cache the inner HTML to fix #855 + innerHTML = root._innerHTML = root._innerHTML || root.innerHTML + + // clear the inner html + root.innerHTML = '' + + if (tag && root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML) + + if (tag && tag.mount) { + tag.mount() + // add this tag to the virtualDom variable + if (!contains(__virtualDom, tag)) __virtualDom.push(tag) + } + + return tag +} +/** + * Riot public api + */ + +// share methods for other riot parts, e.g. compiler +riot.util = { brackets: brackets, tmpl: tmpl } + +/** + * Create a mixin that could be globally shared across all the tags + */ +riot.mixin = (function() { + var mixins = {} + + /** + * Create/Return a mixin by its name + * @param { String } name - mixin name + * @param { Object } mixin - mixin logic + * @returns { Object } the mixin logic + */ + return function(name, mixin) { + if (!mixin) return mixins[name] + mixins[name] = mixin + } + +})() + +/** + * Create a new riot tag implementation + * @param { String } name - name/id of the new riot tag + * @param { String } html - tag template + * @param { String } css - custom tag css + * @param { String } attrs - root tag attributes + * @param { Function } fn - user function + * @returns { String } name/id of the tag just created + */ +riot.tag = function(name, html, css, attrs, fn) { + if (isFunction(attrs)) { + fn = attrs + if (/^[\w\-]+\s?=/.test(css)) { + attrs = css + css = '' + } else attrs = '' + } + if (css) { + if (isFunction(css)) fn = css + else styleManager.add(css) + } + __tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } + return name +} + +/** + * Create a new riot tag implementation (for use by the compiler) + * @param { String } name - name/id of the new riot tag + * @param { String } html - tag template + * @param { String } css - custom tag css + * @param { String } attrs - root tag attributes + * @param { Function } fn - user function + * @param { string } [bpair] - brackets used in the compilation + * @returns { String } name/id of the tag just created + */ +riot.tag2 = function(name, html, css, attrs, fn, bpair) { + if (css) styleManager.add(css) + //if (bpair) riot.settings.brackets = bpair + __tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn } + return name +} + +/** + * Mount a tag using a specific tag implementation + * @param { String } selector - tag DOM selector + * @param { String } tagName - tag implementation name + * @param { Object } opts - tag logic + * @returns { Array } new tags instances + */ +riot.mount = function(selector, tagName, opts) { + + var els, + allTags, + tags = [] + + // helper functions + + function addRiotTags(arr) { + var list = '' + each(arr, function (e) { + if (!/[^-\w]/.test(e)) + list += ',*[' + RIOT_TAG + '=' + e.trim() + ']' + }) + return list + } + + function selectAllTags() { + var keys = Object.keys(__tagImpl) + return keys + addRiotTags(keys) + } + + function pushTags(root) { + var last + + if (root.tagName) { + if (tagName && (!(last = getAttr(root, RIOT_TAG)) || last != tagName)) + setAttr(root, RIOT_TAG, tagName) + + var tag = mountTo(root, tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts) + + if (tag) tags.push(tag) + } else if (root.length) + each(root, pushTags) // assume nodeList + + } + + // ----- mount code ----- + + // inject styles into DOM + styleManager.inject() + + if (typeof tagName === T_OBJECT) { + opts = tagName + tagName = 0 + } + + // crawl the DOM to find the tag + if (typeof selector === T_STRING) { + if (selector === '*') + // select all the tags registered + // and also the tags found with the riot-tag attribute set + selector = allTags = selectAllTags() + else + // or just the ones named like the selector + selector += addRiotTags(selector.split(',')) + + // make sure to pass always a selector + // to the querySelectorAll function + els = selector ? $$(selector) : [] + } + else + // probably you have passed already a tag or a NodeList + els = selector + + // select all the registered and mount them inside their root elements + if (tagName === '*') { + // get all custom tags + tagName = allTags || selectAllTags() + // if the root els it's just a single tag + if (els.tagName) + els = $$(tagName, els) + else { + // select all the children for all the different root elements + var nodeList = [] + each(els, function (_el) { + nodeList.push($$(tagName, _el)) + }) + els = nodeList + } + // get rid of the tagName + tagName = 0 + } + + if (els.tagName) + pushTags(els) + else + each(els, pushTags) + + return tags +} + +/** + * Update all the tags instances created + * @returns { Array } all the tags instances + */ +riot.update = function() { + return each(__virtualDom, function(tag) { + tag.update() + }) +} + +/** + * Export the Tag constructor + */ +riot.Tag = Tag +/* istanbul ignore next */ + +/** + * @module parsers + */ +var parsers = (function () { + var _mods = { + none: function (js) { + return js + } + } + _mods.javascript = _mods.none + + function _try(name, req) { //eslint-disable-line complexity + var parser + + switch (name) { + case 'coffee': + req = 'CoffeeScript' + break + case 'es6': + case 'babel': + req = 'babel' + break + case 'none': + case 'javascript': + return _mods.none + default: + if (!req) req = name + break + } + parser = window[req] + + if (!parser) + throw new Error(req + ' parser not found.') + _mods[name] = parser + + return parser + } + + function _req(name, req) { + return name in _mods ? _mods[name] : _try(name, req) + } + + var _html = { + jade: function (html, opts, url) { + return _req('jade').render(html, extend({ + pretty: true, + filename: url, + doctype: 'html' + }, opts)) + } + } + + var _css = { + less: function(tag, css, opts, url) { + var less = _req('less'), + ret + + less.render(css, extend({ + sync: true, + syncImport: true, + filename: url, + compress: true + }, opts), function (err, result) { + // istanbul ignore next + if (err) throw err + ret = result.css + }) + return ret + }, + stylus: function (tag, css, opts, url) { + var + stylus = _req('stylus'), nib = _req('nib') + /* istanbul ignore next: can't run both */ + return nib ? + stylus(css).use(nib()).import('nib').render() : stylus.render(css) + } + } + + var _js = { + livescript: function (js, opts, url) { + return _req('livescript').compile(js, extend({bare: true, header: false}, opts)) + }, + typescript: function (js, opts, url) { + return _req('typescript')(js, opts).replace(/\r\n?/g, '\n') + }, + es6: function (js, opts, url) { + return _req('es6').transform(js, extend({ + blacklist: ['useStrict', 'strict', 'react'], sourceMaps: false, comments: false + }, opts)).code + }, + babel: function (js, opts, url) { + return _req('babel').transform(js, + extend({ + filename: url + }, opts) + ).code + }, + coffee: function (js, opts, url) { + return _req('coffee').compile(js, extend({bare: true}, opts)) + }, + none: _mods.none + } + + _js.javascript = _js.none + _js.coffeescript = _js.coffee + + return { + html: _html, + css: _css, + js: _js, + _req: _req} + +})() + +riot.parsers = parsers + +/** + * Compiler for riot custom tags + * @version WIP + */ +var compile = (function () { + + function _regEx(str, opt) { return new RegExp(str, opt) } + + var + + BOOL_ATTRS = _regEx( + '^(?:disabled|checked|readonly|required|allowfullscreen|auto(?:focus|play)|' + + 'compact|controls|default|formnovalidate|hidden|ismap|itemscope|loop|' + + 'multiple|muted|no(?:resize|shade|validate|wrap)?|open|reversed|seamless|' + + 'selected|sortable|truespeed|typemustmatch)$'), + + RIOT_ATTRS = ['style', 'src', 'd'], + + VOID_TAGS = /^(?:input|img|br|wbr|hr|area|base|col|embed|keygen|link|meta|param|source|track)$/, + + HTML_ATTR = /\s*([-\w:\xA0-\xFF]+)\s*(?:=\s*('[^']+'|"[^"]+"|\S+))?/g, + SPEC_TYPES = /^"(?:number|date(?:time)?|time|month|email|color)\b/i, + TRIM_TRAIL = /[ \t]+$/gm, + S_STRINGS = brackets.R_STRINGS.source + + function q(s) { + return "'" + (s ? s + .replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r') : + '') + "'" + } + + function mktag(name, html, css, attrs, js, pcex) { + var + c = ', ', + s = '}' + (pcex.length ? ', ' + q(pcex._bp[8]) : '') + ');' + + if (js && js.slice(-1) !== '\n') s = '\n' + s + + return 'riot.tag2(\'' + name + "'" + c + q(html) + c + q(css) + c + q(attrs) + + ', function(opts) {\n' + js + s + } + + function extend(obj, props) { + if (props) { + for (var prop in props) { + /* istanbul ignore next */ + if (props.hasOwnProperty(prop)) { + obj[prop] = props[prop] + } + } + } + return obj + } + + function parseAttrs(str, pcex) { + var + list = [], + match, + k, v, t, e, + DQ = '"' + + HTML_ATTR.lastIndex = 0 + + str = str.replace(/\s+/g, ' ') + + while (match = HTML_ATTR.exec(str)) { + + k = match[1].toLowerCase() + v = match[2] + + if (!v) { + list.push(k) + } + else { + + if (v[0] !== DQ) + v = DQ + (v[0] === "'" ? v.slice(1, -1) : v) + DQ + + if (k === 'type' && SPEC_TYPES.test(v)) { + t = v + continue + } + else if (/\u0001\d/.test(v)) { + + if (BOOL_ATTRS.test(k)) { + k = '__' + k + } + else if (~RIOT_ATTRS.indexOf(k)) { + k = 'riot-' + k + } + else if (k.toLowerCase() === 'value') { + e = 1 + } + } + + list.push(k + '=' + v) + } + } + + if (t) { + if (e) t = DQ + pcex._bp[0] + "'" + v.slice(1, -1) + "'" + pcex._bp[1] + DQ + list.push('type=' + t) + } + return list.join(' ') + } + + function splitHtml(html, opts, pcex) { + var _bp = pcex._bp + + if (html && _bp[4].test(html)) { + var + jsfn = opts.expr && (opts.parser || opts.type) ? _compileJS : 0, + list = brackets.split(html, 0, _bp), + expr + + for (var i = 1; i < list.length; i += 2) { + expr = list[i] + if (expr[0] === '^') + expr = expr.slice(1) + else if (jsfn) { + var israw = expr[0] === '=' + expr = jsfn(israw ? expr.slice(1) : expr, opts).trim() + if (expr.slice(-1) === ';') expr = expr.slice(0, -1) + if (israw) expr = '=' + expr + } + list[i] = '\u0001' + (pcex.push(expr.replace(/[\r\n]+/g, ' ').trim()) - 1) + _bp[1] + } + html = list.join('') + } + return html + } + + function restoreExpr(html, pcex) { + if (pcex.length) { + html = html + .replace(/\u0001(\d+)/g, function (_, d) { + var expr = pcex[d] + if (expr[0] === '=') { + expr = expr.replace(brackets.R_STRINGS, function (qs) { + return qs + .replace(//g, '>') + }) + } + return pcex._bp[0] + expr.replace(/"/g, '\u2057') + }) + } + return html + } + + var + HTML_COMMENT = _regEx(//.source + '|' + S_STRINGS, 'g'), + HTML_TAGS = /<([-\w]+)\s*([^"'\/>]*(?:(?:"[^"]*"|'[^']*'|\/[^>])[^'"\/>]*)*)(\/?)>/g, + PRE_TAG = _regEx( + /]+(?:(?:@Q)|[^>]*)*|\s*)?>([\S\s]*?)<\/pre\s*>/.source.replace('@Q', S_STRINGS), 'gi') + + function _compileHTML(html, opts, pcex) { + + html = splitHtml(html, opts, pcex) + .replace(HTML_TAGS, function (_, name, attr, ends) { + + name = name.toLowerCase() + + ends = ends && !VOID_TAGS.test(name) ? '>' + }) + + if (!opts.whitespace) { + if (/]/.test(html)) { + var p = [] + html = html.replace(PRE_TAG, function (q) + { return p.push(q) && '\u0002' }).trim().replace(/\s+/g, ' ') + // istanbul ignore else + if (p.length) + html = html.replace(/\u0002/g, function (_) { return p.shift() }) + } + else + html = html.trim().replace(/\s+/g, ' ') + } + + if (opts.compact) html = html.replace(/> <([-\w\/])/g, '><$1') + + return restoreExpr(html, pcex) + } + + // istanbul ignore next + function compileHTML(html, opts, pcex) { + if (Array.isArray(opts)) { + pcex = opts + opts = {} + } + else { + if (!pcex) pcex = [] + if (!opts) opts = {} + } + + html = html.replace(/\r\n?/g, '\n').replace(HTML_COMMENT, + function (s) { return s[0] === '<' ? '' : s }).replace(TRIM_TRAIL, '') + + if (!pcex._bp) pcex._bp = brackets.array(opts.brackets) + + return _compileHTML(html, opts, pcex) + } + + var + JS_RMCOMMS = _regEx('(' + brackets.S_QBLOCKS + ')|' + brackets.R_MLCOMMS.source + '|//[^\r\n]*', 'g'), + JS_ES6SIGN = /^([ \t]*)([$_A-Za-z][$\w]*)\s*(\([^()]*\)\s*{)/m + + function riotjs(js) { + var + match, + toes5, + parts = [], + pos + + js = js.replace(JS_RMCOMMS, function (m, q) { return q ? m : ' ' }) + + while (match = js.match(JS_ES6SIGN)) { + + parts.push(RegExp.leftContext) + js = RegExp.rightContext + pos = skipBlock(js) + + toes5 = !/^(?:if|while|for|switch|catch|function)$/.test(match[2]) + if (toes5) + match[0] = match[1] + 'this.' + match[2] + ' = function' + match[3] + + parts.push(match[0], js.slice(0, pos)) + js = js.slice(pos) + if (toes5 && !/^\s*.\s*bind\b/.test(js)) parts.push('.bind(this)') + } + + return parts.length ? parts.join('') + js : js + + function skipBlock(str) { + var + re = _regEx('([{}])|' + brackets.S_QBLOCKS, 'g'), + level = 1, + match + + while (level && (match = re.exec(str))) { + if (match[1]) + match[1] === '{' ? ++level : --level + } + return level ? str.length : re.lastIndex + } + } + + function _compileJS(js, opts, type, parserOpts, url) { + if (!js) return '' + if (!type) type = opts.type + + var parser = opts.parser || (type ? parsers.js[type] : riotjs) + if (!parser) + throw new Error('JS parser not found: "' + type + '"') + + return parser(js, parserOpts, url).replace(TRIM_TRAIL, '') + } + + // istanbul ignore next + function compileJS(js, opts, type, extra) { + if (typeof opts === 'string') { + extra = type + type = opts + opts = {} + } + if (typeof type === 'object') { + extra = type + type = '' + } + else if (!extra) extra = {} + + return _compileJS(js, opts, type, extra.parserOptions, extra.url) + } + + var CSS_SELECTOR = _regEx('(}|{|^)[ ;]*([^@ ;{}][^{}]*)(?={)|' + S_STRINGS, 'g') + + function scopedCSS(tag, style) { + var scope = ':scope' + + return style.replace(CSS_SELECTOR, function (m, p1, p2) { + + if (!p2) return m + + p2 = p2.replace(/[^,]+/g, function (sel) { + var s = sel.trim() + + if (s && s !== 'from' && s !== 'to' && s.slice(-1) !== '%') { + + if (s.indexOf(scope) < 0) s = scope + ' ' + s + s = s.replace(scope, tag) + ',' + + s.replace(scope, '[riot-tag="' + tag + '"]') + } + return sel.slice(-1) === ' ' ? s + ' ' : s + }) + + return p1 ? p1 + ' ' + p2 : p2 + }) + } + + function _compileCSS(style, tag, type, opts) { + var scoped = (opts || (opts = {})).scoped + + if (type) { + if (type === 'scoped-css') { + scoped = true + } + else if (parsers.css[type]) { + style = parsers.css[type](tag, style, opts.parserOpts || {}, opts.url) + } + else if (type !== 'css') { + throw new Error('CSS parser not found: "' + type + '"') + } + } + + style = style.replace(brackets.R_MLCOMMS, '').replace(/\s+/g, ' ').trim() + + if (scoped) { + // istanbul ignore next + if (!tag) + throw new Error('Can not parse scoped CSS without a tagName') + style = scopedCSS(tag, style) + } + return style + } + + // istanbul ignore next + function compileCSS(style, parser, opts) { + if (typeof parser === 'object') { + opts = parser + parser = '' + } + return _compileCSS(style, opts.tagName, parser, opts) + } + + var + TYPE_ATTR = /\stype\s*=\s*(?:(['"])(.+?)\1|(\S+))/i, + MISC_ATTR = /\s*=\s*("(?:\\[\S\s]|[^"\\]*)*"|'(?:\\[\S\s]|[^'\\]*)*'|\{[^}]+}|\S+)/.source + + function getType(str) { + + if (str) { + var match = str.match(TYPE_ATTR) + str = match && (match[2] || match[3]) + } + return str ? str.replace('text/', '') : '' + } + + function getAttr(str, name) { + + if (str) { + var + re = _regEx('\\s' + name + MISC_ATTR, 'i'), + match = str.match(re) + str = match && match[1] + if (str) + return (/^['"]/).test(str) ? str.slice(1, -1) : str + } + return '' + } + + function getParserOptions(attrs) { + var opts = getAttr(attrs, 'options') + + if (opts) opts = JSON.parse(opts) + return opts + } + + function getCode(code, opts, attrs, url) { + var type = getType(attrs), + parserOpts = getParserOptions(attrs) + + return _compileJS(code, opts, type, parserOpts, url) + } + + function cssCode(code, opts, attrs, url, tag) { + var extraOpts = { + parserOpts: getParserOptions(attrs), + scoped: attrs && /\sscoped(\s|=|$)/i.test(attrs), + url: url + } + return _compileCSS(code, tag, getType(attrs) || opts.style, extraOpts) + } + + var END_TAGS = /\/>\n|^<(?:\/[\w\-]+\s*|[\w\-]+(?:\s+(?:[-\w:\xA0-\xFF][\S\s]*?)?)?)>\n/ + + function splitBlocks(str) { + var k, m + + /* istanbul ignore next: this if() can't be true, but just in case... */ + if (str[str.length - 1] === '>') return [str, ''] + + k = str.lastIndexOf('<') + while (~k) { + if (m = str.slice(k).match(END_TAGS)) { + k += m.index + m[0].length + return [str.slice(0, k), str.slice(k)] + } + k = str.lastIndexOf('<', k -1) + } + return ['', str] + } + + function compileTemplate(html, url, lang, opts) { + var parser = parsers.html[lang] + + if (!parser) + throw new Error('Template parser not found: "' + lang + '"') + + return parser(html, opts, url) + } + + var + CUST_TAG = _regEx( + /^([ \t]*)<([-\w]+)(?:\s+([^'"\/>]+(?:(?:@Q|\/[^>])[^'"\/>]*)*)|\s*)?(?:\/>|>[ \t]*\n?([\S\s]*)^\1<\/\2\s*>|>(.*)<\/\2\s*>)/ + .source.replace('@Q', S_STRINGS), 'gim'), + SRC_TAGS = /]*)?>\n?([^<]*(?:<(?!\/style\s*>)[^<]*)*)<\/style\s*>/.source + '|' + S_STRINGS, + STYLES = _regEx(SRC_TAGS, 'gi'), + SCRIPT = _regEx(SRC_TAGS.replace(/style/g, 'script'), 'gi') + + function compile(src, opts, url) { + var + parts = [], + exclude + + if (!opts) opts = {} + + if (!url) url = '' + + exclude = opts.exclude || false + function included(s) { return !(exclude && ~exclude.indexOf(s)) } + + var _bp = brackets.array(opts.brackets) + + if (opts.template) + src = compileTemplate(src, url, opts.template, opts.templateOptions) + + src = src + .replace(/\r\n?/g, '\n') + .replace(CUST_TAG, function (_, indent, tagName, attribs, body, body2) { + + var + jscode = '', + styles = '', + html = '', + pcex = [] + + pcex._bp = _bp + + tagName = tagName.toLowerCase() + + attribs = attribs && included('attribs') ? + restoreExpr(parseAttrs(splitHtml(attribs, opts, pcex), pcex), pcex) : '' + + if (body2) body = body2 + + if (body && (body = body.replace(HTML_COMMENT, + function (s) { return s[0] === '<' ? '' : s })) && /\S/.test(body)) { + + if (body2) { + /* istanbul ignore next */ + html = included('html') ? _compileHTML(body2, opts, pcex) : '' + } + else { + body = body.replace(_regEx('^' + indent, 'gm'), '') + + body = body.replace(STYLES, function (_m, _attrs, _style) { + if (_m[0] !== '<') return _m + if (included('css')) + styles += (styles ? ' ' : '') + cssCode(_style, opts, _attrs, url, tagName) + return '' + }) + + body = body.replace(SCRIPT, function (_m, _attrs, _script) { + if (_m[0] !== '<') return _m + if (included('js')) + jscode += (jscode ? '\n' : '') + getCode(_script, opts, _attrs, url) + return '' + }) + + var blocks = splitBlocks(body.replace(TRIM_TRAIL, '')) + + if (included('html')) { + body = blocks[0] + if (body) + html = _compileHTML(body, opts, pcex) + } + + if (included('js')) { + body = blocks[1] + if (/\S/.test(body)) + jscode += (jscode ? '\n' : '') + _compileJS(body, opts, null, null, url) + } + } + } + + jscode = /\S/.test(jscode) ? jscode.replace(/\n{3,}/g, '\n\n') : '' + + if (opts.entities) { + parts.push({ + tagName: tagName, + html: html, + css: styles, + attribs: attribs, + js: jscode + }) + return '' + } + + return mktag(tagName, html, styles, attribs, jscode, pcex) + }) + + if (opts.entities) return parts + + return src + } + + riot.util.compiler = { + compile: compile, + html: compileHTML, + css: compileCSS, + js: compileJS, + version: 'WIP' + } + return compile + +})() + +/* + Compilation for the browser +*/ +riot.compile = (function () { + + var + doc = window.document, + promise, + ready + + function GET(url, fn, opts) { + var req = new XMLHttpRequest() + + req.onreadystatechange = function() { + if (req.readyState === 4 && + (req.status === 200 || !req.status && req.responseText.length)) + fn(req.responseText, opts, url) + } + req.open('GET', url, true) + req.send('') + } + + function globalEval(js) { + if (typeof js === T_STRING) { + var + node = doc.createElement('script'), + root = doc.documentElement + + node.text = js + root.appendChild(node) + root.removeChild(node) + } + } + + function compileScripts(fn, exopt) { + var + scripts = $$('script[type="riot/tag"]'), + scriptsAmount = scripts.length + + function done() { + promise.trigger('ready') + ready = true + if (fn) fn() + } + + function compileTag(src, opts, url) { + var code = compile(src, opts, url) + + if (url) code += '\n//# sourceURL=' + url + '.js' + globalEval(code) + if (!--scriptsAmount) done() + } + + if (!scriptsAmount) done() + else { + for (var i = 0; i < scripts.length; ++i) { + var + script = scripts[i], + opts = extend({template: getAttr(script, 'template')}, exopt), + url = getAttr(script, 'src') + + url ? GET(url, compileTag, opts) : compileTag(script.innerHTML, opts) + } + } + } + + //// Entry point ----- + + return function (arg, fn, opts) { + + if (typeof arg === T_STRING) { + + if (typeof fn === T_OBJECT) { + opts = fn + fn = false + } + + if (/^\s* MyComponent + * some_else => SomeElse + * some/comp => SomeComp + * + * @param {String} str + * @return {String} + */ + + var classifyRE = /(?:^|[-_\/])(\w)/g; + + function classify(str) { + return str.replace(classifyRE, toUpper); + } + + /** + * Simple bind, faster than native + * + * @param {Function} fn + * @param {Object} ctx + * @return {Function} + */ + + function bind$1(fn, ctx) { + return function (a) { + var l = arguments.length; + return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx); + }; + } + + /** + * Convert an Array-like object to a real Array. + * + * @param {Array-like} list + * @param {Number} [start] - start index + * @return {Array} + */ + + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + + /** + * Mix properties into target object. + * + * @param {Object} to + * @param {Object} from + */ + + function extend(to, from) { + var keys = Object.keys(from); + var i = keys.length; + while (i--) { + to[keys[i]] = from[keys[i]]; + } + return to; + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + * + * @param {*} obj + * @return {Boolean} + */ + + function isObject(obj) { + return obj !== null && typeof obj === 'object'; + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + * + * @param {*} obj + * @return {Boolean} + */ + + var toString = Object.prototype.toString; + var OBJECT_STRING = '[object Object]'; + + function isPlainObject(obj) { + return toString.call(obj) === OBJECT_STRING; + } + + /** + * Array type check. + * + * @param {*} obj + * @return {Boolean} + */ + + var isArray = Array.isArray; + + /** + * Define a non-enumerable property + * + * @param {Object} obj + * @param {String} key + * @param {*} val + * @param {Boolean} [enumerable] + */ + + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Debounce a function so it only gets called after the + * input stops arriving after the given wait period. + * + * @param {Function} func + * @param {Number} wait + * @return {Function} - the debounced function + */ + + function _debounce(func, wait) { + var timeout, args, context, timestamp, result; + var later = function later() { + var last = Date.now() - timestamp; + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + } + }; + return function () { + context = this; + args = arguments; + timestamp = Date.now(); + if (!timeout) { + timeout = setTimeout(later, wait); + } + return result; + }; + } + + /** + * Manual indexOf because it's slightly faster than + * native. + * + * @param {Array} arr + * @param {*} obj + */ + + function indexOf(arr, obj) { + var i = arr.length; + while (i--) { + if (arr[i] === obj) return i; + } + return -1; + } + + /** + * Make a cancellable version of an async callback. + * + * @param {Function} fn + * @return {Function} + */ + + function cancellable(fn) { + var cb = function cb() { + if (!cb.cancelled) { + return fn.apply(this, arguments); + } + }; + cb.cancel = function () { + cb.cancelled = true; + }; + return cb; + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + * + * @param {*} a + * @param {*} b + * @return {Boolean} + */ + + function looseEqual(a, b) { + /* eslint-disable eqeqeq */ + return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false); + /* eslint-enable eqeqeq */ + } + + var hasProto = ('__proto__' in {}); + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]'; + + var isIE9 = inBrowser && navigator.userAgent.toLowerCase().indexOf('msie 9.0') > 0; + + var isAndroid = inBrowser && navigator.userAgent.toLowerCase().indexOf('android') > 0; + + var transitionProp = undefined; + var transitionEndEvent = undefined; + var animationProp = undefined; + var animationEndEvent = undefined; + + // Transition property/event sniffing + if (inBrowser && !isIE9) { + var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined; + var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined; + transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition'; + transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend'; + animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation'; + animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend'; + } + + /** + * Defer a task to execute it asynchronously. Ideally this + * should be executed as a microtask, so we leverage + * MutationObserver if it's available, and fallback to + * setTimeout(0). + * + * @param {Function} cb + * @param {Object} ctx + */ + + var nextTick = (function () { + var callbacks = []; + var pending = false; + var timerFunc; + function nextTickHandler() { + pending = false; + var copies = callbacks.slice(0); + callbacks = []; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + /* istanbul ignore if */ + if (typeof MutationObserver !== 'undefined') { + var counter = 1; + var observer = new MutationObserver(nextTickHandler); + var textNode = document.createTextNode(counter); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = counter; + }; + } else { + timerFunc = setTimeout; + } + return function (cb, ctx) { + var func = ctx ? function () { + cb.call(ctx); + } : cb; + callbacks.push(func); + if (pending) return; + pending = true; + timerFunc(nextTickHandler, 0); + }; + })(); + + function Cache(limit) { + this.size = 0; + this.limit = limit; + this.head = this.tail = undefined; + this._keymap = Object.create(null); + } + + var p = Cache.prototype; + + /** + * Put into the cache associated with . + * Returns the entry which was removed to make room for + * the new entry. Otherwise undefined is returned. + * (i.e. if there was enough room already). + * + * @param {String} key + * @param {*} value + * @return {Entry|undefined} + */ + + p.put = function (key, value) { + var entry = { + key: key, + value: value + }; + this._keymap[key] = entry; + if (this.tail) { + this.tail.newer = entry; + entry.older = this.tail; + } else { + this.head = entry; + } + this.tail = entry; + if (this.size === this.limit) { + return this.shift(); + } else { + this.size++; + } + }; + + /** + * Purge the least recently used (oldest) entry from the + * cache. Returns the removed entry or undefined if the + * cache was empty. + */ + + p.shift = function () { + var entry = this.head; + if (entry) { + this.head = this.head.newer; + this.head.older = undefined; + entry.newer = entry.older = undefined; + this._keymap[entry.key] = undefined; + } + return entry; + }; + + /** + * Get and register recent use of . Returns the value + * associated with or undefined if not in cache. + * + * @param {String} key + * @param {Boolean} returnEntry + * @return {Entry|*} + */ + + p.get = function (key, returnEntry) { + var entry = this._keymap[key]; + if (entry === undefined) return; + if (entry === this.tail) { + return returnEntry ? entry : entry.value; + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry.newer) { + if (entry === this.head) { + this.head = entry.newer; + } + entry.newer.older = entry.older; // C <-- E. + } + if (entry.older) { + entry.older.newer = entry.newer; // C. --> E + } + entry.newer = undefined; // D --x + entry.older = this.tail; // D. --> E + if (this.tail) { + this.tail.newer = entry; // E. <-- D + } + this.tail = entry; + return returnEntry ? entry : entry.value; + }; + + var cache$1 = new Cache(1000); + var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g; + var reservedArgRE = /^in$|^-?\d+/; + + /** + * Parser state + */ + + var str; + var dir; + var c; + var prev; + var i; + var l; + var lastFilterIndex; + var inSingle; + var inDouble; + var curly; + var square; + var paren; + /** + * Push a filter to the current directive object + */ + + function pushFilter() { + var exp = str.slice(lastFilterIndex, i).trim(); + var filter; + if (exp) { + filter = {}; + var tokens = exp.match(filterTokenRE); + filter.name = tokens[0]; + if (tokens.length > 1) { + filter.args = tokens.slice(1).map(processFilterArg); + } + } + if (filter) { + (dir.filters = dir.filters || []).push(filter); + } + lastFilterIndex = i + 1; + } + + /** + * Check if an argument is dynamic and strip quotes. + * + * @param {String} arg + * @return {Object} + */ + + function processFilterArg(arg) { + if (reservedArgRE.test(arg)) { + return { + value: toNumber(arg), + dynamic: false + }; + } else { + var stripped = stripQuotes(arg); + var dynamic = stripped === arg; + return { + value: dynamic ? arg : stripped, + dynamic: dynamic + }; + } + } + + /** + * Parse a directive value and extract the expression + * and its filters into a descriptor. + * + * Example: + * + * "a + 1 | uppercase" will yield: + * { + * expression: 'a + 1', + * filters: [ + * { name: 'uppercase', args: null } + * ] + * } + * + * @param {String} str + * @return {Object} + */ + + function parseDirective(s) { + + var hit = cache$1.get(s); + if (hit) { + return hit; + } + + // reset parser state + str = s; + inSingle = inDouble = false; + curly = square = paren = 0; + lastFilterIndex = 0; + dir = {}; + + for (i = 0, l = str.length; i < l; i++) { + prev = c; + c = str.charCodeAt(i); + if (inSingle) { + // check single quote + if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle; + } else if (inDouble) { + // check double quote + if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble; + } else if (c === 0x7C && // pipe + str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) { + if (dir.expression == null) { + // first filter, end of expression + lastFilterIndex = i + 1; + dir.expression = str.slice(0, i).trim(); + } else { + // already has filter + pushFilter(); + } + } else { + switch (c) { + case 0x22: + inDouble = true;break; // " + case 0x27: + inSingle = true;break; // ' + case 0x28: + paren++;break; // ( + case 0x29: + paren--;break; // ) + case 0x5B: + square++;break; // [ + case 0x5D: + square--;break; // ] + case 0x7B: + curly++;break; // { + case 0x7D: + curly--;break; // } + } + } + } + + if (dir.expression == null) { + dir.expression = str.slice(0, i).trim(); + } else if (lastFilterIndex !== 0) { + pushFilter(); + } + + cache$1.put(s, dir); + return dir; + } + + var directive = Object.freeze({ + parseDirective: parseDirective + }); + + var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g; + var cache = undefined; + var tagRE = undefined; + var htmlRE = undefined; + /** + * Escape a string so it can be used in a RegExp + * constructor. + * + * @param {String} str + */ + + function escapeRegex(str) { + return str.replace(regexEscapeRE, '\\$&'); + } + + function compileRegex() { + var open = escapeRegex(config.delimiters[0]); + var close = escapeRegex(config.delimiters[1]); + var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]); + var unsafeClose = escapeRegex(config.unsafeDelimiters[1]); + tagRE = new RegExp(unsafeOpen + '(.+?)' + unsafeClose + '|' + open + '(.+?)' + close, 'g'); + htmlRE = new RegExp('^' + unsafeOpen + '.*' + unsafeClose + '$'); + // reset cache + cache = new Cache(1000); + } + + /** + * Parse a template text string into an array of tokens. + * + * @param {String} text + * @return {Array | null} + * - {String} type + * - {String} value + * - {Boolean} [html] + * - {Boolean} [oneTime] + */ + + function parseText(text) { + if (!cache) { + compileRegex(); + } + var hit = cache.get(text); + if (hit) { + return hit; + } + text = text.replace(/\n/g, ''); + if (!tagRE.test(text)) { + return null; + } + var tokens = []; + var lastIndex = tagRE.lastIndex = 0; + var match, index, html, value, first, oneTime; + /* eslint-disable no-cond-assign */ + while (match = tagRE.exec(text)) { + /* eslint-enable no-cond-assign */ + index = match.index; + // push text token + if (index > lastIndex) { + tokens.push({ + value: text.slice(lastIndex, index) + }); + } + // tag token + html = htmlRE.test(match[0]); + value = html ? match[1] : match[2]; + first = value.charCodeAt(0); + oneTime = first === 42; // * + value = oneTime ? value.slice(1) : value; + tokens.push({ + tag: true, + value: value.trim(), + html: html, + oneTime: oneTime + }); + lastIndex = index + match[0].length; + } + if (lastIndex < text.length) { + tokens.push({ + value: text.slice(lastIndex) + }); + } + cache.put(text, tokens); + return tokens; + } + + /** + * Format a list of tokens into an expression. + * e.g. tokens parsed from 'a {{b}} c' can be serialized + * into one single expression as '"a " + b + " c"'. + * + * @param {Array} tokens + * @return {String} + */ + + function tokensToExp(tokens) { + if (tokens.length > 1) { + return tokens.map(function (token) { + return formatToken(token); + }).join('+'); + } else { + return formatToken(tokens[0], true); + } + } + + /** + * Format a single token. + * + * @param {Object} token + * @param {Boolean} single + * @return {String} + */ + + function formatToken(token, single) { + return token.tag ? inlineFilters(token.value, single) : '"' + token.value + '"'; + } + + /** + * For an attribute with multiple interpolation tags, + * e.g. attr="some-{{thing | filter}}", in order to combine + * the whole thing into a single watchable expression, we + * have to inline those filters. This function does exactly + * that. This is a bit hacky but it avoids heavy changes + * to directive parser and watcher mechanism. + * + * @param {String} exp + * @param {Boolean} single + * @return {String} + */ + + var filterRE$1 = /[^|]\|[^|]/; + function inlineFilters(exp, single) { + if (!filterRE$1.test(exp)) { + return single ? exp : '(' + exp + ')'; + } else { + var dir = parseDirective(exp); + if (!dir.filters) { + return '(' + exp + ')'; + } else { + return 'this._applyFilters(' + dir.expression + // value + ',null,' + // oldValue (null for read) + JSON.stringify(dir.filters) + // filter descriptors + ',false)'; // write? + } + } + } + + var text$1 = Object.freeze({ + compileRegex: compileRegex, + parseText: parseText, + tokensToExp: tokensToExp + }); + + var delimiters = ['{{', '}}']; + var unsafeDelimiters = ['{{{', '}}}']; + + var config = Object.defineProperties({ + + /** + * Whether to print debug messages. + * Also enables stack trace for warnings. + * + * @type {Boolean} + */ + + debug: false, + + /** + * Whether to suppress warnings. + * + * @type {Boolean} + */ + + silent: false, + + /** + * Whether to use async rendering. + */ + + async: true, + + /** + * Whether to warn against errors caught when evaluating + * expressions. + */ + + warnExpressionErrors: true, + + /** + * Whether or not to handle fully object properties which + * are already backed by getters and seters. Depending on + * use case and environment, this might introduce non-neglible + * performance penalties. + */ + convertAllProperties: false, + + /** + * Internal flag to indicate the delimiters have been + * changed. + * + * @type {Boolean} + */ + + _delimitersChanged: true, + + /** + * List of asset types that a component can own. + * + * @type {Array} + */ + + _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'], + + /** + * prop binding modes + */ + + _propBindingModes: { + ONE_WAY: 0, + TWO_WAY: 1, + ONE_TIME: 2 + }, + + /** + * Max circular updates allowed in a batcher flush cycle. + */ + + _maxUpdateCount: 100 + + }, { + delimiters: { /** + * Interpolation delimiters. Changing these would trigger + * the text parser to re-compile the regular expressions. + * + * @type {Array} + */ + + get: function get() { + return delimiters; + }, + set: function set(val) { + delimiters = val; + compileRegex(); + }, + configurable: true, + enumerable: true + }, + unsafeDelimiters: { + get: function get() { + return unsafeDelimiters; + }, + set: function set(val) { + unsafeDelimiters = val; + compileRegex(); + }, + configurable: true, + enumerable: true + } + }); + + var warn = undefined; + + if ('development' !== 'production') { + (function () { + var hasConsole = typeof console !== 'undefined'; + warn = function (msg, e) { + if (hasConsole && (!config.silent || config.debug)) { + console.warn('[Vue warn]: ' + msg); + /* istanbul ignore if */ + if (config.debug) { + if (e) { + throw e; + } else { + console.warn(new Error('Warning Stack Trace').stack); + } + } + } + }; + })(); + } + + /** + * Append with transition. + * + * @param {Element} el + * @param {Element} target + * @param {Vue} vm + * @param {Function} [cb] + */ + + function appendWithTransition(el, target, vm, cb) { + applyTransition(el, 1, function () { + target.appendChild(el); + }, vm, cb); + } + + /** + * InsertBefore with transition. + * + * @param {Element} el + * @param {Element} target + * @param {Vue} vm + * @param {Function} [cb] + */ + + function beforeWithTransition(el, target, vm, cb) { + applyTransition(el, 1, function () { + before(el, target); + }, vm, cb); + } + + /** + * Remove with transition. + * + * @param {Element} el + * @param {Vue} vm + * @param {Function} [cb] + */ + + function removeWithTransition(el, vm, cb) { + applyTransition(el, -1, function () { + remove(el); + }, vm, cb); + } + + /** + * Apply transitions with an operation callback. + * + * @param {Element} el + * @param {Number} direction + * 1: enter + * -1: leave + * @param {Function} op - the actual DOM operation + * @param {Vue} vm + * @param {Function} [cb] + */ + + function applyTransition(el, direction, op, vm, cb) { + var transition = el.__v_trans; + if (!transition || + // skip if there are no js hooks and CSS transition is + // not supported + !transition.hooks && !transitionEndEvent || + // skip transitions for initial compile + !vm._isCompiled || + // if the vm is being manipulated by a parent directive + // during the parent's compilation phase, skip the + // animation. + vm.$parent && !vm.$parent._isCompiled) { + op(); + if (cb) cb(); + return; + } + var action = direction > 0 ? 'enter' : 'leave'; + transition[action](op, cb); + } + + /** + * Query an element selector if it's not an element already. + * + * @param {String|Element} el + * @return {Element} + */ + + function query(el) { + if (typeof el === 'string') { + var selector = el; + el = document.querySelector(el); + if (!el) { + 'development' !== 'production' && warn('Cannot find element: ' + selector); + } + } + return el; + } + + /** + * Check if a node is in the document. + * Note: document.documentElement.contains should work here + * but always returns false for comment nodes in phantomjs, + * making unit tests difficult. This is fixed by doing the + * contains() check on the node's parentNode instead of + * the node itself. + * + * @param {Node} node + * @return {Boolean} + */ + + function inDoc(node) { + var doc = document.documentElement; + var parent = node && node.parentNode; + return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent)); + } + + /** + * Get and remove an attribute from a node. + * + * @param {Node} node + * @param {String} _attr + */ + + function getAttr(node, _attr) { + var val = node.getAttribute(_attr); + if (val !== null) { + node.removeAttribute(_attr); + } + return val; + } + + /** + * Get an attribute with colon or v-bind: prefix. + * + * @param {Node} node + * @param {String} name + * @return {String|null} + */ + + function getBindAttr(node, name) { + var val = getAttr(node, ':' + name); + if (val === null) { + val = getAttr(node, 'v-bind:' + name); + } + return val; + } + + /** + * Check the presence of a bind attribute. + * + * @param {Node} node + * @param {String} name + * @return {Boolean} + */ + + function hasBindAttr(node, name) { + return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name); + } + + /** + * Insert el before target + * + * @param {Element} el + * @param {Element} target + */ + + function before(el, target) { + target.parentNode.insertBefore(el, target); + } + + /** + * Insert el after target + * + * @param {Element} el + * @param {Element} target + */ + + function after(el, target) { + if (target.nextSibling) { + before(el, target.nextSibling); + } else { + target.parentNode.appendChild(el); + } + } + + /** + * Remove el from DOM + * + * @param {Element} el + */ + + function remove(el) { + el.parentNode.removeChild(el); + } + + /** + * Prepend el to target + * + * @param {Element} el + * @param {Element} target + */ + + function prepend(el, target) { + if (target.firstChild) { + before(el, target.firstChild); + } else { + target.appendChild(el); + } + } + + /** + * Replace target with el + * + * @param {Element} target + * @param {Element} el + */ + + function replace(target, el) { + var parent = target.parentNode; + if (parent) { + parent.replaceChild(el, target); + } + } + + /** + * Add event listener shorthand. + * + * @param {Element} el + * @param {String} event + * @param {Function} cb + */ + + function on$1(el, event, cb) { + el.addEventListener(event, cb); + } + + /** + * Remove event listener shorthand. + * + * @param {Element} el + * @param {String} event + * @param {Function} cb + */ + + function off(el, event, cb) { + el.removeEventListener(event, cb); + } + + /** + * In IE9, setAttribute('class') will result in empty class + * if the element also has the :class attribute; However in + * PhantomJS, setting `className` does not work on SVG elements... + * So we have to do a conditional check here. + * + * @param {Element} el + * @param {String} cls + */ + + function setClass(el, cls) { + /* istanbul ignore if */ + if (isIE9 && !(el instanceof SVGElement)) { + el.className = cls; + } else { + el.setAttribute('class', cls); + } + } + + /** + * Add class with compatibility for IE & SVG + * + * @param {Element} el + * @param {String} cls + */ + + function addClass(el, cls) { + if (el.classList) { + el.classList.add(cls); + } else { + var cur = ' ' + (el.getAttribute('class') || '') + ' '; + if (cur.indexOf(' ' + cls + ' ') < 0) { + setClass(el, (cur + cls).trim()); + } + } + } + + /** + * Remove class with compatibility for IE & SVG + * + * @param {Element} el + * @param {String} cls + */ + + function removeClass(el, cls) { + if (el.classList) { + el.classList.remove(cls); + } else { + var cur = ' ' + (el.getAttribute('class') || '') + ' '; + var tar = ' ' + cls + ' '; + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, ' '); + } + setClass(el, cur.trim()); + } + if (!el.className) { + el.removeAttribute('class'); + } + } + + /** + * Extract raw content inside an element into a temporary + * container div + * + * @param {Element} el + * @param {Boolean} asFragment + * @return {Element} + */ + + function extractContent(el, asFragment) { + var child; + var rawContent; + /* istanbul ignore if */ + if (isTemplate(el) && el.content instanceof DocumentFragment) { + el = el.content; + } + if (el.hasChildNodes()) { + trimNode(el); + rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div'); + /* eslint-disable no-cond-assign */ + while (child = el.firstChild) { + /* eslint-enable no-cond-assign */ + rawContent.appendChild(child); + } + } + return rawContent; + } + + /** + * Trim possible empty head/tail textNodes inside a parent. + * + * @param {Node} node + */ + + function trimNode(node) { + trim(node, node.firstChild); + trim(node, node.lastChild); + } + + function trim(parent, node) { + if (node && node.nodeType === 3 && !node.data.trim()) { + parent.removeChild(node); + } + } + + /** + * Check if an element is a template tag. + * Note if the template appears inside an SVG its tagName + * will be in lowercase. + * + * @param {Element} el + */ + + function isTemplate(el) { + return el.tagName && el.tagName.toLowerCase() === 'template'; + } + + /** + * Create an "anchor" for performing dom insertion/removals. + * This is used in a number of scenarios: + * - fragment instance + * - v-html + * - v-if + * - v-for + * - component + * + * @param {String} content + * @param {Boolean} persist - IE trashes empty textNodes on + * cloneNode(true), so in certain + * cases the anchor needs to be + * non-empty to be persisted in + * templates. + * @return {Comment|Text} + */ + + function createAnchor(content, persist) { + var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : ''); + anchor.__vue_anchor = true; + return anchor; + } + + /** + * Find a component ref attribute that starts with $. + * + * @param {Element} node + * @return {String|undefined} + */ + + var refRE = /^v-ref:/; + + function findRef(node) { + if (node.hasAttributes()) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var name = attrs[i].name; + if (refRE.test(name)) { + return camelize(name.replace(refRE, '')); + } + } + } + } + + /** + * Map a function to a range of nodes . + * + * @param {Node} node + * @param {Node} end + * @param {Function} op + */ + + function mapNodeRange(node, end, op) { + var next; + while (node !== end) { + next = node.nextSibling; + op(node); + node = next; + } + op(end); + } + + /** + * Remove a range of nodes with transition, store + * the nodes in a fragment with correct ordering, + * and call callback when done. + * + * @param {Node} start + * @param {Node} end + * @param {Vue} vm + * @param {DocumentFragment} frag + * @param {Function} cb + */ + + function removeNodeRange(start, end, vm, frag, cb) { + var done = false; + var removed = 0; + var nodes = []; + mapNodeRange(start, end, function (node) { + if (node === end) done = true; + nodes.push(node); + removeWithTransition(node, vm, onRemoved); + }); + function onRemoved() { + removed++; + if (done && removed >= nodes.length) { + for (var i = 0; i < nodes.length; i++) { + frag.appendChild(nodes[i]); + } + cb && cb(); + } + } + } + + var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/; + var reservedTagRE = /^(slot|partial|component)$/; + + /** + * Check if an element is a component, if yes return its + * component id. + * + * @param {Element} el + * @param {Object} options + * @return {Object|undefined} + */ + + function checkComponentAttr(el, options) { + var tag = el.tagName.toLowerCase(); + var hasAttrs = el.hasAttributes(); + if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) { + if (resolveAsset(options, 'components', tag)) { + return { id: tag }; + } else { + var is = hasAttrs && getIsBinding(el); + if (is) { + return is; + } else if ('development' !== 'production') { + if (tag.indexOf('-') > -1 || /HTMLUnknownElement/.test(el.toString()) && + // Chrome returns unknown for several HTML5 elements. + // https://code.google.com/p/chromium/issues/detail?id=540526 + !/^(data|time|rtc|rb)$/.test(tag)) { + warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly?'); + } + } + } + } else if (hasAttrs) { + return getIsBinding(el); + } + } + + /** + * Get "is" binding from an element. + * + * @param {Element} el + * @return {Object|undefined} + */ + + function getIsBinding(el) { + // dynamic syntax + var exp = getAttr(el, 'is'); + if (exp != null) { + return { id: exp }; + } else { + exp = getBindAttr(el, 'is'); + if (exp != null) { + return { id: exp, dynamic: true }; + } + } + } + + /** + * Set a prop's initial value on a vm and its data object. + * + * @param {Vue} vm + * @param {Object} prop + * @param {*} value + */ + + function initProp(vm, prop, value) { + var key = prop.path; + value = coerceProp(prop, value); + vm[key] = vm._data[key] = assertProp(prop, value) ? value : undefined; + } + + /** + * Assert whether a prop is valid. + * + * @param {Object} prop + * @param {*} value + */ + + function assertProp(prop, value) { + // if a prop is not provided and is not required, + // skip the check. + if (prop.raw === null && !prop.required) { + return true; + } + var options = prop.options; + var type = options.type; + var valid = true; + var expectedType; + if (type) { + if (type === String) { + expectedType = 'string'; + valid = typeof value === expectedType; + } else if (type === Number) { + expectedType = 'number'; + valid = typeof value === 'number'; + } else if (type === Boolean) { + expectedType = 'boolean'; + valid = typeof value === 'boolean'; + } else if (type === Function) { + expectedType = 'function'; + valid = typeof value === 'function'; + } else if (type === Object) { + expectedType = 'object'; + valid = isPlainObject(value); + } else if (type === Array) { + expectedType = 'array'; + valid = isArray(value); + } else { + valid = value instanceof type; + } + } + if (!valid) { + 'development' !== 'production' && warn('Invalid prop: type check failed for ' + prop.path + '="' + prop.raw + '".' + ' Expected ' + formatType(expectedType) + ', got ' + formatValue(value) + '.'); + return false; + } + var validator = options.validator; + if (validator) { + if (!validator.call(null, value)) { + 'development' !== 'production' && warn('Invalid prop: custom validator check failed for ' + prop.path + '="' + prop.raw + '"'); + return false; + } + } + return true; + } + + /** + * Force parsing value with coerce option. + * + * @param {*} value + * @param {Object} options + * @return {*} + */ + + function coerceProp(prop, value) { + var coerce = prop.options.coerce; + if (!coerce) { + return value; + } + // coerce is a function + return coerce(value); + } + + function formatType(val) { + return val ? val.charAt(0).toUpperCase() + val.slice(1) : 'custom type'; + } + + function formatValue(val) { + return Object.prototype.toString.call(val).slice(8, -1); + } + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + * + * All strategy functions follow the same signature: + * + * @param {*} parentVal + * @param {*} childVal + * @param {Vue} [vm] + */ + + var strats = config.optionMergeStrategies = Object.create(null); + + /** + * Helper that recursively merges two data objects together. + */ + + function mergeData(to, from) { + var key, toVal, fromVal; + for (key in from) { + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (isObject(toVal) && isObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to; + } + + /** + * Data + */ + + strats.data = function (parentVal, childVal, vm) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal; + } + if (typeof childVal !== 'function') { + 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.'); + return parentVal; + } + if (!parentVal) { + return childVal; + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn() { + return mergeData(childVal.call(this), parentVal.call(this)); + }; + } else if (parentVal || childVal) { + return function mergedInstanceDataFn() { + // instance merge + var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal; + var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined; + if (instanceData) { + return mergeData(instanceData, defaultData); + } else { + return defaultData; + } + }; + } + }; + + /** + * El + */ + + strats.el = function (parentVal, childVal, vm) { + if (!vm && childVal && typeof childVal !== 'function') { + 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.'); + return; + } + var ret = childVal || parentVal; + // invoke the element factory if this is instance merge + return vm && typeof ret === 'function' ? ret.call(vm) : ret; + }; + + /** + * Hooks and param attributes are merged as arrays. + */ + + strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = function (parentVal, childVal) { + return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal; + }; + + /** + * 0.11 deprecation warning + */ + + strats.paramAttributes = function () { + /* istanbul ignore next */ + 'development' !== 'production' && warn('"paramAttributes" option has been deprecated in 0.12. ' + 'Use "props" instead.'); + }; + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + + function mergeAssets(parentVal, childVal) { + var res = Object.create(parentVal); + return childVal ? extend(res, guardArrayAssets(childVal)) : res; + } + + config._assetTypes.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Events & Watchers. + * + * Events & watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + + strats.watch = strats.events = function (parentVal, childVal) { + if (!childVal) return parentVal; + if (!parentVal) return childVal; + var ret = {}; + extend(ret, parentVal); + for (var key in childVal) { + var parent = ret[key]; + var child = childVal[key]; + if (parent && !isArray(parent)) { + parent = [parent]; + } + ret[key] = parent ? parent.concat(child) : [child]; + } + return ret; + }; + + /** + * Other object hashes. + */ + + strats.props = strats.methods = strats.computed = function (parentVal, childVal) { + if (!childVal) return parentVal; + if (!parentVal) return childVal; + var ret = Object.create(null); + extend(ret, parentVal); + extend(ret, childVal); + return ret; + }; + + /** + * Default strategy. + */ + + var defaultStrat = function defaultStrat(parentVal, childVal) { + return childVal === undefined ? parentVal : childVal; + }; + + /** + * Make sure component options get converted to actual + * constructors. + * + * @param {Object} options + */ + + function guardComponents(options) { + if (options.components) { + var components = options.components = guardArrayAssets(options.components); + var def; + var ids = Object.keys(components); + for (var i = 0, l = ids.length; i < l; i++) { + var key = ids[i]; + if (commonTagRE.test(key) || reservedTagRE.test(key)) { + 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key); + continue; + } + def = components[key]; + if (isPlainObject(def)) { + components[key] = Vue.extend(def); + } + } + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + * + * @param {Object} options + */ + + function guardProps(options) { + var props = options.props; + var i, val; + if (isArray(props)) { + options.props = {}; + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + options.props[val] = null; + } else if (val.name) { + options.props[val.name] = val; + } + } + } else if (isPlainObject(props)) { + var keys = Object.keys(props); + i = keys.length; + while (i--) { + val = props[keys[i]]; + if (typeof val === 'function') { + props[keys[i]] = { type: val }; + } + } + } + } + + /** + * Guard an Array-format assets option and converted it + * into the key-value Object format. + * + * @param {Object|Array} assets + * @return {Object} + */ + + function guardArrayAssets(assets) { + if (isArray(assets)) { + var res = {}; + var i = assets.length; + var asset; + while (i--) { + asset = assets[i]; + var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id; + if (!id) { + 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.'); + } else { + res[id] = asset; + } + } + return res; + } + return assets; + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + * + * @param {Object} parent + * @param {Object} child + * @param {Vue} [vm] - if vm is present, indicates this is + * an instantiation merge. + */ + + function mergeOptions(parent, child, vm) { + guardComponents(child); + guardProps(child); + var options = {}; + var key; + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField(key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options; + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + * + * @param {Object} options + * @param {String} type + * @param {String} id + * @return {Object|Function} + */ + + function resolveAsset(options, type, id) { + var assets = options[type]; + var camelizedId; + return assets[id] || + // camelCase ID + assets[camelizedId = camelize(id)] || + // Pascal Case ID + assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]; + } + + /** + * Assert asset exists + */ + + function assertAsset(val, type, id) { + if (!val) { + 'development' !== 'production' && warn('Failed to resolve ' + type + ': ' + id); + } + } + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto) + + /** + * Intercept mutating methods and emit events + */ + + ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + // avoid leaking arguments: + // http://jsperf.com/closure-with-arguments + var i = arguments.length; + var args = new Array(i); + while (i--) { + args[i] = arguments[i]; + } + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + inserted = args; + break; + case 'unshift': + inserted = args; + break; + case 'splice': + inserted = args.slice(2); + break; + } + if (inserted) ob.observeArray(inserted); + // notify change + ob.dep.notify(); + return result; + }); + }); + + /** + * Swap the element at the given index with a new value + * and emits corresponding event. + * + * @param {Number} index + * @param {*} val + * @return {*} - replaced element + */ + + def(arrayProto, '$set', function $set(index, val) { + if (index >= this.length) { + this.length = Number(index) + 1; + } + return this.splice(index, 1, val)[0]; + }); + + /** + * Convenience method to remove the element at given index. + * + * @param {Number} index + * @param {*} val + */ + + def(arrayProto, '$remove', function $remove(item) { + /* istanbul ignore if */ + if (!this.length) return; + var index = indexOf(this, item); + if (index > -1) { + return this.splice(index, 1); + } + }); + + var uid$3 = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + * + * @constructor + */ + function Dep() { + this.id = uid$3++; + this.subs = []; + } + + // the current target watcher being evaluated. + // this is globally unique because there could be only one + // watcher being evaluated at any time. + Dep.target = null; + + /** + * Add a directive subscriber. + * + * @param {Directive} sub + */ + + Dep.prototype.addSub = function (sub) { + this.subs.push(sub); + }; + + /** + * Remove a directive subscriber. + * + * @param {Directive} sub + */ + + Dep.prototype.removeSub = function (sub) { + this.subs.$remove(sub); + }; + + /** + * Add self as a dependency to the target watcher. + */ + + Dep.prototype.depend = function () { + Dep.target.addDep(this); + }; + + /** + * Notify all subscribers of a new value. + */ + + Dep.prototype.notify = function () { + // stablize the subscriber list first + var subs = toArray(this.subs); + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * Observer class that are attached to each observed + * object. Once attached, the observer converts target + * object's property keys into getter/setters that + * collect dependencies and dispatches updates. + * + * @param {Array|Object} value + * @constructor + */ + + function Observer(value) { + this.value = value; + this.dep = new Dep(); + def(value, '__ob__', this); + if (isArray(value)) { + var augment = hasProto ? protoAugment : copyAugment; + augment(value, arrayMethods, arrayKeys); + this.observeArray(value); + } else { + this.walk(value); + } + } + + // Instance methods + + /** + * Walk through each property and convert them into + * getter/setters. This method should only be called when + * value type is Object. + * + * @param {Object} obj + */ + + Observer.prototype.walk = function (obj) { + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + this.convert(keys[i], obj[keys[i]]); + } + }; + + /** + * Observe a list of Array items. + * + * @param {Array} items + */ + + Observer.prototype.observeArray = function (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + /** + * Convert a property into getter/setter so we can emit + * the events when the property is accessed/changed. + * + * @param {String} key + * @param {*} val + */ + + Observer.prototype.convert = function (key, val) { + defineReactive(this.value, key, val); + }; + + /** + * Add an owner vm, so that when $set/$delete mutations + * happen we can notify owner vms to proxy the keys and + * digest the watchers. This is only called when the object + * is observed as an instance's root $data. + * + * @param {Vue} vm + */ + + Observer.prototype.addVm = function (vm) { + (this.vms || (this.vms = [])).push(vm); + }; + + /** + * Remove an owner vm. This is called when the object is + * swapped out as an instance's $data object. + * + * @param {Vue} vm + */ + + Observer.prototype.removeVm = function (vm) { + this.vms.$remove(vm); + }; + + // helpers + + /** + * Augment an target Object or Array by intercepting + * the prototype chain using __proto__ + * + * @param {Object|Array} target + * @param {Object} proto + */ + + function protoAugment(target, src) { + target.__proto__ = src; + } + + /** + * Augment an target Object or Array by defining + * hidden properties. + * + * @param {Object|Array} target + * @param {Object} proto + */ + + function copyAugment(target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + * + * @param {*} value + * @param {Vue} [vm] + * @return {Observer|undefined} + * @static + */ + + function observe(value, vm) { + if (!value || typeof value !== 'object') { + return; + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ((isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { + ob = new Observer(value); + } + if (ob && vm) { + ob.addVm(vm); + } + return ob; + } + + /** + * Define a reactive property on an Object. + * + * @param {Object} obj + * @param {String} key + * @param {*} val + */ + + function defineReactive(obj, key, val) { + var dep = new Dep(); + + // cater for pre-defined getter/setters + var getter, setter; + if (config.convertAllProperties) { + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + getter = property && property.get; + setter = property && property.set; + } + + var childOb = observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + } + if (isArray(value)) { + for (var e, i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + } + } + } + return value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (newVal === value) { + return; + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = observe(newVal); + dep.notify(); + } + }); + } + + var util = Object.freeze({ + defineReactive: defineReactive, + set: set, + del: del, + hasOwn: hasOwn, + isLiteral: isLiteral, + isReserved: isReserved, + _toString: _toString, + toNumber: toNumber, + toBoolean: toBoolean, + stripQuotes: stripQuotes, + camelize: camelize, + hyphenate: hyphenate, + classify: classify, + bind: bind$1, + toArray: toArray, + extend: extend, + isObject: isObject, + isPlainObject: isPlainObject, + def: def, + debounce: _debounce, + indexOf: indexOf, + cancellable: cancellable, + looseEqual: looseEqual, + isArray: isArray, + hasProto: hasProto, + inBrowser: inBrowser, + isIE9: isIE9, + isAndroid: isAndroid, + get transitionProp () { return transitionProp; }, + get transitionEndEvent () { return transitionEndEvent; }, + get animationProp () { return animationProp; }, + get animationEndEvent () { return animationEndEvent; }, + nextTick: nextTick, + query: query, + inDoc: inDoc, + getAttr: getAttr, + getBindAttr: getBindAttr, + hasBindAttr: hasBindAttr, + before: before, + after: after, + remove: remove, + prepend: prepend, + replace: replace, + on: on$1, + off: off, + setClass: setClass, + addClass: addClass, + removeClass: removeClass, + extractContent: extractContent, + trimNode: trimNode, + isTemplate: isTemplate, + createAnchor: createAnchor, + findRef: findRef, + mapNodeRange: mapNodeRange, + removeNodeRange: removeNodeRange, + mergeOptions: mergeOptions, + resolveAsset: resolveAsset, + assertAsset: assertAsset, + checkComponentAttr: checkComponentAttr, + initProp: initProp, + assertProp: assertProp, + coerceProp: coerceProp, + commonTagRE: commonTagRE, + reservedTagRE: reservedTagRE, + get warn () { return warn; } + }); + + var uid = 0; + + function initMixin (Vue) { + + /** + * The main init sequence. This is called for every + * instance, including ones that are created from extended + * constructors. + * + * @param {Object} options - this options object should be + * the result of merging class + * options and the options passed + * in to the constructor. + */ + + Vue.prototype._init = function (options) { + + options = options || {}; + + this.$el = null; + this.$parent = options.parent; + this.$root = this.$parent ? this.$parent.$root : this; + this.$children = []; + this.$refs = {}; // child vm references + this.$els = {}; // element references + this._watchers = []; // all watchers as an array + this._directives = []; // all directives + + // a uid + this._uid = uid++; + + // a flag to avoid this being observed + this._isVue = true; + + // events bookkeeping + this._events = {}; // registered callbacks + this._eventsCount = {}; // for $broadcast optimization + + // fragment instance properties + this._isFragment = false; + this._fragment = // @type {DocumentFragment} + this._fragmentStart = // @type {Text|Comment} + this._fragmentEnd = null; // @type {Text|Comment} + + // lifecycle state + this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = false; + this._unlinkFn = null; + + // context: + // if this is a transcluded component, context + // will be the common parent vm of this instance + // and its host. + this._context = options._context || this.$parent; + + // scope: + // if this is inside an inline v-for, the scope + // will be the intermediate scope created for this + // repeat fragment. this is used for linking props + // and container directives. + this._scope = options._scope; + + // fragment: + // if this instance is compiled inside a Fragment, it + // needs to reigster itself as a child of that fragment + // for attach/detach to work properly. + this._frag = options._frag; + if (this._frag) { + this._frag.children.push(this); + } + + // push self into parent / transclusion host + if (this.$parent) { + this.$parent.$children.push(this); + } + + // merge options. + options = this.$options = mergeOptions(this.constructor.options, options, this); + + // set ref + this._updateRef(); + + // initialize data as empty object. + // it will be filled up in _initScope(). + this._data = {}; + + // call init hook + this._callHook('init'); + + // initialize data observation and scope inheritance. + this._initState(); + + // setup event system and option events. + this._initEvents(); + + // call created hook + this._callHook('created'); + + // if `el` option is passed, start compilation. + if (options.el) { + this.$mount(options.el); + } + }; + } + + var pathCache = new Cache(1000); + + // actions + var APPEND = 0; + var PUSH = 1; + var INC_SUB_PATH_DEPTH = 2; + var PUSH_SUB_PATH = 3; + + // states + var BEFORE_PATH = 0; + var IN_PATH = 1; + var BEFORE_IDENT = 2; + var IN_IDENT = 3; + var IN_SUB_PATH = 4; + var IN_SINGLE_QUOTE = 5; + var IN_DOUBLE_QUOTE = 6; + var AFTER_PATH = 7; + var ERROR = 8; + + var pathStateMachine = []; + + pathStateMachine[BEFORE_PATH] = { + 'ws': [BEFORE_PATH], + 'ident': [IN_IDENT, APPEND], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] + }; + + pathStateMachine[IN_PATH] = { + 'ws': [IN_PATH], + '.': [BEFORE_IDENT], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] + }; + + pathStateMachine[BEFORE_IDENT] = { + 'ws': [BEFORE_IDENT], + 'ident': [IN_IDENT, APPEND] + }; + + pathStateMachine[IN_IDENT] = { + 'ident': [IN_IDENT, APPEND], + '0': [IN_IDENT, APPEND], + 'number': [IN_IDENT, APPEND], + 'ws': [IN_PATH, PUSH], + '.': [BEFORE_IDENT, PUSH], + '[': [IN_SUB_PATH, PUSH], + 'eof': [AFTER_PATH, PUSH] + }; + + pathStateMachine[IN_SUB_PATH] = { + "'": [IN_SINGLE_QUOTE, APPEND], + '"': [IN_DOUBLE_QUOTE, APPEND], + '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH], + ']': [IN_PATH, PUSH_SUB_PATH], + 'eof': ERROR, + 'else': [IN_SUB_PATH, APPEND] + }; + + pathStateMachine[IN_SINGLE_QUOTE] = { + "'": [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_SINGLE_QUOTE, APPEND] + }; + + pathStateMachine[IN_DOUBLE_QUOTE] = { + '"': [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_DOUBLE_QUOTE, APPEND] + }; + + /** + * Determine the type of a character in a keypath. + * + * @param {Char} ch + * @return {String} type + */ + + function getPathCharType(ch) { + if (ch === undefined) { + return 'eof'; + } + + var code = ch.charCodeAt(0); + + switch (code) { + case 0x5B: // [ + case 0x5D: // ] + case 0x2E: // . + case 0x22: // " + case 0x27: // ' + case 0x30: + // 0 + return ch; + + case 0x5F: // _ + case 0x24: + // $ + return 'ident'; + + case 0x20: // Space + case 0x09: // Tab + case 0x0A: // Newline + case 0x0D: // Return + case 0xA0: // No-break space + case 0xFEFF: // Byte Order Mark + case 0x2028: // Line Separator + case 0x2029: + // Paragraph Separator + return 'ws'; + } + + // a-z, A-Z + if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) { + return 'ident'; + } + + // 1-9 + if (code >= 0x31 && code <= 0x39) { + return 'number'; + } + + return 'else'; + } + + /** + * Format a subPath, return its plain form if it is + * a literal string or number. Otherwise prepend the + * dynamic indicator (*). + * + * @param {String} path + * @return {String} + */ + + function formatSubPath(path) { + var trimmed = path.trim(); + // invalid leading 0 + if (path.charAt(0) === '0' && isNaN(path)) { + return false; + } + return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed; + } + + /** + * Parse a string path into an array of segments + * + * @param {String} path + * @return {Array|undefined} + */ + + function parse(path) { + var keys = []; + var index = -1; + var mode = BEFORE_PATH; + var subPathDepth = 0; + var c, newChar, key, type, transition, action, typeMap; + + var actions = []; + + actions[PUSH] = function () { + if (key !== undefined) { + keys.push(key); + key = undefined; + } + }; + + actions[APPEND] = function () { + if (key === undefined) { + key = newChar; + } else { + key += newChar; + } + }; + + actions[INC_SUB_PATH_DEPTH] = function () { + actions[APPEND](); + subPathDepth++; + }; + + actions[PUSH_SUB_PATH] = function () { + if (subPathDepth > 0) { + subPathDepth--; + mode = IN_SUB_PATH; + actions[APPEND](); + } else { + subPathDepth = 0; + key = formatSubPath(key); + if (key === false) { + return false; + } else { + actions[PUSH](); + } + } + }; + + function maybeUnescapeQuote() { + var nextChar = path[index + 1]; + if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') { + index++; + newChar = '\\' + nextChar; + actions[APPEND](); + return true; + } + } + + while (mode != null) { + index++; + c = path[index]; + + if (c === '\\' && maybeUnescapeQuote()) { + continue; + } + + type = getPathCharType(c); + typeMap = pathStateMachine[mode]; + transition = typeMap[type] || typeMap['else'] || ERROR; + + if (transition === ERROR) { + return; // parse error + } + + mode = transition[0]; + action = actions[transition[1]]; + if (action) { + newChar = transition[2]; + newChar = newChar === undefined ? c : newChar; + if (action() === false) { + return; + } + } + + if (mode === AFTER_PATH) { + keys.raw = path; + return keys; + } + } + } + + /** + * External parse that check for a cache hit first + * + * @param {String} path + * @return {Array|undefined} + */ + + function parsePath(path) { + var hit = pathCache.get(path); + if (!hit) { + hit = parse(path); + if (hit) { + pathCache.put(path, hit); + } + } + return hit; + } + + /** + * Get from an object from a path string + * + * @param {Object} obj + * @param {String} path + */ + + function getPath(obj, path) { + return parseExpression(path).get(obj); + } + + /** + * Warn against setting non-existent root path on a vm. + */ + + var warnNonExistent; + if ('development' !== 'production') { + warnNonExistent = function (path) { + warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.'); + }; + } + + /** + * Set on an object from a path + * + * @param {Object} obj + * @param {String | Array} path + * @param {*} val + */ + + function setPath(obj, path, val) { + var original = obj; + if (typeof path === 'string') { + path = parse(path); + } + if (!path || !isObject(obj)) { + return false; + } + var last, key; + for (var i = 0, l = path.length; i < l; i++) { + last = obj; + key = path[i]; + if (key.charAt(0) === '*') { + key = parseExpression(key.slice(1)).get.call(original, original); + } + if (i < l - 1) { + obj = obj[key]; + if (!isObject(obj)) { + obj = {}; + if ('development' !== 'production' && last._isVue) { + warnNonExistent(path); + } + set(last, key, obj); + } + } else { + if (isArray(obj)) { + obj.$set(key, val); + } else if (key in obj) { + obj[key] = val; + } else { + if ('development' !== 'production' && obj._isVue) { + warnNonExistent(path); + } + set(obj, key, val); + } + } + } + return true; + } + + var path = Object.freeze({ + parsePath: parsePath, + getPath: getPath, + setPath: setPath + }); + + var expressionCache = new Cache(1000); + + var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat'; + var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)'); + + // keywords that don't make sense inside expressions + var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'proctected,static,interface,private,public'; + var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)'); + + var wsRE = /\s/g; + var newlineRE = /\n/g; + var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|new |typeof |void /g; + var restoreRE = /"(\d+)"/g; + var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/; + var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g; + var booleanLiteralRE = /^(?:true|false)$/; + + /** + * Save / Rewrite / Restore + * + * When rewriting paths found in an expression, it is + * possible for the same letter sequences to be found in + * strings and Object literal property keys. Therefore we + * remove and store these parts in a temporary array, and + * restore them after the path rewrite. + */ + + var saved = []; + + /** + * Save replacer + * + * The save regex can match two possible cases: + * 1. An opening object literal + * 2. A string + * If matched as a plain string, we need to escape its + * newlines, since the string needs to be preserved when + * generating the function body. + * + * @param {String} str + * @param {String} isString - str if matched as a string + * @return {String} - placeholder with index + */ + + function save(str, isString) { + var i = saved.length; + saved[i] = isString ? str.replace(newlineRE, '\\n') : str; + return '"' + i + '"'; + } + + /** + * Path rewrite replacer + * + * @param {String} raw + * @return {String} + */ + + function rewrite(raw) { + var c = raw.charAt(0); + var path = raw.slice(1); + if (allowedKeywordsRE.test(path)) { + return raw; + } else { + path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path; + return c + 'scope.' + path; + } + } + + /** + * Restore replacer + * + * @param {String} str + * @param {String} i - matched save index + * @return {String} + */ + + function restore(str, i) { + return saved[i]; + } + + /** + * Rewrite an expression, prefixing all path accessors with + * `scope.` and generate getter/setter functions. + * + * @param {String} exp + * @return {Function} + */ + + function compileGetter(exp) { + if (improperKeywordsRE.test(exp)) { + 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp); + } + // reset state + saved.length = 0; + // save strings and object literal keys + var body = exp.replace(saveRE, save).replace(wsRE, ''); + // rewrite all paths + // pad 1 space here becaue the regex matches 1 extra char + body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore); + return makeGetterFn(body); + } + + /** + * Build a getter function. Requires eval. + * + * We isolate the try/catch so it doesn't affect the + * optimization of the parse function when it is not called. + * + * @param {String} body + * @return {Function|undefined} + */ + + function makeGetterFn(body) { + try { + return new Function('scope', 'return ' + body + ';'); + } catch (e) { + 'development' !== 'production' && warn('Invalid expression. ' + 'Generated function body: ' + body); + } + } + + /** + * Compile a setter function for the expression. + * + * @param {String} exp + * @return {Function|undefined} + */ + + function compileSetter(exp) { + var path = parsePath(exp); + if (path) { + return function (scope, val) { + setPath(scope, path, val); + }; + } else { + 'development' !== 'production' && warn('Invalid setter expression: ' + exp); + } + } + + /** + * Parse an expression into re-written getter/setters. + * + * @param {String} exp + * @param {Boolean} needSet + * @return {Function} + */ + + function parseExpression(exp, needSet) { + exp = exp.trim(); + // try cache + var hit = expressionCache.get(exp); + if (hit) { + if (needSet && !hit.set) { + hit.set = compileSetter(hit.exp); + } + return hit; + } + var res = { exp: exp }; + res.get = isSimplePath(exp) && exp.indexOf('[') < 0 + // optimized super simple getter + ? makeGetterFn('scope.' + exp) + // dynamic getter + : compileGetter(exp); + if (needSet) { + res.set = compileSetter(exp); + } + expressionCache.put(exp, res); + return res; + } + + /** + * Check if an expression is a simple path. + * + * @param {String} exp + * @return {Boolean} + */ + + function isSimplePath(exp) { + return pathTestRE.test(exp) && + // don't treat true/false as paths + !booleanLiteralRE.test(exp) && + // Math constants e.g. Math.PI, Math.E etc. + exp.slice(0, 5) !== 'Math.'; + } + + var expression = Object.freeze({ + parseExpression: parseExpression, + isSimplePath: isSimplePath + }); + + // we have two separate queues: one for directive updates + // and one for user watcher registered via $watch(). + // we want to guarantee directive updates to be called + // before user watchers so that when user watchers are + // triggered, the DOM would have already been in updated + // state. + var queue = []; + var userQueue = []; + var has = {}; + var circular = {}; + var waiting = false; + var internalQueueDepleted = false; + + /** + * Reset the batcher's state. + */ + + function resetBatcherState() { + queue = []; + userQueue = []; + has = {}; + circular = {}; + waiting = internalQueueDepleted = false; + } + + /** + * Flush both queues and run the watchers. + */ + + function flushBatcherQueue() { + runBatcherQueue(queue); + internalQueueDepleted = true; + runBatcherQueue(userQueue); + // dev tool hook + /* istanbul ignore if */ + if ('development' !== 'production') { + if (inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) { + window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('flush'); + } + } + resetBatcherState(); + } + + /** + * Run the watchers in a single queue. + * + * @param {Array} queue + */ + + function runBatcherQueue(queue) { + // do not cache length because more watchers might be pushed + // as we run existing watchers + for (var i = 0; i < queue.length; i++) { + var watcher = queue[i]; + var id = watcher.id; + has[id] = null; + watcher.run(); + // in dev build, check and stop circular updates. + if ('development' !== 'production' && has[id] != null) { + circular[id] = (circular[id] || 0) + 1; + if (circular[id] > config._maxUpdateCount) { + queue.splice(has[id], 1); + warn('You may have an infinite update loop for watcher ' + 'with expression: ' + watcher.expression); + } + } + } + } + + /** + * Push a watcher into the watcher queue. + * Jobs with duplicate IDs will be skipped unless it's + * pushed when the queue is being flushed. + * + * @param {Watcher} watcher + * properties: + * - {Number} id + * - {Function} run + */ + + function pushWatcher(watcher) { + var id = watcher.id; + if (has[id] == null) { + // if an internal watcher is pushed, but the internal + // queue is already depleted, we run it immediately. + if (internalQueueDepleted && !watcher.user) { + watcher.run(); + return; + } + // push watcher into appropriate queue + var q = watcher.user ? userQueue : queue; + has[id] = q.length; + q.push(watcher); + // queue the flush + if (!waiting) { + waiting = true; + nextTick(flushBatcherQueue); + } + } + } + + var uid$2 = 0; + + /** + * A watcher parses an expression, collects dependencies, + * and fires callback when the expression value changes. + * This is used for both the $watch() api and directives. + * + * @param {Vue} vm + * @param {String} expression + * @param {Function} cb + * @param {Object} options + * - {Array} filters + * - {Boolean} twoWay + * - {Boolean} deep + * - {Boolean} user + * - {Boolean} sync + * - {Boolean} lazy + * - {Function} [preProcess] + * - {Function} [postProcess] + * @constructor + */ + function Watcher(vm, expOrFn, cb, options) { + // mix in options + if (options) { + extend(this, options); + } + var isFn = typeof expOrFn === 'function'; + this.vm = vm; + vm._watchers.push(this); + this.expression = isFn ? expOrFn.toString() : expOrFn; + this.cb = cb; + this.id = ++uid$2; // uid for batching + this.active = true; + this.dirty = this.lazy; // for lazy watchers + this.deps = Object.create(null); + this.newDeps = null; + this.prevError = null; // for async error stacks + // parse expression for getter/setter + if (isFn) { + this.getter = expOrFn; + this.setter = undefined; + } else { + var res = parseExpression(expOrFn, this.twoWay); + this.getter = res.get; + this.setter = res.set; + } + this.value = this.lazy ? undefined : this.get(); + // state for avoiding false triggers for deep and Array + // watchers during vm._digest() + this.queued = this.shallow = false; + } + + /** + * Add a dependency to this directive. + * + * @param {Dep} dep + */ + + Watcher.prototype.addDep = function (dep) { + var id = dep.id; + if (!this.newDeps[id]) { + this.newDeps[id] = dep; + if (!this.deps[id]) { + this.deps[id] = dep; + dep.addSub(this); + } + } + }; + + /** + * Evaluate the getter, and re-collect dependencies. + */ + + Watcher.prototype.get = function () { + this.beforeGet(); + var scope = this.scope || this.vm; + var value; + try { + value = this.getter.call(scope, scope); + } catch (e) { + if ('development' !== 'production' && config.warnExpressionErrors) { + warn('Error when evaluating expression "' + this.expression + '". ' + (config.debug ? '' : 'Turn on debug mode to see stack trace.'), e); + } + } + // "touch" every property so they are all tracked as + // dependencies for deep watching + if (this.deep) { + traverse(value); + } + if (this.preProcess) { + value = this.preProcess(value); + } + if (this.filters) { + value = scope._applyFilters(value, null, this.filters, false); + } + if (this.postProcess) { + value = this.postProcess(value); + } + this.afterGet(); + return value; + }; + + /** + * Set the corresponding value with the setter. + * + * @param {*} value + */ + + Watcher.prototype.set = function (value) { + var scope = this.scope || this.vm; + if (this.filters) { + value = scope._applyFilters(value, this.value, this.filters, true); + } + try { + this.setter.call(scope, scope, value); + } catch (e) { + if ('development' !== 'production' && config.warnExpressionErrors) { + warn('Error when evaluating setter "' + this.expression + '"', e); + } + } + // two-way sync for v-for alias + var forContext = scope.$forContext; + if (forContext && forContext.alias === this.expression) { + if (forContext.filters) { + 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.'); + return; + } + forContext._withLock(function () { + if (scope.$key) { + // original is an object + forContext.rawValue[scope.$key] = value; + } else { + forContext.rawValue.$set(scope.$index, value); + } + }); + } + }; + + /** + * Prepare for dependency collection. + */ + + Watcher.prototype.beforeGet = function () { + Dep.target = this; + this.newDeps = Object.create(null); + }; + + /** + * Clean up for dependency collection. + */ + + Watcher.prototype.afterGet = function () { + Dep.target = null; + var ids = Object.keys(this.deps); + var i = ids.length; + while (i--) { + var id = ids[i]; + if (!this.newDeps[id]) { + this.deps[id].removeSub(this); + } + } + this.deps = this.newDeps; + }; + + /** + * Subscriber interface. + * Will be called when a dependency changes. + * + * @param {Boolean} shallow + */ + + Watcher.prototype.update = function (shallow) { + if (this.lazy) { + this.dirty = true; + } else if (this.sync || !config.async) { + this.run(); + } else { + // if queued, only overwrite shallow with non-shallow, + // but not the other way around. + this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow; + this.queued = true; + // record before-push error stack in debug mode + /* istanbul ignore if */ + if ('development' !== 'production' && config.debug) { + this.prevError = new Error('[vue] async stack trace'); + } + pushWatcher(this); + } + }; + + /** + * Batcher job interface. + * Will be called by the batcher. + */ + + Watcher.prototype.run = function () { + if (this.active) { + var value = this.get(); + if (value !== this.value || + // Deep watchers and watchers on Object/Arrays should fire even + // when the value is the same, because the value may + // have mutated; but only do so if this is a + // non-shallow update (caused by a vm digest). + (isObject(value) || this.deep) && !this.shallow) { + // set new value + var oldValue = this.value; + this.value = value; + // in debug + async mode, when a watcher callbacks + // throws, we also throw the saved before-push error + // so the full cross-tick stack trace is available. + var prevError = this.prevError; + /* istanbul ignore if */ + if ('development' !== 'production' && config.debug && prevError) { + this.prevError = null; + try { + this.cb.call(this.vm, value, oldValue); + } catch (e) { + nextTick(function () { + throw prevError; + }, 0); + throw e; + } + } else { + this.cb.call(this.vm, value, oldValue); + } + } + this.queued = this.shallow = false; + } + }; + + /** + * Evaluate the value of the watcher. + * This only gets called for lazy watchers. + */ + + Watcher.prototype.evaluate = function () { + // avoid overwriting another watcher that is being + // collected. + var current = Dep.target; + this.value = this.get(); + this.dirty = false; + Dep.target = current; + }; + + /** + * Depend on all deps collected by this watcher. + */ + + Watcher.prototype.depend = function () { + var depIds = Object.keys(this.deps); + var i = depIds.length; + while (i--) { + this.deps[depIds[i]].depend(); + } + }; + + /** + * Remove self from all dependencies' subcriber list. + */ + + Watcher.prototype.teardown = function () { + if (this.active) { + // remove self from vm's watcher list + // we can skip this if the vm if being destroyed + // which can improve teardown performance. + if (!this.vm._isBeingDestroyed) { + this.vm._watchers.$remove(this); + } + var depIds = Object.keys(this.deps); + var i = depIds.length; + while (i--) { + this.deps[depIds[i]].removeSub(this); + } + this.active = false; + this.vm = this.cb = this.value = null; + } + }; + + /** + * Recrusively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + * + * @param {*} val + */ + + function traverse(val) { + var i, keys; + if (isArray(val)) { + i = val.length; + while (i--) traverse(val[i]); + } else if (isObject(val)) { + keys = Object.keys(val); + i = keys.length; + while (i--) traverse(val[keys[i]]); + } + } + + var cloak = { + bind: function bind() { + var el = this.el; + this.vm.$once('pre-hook:compiled', function () { + el.removeAttribute('v-cloak'); + }); + } + }; + + var ref = { + bind: function bind() { + 'development' !== 'production' && warn('v-ref:' + this.arg + ' must be used on a child ' + 'component. Found on <' + this.el.tagName.toLowerCase() + '>.'); + } + }; + + var ON = 700; + var MODEL = 800; + var BIND = 850; + var TRANSITION = 1100; + var EL = 1500; + var COMPONENT = 1500; + var PARTIAL = 1750; + var SLOT = 1750; + var FOR = 2000; + var IF = 2000; + + var el = { + + priority: EL, + + bind: function bind() { + /* istanbul ignore if */ + if (!this.arg) { + return; + } + var id = this.id = camelize(this.arg); + var refs = (this._scope || this.vm).$els; + if (hasOwn(refs, id)) { + refs[id] = this.el; + } else { + defineReactive(refs, id, this.el); + } + }, + + unbind: function unbind() { + var refs = (this._scope || this.vm).$els; + if (refs[this.id] === this.el) { + refs[this.id] = null; + } + } + }; + + var prefixes = ['-webkit-', '-moz-', '-ms-']; + var camelPrefixes = ['Webkit', 'Moz', 'ms']; + var importantRE = /!important;?$/; + var propCache = Object.create(null); + + var testEl = null; + + var style = { + + deep: true, + + update: function update(value) { + if (typeof value === 'string') { + this.el.style.cssText = value; + } else if (isArray(value)) { + this.handleObject(value.reduce(extend, {})); + } else { + this.handleObject(value || {}); + } + }, + + handleObject: function handleObject(value) { + // cache object styles so that only changed props + // are actually updated. + var cache = this.cache || (this.cache = {}); + var name, val; + for (name in cache) { + if (!(name in value)) { + this.handleSingle(name, null); + delete cache[name]; + } + } + for (name in value) { + val = value[name]; + if (val !== cache[name]) { + cache[name] = val; + this.handleSingle(name, val); + } + } + }, + + handleSingle: function handleSingle(prop, value) { + prop = normalize(prop); + if (!prop) return; // unsupported prop + // cast possible numbers/booleans into strings + if (value != null) value += ''; + if (value) { + var isImportant = importantRE.test(value) ? 'important' : ''; + if (isImportant) { + value = value.replace(importantRE, '').trim(); + } + this.el.style.setProperty(prop, value, isImportant); + } else { + this.el.style.removeProperty(prop); + } + } + + }; + + /** + * Normalize a CSS property name. + * - cache result + * - auto prefix + * - camelCase -> dash-case + * + * @param {String} prop + * @return {String} + */ + + function normalize(prop) { + if (propCache[prop]) { + return propCache[prop]; + } + var res = prefix(prop); + propCache[prop] = propCache[res] = res; + return res; + } + + /** + * Auto detect the appropriate prefix for a CSS property. + * https://gist.github.com/paulirish/523692 + * + * @param {String} prop + * @return {String} + */ + + function prefix(prop) { + prop = hyphenate(prop); + var camel = camelize(prop); + var upper = camel.charAt(0).toUpperCase() + camel.slice(1); + if (!testEl) { + testEl = document.createElement('div'); + } + if (camel in testEl.style) { + return prop; + } + var i = prefixes.length; + var prefixed; + while (i--) { + prefixed = camelPrefixes[i] + upper; + if (prefixed in testEl.style) { + return prefixes[i] + prop; + } + } + } + + // xlink + var xlinkNS = 'http://www.w3.org/1999/xlink'; + var xlinkRE = /^xlink:/; + + // check for attributes that prohibit interpolations + var disallowedInterpAttrRE = /^v-|^:|^@|^(is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/; + + // these attributes should also set their corresponding properties + // because they only affect the initial state of the element + var attrWithPropsRE = /^(value|checked|selected|muted)$/; + + // these attributes should set a hidden property for + // binding v-model to object values + var modelProps = { + value: '_value', + 'true-value': '_trueValue', + 'false-value': '_falseValue' + }; + + var bind = { + + priority: BIND, + + bind: function bind() { + var attr = this.arg; + var tag = this.el.tagName; + // should be deep watch on object mode + if (!attr) { + this.deep = true; + } + // handle interpolation bindings + if (this.descriptor.interp) { + // only allow binding on native attributes + if (disallowedInterpAttrRE.test(attr) || attr === 'name' && (tag === 'PARTIAL' || tag === 'SLOT')) { + 'development' !== 'production' && warn(attr + '="' + this.descriptor.raw + '": ' + 'attribute interpolation is not allowed in Vue.js ' + 'directives and special attributes.'); + this.el.removeAttribute(attr); + this.invalid = true; + } + + /* istanbul ignore if */ + if ('development' !== 'production') { + var raw = attr + '="' + this.descriptor.raw + '": '; + // warn src + if (attr === 'src') { + warn(raw + 'interpolation in "src" attribute will cause ' + 'a 404 request. Use v-bind:src instead.'); + } + + // warn style + if (attr === 'style') { + warn(raw + 'interpolation in "style" attribute will cause ' + 'the attribute to be discarded in Internet Explorer. ' + 'Use v-bind:style instead.'); + } + } + } + }, + + update: function update(value) { + if (this.invalid) { + return; + } + var attr = this.arg; + if (this.arg) { + this.handleSingle(attr, value); + } else { + this.handleObject(value || {}); + } + }, + + // share object handler with v-bind:class + handleObject: style.handleObject, + + handleSingle: function handleSingle(attr, value) { + var el = this.el; + var interp = this.descriptor.interp; + if (!interp && attrWithPropsRE.test(attr) && attr in el) { + el[attr] = attr === 'value' ? value == null // IE9 will set input.value to "null" for null... + ? '' : value : value; + } + // set model props + var modelProp = modelProps[attr]; + if (!interp && modelProp) { + el[modelProp] = value; + // update v-model if present + var model = el.__v_model; + if (model) { + model.listener(); + } + } + // do not set value attribute for textarea + if (attr === 'value' && el.tagName === 'TEXTAREA') { + el.removeAttribute(attr); + return; + } + // update attribute + if (value != null && value !== false) { + if (attr === 'class') { + // handle edge case #1960: + // class interpolation should not overwrite Vue transition class + if (el.__v_trans) { + value += ' ' + el.__v_trans.id + '-transition'; + } + setClass(el, value); + } else if (xlinkRE.test(attr)) { + el.setAttributeNS(xlinkNS, attr, value); + } else { + el.setAttribute(attr, value); + } + } else { + el.removeAttribute(attr); + } + } + }; + + // keyCode aliases + var keyCodes = { + esc: 27, + tab: 9, + enter: 13, + space: 32, + 'delete': 46, + up: 38, + left: 37, + right: 39, + down: 40 + }; + + function keyFilter(handler, keys) { + var codes = keys.map(function (key) { + var charCode = key.charCodeAt(0); + if (charCode > 47 && charCode < 58) { + return parseInt(key, 10); + } + if (key.length === 1) { + charCode = key.toUpperCase().charCodeAt(0); + if (charCode > 64 && charCode < 91) { + return charCode; + } + } + return keyCodes[key]; + }); + return function keyHandler(e) { + if (codes.indexOf(e.keyCode) > -1) { + return handler.call(this, e); + } + }; + } + + function stopFilter(handler) { + return function stopHandler(e) { + e.stopPropagation(); + return handler.call(this, e); + }; + } + + function preventFilter(handler) { + return function preventHandler(e) { + e.preventDefault(); + return handler.call(this, e); + }; + } + + var on = { + + acceptStatement: true, + priority: ON, + + bind: function bind() { + // deal with iframes + if (this.el.tagName === 'IFRAME' && this.arg !== 'load') { + var self = this; + this.iframeBind = function () { + on$1(self.el.contentWindow, self.arg, self.handler); + }; + this.on('load', this.iframeBind); + } + }, + + update: function update(handler) { + // stub a noop for v-on with no value, + // e.g. @mousedown.prevent + if (!this.descriptor.raw) { + handler = function () {}; + } + + if (typeof handler !== 'function') { + 'development' !== 'production' && warn('v-on:' + this.arg + '="' + this.expression + '" expects a function value, ' + 'got ' + handler); + return; + } + + // apply modifiers + if (this.modifiers.stop) { + handler = stopFilter(handler); + } + if (this.modifiers.prevent) { + handler = preventFilter(handler); + } + // key filter + var keys = Object.keys(this.modifiers).filter(function (key) { + return key !== 'stop' && key !== 'prevent'; + }); + if (keys.length) { + handler = keyFilter(handler, keys); + } + + this.reset(); + this.handler = handler; + + if (this.iframeBind) { + this.iframeBind(); + } else { + on$1(this.el, this.arg, this.handler); + } + }, + + reset: function reset() { + var el = this.iframeBind ? this.el.contentWindow : this.el; + if (this.handler) { + off(el, this.arg, this.handler); + } + }, + + unbind: function unbind() { + this.reset(); + } + }; + + var checkbox = { + + bind: function bind() { + var self = this; + var el = this.el; + + this.getValue = function () { + return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value; + }; + + function getBooleanValue() { + var val = el.checked; + if (val && el.hasOwnProperty('_trueValue')) { + return el._trueValue; + } + if (!val && el.hasOwnProperty('_falseValue')) { + return el._falseValue; + } + return val; + } + + this.listener = function () { + var model = self._watcher.value; + if (isArray(model)) { + var val = self.getValue(); + if (el.checked) { + if (indexOf(model, val) < 0) { + model.push(val); + } + } else { + model.$remove(val); + } + } else { + self.set(getBooleanValue()); + } + }; + + this.on('change', this.listener); + if (el.hasAttribute('checked')) { + this.afterBind = this.listener; + } + }, + + update: function update(value) { + var el = this.el; + if (isArray(value)) { + el.checked = indexOf(value, this.getValue()) > -1; + } else { + if (el.hasOwnProperty('_trueValue')) { + el.checked = looseEqual(value, el._trueValue); + } else { + el.checked = !!value; + } + } + } + }; + + var select = { + + bind: function bind() { + var self = this; + var el = this.el; + + // method to force update DOM using latest value. + this.forceUpdate = function () { + if (self._watcher) { + self.update(self._watcher.get()); + } + }; + + // check if this is a multiple select + var multiple = this.multiple = el.hasAttribute('multiple'); + + // attach listener + this.listener = function () { + var value = getValue(el, multiple); + value = self.params.number ? isArray(value) ? value.map(toNumber) : toNumber(value) : value; + self.set(value); + }; + this.on('change', this.listener); + + // if has initial value, set afterBind + var initValue = getValue(el, multiple, true); + if (multiple && initValue.length || !multiple && initValue !== null) { + this.afterBind = this.listener; + } + + // All major browsers except Firefox resets + // selectedIndex with value -1 to 0 when the element + // is appended to a new parent, therefore we have to + // force a DOM update whenever that happens... + this.vm.$on('hook:attached', this.forceUpdate); + }, + + update: function update(value) { + var el = this.el; + el.selectedIndex = -1; + var multi = this.multiple && isArray(value); + var options = el.options; + var i = options.length; + var op, val; + while (i--) { + op = options[i]; + val = op.hasOwnProperty('_value') ? op._value : op.value; + /* eslint-disable eqeqeq */ + op.selected = multi ? indexOf$1(value, val) > -1 : looseEqual(value, val); + /* eslint-enable eqeqeq */ + } + }, + + unbind: function unbind() { + /* istanbul ignore next */ + this.vm.$off('hook:attached', this.forceUpdate); + } + }; + + /** + * Get select value + * + * @param {SelectElement} el + * @param {Boolean} multi + * @param {Boolean} init + * @return {Array|*} + */ + + function getValue(el, multi, init) { + var res = multi ? [] : null; + var op, val, selected; + for (var i = 0, l = el.options.length; i < l; i++) { + op = el.options[i]; + selected = init ? op.hasAttribute('selected') : op.selected; + if (selected) { + val = op.hasOwnProperty('_value') ? op._value : op.value; + if (multi) { + res.push(val); + } else { + return val; + } + } + } + return res; + } + + /** + * Native Array.indexOf uses strict equal, but in this + * case we need to match string/numbers with custom equal. + * + * @param {Array} arr + * @param {*} val + */ + + function indexOf$1(arr, val) { + var i = arr.length; + while (i--) { + if (looseEqual(arr[i], val)) { + return i; + } + } + return -1; + } + + var radio = { + + bind: function bind() { + var self = this; + var el = this.el; + + this.getValue = function () { + // value overwrite via v-bind:value + if (el.hasOwnProperty('_value')) { + return el._value; + } + var val = el.value; + if (self.params.number) { + val = toNumber(val); + } + return val; + }; + + this.listener = function () { + self.set(self.getValue()); + }; + this.on('change', this.listener); + + if (el.hasAttribute('checked')) { + this.afterBind = this.listener; + } + }, + + update: function update(value) { + this.el.checked = looseEqual(value, this.getValue()); + } + }; + + var text$2 = { + + bind: function bind() { + var self = this; + var el = this.el; + var isRange = el.type === 'range'; + var lazy = this.params.lazy; + var number = this.params.number; + var debounce = this.params.debounce; + + // handle composition events. + // http://blog.evanyou.me/2014/01/03/composition-event/ + // skip this for Android because it handles composition + // events quite differently. Android doesn't trigger + // composition events for language input methods e.g. + // Chinese, but instead triggers them for spelling + // suggestions... (see Discussion/#162) + var composing = false; + if (!isAndroid && !isRange) { + this.on('compositionstart', function () { + composing = true; + }); + this.on('compositionend', function () { + composing = false; + // in IE11 the "compositionend" event fires AFTER + // the "input" event, so the input handler is blocked + // at the end... have to call it here. + // + // #1327: in lazy mode this is unecessary. + if (!lazy) { + self.listener(); + } + }); + } + + // prevent messing with the input when user is typing, + // and force update on blur. + this.focused = false; + if (!isRange) { + this.on('focus', function () { + self.focused = true; + }); + this.on('blur', function () { + self.focused = false; + // do not sync value after fragment removal (#2017) + if (!self._frag || self._frag.inserted) { + self.rawListener(); + } + }); + } + + // Now attach the main listener + this.listener = this.rawListener = function () { + if (composing || !self._bound) { + return; + } + var val = number || isRange ? toNumber(el.value) : el.value; + self.set(val); + // force update on next tick to avoid lock & same value + // also only update when user is not typing + nextTick(function () { + if (self._bound && !self.focused) { + self.update(self._watcher.value); + } + }); + }; + + // apply debounce + if (debounce) { + this.listener = _debounce(this.listener, debounce); + } + + // Support jQuery events, since jQuery.trigger() doesn't + // trigger native events in some cases and some plugins + // rely on $.trigger() + // + // We want to make sure if a listener is attached using + // jQuery, it is also removed with jQuery, that's why + // we do the check for each directive instance and + // store that check result on itself. This also allows + // easier test coverage control by unsetting the global + // jQuery variable in tests. + this.hasjQuery = typeof jQuery === 'function'; + if (this.hasjQuery) { + jQuery(el).on('change', this.listener); + if (!lazy) { + jQuery(el).on('input', this.listener); + } + } else { + this.on('change', this.listener); + if (!lazy) { + this.on('input', this.listener); + } + } + + // IE9 doesn't fire input event on backspace/del/cut + if (!lazy && isIE9) { + this.on('cut', function () { + nextTick(self.listener); + }); + this.on('keyup', function (e) { + if (e.keyCode === 46 || e.keyCode === 8) { + self.listener(); + } + }); + } + + // set initial value if present + if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) { + this.afterBind = this.listener; + } + }, + + update: function update(value) { + this.el.value = _toString(value); + }, + + unbind: function unbind() { + var el = this.el; + if (this.hasjQuery) { + jQuery(el).off('change', this.listener); + jQuery(el).off('input', this.listener); + } + } + }; + + var handlers = { + text: text$2, + radio: radio, + select: select, + checkbox: checkbox + }; + + var model = { + + priority: MODEL, + twoWay: true, + handlers: handlers, + params: ['lazy', 'number', 'debounce'], + + /** + * Possible elements: + *