var _ = require('../util') var compileProps = require('./compile-props') var config = require('../config') var textParser = require('../parsers/text') var dirParser = require('../parsers/directive') var templateParser = require('../parsers/template') var resolveAsset = _.resolveAsset var componentDef = require('../directives/component') // terminal directives var terminalDirectives = [ 'repeat', 'if' ] /** * 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 * @param {Vue} [host] - host vm of transcluded content * @return {Function} */ exports.compile = function (el, options, partial, host) { // 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 * @return {Function|undefined} */ return function compositeLinkFn (vm, el) { // cache childNodes before linking parent, fix #657 var childNodes = _.toArray(el.childNodes) // link var dirs = linkAndCapture(function () { if (nodeLinkFn) nodeLinkFn(vm, el, host) if (childLinkFn) childLinkFn(vm, childNodes, host) }, 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() return vm._directives.slice(originalDirCount) } /** * 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} options * @return {Function} */ exports.compileAndLinkProps = function (vm, el, props) { var propsLinkFn = compileProps(el, props) var propDirs = linkAndCapture(function () { propsLinkFn(vm, null) }, 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. * * This function does compile and link at the same time, * since root linkers can not be reused. It returns the * unlink function for potential context directives on the * container. * * @param {Vue} vm * @param {Element} el * @param {Object} options * @return {Function} */ exports.compileAndLinkRoot = function (vm, el, options) { 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) { contextLinkFn = compileDirectives(containerAttrs, options) } if (replacerAttrs) { // 3. replacer attributes replacerLinkFn = compileDirectives(replacerAttrs, options) } } else { // non-component, just compile as a normal element. replacerLinkFn = compileDirectives(el.attributes, options) } } // link context scope dirs var context = vm._context var contextDirs if (context && contextLinkFn) { contextDirs = linkAndCapture(function () { contextLinkFn(context, el) }, 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 && config.interpolate && 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) { var linkFn var hasAttrs = el.hasAttributes() // check terminal directives (repeat & 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) } // if the element is a textarea, we need to interpolate // its content on initial render. if (el.tagName === 'TEXTAREA') { var realLinkFn = linkFn linkFn = function (vm, el) { el.value = vm.$interpolate(el.value) if (realLinkFn) realLinkFn(vm, el) } linkFn.terminal = true } return linkFn } /** * Compile a textNode and return a nodeLinkFn. * * @param {TextNode} node * @param {Object} options * @return {Function|null} textNodeLinkFn */ function compileTextNode (node, options) { var tokens = textParser.parse(node.data) if (!tokens) { return null } 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) } /** * 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) { token.type = type token.def = resolveAsset(options, 'directives', type) token.descriptor = dirParser.parse(token.value)[0] } return el } /** * Build a function that processes a textNode. * * @param {Array} tokens * @param {DocumentFragment} frag */ function makeTextNodeLinkFn (tokens, frag) { return function textNodeLinkFn (vm, el) { 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 = vm.$eval(value) if (token.html) { _.replace(node, templateParser.parse(value, true)) } else { node.data = value } } else { vm._bindDir(token.type, node, token.descriptor, token.def) } } } _.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) { 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) } if (childrenLinkFn) { childrenLinkFn(vm, childNodes, host) } } } } /** * 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 * @param {Boolean} hasAttrs * @return {Function|undefined} */ function checkComponent (el, options, hasAttrs) { var componentId = _.checkComponent(el, options, hasAttrs) if (componentId) { var componentLinkFn = function (vm, el, host) { vm._bindDir('component', el, { expression: componentId }, componentDef, host) } 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) { if (_.attr(el, 'pre') !== null) { return skip } var value, dirName for (var i = 0, l = terminalDirectives.length; i < l; i++) { dirName = terminalDirectives[i] if ((value = _.attr(el, dirName)) !== null) { return makeTerminalNodeLinkFn(el, dirName, value, options) } } } 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 descriptor = dirParser.parse(value)[0] // no need to call resolveAsset since terminal directives // are always internal def = def || options.directives[dirName] var fn = function terminalNodeLinkFn (vm, el, host) { vm._bindDir(dirName, el, descriptor, def, host) } 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, dir, dirName, dirDef while (i--) { attr = attrs[i] name = attr.name value = attr.value if (name.indexOf(config.prefix) === 0) { dirName = name.slice(config.prefix.length) dirDef = resolveAsset(options, 'directives', dirName) if (process.env.NODE_ENV !== 'production') { _.assertAsset(dirDef, 'directive', dirName) } if (dirDef) { dirs.push({ name: dirName, descriptors: dirParser.parse(value), def: dirDef }) } } else if (config.interpolate) { dir = collectAttrDirective(name, value, options) if (dir) { dirs.push(dir) } } } // sort by priority, LOW to HIGH if (dirs.length) { dirs.sort(directiveComparator) return makeNodeLinkFn(dirs) } } /** * 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) { // reverse apply because it's sorted low to high var i = directives.length var dir, j, k while (i--) { dir = directives[i] if (dir._link) { // custom link fn dir._link(vm, el) } else { k = dir.descriptors.length for (j = 0; j < k; j++) { vm._bindDir(dir.name, el, dir.descriptors[j], dir.def, host) } } } } } /** * Check an attribute for potential dynamic bindings, * and return a directive object. * * Special case: class interpolations are translated into * v-class instead v-attr, so that it can work with user * provided v-class bindings. * * @param {String} name * @param {String} value * @param {Object} options * @return {Object} */ function collectAttrDirective (name, value, options) { var tokens = textParser.parse(value) var isClass = name === 'class' if (tokens) { var dirName = isClass ? 'class' : 'attr' var def = options.directives[dirName] var i = tokens.length var allOneTime = true while (i--) { var token = tokens[i] if (token.tag && !token.oneTime) { allOneTime = false } } return { def: def, _link: allOneTime ? function (vm, el) { el.setAttribute(name, vm.$interpolate(value)) } : function (vm, el) { var exp = textParser.tokensToExp(tokens, vm) var desc = isClass ? dirParser.parse(exp)[0] : dirParser.parse(name + ':' + exp)[0] if (isClass) { desc._rawClass = value } vm._bindDir(dirName, el, desc, def) } } } } /** * Directive priority sort comparator * * @param {Object} a * @param {Object} b */ function directiveComparator (a, b) { a = a.def.priority || 0 b = b.def.priority || 0 return a > b ? 1 : -1 }