import publicDirectives from '../directives/public/index'
import internalDirectives from '../directives/internal/index'
import { compileProps } from './compile-props'
import { parseText, tokensToExp } from '../parsers/text'
import { parseDirective } from '../parsers/directive'
import { parseTemplate } from '../parsers/template'
import {
_toString,
resolveAsset,
toArray,
warn,
remove,
replace,
commonTagRE,
checkComponentAttr,
findRef,
defineReactive,
getAttr
} from '../util/index'
// special binding prefixes
const bindRE = /^v-bind:|^:/
const onRE = /^v-on:|^@/
const dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/
const modifierRE = /\.[^\.]+/g
const transitionRE = /^(v-bind:|:)?transition$/
// default directive priority
const DEFAULT_PRIORITY = 1000
const DEFAULT_TERMINAL_PRIORITY = 2000
/**
* 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}
*/
export function compile (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) &&
!isScript(el) &&
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) {
/* istanbul ignore if */
if (process.env.NODE_ENV === 'production') {
// reset directives before every capture in production
// mode, so that when unlinking we don't need to splice
// them out (which turns out to be a perf hit).
// they are kept in development mode because they are
// useful for Vue's own tests.
vm._directives = []
}
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) {
function unlink (destroying) {
teardownDirs(vm, dirs, destroying)
if (context && contextDirs) {
teardownDirs(context, contextDirs)
}
}
// expose linked directives
unlink.dirs = dirs
return unlink
}
/**
* 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 (process.env.NODE_ENV !== 'production' && !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}
*/
export function compileAndLinkProps (vm, el, props, scope) {
var propsLinkFn = compileProps(el, props, vm)
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 {Element} el
* @param {Object} options
* @param {Object} contextOptions
* @return {Function}
*/
export function compileRoot (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'
)
}
}
options._containerAttrs = options._replacerAttrs = null
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 && !isScript(node)) {
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 = parseText(el.value)
if (tokens) {
el.setAttribute(':value', tokensToExp(tokens))
el.value = ''
}
}
var linkFn
var hasAttrs = el.hasAttributes()
var attrs = hasAttrs && toArray(el.attributes)
// check terminal directives (for & if)
if (hasAttrs) {
linkFn = checkTerminalDirectives(el, attrs, 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(attrs, 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 = parseText(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 = parseDirective(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<Object>} 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, parseTemplate(value, true))
} else {
node.data = _toString(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<Function>} 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 = checkComponentAttr(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 {Array} attrs
* @param {Object} options
* @return {Function} terminalLinkFn
*/
function checkTerminalDirectives (el, attrs, options) {
// skip v-pre
if (getAttr(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 attr, name, value, modifiers, matched, dirName, rawName, arg, def, termDef
for (var i = 0, j = attrs.length; i < j; i++) {
attr = attrs[i]
name = attr.name.replace(modifierRE, '')
if ((matched = name.match(dirAttrRE))) {
def = resolveAsset(options, 'directives', matched[1])
if (def && def.terminal) {
if (!termDef || ((def.priority || DEFAULT_TERMINAL_PRIORITY) > termDef.priority)) {
termDef = def
rawName = attr.name
modifiers = parseModifiers(attr.name)
value = attr.value
dirName = matched[1]
arg = matched[2]
}
}
}
}
if (termDef) {
return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers)
}
}
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
* @param {String} [rawName]
* @param {String} [arg]
* @param {Object} [modifiers]
* @return {Function} terminalLinkFn
*/
function makeTerminalNodeLinkFn (el, dirName, value, options, def, rawName, arg, modifiers) {
var parsed = parseDirective(value)
var descriptor = {
name: dirName,
arg: arg,
expression: parsed.expression,
filters: parsed.filters,
raw: value,
attr: rawName,
modifiers: modifiers,
def: def
}
// 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, matched
while (i--) {
attr = attrs[i]
name = rawName = attr.name
value = rawValue = attr.value
tokens = parseText(value)
// reset arg
arg = null
// check modifiers
modifiers = parseModifiers(name)
name = name.replace(modifierRE, '')
// attribute interpolations
if (tokens) {
value = tokensToExp(tokens)
arg = name
pushDir('bind', publicDirectives.bind, tokens)
// 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.',
options
)
}
}
} 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 ((matched = name.match(dirAttrRE))) {
dirName = matched[1]
arg = matched[2]
// skip v-else (when used with v-show)
if (dirName === 'else') {
continue
}
dirDef = resolveAsset(options, 'directives', dirName, true)
if (dirDef) {
pushDir(dirName, dirDef)
}
}
}
/**
* Push a directive.
*
* @param {String} dirName
* @param {Object|Function} def
* @param {Array} [interpTokens]
*/
function pushDir (dirName, def, interpTokens) {
var hasOneTimeToken = interpTokens && hasOneTime(interpTokens)
var parsed = !hasOneTimeToken && parseDirective(value)
dirs.push({
name: dirName,
attr: rawName,
raw: rawValue,
def: def,
arg: arg,
modifiers: modifiers,
// conversion from interpolation strings with one-time token
// to expression is differed until directive bind time so that we
// have access to the actual vm context for one-time bindings.
expression: parsed && parsed.expression,
filters: parsed && parsed.filters,
interp: interpTokens,
hasOneTime: hasOneTimeToken
})
}
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)
}
}
}
/**
* Check if an interpolation string contains one-time tokens.
*
* @param {Array} tokens
* @return {Boolean}
*/
function hasOneTime (tokens) {
var i = tokens.length
while (i--) {
if (tokens[i].oneTime) return true
}
}
function isScript (el) {
return el.tagName === 'SCRIPT' && (
!el.hasAttribute('type') ||
el.getAttribute('type') === 'text/javascript'
)
}
|