UNPKG

27.6 kBJavaScriptView Raw
1/* @flow */
2
3import he from 'he'
4import { parseHTML } from './html-parser'
5import { parseText } from './text-parser'
6import { parseFilters } from './filter-parser'
7import { genAssignmentCode } from '../directives/model'
8import { extend, cached, no, camelize, hyphenate } from 'shared/util'
9import { isIE, isEdge, isServerRendering } from 'core/util/env'
10
11import {
12 addProp,
13 addAttr,
14 baseWarn,
15 addHandler,
16 addDirective,
17 getBindingAttr,
18 getAndRemoveAttr,
19 getRawBindingAttr,
20 pluckModuleFunction,
21 getAndRemoveAttrByRegex
22} from '../helpers'
23
24export const onRE = /^@|^v-on:/
25export const dirRE = process.env.VBIND_PROP_SHORTHAND
26 ? /^v-|^@|^:|^\./
27 : /^v-|^@|^:/
28export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
29export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
30const stripParensRE = /^\(|\)$/g
31const dynamicArgRE = /^\[.*\]$/
32
33const argRE = /:(.*)$/
34export const bindRE = /^:|^\.|^v-bind:/
35const propBindRE = /^\./
36const modifierRE = /\.[^.]+/g
37
38const slotRE = /^v-slot(:|$)|^#/
39
40const lineBreakRE = /[\r\n]/
41const whitespaceRE = /\s+/g
42
43const invalidAttributeRE = /[\s"'<>\/=]/
44
45const decodeHTMLCached = cached(he.decode)
46
47// configurable state
48export let warn: any
49let delimiters
50let transforms
51let preTransforms
52let postTransforms
53let platformIsPreTag
54let platformMustUseProp
55let platformGetTagNamespace
56let maybeComponent
57
58export function createASTElement (
59 tag: string,
60 attrs: Array<ASTAttr>,
61 parent: ASTElement | void
62): ASTElement {
63 return {
64 type: 1,
65 tag,
66 attrsList: attrs,
67 attrsMap: makeAttrsMap(attrs),
68 rawAttrsMap: {},
69 parent,
70 children: []
71 }
72}
73
74/**
75 * Convert HTML string to AST.
76 */
77export function parse (
78 template: string,
79 options: CompilerOptions
80): ASTElement | void {
81 warn = options.warn || baseWarn
82
83 platformIsPreTag = options.isPreTag || no
84 platformMustUseProp = options.mustUseProp || no
85 platformGetTagNamespace = options.getTagNamespace || no
86 const isReservedTag = options.isReservedTag || no
87 maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
88
89 transforms = pluckModuleFunction(options.modules, 'transformNode')
90 preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
91 postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
92
93 delimiters = options.delimiters
94
95 const stack = []
96 const preserveWhitespace = options.preserveWhitespace !== false
97 const whitespaceOption = options.whitespace
98 let root
99 let currentParent
100 let inVPre = false
101 let inPre = false
102 let warned = false
103
104 function warnOnce (msg, range) {
105 if (!warned) {
106 warned = true
107 warn(msg, range)
108 }
109 }
110
111 function closeElement (element) {
112 trimEndingWhitespace(element)
113 if (!inVPre && !element.processed) {
114 element = processElement(element, options)
115 }
116 // tree management
117 if (!stack.length && element !== root) {
118 // allow root elements with v-if, v-else-if and v-else
119 if (root.if && (element.elseif || element.else)) {
120 if (process.env.NODE_ENV !== 'production') {
121 checkRootConstraints(element)
122 }
123 addIfCondition(root, {
124 exp: element.elseif,
125 block: element
126 })
127 } else if (process.env.NODE_ENV !== 'production') {
128 warnOnce(
129 `Component template should contain exactly one root element. ` +
130 `If you are using v-if on multiple elements, ` +
131 `use v-else-if to chain them instead.`,
132 { start: element.start }
133 )
134 }
135 }
136 if (currentParent && !element.forbidden) {
137 if (element.elseif || element.else) {
138 processIfConditions(element, currentParent)
139 } else {
140 if (element.slotScope) {
141 // scoped slot
142 // keep it in the children list so that v-else(-if) conditions can
143 // find it as the prev node.
144 const name = element.slotTarget || '"default"'
145 ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
146 }
147 currentParent.children.push(element)
148 element.parent = currentParent
149 }
150 }
151
152 // final children cleanup
153 // filter out scoped slots
154 element.children = element.children.filter(c => !(c: any).slotScope)
155 // remove trailing whitespace node again
156 trimEndingWhitespace(element)
157
158 // check pre state
159 if (element.pre) {
160 inVPre = false
161 }
162 if (platformIsPreTag(element.tag)) {
163 inPre = false
164 }
165 // apply post-transforms
166 for (let i = 0; i < postTransforms.length; i++) {
167 postTransforms[i](element, options)
168 }
169 }
170
171 function trimEndingWhitespace (el) {
172 // remove trailing whitespace node
173 if (!inPre) {
174 let lastNode
175 while (
176 (lastNode = el.children[el.children.length - 1]) &&
177 lastNode.type === 3 &&
178 lastNode.text === ' '
179 ) {
180 el.children.pop()
181 }
182 }
183 }
184
185 function checkRootConstraints (el) {
186 if (el.tag === 'slot' || el.tag === 'template') {
187 warnOnce(
188 `Cannot use <${el.tag}> as component root element because it may ` +
189 'contain multiple nodes.',
190 { start: el.start }
191 )
192 }
193 if (el.attrsMap.hasOwnProperty('v-for')) {
194 warnOnce(
195 'Cannot use v-for on stateful component root element because ' +
196 'it renders multiple elements.',
197 el.rawAttrsMap['v-for']
198 )
199 }
200 }
201
202 parseHTML(template, {
203 warn,
204 expectHTML: options.expectHTML,
205 isUnaryTag: options.isUnaryTag,
206 canBeLeftOpenTag: options.canBeLeftOpenTag,
207 shouldDecodeNewlines: options.shouldDecodeNewlines,
208 shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
209 shouldKeepComment: options.comments,
210 outputSourceRange: options.outputSourceRange,
211 start (tag, attrs, unary, start) {
212 // check namespace.
213 // inherit parent ns if there is one
214 const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
215
216 // handle IE svg bug
217 /* istanbul ignore if */
218 if (isIE && ns === 'svg') {
219 attrs = guardIESVGBug(attrs)
220 }
221
222 let element: ASTElement = createASTElement(tag, attrs, currentParent)
223 if (ns) {
224 element.ns = ns
225 }
226
227 if (process.env.NODE_ENV !== 'production') {
228 if (options.outputSourceRange) {
229 element.start = start
230 element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
231 cumulated[attr.name] = attr
232 return cumulated
233 }, {})
234 }
235 attrs.forEach(attr => {
236 if (invalidAttributeRE.test(attr.name)) {
237 warn(
238 `Invalid dynamic argument expression: attribute names cannot contain ` +
239 `spaces, quotes, <, >, / or =.`,
240 {
241 start: attr.start + attr.name.indexOf(`[`),
242 end: attr.start + attr.name.length
243 }
244 )
245 }
246 })
247 }
248
249 if (isForbiddenTag(element) && !isServerRendering()) {
250 element.forbidden = true
251 process.env.NODE_ENV !== 'production' && warn(
252 'Templates should only be responsible for mapping the state to the ' +
253 'UI. Avoid placing tags with side-effects in your templates, such as ' +
254 `<${tag}>` + ', as they will not be parsed.',
255 { start: element.start }
256 )
257 }
258
259 // apply pre-transforms
260 for (let i = 0; i < preTransforms.length; i++) {
261 element = preTransforms[i](element, options) || element
262 }
263
264 if (!inVPre) {
265 processPre(element)
266 if (element.pre) {
267 inVPre = true
268 }
269 }
270 if (platformIsPreTag(element.tag)) {
271 inPre = true
272 }
273 if (inVPre) {
274 processRawAttrs(element)
275 } else if (!element.processed) {
276 // structural directives
277 processFor(element)
278 processIf(element)
279 processOnce(element)
280 }
281
282 if (!root) {
283 root = element
284 if (process.env.NODE_ENV !== 'production') {
285 checkRootConstraints(root)
286 }
287 }
288
289 if (!unary) {
290 currentParent = element
291 stack.push(element)
292 } else {
293 closeElement(element)
294 }
295 },
296
297 end (tag, start, end) {
298 const element = stack[stack.length - 1]
299 // pop stack
300 stack.length -= 1
301 currentParent = stack[stack.length - 1]
302 if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
303 element.end = end
304 }
305 closeElement(element)
306 },
307
308 chars (text: string, start: number, end: number) {
309 if (!currentParent) {
310 if (process.env.NODE_ENV !== 'production') {
311 if (text === template) {
312 warnOnce(
313 'Component template requires a root element, rather than just text.',
314 { start }
315 )
316 } else if ((text = text.trim())) {
317 warnOnce(
318 `text "${text}" outside root element will be ignored.`,
319 { start }
320 )
321 }
322 }
323 return
324 }
325 // IE textarea placeholder bug
326 /* istanbul ignore if */
327 if (isIE &&
328 currentParent.tag === 'textarea' &&
329 currentParent.attrsMap.placeholder === text
330 ) {
331 return
332 }
333 const children = currentParent.children
334 if (inPre || text.trim()) {
335 text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
336 } else if (!children.length) {
337 // remove the whitespace-only node right after an opening tag
338 text = ''
339 } else if (whitespaceOption) {
340 if (whitespaceOption === 'condense') {
341 // in condense mode, remove the whitespace node if it contains
342 // line break, otherwise condense to a single space
343 text = lineBreakRE.test(text) ? '' : ' '
344 } else {
345 text = ' '
346 }
347 } else {
348 text = preserveWhitespace ? ' ' : ''
349 }
350 if (text) {
351 if (whitespaceOption === 'condense') {
352 // condense consecutive whitespaces into single space
353 text = text.replace(whitespaceRE, ' ')
354 }
355 let res
356 let child: ?ASTNode
357 if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
358 child = {
359 type: 2,
360 expression: res.expression,
361 tokens: res.tokens,
362 text
363 }
364 } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
365 child = {
366 type: 3,
367 text
368 }
369 }
370 if (child) {
371 if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
372 child.start = start
373 child.end = end
374 }
375 children.push(child)
376 }
377 }
378 },
379 comment (text: string, start, end) {
380 const child: ASTText = {
381 type: 3,
382 text,
383 isComment: true
384 }
385 if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
386 child.start = start
387 child.end = end
388 }
389 currentParent.children.push(child)
390 }
391 })
392 return root
393}
394
395function processPre (el) {
396 if (getAndRemoveAttr(el, 'v-pre') != null) {
397 el.pre = true
398 }
399}
400
401function processRawAttrs (el) {
402 const list = el.attrsList
403 const len = list.length
404 if (len) {
405 const attrs: Array<ASTAttr> = el.attrs = new Array(len)
406 for (let i = 0; i < len; i++) {
407 attrs[i] = {
408 name: list[i].name,
409 value: JSON.stringify(list[i].value)
410 }
411 if (list[i].start != null) {
412 attrs[i].start = list[i].start
413 attrs[i].end = list[i].end
414 }
415 }
416 } else if (!el.pre) {
417 // non root node in pre blocks with no attributes
418 el.plain = true
419 }
420}
421
422export function processElement (
423 element: ASTElement,
424 options: CompilerOptions
425) {
426 processKey(element)
427
428 // determine whether this is a plain element after
429 // removing structural attributes
430 element.plain = (
431 !element.key &&
432 !element.scopedSlots &&
433 !element.attrsList.length
434 )
435
436 processRef(element)
437 processSlotContent(element)
438 processSlotOutlet(element)
439 processComponent(element)
440 for (let i = 0; i < transforms.length; i++) {
441 element = transforms[i](element, options) || element
442 }
443 processAttrs(element)
444 return element
445}
446
447function processKey (el) {
448 const exp = getBindingAttr(el, 'key')
449 if (exp) {
450 if (process.env.NODE_ENV !== 'production') {
451 if (el.tag === 'template') {
452 warn(
453 `<template> cannot be keyed. Place the key on real elements instead.`,
454 getRawBindingAttr(el, 'key')
455 )
456 }
457 if (el.for) {
458 const iterator = el.iterator2 || el.iterator1
459 const parent = el.parent
460 if (iterator && iterator === exp && parent && parent.tag === 'transition-group') {
461 warn(
462 `Do not use v-for index as key on <transition-group> children, ` +
463 `this is the same as not using keys.`,
464 getRawBindingAttr(el, 'key'),
465 true /* tip */
466 )
467 }
468 }
469 }
470 el.key = exp
471 }
472}
473
474function processRef (el) {
475 const ref = getBindingAttr(el, 'ref')
476 if (ref) {
477 el.ref = ref
478 el.refInFor = checkInFor(el)
479 }
480}
481
482export function processFor (el: ASTElement) {
483 let exp
484 if ((exp = getAndRemoveAttr(el, 'v-for'))) {
485 const res = parseFor(exp)
486 if (res) {
487 extend(el, res)
488 } else if (process.env.NODE_ENV !== 'production') {
489 warn(
490 `Invalid v-for expression: ${exp}`,
491 el.rawAttrsMap['v-for']
492 )
493 }
494 }
495}
496
497type ForParseResult = {
498 for: string;
499 alias: string;
500 iterator1?: string;
501 iterator2?: string;
502};
503
504export function parseFor (exp: string): ?ForParseResult {
505 const inMatch = exp.match(forAliasRE)
506 if (!inMatch) return
507 const res = {}
508 res.for = inMatch[2].trim()
509 const alias = inMatch[1].trim().replace(stripParensRE, '')
510 const iteratorMatch = alias.match(forIteratorRE)
511 if (iteratorMatch) {
512 res.alias = alias.replace(forIteratorRE, '').trim()
513 res.iterator1 = iteratorMatch[1].trim()
514 if (iteratorMatch[2]) {
515 res.iterator2 = iteratorMatch[2].trim()
516 }
517 } else {
518 res.alias = alias
519 }
520 return res
521}
522
523function processIf (el) {
524 const exp = getAndRemoveAttr(el, 'v-if')
525 if (exp) {
526 el.if = exp
527 addIfCondition(el, {
528 exp: exp,
529 block: el
530 })
531 } else {
532 if (getAndRemoveAttr(el, 'v-else') != null) {
533 el.else = true
534 }
535 const elseif = getAndRemoveAttr(el, 'v-else-if')
536 if (elseif) {
537 el.elseif = elseif
538 }
539 }
540}
541
542function processIfConditions (el, parent) {
543 const prev = findPrevElement(parent.children)
544 if (prev && prev.if) {
545 addIfCondition(prev, {
546 exp: el.elseif,
547 block: el
548 })
549 } else if (process.env.NODE_ENV !== 'production') {
550 warn(
551 `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
552 `used on element <${el.tag}> without corresponding v-if.`,
553 el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
554 )
555 }
556}
557
558function findPrevElement (children: Array<any>): ASTElement | void {
559 let i = children.length
560 while (i--) {
561 if (children[i].type === 1) {
562 return children[i]
563 } else {
564 if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
565 warn(
566 `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
567 `will be ignored.`,
568 children[i]
569 )
570 }
571 children.pop()
572 }
573 }
574}
575
576export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
577 if (!el.ifConditions) {
578 el.ifConditions = []
579 }
580 el.ifConditions.push(condition)
581}
582
583function processOnce (el) {
584 const once = getAndRemoveAttr(el, 'v-once')
585 if (once != null) {
586 el.once = true
587 }
588}
589
590// handle content being passed to a component as slot,
591// e.g. <template slot="xxx">, <div slot-scope="xxx">
592function processSlotContent (el) {
593 let slotScope
594 if (el.tag === 'template') {
595 slotScope = getAndRemoveAttr(el, 'scope')
596 /* istanbul ignore if */
597 if (process.env.NODE_ENV !== 'production' && slotScope) {
598 warn(
599 `the "scope" attribute for scoped slots have been deprecated and ` +
600 `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
601 `can also be used on plain elements in addition to <template> to ` +
602 `denote scoped slots.`,
603 el.rawAttrsMap['scope'],
604 true
605 )
606 }
607 el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
608 } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
609 /* istanbul ignore if */
610 if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
611 warn(
612 `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
613 `(v-for takes higher priority). Use a wrapper <template> for the ` +
614 `scoped slot to make it clearer.`,
615 el.rawAttrsMap['slot-scope'],
616 true
617 )
618 }
619 el.slotScope = slotScope
620 }
621
622 // slot="xxx"
623 const slotTarget = getBindingAttr(el, 'slot')
624 if (slotTarget) {
625 el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
626 el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
627 // preserve slot as an attribute for native shadow DOM compat
628 // only for non-scoped slots.
629 if (el.tag !== 'template' && !el.slotScope) {
630 addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
631 }
632 }
633
634 // 2.6 v-slot syntax
635 if (process.env.NEW_SLOT_SYNTAX) {
636 if (el.tag === 'template') {
637 // v-slot on <template>
638 const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
639 if (slotBinding) {
640 if (process.env.NODE_ENV !== 'production') {
641 if (el.slotTarget || el.slotScope) {
642 warn(
643 `Unexpected mixed usage of different slot syntaxes.`,
644 el
645 )
646 }
647 if (el.parent && !maybeComponent(el.parent)) {
648 warn(
649 `<template v-slot> can only appear at the root level inside ` +
650 `the receiving the component`,
651 el
652 )
653 }
654 }
655 const { name, dynamic } = getSlotName(slotBinding)
656 el.slotTarget = name
657 el.slotTargetDynamic = dynamic
658 el.slotScope = slotBinding.value || `_` // force it into a scoped slot for perf
659 }
660 } else {
661 // v-slot on component, denotes default slot
662 const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
663 if (slotBinding) {
664 if (process.env.NODE_ENV !== 'production') {
665 if (!maybeComponent(el)) {
666 warn(
667 `v-slot can only be used on components or <template>.`,
668 slotBinding
669 )
670 }
671 if (el.slotScope || el.slotTarget) {
672 warn(
673 `Unexpected mixed usage of different slot syntaxes.`,
674 el
675 )
676 }
677 if (el.scopedSlots) {
678 warn(
679 `To avoid scope ambiguity, the default slot should also use ` +
680 `<template> syntax when there are other named slots.`,
681 slotBinding
682 )
683 }
684 }
685 // add the component's children to its default slot
686 const slots = el.scopedSlots || (el.scopedSlots = {})
687 const { name, dynamic } = getSlotName(slotBinding)
688 const slotContainer = slots[name] = createASTElement('template', [], el)
689 slotContainer.slotTarget = name
690 slotContainer.slotTargetDynamic = dynamic
691 slotContainer.children = el.children.filter(c => !(c: any).slotScope)
692 slotContainer.slotScope = slotBinding.value || `_`
693 // remove children as they are returned from scopedSlots now
694 el.children = []
695 // mark el non-plain so data gets generated
696 el.plain = false
697 }
698 }
699 }
700}
701
702function getSlotName (binding) {
703 let name = binding.name.replace(slotRE, '')
704 if (!name) {
705 if (binding.name[0] !== '#') {
706 name = 'default'
707 } else if (process.env.NODE_ENV !== 'production') {
708 warn(
709 `v-slot shorthand syntax requires a slot name.`,
710 binding
711 )
712 }
713 }
714 return dynamicArgRE.test(name)
715 // dynamic [name]
716 ? { name: name.slice(1, -1), dynamic: true }
717 // static name
718 : { name: `"${name}"`, dynamic: false }
719}
720
721// handle <slot/> outlets
722function processSlotOutlet (el) {
723 if (el.tag === 'slot') {
724 el.slotName = getBindingAttr(el, 'name')
725 if (process.env.NODE_ENV !== 'production' && el.key) {
726 warn(
727 `\`key\` does not work on <slot> because slots are abstract outlets ` +
728 `and can possibly expand into multiple elements. ` +
729 `Use the key on a wrapping element instead.`,
730 getRawBindingAttr(el, 'key')
731 )
732 }
733 }
734}
735
736function processComponent (el) {
737 let binding
738 if ((binding = getBindingAttr(el, 'is'))) {
739 el.component = binding
740 }
741 if (getAndRemoveAttr(el, 'inline-template') != null) {
742 el.inlineTemplate = true
743 }
744}
745
746function processAttrs (el) {
747 const list = el.attrsList
748 let i, l, name, rawName, value, modifiers, syncGen, isDynamic
749 for (i = 0, l = list.length; i < l; i++) {
750 name = rawName = list[i].name
751 value = list[i].value
752 if (dirRE.test(name)) {
753 // mark element as dynamic
754 el.hasBindings = true
755 // modifiers
756 modifiers = parseModifiers(name.replace(dirRE, ''))
757 // support .foo shorthand syntax for the .prop modifier
758 if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
759 (modifiers || (modifiers = {})).prop = true
760 name = `.` + name.slice(1).replace(modifierRE, '')
761 } else if (modifiers) {
762 name = name.replace(modifierRE, '')
763 }
764 if (bindRE.test(name)) { // v-bind
765 name = name.replace(bindRE, '')
766 value = parseFilters(value)
767 isDynamic = dynamicArgRE.test(name)
768 if (isDynamic) {
769 name = name.slice(1, -1)
770 }
771 if (
772 process.env.NODE_ENV !== 'production' &&
773 value.trim().length === 0
774 ) {
775 warn(
776 `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
777 )
778 }
779 if (modifiers) {
780 if (modifiers.prop && !isDynamic) {
781 name = camelize(name)
782 if (name === 'innerHtml') name = 'innerHTML'
783 }
784 if (modifiers.camel && !isDynamic) {
785 name = camelize(name)
786 }
787 if (modifiers.sync) {
788 syncGen = genAssignmentCode(value, `$event`)
789 if (!isDynamic) {
790 addHandler(
791 el,
792 `update:${camelize(name)}`,
793 syncGen,
794 null,
795 false,
796 warn,
797 list[i]
798 )
799 if (hyphenate(name) !== camelize(name)) {
800 addHandler(
801 el,
802 `update:${hyphenate(name)}`,
803 syncGen,
804 null,
805 false,
806 warn,
807 list[i]
808 )
809 }
810 } else {
811 // handler w/ dynamic event name
812 addHandler(
813 el,
814 `"update:"+(${name})`,
815 syncGen,
816 null,
817 false,
818 warn,
819 list[i],
820 true // dynamic
821 )
822 }
823 }
824 }
825 if ((modifiers && modifiers.prop) || (
826 !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
827 )) {
828 addProp(el, name, value, list[i], isDynamic)
829 } else {
830 addAttr(el, name, value, list[i], isDynamic)
831 }
832 } else if (onRE.test(name)) { // v-on
833 name = name.replace(onRE, '')
834 isDynamic = dynamicArgRE.test(name)
835 if (isDynamic) {
836 name = name.slice(1, -1)
837 }
838 addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
839 } else { // normal directives
840 name = name.replace(dirRE, '')
841 // parse arg
842 const argMatch = name.match(argRE)
843 let arg = argMatch && argMatch[1]
844 isDynamic = false
845 if (arg) {
846 name = name.slice(0, -(arg.length + 1))
847 if (dynamicArgRE.test(arg)) {
848 arg = arg.slice(1, -1)
849 isDynamic = true
850 }
851 }
852 addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
853 if (process.env.NODE_ENV !== 'production' && name === 'model') {
854 checkForAliasModel(el, value)
855 }
856 }
857 } else {
858 // literal attribute
859 if (process.env.NODE_ENV !== 'production') {
860 const res = parseText(value, delimiters)
861 if (res) {
862 warn(
863 `${name}="${value}": ` +
864 'Interpolation inside attributes has been removed. ' +
865 'Use v-bind or the colon shorthand instead. For example, ' +
866 'instead of <div id="{{ val }}">, use <div :id="val">.',
867 list[i]
868 )
869 }
870 }
871 addAttr(el, name, JSON.stringify(value), list[i])
872 // #6887 firefox doesn't update muted state if set via attribute
873 // even immediately after element creation
874 if (!el.component &&
875 name === 'muted' &&
876 platformMustUseProp(el.tag, el.attrsMap.type, name)) {
877 addProp(el, name, 'true', list[i])
878 }
879 }
880 }
881}
882
883function checkInFor (el: ASTElement): boolean {
884 let parent = el
885 while (parent) {
886 if (parent.for !== undefined) {
887 return true
888 }
889 parent = parent.parent
890 }
891 return false
892}
893
894function parseModifiers (name: string): Object | void {
895 const match = name.match(modifierRE)
896 if (match) {
897 const ret = {}
898 match.forEach(m => { ret[m.slice(1)] = true })
899 return ret
900 }
901}
902
903function makeAttrsMap (attrs: Array<Object>): Object {
904 const map = {}
905 for (let i = 0, l = attrs.length; i < l; i++) {
906 if (
907 process.env.NODE_ENV !== 'production' &&
908 map[attrs[i].name] && !isIE && !isEdge
909 ) {
910 warn('duplicate attribute: ' + attrs[i].name, attrs[i])
911 }
912 map[attrs[i].name] = attrs[i].value
913 }
914 return map
915}
916
917// for script (e.g. type="x/template") or style, do not decode content
918function isTextTag (el): boolean {
919 return el.tag === 'script' || el.tag === 'style'
920}
921
922function isForbiddenTag (el): boolean {
923 return (
924 el.tag === 'style' ||
925 (el.tag === 'script' && (
926 !el.attrsMap.type ||
927 el.attrsMap.type === 'text/javascript'
928 ))
929 )
930}
931
932const ieNSBug = /^xmlns:NS\d+/
933const ieNSPrefix = /^NS\d+:/
934
935/* istanbul ignore next */
936function guardIESVGBug (attrs) {
937 const res = []
938 for (let i = 0; i < attrs.length; i++) {
939 const attr = attrs[i]
940 if (!ieNSBug.test(attr.name)) {
941 attr.name = attr.name.replace(ieNSPrefix, '')
942 res.push(attr)
943 }
944 }
945 return res
946}
947
948function checkForAliasModel (el, value) {
949 let _el = el
950 while (_el) {
951 if (_el.for && _el.alias === value) {
952 warn(
953 `<${el.tag} v-model="${value}">: ` +
954 `You are binding v-model directly to a v-for iteration alias. ` +
955 `This will not be able to modify the v-for source array because ` +
956 `writing to the alias is like modifying a function local variable. ` +
957 `Consider using an array of objects and use v-model on an object property instead.`,
958 el.rawAttrsMap['v-model']
959 )
960 }
961 _el = _el.parent
962 }
963}