var _ = require('../util') var config = require('../config') var textParser = require('../parsers/text') var dirParser = require('../parsers/directive') var templateParser = require('../parsers/template') /** * Compile a template and return a reusable composite link * function, which recursively contains more link functions * inside. This top level compile function should only be * called on instance root nodes. * * When the `asParent` flag is true, this means we are doing * a partial compile for a component's parent scope markup * (See #502). This could **only** be triggered during * compilation of `v-component`, and we need to skip v-with, * v-ref & v-component in this situation. * * @param {Element|DocumentFragment} el * @param {Object} options * @param {Boolean} partial * @param {Boolean} asParent * @return {Function} */ module.exports = function compile (el, options, partial, asParent) { var params = !partial && options.paramAttributes var paramsLinkFn = params ? compileParamAttributes(el, params, options) : null var nodeLinkFn = el instanceof DocumentFragment ? null : compileNode(el, options, asParent) var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && el.tagName !== 'SCRIPT' && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null /** * A 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 link (vm, el) { var originalDirCount = vm._directives.length if (paramsLinkFn) paramsLinkFn(vm, el) if (nodeLinkFn) nodeLinkFn(vm, el) if (childLinkFn) childLinkFn(vm, el.childNodes) /** * If this is a partial compile, the linker function * returns an unlink function that tearsdown all * directives instances generated during the partial * linking. */ if (partial) { var dirs = vm._directives.slice(originalDirCount) return function unlink () { var i = dirs.length while (i--) { dirs[i]._teardown() } i = vm._directives.indexOf(dirs[0]) vm._directives.splice(i, dirs.length) } } } } /** * Compile a node and return a nodeLinkFn based on the * node type. * * @param {Node} node * @param {Object} options * @param {Boolean} asParent * @return {Function|undefined} */ function compileNode (node, options, asParent) { var type = node.nodeType if (type === 1 && node.tagName !== 'SCRIPT') { return compileElement(node, options, asParent) } else if (type === 3 && config.interpolate) { return compileTextNode(node, options) } } /** * Compile an element and return a nodeLinkFn. * * @param {Element} el * @param {Object} options * @param {Boolean} asParent * @return {Function|null} */ function compileElement (el, options, asParent) { var linkFn, tag, component // check custom element component, but only on non-root if (!asParent && !el.__vue__) { tag = el.tagName.toLowerCase() component = tag.indexOf('-') > 0 && options.components[tag] if (component) { el.setAttribute(config.prefix + 'component', tag) } } if (component || el.hasAttributes()) { // check terminal direcitves if (!asParent) { linkFn = checkTerminalDirectives(el, options) } // if not terminal, build normal link function if (!linkFn) { var dirs = collectDirectives(el, options, asParent) linkFn = dirs.length ? makeDirectivesLinkFn(dirs) : null } } // 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 } /** * Build a multi-directive link function. * * @param {Array} directives * @return {Function} directivesLinkFn */ function makeDirectivesLinkFn (directives) { return function directivesLinkFn (vm, el) { // 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) } } } } } /** * 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.nodeValue) 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 if (token.partial) { el = document.createComment('v-partial') setTokenType('partial') } 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 = 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.nodeValue = 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) { // stablize nodes nodes = _.toArray(nodes) 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++] if (nodeLinkFn) { nodeLinkFn(vm, node) } if (childrenLinkFn) { childrenLinkFn(vm, node.childNodes) } } } } /** * Compile param attributes on a root element and return * a paramAttributes link function. * * @param {Element} el * @param {Array} attrs * @param {Object} options * @return {Function} paramsLinkFn */ function compileParamAttributes (el, attrs, options) { var params = [] var i = attrs.length var name, value, param while (i--) { name = attrs[i] value = el.getAttribute(name) if (value !== null) { param = { name: name, value: value } var tokens = textParser.parse(value) if (tokens) { el.removeAttribute(name) if (tokens.length > 1) { _.warn( 'Invalid param attribute binding: "' + name + '="' + value + '"' + '\nDon\'t mix binding tags with plain text ' + 'in param attribute bindings.' ) continue } else { param.dynamic = true param.value = tokens[0].value } } params.push(param) } } return makeParamsLinkFn(params, options) } /** * Build a function that applies param attributes to a vm. * * @param {Array} params * @param {Object} options * @return {Function} paramsLinkFn */ var dataAttrRE = /^data-/ function makeParamsLinkFn (params, options) { var def = options.directives['with'] return function paramsLinkFn (vm, el) { var i = params.length var param, path while (i--) { param = params[i] // params could contain dashes, which will be // interpreted as minus calculations by the parser // so we need to wrap the path here path = _.camelize(param.name.replace(dataAttrRE, '')) if (param.dynamic) { // dynamic param attribtues are bound as v-with. // we can directly duck the descriptor here beacuse // param attributes cannot use expressions or // filters. vm._bindDir('with', el, { arg: path, expression: param.value }, def) } else { // just set once vm.$set(path, param.value) } } } } /** * 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 */ var terminalDirectives = [ 'repeat', 'if', 'component' ] function skip () {} skip.terminal = true function checkTerminalDirectives (el, options) { if (_.attr(el, 'pre') !== null) { return skip } var value, dirName /* jshint boss: true */ for (var i = 0; i < 3; i++) { dirName = terminalDirectives[i] if (value = _.attr(el, dirName)) { return makeTeriminalLinkFn(el, dirName, value, options) } } } /** * Build a link function for a terminal directive. * * @param {Element} el * @param {String} dirName * @param {String} value * @param {Object} options * @return {Function} terminalLinkFn */ function makeTeriminalLinkFn (el, dirName, value, options) { var descriptor = dirParser.parse(value)[0] var def = options.directives[dirName] var terminalLinkFn = function (vm, el) { vm._bindDir(dirName, el, descriptor, def) } terminalLinkFn.terminal = true return terminalLinkFn } /** * Collect the directives on an element. * * @param {Element} el * @param {Object} options * @param {Boolean} asParent * @return {Array} */ function collectDirectives (el, options, asParent) { var attrs = _.toArray(el.attributes) var i = attrs.length var dirs = [] var attr, attrName, dir, dirName, dirDef while (i--) { attr = attrs[i] attrName = attr.name if (attrName.indexOf(config.prefix) === 0) { dirName = attrName.slice(config.prefix.length) if (asParent && (dirName === 'with' || dirName === 'ref' || dirName === 'component')) { continue } dirDef = options.directives[dirName] _.assertAsset(dirDef, 'directive', dirName) if (dirDef) { dirs.push({ name: dirName, descriptors: dirParser.parse(attr.value), def: dirDef }) } } else if (config.interpolate) { dir = collectAttrDirective(el, attrName, attr.value, options) if (dir) { dirs.push(dir) } } } // sort by priority, LOW to HIGH dirs.sort(directiveComparator) return dirs } /** * Check an attribute for potential dynamic bindings, * and return a directive object. * * @param {Element} el * @param {String} name * @param {String} value * @param {Object} options * @return {Object} */ function collectAttrDirective (el, name, value, options) { var tokens = textParser.parse(value) if (tokens) { var def = options.directives.attr 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 value = textParser.tokensToExp(tokens, vm) var desc = dirParser.parse(name + ':' + value)[0] vm._bindDir('attr', 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 }