import { decodeHTML } from 'entities' import { parseHTML } from './html-parser' import { parseText } from './text-parser' import { addHandler } from '../helpers' import { hyphenate, makeMap } from '../../shared/util' const dirRE = /^v-|^@|^:/ const bindRE = /^:|^v-bind:/ const onRE = /^@|^v-on:/ const argRE = /:(.*)$/ const modifierRE = /\.[^\.]+/g const forAliasRE = /(.*)\s+(?:in|of)\s+(.*)/ const forIteratorRE = /\((.*),(.*)\)/ const camelRE = /[a-z\d][A-Z]/ // attributes that should be using props for binding const mustUseProp = makeMap('value,selected,checked,muted') // this map covers SVG elements that can appear as template root nodes const isSVG = makeMap('svg,g,defs,symbol,use,image,text,circle,ellipse,line,path,polygon,polyline,rect', true) // make warning customizable depending on environment. let warn const baseWarn = msg => console.error(`[Vue parser]: ${msg}`) /** * Convert HTML string to AST * * @param {String} template * @param {Object} options * @return {Object} */ export function parse (template, options) { options = options || {} warn = options.warn || baseWarn const stack = [] let root let currentParent let inSvg = false let svgIndex = -1 let warned = false parseHTML(template, { html5: true, start (tag, attrs, unary) { // check camelCase tag if (camelRE.test(tag)) { process.env.NODE_ENV !== 'production' && warn( `Found camelCase tag in template: <${tag}>. ` + `I've converted it to <${hyphenate(tag)}> for you.` ) tag = hyphenate(tag) } tag = tag.toLowerCase() const element = { tag, plain: !attrs.length, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent: currentParent, children: [] } // check svg if (inSvg) { element.svg = true } else if (isSVG(tag)) { element.svg = true inSvg = true svgIndex = stack.length } processFor(element) processIf(element) processRender(element) processSlot(element) processClassBinding(element) processStyleBinding(element) processAttributes(element) // tree management if (!root) { root = element } else if (process.env.NODE_ENV !== 'production' && !stack.length && !warned) { warned = true warn( `Component template should contain exactly one root element:\n\n${template}` ) } if (currentParent && tag !== 'script') { if (element.else) { processElse(element, currentParent) } else { currentParent.children.push(element) } } if (!unary) { currentParent = element stack.push(element) } }, end (tag) { // remove trailing whitespace const element = stack[stack.length - 1] const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.text === ' ') element.children.pop() // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] // check svg state if (inSvg && stack.length <= svgIndex) { inSvg = false svgIndex = -1 } }, chars (text) { if (!currentParent) { if (process.env.NODE_ENV !== 'production' && !warned) { warned = true warn( 'Component template should contain exactly one root element:\n\n' + template ) } return } text = currentParent.tag === 'pre' || text.trim() ? decodeHTML(text) // only preserve whitespace if its not right after a starting tag : options.preserveWhitespace && currentParent.children.length ? ' ' : null if (text) { let expression if (text !== ' ' && (expression = parseText(text))) { currentParent.children.push({ expression }) } else { currentParent.children.push({ text }) } } } }) return root } function processFor (el) { let exp if ((exp = getAndRemoveAttr(el, 'v-for'))) { const inMatch = exp.match(forAliasRE) if (!inMatch) { process.env.NODE_ENV !== 'production' && warn( `Invalid v-for expression: ${exp}` ) return } el.for = inMatch[2].trim() const alias = inMatch[1].trim() const iteratorMatch = alias.match(forIteratorRE) if (iteratorMatch) { el.iterator = iteratorMatch[1].trim() el.alias = iteratorMatch[2].trim() } else { el.alias = alias } if ((exp = getAndRemoveAttr(el, 'track-by'))) { el.key = exp === '$index' ? exp : el.alias + '["' + exp + '"]' } } } function processIf (el) { let exp = getAndRemoveAttr(el, 'v-if') if (exp) { el.if = exp } if (getAndRemoveAttr(el, 'v-else') != null) { el.else = true } } function processElse (el, parent) { const prev = findPrevElement(parent.children) if (prev && (prev.if || prev.attrsMap['v-show'])) { prev.elseBlock = el if (!prev.if) { parent.children.push(el) } } else if (process.env.NODE_ENV !== 'production') { warn( `v-else used on element <${el.tag}> without corresponding v-if/v-show.` ) } } function processRender (el) { if (el.tag === 'render') { el.render = true el.method = el.attrsMap.method el.args = el.attrsMap.args if (process.env.NODE_ENV !== 'production' && !el.method) { warn('method attribute is required on .') } } } function processSlot (el) { if (el.tag === 'slot') { el.name = el.attrsMap.name el.dynamicName = el.attrsMap[':name'] || el.attrsMap['v-bind:name'] } } function processClassBinding (el) { const staticClass = getAndRemoveAttr(el, 'class') el.staticClass = parseText(staticClass) || JSON.stringify(staticClass) el.classBinding = getAndRemoveAttr(el, ':class') || getAndRemoveAttr(el, 'v-bind:class') } function processStyleBinding (el) { el.styleBinding = getAndRemoveAttr(el, ':style') || getAndRemoveAttr(el, 'v-bind:style') } function processAttributes (el) { const list = el.attrsList let i, l, name, value, arg, modifiers for (i = 0, l = list.length; i < l; i++) { name = list[i].name value = list[i].value if (dirRE.test(name)) { // modifiers modifiers = parseModifiers(name) if (modifiers) { name = name.replace(modifierRE, '') } if (bindRE.test(name)) { // v-bind name = name.replace(bindRE, '') if (mustUseProp(name)) { (el.props || (el.props = [])).push({ name, value }) } else { (el.attrs || (el.attrs = [])).push({ name, value }) } } else if (onRE.test(name)) { // v-on name = name.replace(onRE, '') addHandler((el.events || (el.events = {})), name, value, modifiers) } else { // normal directives name = name.replace(dirRE, '') // parse arg if ((arg = name.match(argRE)) && (arg = arg[1])) { name = name.slice(0, -(arg.length + 1)) } ;(el.directives || (el.directives = [])).push({ name, value, arg, modifiers }) } } else { // literal attribute (el.attrs || (el.attrs = [])).push({ name, value: parseText(value) || JSON.stringify(value) }) } } } function parseModifiers (name) { const match = name.match(modifierRE) if (match) { const ret = {} match.forEach(m => { ret[m.slice(1)] = true }) return ret } } function makeAttrsMap (attrs) { const map = {} for (let i = 0, l = attrs.length; i < l; i++) { if (process.env.NODE_ENV !== 'production' && map[attrs[i].name]) { warn('duplicate attribute: ' + attrs[i].name) } map[attrs[i].name] = attrs[i].value } return map } function getAndRemoveAttr (el, attr) { let val if ((val = el.attrsMap[attr]) != null) { el.attrsMap[attr] = null const list = el.attrsList for (let i = 0, l = list.length; i < l; i++) { if (list[i].name === attr) { list.splice(i, 1) break } } } return val } function findPrevElement (children) { let i = children.length while (i--) { if (children[i].tag) return children[i] } }