var _ = require('../util') var publicDirectives = require('../directives/public') var internalDirectives = require('../directives/internal') var compileProps = require('./compile-props') var textParser = require('../parsers/text') var dirParser = require('../parsers/directive') var templateParser = require('../parsers/template') var resolveAsset = _.resolveAsset // special binding prefixes var bindRE = /^v-bind:|^:/ var onRE = /^v-on:|^@/ var argRE = /:(.*)$/ var modifierRE = /\.[^\.]+/g var transitionRE = /^(v-bind:|:)?transition$/ // terminal directives var terminalDirectives = [ 'for', 'if' ] // default directive priority var DEFAULT_PRIORITY = 1000 /** * Compile a template and return a reusable composite link * function, which recursively contains more link functions * inside. This top level compile function would normally * be called on instance root nodes, but can also be used * for partial compilation if the partial argument is true. * * The returned composite link function, when called, will * return an unlink function that tearsdown all directives * created during the linking phase. * * @param {Element|DocumentFragment} el * @param {Object} options * @param {Boolean} partial * @return {Function} */ exports.compile = function (el, options, partial) { // link function for the node itself. var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null // link function for the childNodes var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && el.tagName !== 'SCRIPT' && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null /** * A composite linker function to be called on a already * compiled piece of DOM, which instantiates all directive * instances. * * @param {Vue} vm * @param {Element|DocumentFragment} el * @param {Vue} [host] - host vm of transcluded content * @param {Object} [scope] - v-for scope * @param {Fragment} [frag] - link context fragment * @return {Function|undefined} */ return function compositeLinkFn (vm, el, host, scope, frag) { // cache childNodes before linking parent, fix #657 var childNodes = _.toArray(el.childNodes) // link var dirs = linkAndCapture(function compositeLinkCapturer () { if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag) if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag) }, vm) return makeUnlinkFn(vm, dirs) } } /** * Apply a linker to a vm/element pair and capture the * directives created during the process. * * @param {Function} linker * @param {Vue} vm */ function linkAndCapture (linker, vm) { var originalDirCount = vm._directives.length linker() var dirs = vm._directives.slice(originalDirCount) dirs.sort(directiveComparator) for (var i = 0, l = dirs.length; i < l; i++) { dirs[i]._bind() } return dirs } /** * Directive priority sort comparator * * @param {Object} a * @param {Object} b */ function directiveComparator (a, b) { a = a.descriptor.def.priority || DEFAULT_PRIORITY b = b.descriptor.def.priority || DEFAULT_PRIORITY return a > b ? -1 : a === b ? 0 : 1 } /** * Linker functions return an unlink function that * tearsdown all directives instances generated during * the process. * * We create unlink functions with only the necessary * information to avoid retaining additional closures. * * @param {Vue} vm * @param {Array} dirs * @param {Vue} [context] * @param {Array} [contextDirs] * @return {Function} */ function makeUnlinkFn (vm, dirs, context, contextDirs) { return function unlink (destroying) { teardownDirs(vm, dirs, destroying) if (context && contextDirs) { teardownDirs(context, contextDirs) } } } /** * Teardown partial linked directives. * * @param {Vue} vm * @param {Array} dirs * @param {Boolean} destroying */ function teardownDirs (vm, dirs, destroying) { var i = dirs.length while (i--) { dirs[i]._teardown() if (!destroying) { vm._directives.$remove(dirs[i]) } } } /** * Compile link props on an instance. * * @param {Vue} vm * @param {Element} el * @param {Object} props * @param {Object} [scope] * @return {Function} */ exports.compileAndLinkProps = function (vm, el, props, scope) { var propsLinkFn = compileProps(el, props) var propDirs = linkAndCapture(function () { propsLinkFn(vm, scope) }, vm) return makeUnlinkFn(vm, propDirs) } /** * Compile the root element of an instance. * * 1. attrs on context container (context scope) * 2. attrs on the component template root node, if * replace:true (child scope) * * If this is a fragment instance, we only need to compile 1. * * @param {Vue} vm * @param {Element} el * @param {Object} options * @param {Object} contextOptions * @return {Function} */ exports.compileRoot = function (el, options, contextOptions) { var containerAttrs = options._containerAttrs var replacerAttrs = options._replacerAttrs var contextLinkFn, replacerLinkFn // only need to compile other attributes for // non-fragment instances if (el.nodeType !== 11) { // for components, container and replacer need to be // compiled separately and linked in different scopes. if (options._asComponent) { // 2. container attributes if (containerAttrs && contextOptions) { contextLinkFn = compileDirectives(containerAttrs, contextOptions) } if (replacerAttrs) { // 3. replacer attributes replacerLinkFn = compileDirectives(replacerAttrs, options) } } else { // non-component, just compile as a normal element. replacerLinkFn = compileDirectives(el.attributes, options) } } else if (process.env.NODE_ENV !== 'production' && containerAttrs) { // warn container directives for fragment instances var names = containerAttrs .filter(function (attr) { // allow vue-loader/vueify scoped css attributes return attr.name.indexOf('_v-') < 0 && // allow event listeners !onRE.test(attr.name) && // allow slots attr.name !== 'slot' }) .map(function (attr) { return '"' + attr.name + '"' }) if (names.length) { var plural = names.length > 1 _.warn( 'Attribute' + (plural ? 's ' : ' ') + names.join(', ') + (plural ? ' are' : ' is') + ' ignored on component ' + '<' + options.el.tagName.toLowerCase() + '> because ' + 'the component is a fragment instance: ' + 'http://vuejs.org/guide/components.html#Fragment_Instance' ) } } return function rootLinkFn (vm, el, scope) { // link context scope dirs var context = vm._context var contextDirs if (context && contextLinkFn) { contextDirs = linkAndCapture(function () { contextLinkFn(context, el, null, scope) }, context) } // link self var selfDirs = linkAndCapture(function () { if (replacerLinkFn) replacerLinkFn(vm, el) }, vm) // return the unlink function that tearsdown context // container directives. return makeUnlinkFn(vm, selfDirs, context, contextDirs) } } /** * Compile a node and return a nodeLinkFn based on the * node type. * * @param {Node} node * @param {Object} options * @return {Function|null} */ function compileNode (node, options) { var type = node.nodeType if (type === 1 && node.tagName !== 'SCRIPT') { return compileElement(node, options) } else if (type === 3 && node.data.trim()) { return compileTextNode(node, options) } else { return null } } /** * Compile an element and return a nodeLinkFn. * * @param {Element} el * @param {Object} options * @return {Function|null} */ function compileElement (el, options) { // preprocess textareas. // textarea treats its text content as the initial value. // just bind it as an attr directive for value. if (el.tagName === 'TEXTAREA') { var tokens = textParser.parse(el.value) if (tokens) { el.setAttribute(':value', textParser.tokensToExp(tokens)) el.value = '' } } var linkFn var hasAttrs = el.hasAttributes() // check terminal directives (for & if) if (hasAttrs) { linkFn = checkTerminalDirectives(el, options) } // check element directives if (!linkFn) { linkFn = checkElementDirectives(el, options) } // check component if (!linkFn) { linkFn = checkComponent(el, options) } // normal directives if (!linkFn && hasAttrs) { linkFn = compileDirectives(el.attributes, options) } return linkFn } /** * Compile a textNode and return a nodeLinkFn. * * @param {TextNode} node * @param {Object} options * @return {Function|null} textNodeLinkFn */ function compileTextNode (node, options) { // skip marked text nodes if (node._skip) { return removeText } var tokens = textParser.parse(node.wholeText) if (!tokens) { return null } // mark adjacent text nodes as skipped, // because we are using node.wholeText to compile // all adjacent text nodes together. This fixes // issues in IE where sometimes it splits up a single // text node into multiple ones. var next = node.nextSibling while (next && next.nodeType === 3) { next._skip = true next = next.nextSibling } var frag = document.createDocumentFragment() var el, token for (var i = 0, l = tokens.length; i < l; i++) { token = tokens[i] el = token.tag ? processTextToken(token, options) : document.createTextNode(token.value) frag.appendChild(el) } return makeTextNodeLinkFn(tokens, frag, options) } /** * Linker for an skipped text node. * * @param {Vue} vm * @param {Text} node */ function removeText (vm, node) { _.remove(node) } /** * Process a single text token. * * @param {Object} token * @param {Object} options * @return {Node} */ function processTextToken (token, options) { var el if (token.oneTime) { el = document.createTextNode(token.value) } else { if (token.html) { el = document.createComment('v-html') setTokenType('html') } else { // IE will clean up empty textNodes during // frag.cloneNode(true), so we have to give it // something here... el = document.createTextNode(' ') setTokenType('text') } } function setTokenType (type) { if (token.descriptor) return var parsed = dirParser.parse(token.value) token.descriptor = { name: type, def: publicDirectives[type], expression: parsed.expression, filters: parsed.filters } } return el } /** * Build a function that processes a textNode. * * @param {Array} tokens * @param {DocumentFragment} frag */ function makeTextNodeLinkFn (tokens, frag) { return function textNodeLinkFn (vm, el, host, scope) { var fragClone = frag.cloneNode(true) var childNodes = _.toArray(fragClone.childNodes) var token, value, node for (var i = 0, l = tokens.length; i < l; i++) { token = tokens[i] value = token.value if (token.tag) { node = childNodes[i] if (token.oneTime) { value = (scope || vm).$eval(value) if (token.html) { _.replace(node, templateParser.parse(value, true)) } else { node.data = value } } else { vm._bindDir(token.descriptor, node, host, scope) } } } _.replace(el, fragClone) } } /** * Compile a node list and return a childLinkFn. * * @param {NodeList} nodeList * @param {Object} options * @return {Function|undefined} */ function compileNodeList (nodeList, options) { var linkFns = [] var nodeLinkFn, childLinkFn, node for (var i = 0, l = nodeList.length; i < l; i++) { node = nodeList[i] nodeLinkFn = compileNode(node, options) childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && node.tagName !== 'SCRIPT' && node.hasChildNodes() ? compileNodeList(node.childNodes, options) : null linkFns.push(nodeLinkFn, childLinkFn) } return linkFns.length ? makeChildLinkFn(linkFns) : null } /** * Make a child link function for a node's childNodes. * * @param {Array} linkFns * @return {Function} childLinkFn */ function makeChildLinkFn (linkFns) { return function childLinkFn (vm, nodes, host, scope, frag) { var node, nodeLinkFn, childrenLinkFn for (var i = 0, n = 0, l = linkFns.length; i < l; n++) { node = nodes[n] nodeLinkFn = linkFns[i++] childrenLinkFn = linkFns[i++] // cache childNodes before linking parent, fix #657 var childNodes = _.toArray(node.childNodes) if (nodeLinkFn) { nodeLinkFn(vm, node, host, scope, frag) } if (childrenLinkFn) { childrenLinkFn(vm, childNodes, host, scope, frag) } } } } /** * Check for element directives (custom elements that should * be resovled as terminal directives). * * @param {Element} el * @param {Object} options */ function checkElementDirectives (el, options) { var tag = el.tagName.toLowerCase() if (_.commonTagRE.test(tag)) return var def = resolveAsset(options, 'elementDirectives', tag) if (def) { return makeTerminalNodeLinkFn(el, tag, '', options, def) } } /** * Check if an element is a component. If yes, return * a component link function. * * @param {Element} el * @param {Object} options * @return {Function|undefined} */ function checkComponent (el, options) { var component = _.checkComponent(el, options) if (component) { var ref = _.findRef(el) var descriptor = { name: 'component', ref: ref, expression: component.id, def: internalDirectives.component, modifiers: { literal: !component.dynamic } } var componentLinkFn = function (vm, el, host, scope, frag) { if (ref) { _.defineReactive((scope || vm).$refs, ref, null) } vm._bindDir(descriptor, el, host, scope, frag) } componentLinkFn.terminal = true return componentLinkFn } } /** * Check an element for terminal directives in fixed order. * If it finds one, return a terminal link function. * * @param {Element} el * @param {Object} options * @return {Function} terminalLinkFn */ function checkTerminalDirectives (el, options) { // skip v-pre if (_.attr(el, 'v-pre') !== null) { return skip } // skip v-else block, but only if following v-if if (el.hasAttribute('v-else')) { var prev = el.previousElementSibling if (prev && prev.hasAttribute('v-if')) { return skip } } var value, dirName for (var i = 0, l = terminalDirectives.length; i < l; i++) { dirName = terminalDirectives[i] /* eslint-disable no-cond-assign */ if (value = el.getAttribute('v-' + dirName)) { return makeTerminalNodeLinkFn(el, dirName, value, options) } /* eslint-enable no-cond-assign */ } } function skip () {} skip.terminal = true /** * Build a node link function for a terminal directive. * A terminal link function terminates the current * compilation recursion and handles compilation of the * subtree in the directive. * * @param {Element} el * @param {String} dirName * @param {String} value * @param {Object} options * @param {Object} [def] * @return {Function} terminalLinkFn */ function makeTerminalNodeLinkFn (el, dirName, value, options, def) { var parsed = dirParser.parse(value) var descriptor = { name: dirName, expression: parsed.expression, filters: parsed.filters, raw: value, // either an element directive, or if/for def: def || publicDirectives[dirName] } // check ref for v-for and router-view if (dirName === 'for' || dirName === 'router-view') { descriptor.ref = _.findRef(el) } var fn = function terminalNodeLinkFn (vm, el, host, scope, frag) { if (descriptor.ref) { _.defineReactive((scope || vm).$refs, descriptor.ref, null) } vm._bindDir(descriptor, el, host, scope, frag) } fn.terminal = true return fn } /** * Compile the directives on an element and return a linker. * * @param {Array|NamedNodeMap} attrs * @param {Object} options * @return {Function} */ function compileDirectives (attrs, options) { var i = attrs.length var dirs = [] var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens while (i--) { attr = attrs[i] name = rawName = attr.name value = rawValue = attr.value tokens = textParser.parse(value) // reset arg arg = null // check modifiers modifiers = parseModifiers(name) name = name.replace(modifierRE, '') // attribute interpolations if (tokens) { value = textParser.tokensToExp(tokens) arg = name pushDir('bind', publicDirectives.bind, true) // warn against mixing mustaches with v-bind if (process.env.NODE_ENV !== 'production') { if (name === 'class' && Array.prototype.some.call(attrs, function (attr) { return attr.name === ':class' || attr.name === 'v-bind:class' })) { _.warn( 'class="' + rawValue + '": Do not mix mustache interpolation ' + 'and v-bind for "class" on the same element. Use one or the other.' ) } } } else // special attribute: transition if (transitionRE.test(name)) { modifiers.literal = !bindRE.test(name) pushDir('transition', internalDirectives.transition) } else // event handlers if (onRE.test(name)) { arg = name.replace(onRE, '') pushDir('on', publicDirectives.on) } else // attribute bindings if (bindRE.test(name)) { dirName = name.replace(bindRE, '') if (dirName === 'style' || dirName === 'class') { pushDir(dirName, internalDirectives[dirName]) } else { arg = dirName pushDir('bind', publicDirectives.bind) } } else // normal directives if (name.indexOf('v-') === 0) { // check arg arg = (arg = name.match(argRE)) && arg[1] if (arg) { name = name.replace(argRE, '') } // extract directive name dirName = name.slice(2) // skip v-else (when used with v-show) if (dirName === 'else') { continue } dirDef = resolveAsset(options, 'directives', dirName) if (process.env.NODE_ENV !== 'production') { _.assertAsset(dirDef, 'directive', dirName) } if (dirDef) { pushDir(dirName, dirDef) } } } /** * Push a directive. * * @param {String} dirName * @param {Object|Function} def * @param {Boolean} [interp] */ function pushDir (dirName, def, interp) { var parsed = dirParser.parse(value) dirs.push({ name: dirName, attr: rawName, raw: rawValue, def: def, arg: arg, modifiers: modifiers, expression: parsed.expression, filters: parsed.filters, interp: interp }) } if (dirs.length) { return makeNodeLinkFn(dirs) } } /** * Parse modifiers from directive attribute name. * * @param {String} name * @return {Object} */ function parseModifiers (name) { var res = Object.create(null) var match = name.match(modifierRE) if (match) { var i = match.length while (i--) { res[match[i].slice(1)] = true } } return res } /** * Build a link function for all directives on a single node. * * @param {Array} directives * @return {Function} directivesLinkFn */ function makeNodeLinkFn (directives) { return function nodeLinkFn (vm, el, host, scope, frag) { // reverse apply because it's sorted low to high var i = directives.length while (i--) { vm._bindDir(directives[i], el, host, scope, frag) } } }