/* @flow */ import he from 'he' import { parseHTML } from './html-parser' import { parseText } from './text-parser' import { parseFilters } from './filter-parser' import { genAssignmentCode } from '../directives/model' import { extend, cached, no, camelize } from 'shared/util' import { isIE, isEdge, isServerRendering } from 'core/util/env' import { addProp, addAttr, baseWarn, addHandler, addDirective, getBindingAttr, getAndRemoveAttr, pluckModuleFunction } from '../helpers' export const onRE = /^@|^v-on:/ export const dirRE = /^v-|^@|^:/ export const forAliasRE = /([^]*?)\s+(?:in|of)\s+([^]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g const argRE = /:(.*)$/ export const bindRE = /^:|^v-bind:/ const modifierRE = /\.[^.]+/g const decodeHTMLCached = cached(he.decode) // configurable state export let warn: any let delimiters let transforms let preTransforms let postTransforms let platformIsPreTag let platformMustUseProp let platformGetTagNamespace type Attr = { name: string; value: string }; export function createASTElement ( tag: string, attrs: Array, parent: ASTElement | void ): ASTElement { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent, children: [] } } /** * Convert HTML string to AST. */ export function parse ( template: string, options: CompilerOptions ): ASTElement | void { warn = options.warn || baseWarn platformIsPreTag = options.isPreTag || no platformMustUseProp = options.mustUseProp || no platformGetTagNamespace = options.getTagNamespace || no transforms = pluckModuleFunction(options.modules, 'transformNode') preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') delimiters = options.delimiters const stack = [] const preserveWhitespace = options.preserveWhitespace !== false let root let currentParent let inVPre = false let inPre = false let warned = false function warnOnce (msg) { if (!warned) { warned = true warn(msg) } } function closeElement (element) { // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, start (tag, attrs, unary) { // check namespace. // inherit parent ns if there is one const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag) // handle IE svg bug /* istanbul ignore if */ if (isIE && ns === 'svg') { attrs = guardIESVGBug(attrs) } let element: ASTElement = createASTElement(tag, attrs, currentParent) if (ns) { element.ns = ns } if (isForbiddenTag(element) && !isServerRendering()) { element.forbidden = true process.env.NODE_ENV !== 'production' && warn( 'Templates should only be responsible for mapping the state to the ' + 'UI. Avoid placing tags with side-effects in your templates, such as ' + `<${tag}>` + ', as they will not be parsed.' ) } // apply pre-transforms for (let i = 0; i < preTransforms.length; i++) { element = preTransforms[i](element, options) || element } if (!inVPre) { processPre(element) if (element.pre) { inVPre = true } } if (platformIsPreTag(element.tag)) { inPre = true } if (inVPre) { processRawAttrs(element) } else if (!element.processed) { // structural directives processFor(element) processIf(element) processOnce(element) // element-scope stuff processElement(element, options) } function checkRootConstraints (el) { if (process.env.NODE_ENV !== 'production') { if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.' ) } if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.' ) } } } // tree management if (!root) { root = element checkRootConstraints(root) } else if (!stack.length) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { checkRootConstraints(element) addIfCondition(root, { exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.` ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else if (element.slotScope) { // scoped slot currentParent.plain = false const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } else { currentParent.children.push(element) element.parent = currentParent } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }, end () { // remove trailing whitespace const element = stack[stack.length - 1] const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop() } // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] closeElement(element) }, chars (text: string) { if (!currentParent) { if (process.env.NODE_ENV !== 'production') { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.' ) } else if ((text = text.trim())) { warnOnce( `text "${text}" outside root element will be ignored.` ) } } return } // IE textarea placeholder bug /* istanbul ignore if */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } const children = currentParent.children text = inPre || text.trim() ? isTextTag(currentParent) ? text : decodeHTMLCached(text) // only preserve whitespace if its not right after a starting tag : preserveWhitespace && children.length ? ' ' : '' if (text) { let res if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text }) } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text }) } } }, comment (text: string) { currentParent.children.push({ type: 3, text, isComment: true }) } }) return root } function processPre (el) { if (getAndRemoveAttr(el, 'v-pre') != null) { el.pre = true } } function processRawAttrs (el) { const l = el.attrsList.length if (l) { const attrs = el.attrs = new Array(l) for (let i = 0; i < l; i++) { attrs[i] = { name: el.attrsList[i].name, value: JSON.stringify(el.attrsList[i].value) } } } else if (!el.pre) { // non root node in pre blocks with no attributes el.plain = true } } export function processElement (element: ASTElement, options: CompilerOptions) { processKey(element) // determine whether this is a plain element after // removing structural attributes element.plain = !element.key && !element.attrsList.length processRef(element) processSlot(element) processComponent(element) for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } processAttrs(element) } function processKey (el) { const exp = getBindingAttr(el, 'key') if (exp) { if (process.env.NODE_ENV !== 'production' && el.tag === 'template') { warn(`