/* @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, hyphenate } from 'shared/util' import { isIE, isEdge, isServerRendering } from 'core/util/env' import { addProp, addAttr, baseWarn, addHandler, addDirective, getBindingAttr, getAndRemoveAttr, getRawBindingAttr, pluckModuleFunction, getAndRemoveAttrByRegex } from '../helpers' export const onRE = /^@|^v-on:/ export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/ export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g const dynamicArgRE = /^\[.*\]$/ const argRE = /:(.*)$/ export const bindRE = /^:|^\.|^v-bind:/ const propBindRE = /^\./ const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g const slotRE = /^v-slot(:|$)|^#/ const lineBreakRE = /[\r\n]/ const whitespaceRE = /[ \f\t\r\n]+/g const invalidAttributeRE = /[\s"'<>\/=]/ const decodeHTMLCached = cached(he.decode) export const emptySlotScopeToken = `_empty_` // configurable state export let warn: any let delimiters let transforms let preTransforms let postTransforms let platformIsPreTag let platformMustUseProp let platformGetTagNamespace let maybeComponent export function createASTElement ( tag: string, attrs: Array, parent: ASTElement | void ): ASTElement { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, 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 const isReservedTag = options.isReservedTag || no maybeComponent = (el: ASTElement) => !!( el.component || el.attrsMap[':is'] || el.attrsMap['v-bind:is'] || !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag)) ) 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 const whitespaceOption = options.whitespace let root let currentParent let inVPre = false let inPre = false let warned = false function warnOnce (msg, range) { if (!warned) { warned = true warn(msg, range) } } function closeElement (element) { trimEndingWhitespace(element) if (!inVPre && !element.processed) { element = processElement(element, options) } // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { if (process.env.NODE_ENV !== 'production') { 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.`, { start: element.start } ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent) } else { if (element.slotScope) { // scoped slot // keep it in the children list so that v-else(-if) conditions can // find it as the prev node. const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } currentParent.children.push(element) element.parent = currentParent } } // final children cleanup // filter out scoped slots element.children = element.children.filter(c => !(c: any).slotScope) // remove trailing whitespace node again trimEndingWhitespace(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) } } function trimEndingWhitespace (el) { // remove trailing whitespace node if (!inPre) { let lastNode while ( (lastNode = el.children[el.children.length - 1]) && lastNode.type === 3 && lastNode.text === ' ' ) { el.children.pop() } } } function checkRootConstraints (el) { if (el.tag === 'slot' || el.tag === 'template') { warnOnce( `Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.', { start: el.start } ) } if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.', el.rawAttrsMap['v-for'] ) } } parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, outputSourceRange: options.outputSourceRange, start (tag, attrs, unary, start, end) { // 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 (process.env.NODE_ENV !== 'production') { if (options.outputSourceRange) { element.start = start element.end = end element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => { cumulated[attr.name] = attr return cumulated }, {}) } attrs.forEach(attr => { if (invalidAttributeRE.test(attr.name)) { warn( `Invalid dynamic argument expression: attribute names cannot contain ` + `spaces, quotes, <, >, / or =.`, { start: attr.start + attr.name.indexOf(`[`), end: attr.start + attr.name.length } ) } }) } 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.', { start: element.start } ) } // 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) } if (!root) { root = element if (process.env.NODE_ENV !== 'production') { checkRootConstraints(root) } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }, end (tag, start, end) { const element = stack[stack.length - 1] // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { element.end = end } closeElement(element) }, chars (text: string, start: number, end: number) { if (!currentParent) { if (process.env.NODE_ENV !== 'production') { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.', { start } ) } else if ((text = text.trim())) { warnOnce( `text "${text}" outside root element will be ignored.`, { start } ) } } return } // IE textarea placeholder bug /* istanbul ignore if */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } const children = currentParent.children if (inPre || text.trim()) { text = isTextTag(currentParent) ? text : decodeHTMLCached(text) } else if (!children.length) { // remove the whitespace-only node right after an opening tag text = '' } else if (whitespaceOption) { if (whitespaceOption === 'condense') { // in condense mode, remove the whitespace node if it contains // line break, otherwise condense to a single space text = lineBreakRE.test(text) ? '' : ' ' } else { text = ' ' } } else { text = preserveWhitespace ? ' ' : '' } if (text) { if (!inPre && whitespaceOption === 'condense') { // condense consecutive whitespaces into single space text = text.replace(whitespaceRE, ' ') } let res let child: ?ASTNode if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { child = { type: 2, expression: res.expression, tokens: res.tokens, text } } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { child = { type: 3, text } } if (child) { if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { child.start = start child.end = end } children.push(child) } } }, comment (text: string, start, end) { // adding anything as a sibling to the root node is forbidden // comments should still be allowed, but ignored if (currentParent) { const child: ASTText = { type: 3, text, isComment: true } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { child.start = start child.end = end } currentParent.children.push(child) } } }) return root } function processPre (el) { if (getAndRemoveAttr(el, 'v-pre') != null) { el.pre = true } } function processRawAttrs (el) { const list = el.attrsList const len = list.length if (len) { const attrs: Array = el.attrs = new Array(len) for (let i = 0; i < len; i++) { attrs[i] = { name: list[i].name, value: JSON.stringify(list[i].value) } if (list[i].start != null) { attrs[i].start = list[i].start attrs[i].end = list[i].end } } } 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.scopedSlots && !element.attrsList.length ) processRef(element) processSlotContent(element) processSlotOutlet(element) processComponent(element) for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } processAttrs(element) return element } function processKey (el) { const exp = getBindingAttr(el, 'key') if (exp) { if (process.env.NODE_ENV !== 'production') { if (el.tag === 'template') { warn( `