index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /* @flow */
  2. import he from 'he'
  3. import { parseHTML } from './html-parser'
  4. import { parseText } from './text-parser'
  5. import { parseFilters } from './filter-parser'
  6. import { cached, no, camelize } from 'shared/util'
  7. import { genAssignmentCode } from '../directives/model'
  8. import { isIE, isEdge, isServerRendering } from 'core/util/env'
  9. import {
  10. addProp,
  11. addAttr,
  12. baseWarn,
  13. addHandler,
  14. addDirective,
  15. getBindingAttr,
  16. getAndRemoveAttr,
  17. pluckModuleFunction
  18. } from '../helpers'
  19. export const onRE = /^@|^v-on:/
  20. export const dirRE = /^v-|^@|^:/
  21. export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
  22. export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
  23. const stripParensRE = /^\(|\)$/g
  24. const argRE = /:(.*)$/
  25. const bindRE = /^:|^v-bind:/
  26. const modifierRE = /\.[^.]+/g
  27. const literalValueRE = /^(\{.*\}|\[.*\])$/
  28. const decodeHTMLCached = cached(he.decode)
  29. // configurable state
  30. export let warn: any
  31. let literalPropId
  32. let delimiters
  33. let transforms
  34. let preTransforms
  35. let postTransforms
  36. let platformIsPreTag
  37. let platformMustUseProp
  38. let platformIsReservedTag
  39. let platformGetTagNamespace
  40. type Attr = { name: string; value: string };
  41. export function createASTElement (
  42. tag: string,
  43. attrs: Array<Attr>,
  44. parent: ASTElement | void
  45. ): ASTElement {
  46. return {
  47. type: 1,
  48. tag,
  49. attrsList: attrs,
  50. attrsMap: makeAttrsMap(attrs),
  51. parent,
  52. children: []
  53. }
  54. }
  55. /**
  56. * Convert HTML string to AST.
  57. */
  58. export function parse (
  59. template: string,
  60. options: CompilerOptions
  61. ): ASTElement | void {
  62. warn = options.warn || baseWarn
  63. literalPropId = 0
  64. platformIsPreTag = options.isPreTag || no
  65. platformMustUseProp = options.mustUseProp || no
  66. platformIsReservedTag = options.isReservedTag || no
  67. platformGetTagNamespace = options.getTagNamespace || no
  68. transforms = pluckModuleFunction(options.modules, 'transformNode')
  69. preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  70. postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  71. delimiters = options.delimiters
  72. const stack = []
  73. const preserveWhitespace = options.preserveWhitespace !== false
  74. let root
  75. let currentParent
  76. let inVPre = false
  77. let inPre = false
  78. let warned = false
  79. function warnOnce (msg) {
  80. if (!warned) {
  81. warned = true
  82. warn(msg)
  83. }
  84. }
  85. function closeElement (element) {
  86. // check pre state
  87. if (element.pre) {
  88. inVPre = false
  89. }
  90. if (platformIsPreTag(element.tag)) {
  91. inPre = false
  92. }
  93. // apply post-transforms
  94. for (let i = 0; i < postTransforms.length; i++) {
  95. postTransforms[i](element, options)
  96. }
  97. }
  98. parseHTML(template, {
  99. warn,
  100. expectHTML: options.expectHTML,
  101. isUnaryTag: options.isUnaryTag,
  102. canBeLeftOpenTag: options.canBeLeftOpenTag,
  103. shouldDecodeNewlines: options.shouldDecodeNewlines,
  104. shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
  105. shouldKeepComment: options.comments,
  106. start (tag, attrs, unary) {
  107. // check namespace.
  108. // inherit parent ns if there is one
  109. const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
  110. // handle IE svg bug
  111. /* istanbul ignore if */
  112. if (isIE && ns === 'svg') {
  113. attrs = guardIESVGBug(attrs)
  114. }
  115. let element: ASTElement = createASTElement(tag, attrs, currentParent)
  116. if (ns) {
  117. element.ns = ns
  118. }
  119. if (isForbiddenTag(element) && !isServerRendering()) {
  120. element.forbidden = true
  121. process.env.NODE_ENV !== 'production' && warn(
  122. 'Templates should only be responsible for mapping the state to the ' +
  123. 'UI. Avoid placing tags with side-effects in your templates, such as ' +
  124. `<${tag}>` + ', as they will not be parsed.'
  125. )
  126. }
  127. // apply pre-transforms
  128. for (let i = 0; i < preTransforms.length; i++) {
  129. element = preTransforms[i](element, options) || element
  130. }
  131. if (!inVPre) {
  132. processPre(element)
  133. if (element.pre) {
  134. inVPre = true
  135. }
  136. }
  137. if (platformIsPreTag(element.tag)) {
  138. inPre = true
  139. }
  140. if (inVPre) {
  141. processRawAttrs(element)
  142. } else if (!element.processed) {
  143. // structural directives
  144. processFor(element)
  145. processIf(element)
  146. processOnce(element)
  147. // element-scope stuff
  148. processElement(element, options)
  149. }
  150. function checkRootConstraints (el) {
  151. if (process.env.NODE_ENV !== 'production') {
  152. if (el.tag === 'slot' || el.tag === 'template') {
  153. warnOnce(
  154. `Cannot use <${el.tag}> as component root element because it may ` +
  155. 'contain multiple nodes.'
  156. )
  157. }
  158. if (el.attrsMap.hasOwnProperty('v-for')) {
  159. warnOnce(
  160. 'Cannot use v-for on stateful component root element because ' +
  161. 'it renders multiple elements.'
  162. )
  163. }
  164. }
  165. }
  166. // tree management
  167. if (!root) {
  168. root = element
  169. checkRootConstraints(root)
  170. } else if (!stack.length) {
  171. // allow root elements with v-if, v-else-if and v-else
  172. if (root.if && (element.elseif || element.else)) {
  173. checkRootConstraints(element)
  174. addIfCondition(root, {
  175. exp: element.elseif,
  176. block: element
  177. })
  178. } else if (process.env.NODE_ENV !== 'production') {
  179. warnOnce(
  180. `Component template should contain exactly one root element. ` +
  181. `If you are using v-if on multiple elements, ` +
  182. `use v-else-if to chain them instead.`
  183. )
  184. }
  185. }
  186. if (currentParent && !element.forbidden) {
  187. if (element.elseif || element.else) {
  188. processIfConditions(element, currentParent)
  189. } else if (element.slotScope) { // scoped slot
  190. currentParent.plain = false
  191. const name = element.slotTarget || '"default"'
  192. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  193. } else {
  194. currentParent.children.push(element)
  195. element.parent = currentParent
  196. }
  197. }
  198. if (!unary) {
  199. currentParent = element
  200. stack.push(element)
  201. } else {
  202. closeElement(element)
  203. }
  204. },
  205. end () {
  206. // remove trailing whitespace
  207. const element = stack[stack.length - 1]
  208. const lastNode = element.children[element.children.length - 1]
  209. if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  210. element.children.pop()
  211. }
  212. // pop stack
  213. stack.length -= 1
  214. currentParent = stack[stack.length - 1]
  215. closeElement(element)
  216. },
  217. chars (text: string) {
  218. if (!currentParent) {
  219. if (process.env.NODE_ENV !== 'production') {
  220. if (text === template) {
  221. warnOnce(
  222. 'Component template requires a root element, rather than just text.'
  223. )
  224. } else if ((text = text.trim())) {
  225. warnOnce(
  226. `text "${text}" outside root element will be ignored.`
  227. )
  228. }
  229. }
  230. return
  231. }
  232. // IE textarea placeholder bug
  233. /* istanbul ignore if */
  234. if (isIE &&
  235. currentParent.tag === 'textarea' &&
  236. currentParent.attrsMap.placeholder === text
  237. ) {
  238. return
  239. }
  240. const children = currentParent.children
  241. text = inPre || text.trim()
  242. ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
  243. // only preserve whitespace if its not right after a starting tag
  244. : preserveWhitespace && children.length ? ' ' : ''
  245. if (text) {
  246. let res
  247. if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  248. children.push({
  249. type: 2,
  250. expression: res.expression,
  251. tokens: res.tokens,
  252. text
  253. })
  254. } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  255. children.push({
  256. type: 3,
  257. text
  258. })
  259. }
  260. }
  261. },
  262. comment (text: string) {
  263. currentParent.children.push({
  264. type: 3,
  265. text,
  266. isComment: true
  267. })
  268. }
  269. })
  270. return root
  271. }
  272. function processPre (el) {
  273. if (getAndRemoveAttr(el, 'v-pre') != null) {
  274. el.pre = true
  275. }
  276. }
  277. function processRawAttrs (el) {
  278. const l = el.attrsList.length
  279. if (l) {
  280. const attrs = el.attrs = new Array(l)
  281. for (let i = 0; i < l; i++) {
  282. attrs[i] = {
  283. name: el.attrsList[i].name,
  284. value: JSON.stringify(el.attrsList[i].value)
  285. }
  286. }
  287. } else if (!el.pre) {
  288. // non root node in pre blocks with no attributes
  289. el.plain = true
  290. }
  291. }
  292. export function processElement (element: ASTElement, options: CompilerOptions) {
  293. processKey(element)
  294. // determine whether this is a plain element after
  295. // removing structural attributes
  296. element.plain = !element.key && !element.attrsList.length
  297. processRef(element)
  298. processSlot(element)
  299. processComponent(element)
  300. for (let i = 0; i < transforms.length; i++) {
  301. element = transforms[i](element, options) || element
  302. }
  303. processAttrs(element)
  304. }
  305. function processKey (el) {
  306. const exp = getBindingAttr(el, 'key')
  307. if (exp) {
  308. if (process.env.NODE_ENV !== 'production' && el.tag === 'template') {
  309. warn(`<template> cannot be keyed. Place the key on real elements instead.`)
  310. }
  311. el.key = exp
  312. }
  313. }
  314. function processRef (el) {
  315. const ref = getBindingAttr(el, 'ref')
  316. if (ref) {
  317. el.ref = ref
  318. el.refInFor = checkInFor(el)
  319. }
  320. }
  321. export function processFor (el: ASTElement) {
  322. let exp
  323. if ((exp = getAndRemoveAttr(el, 'v-for'))) {
  324. const inMatch = exp.match(forAliasRE)
  325. if (!inMatch) {
  326. process.env.NODE_ENV !== 'production' && warn(
  327. `Invalid v-for expression: ${exp}`
  328. )
  329. return
  330. }
  331. el.for = inMatch[2].trim()
  332. const alias = inMatch[1].trim().replace(stripParensRE, '')
  333. const iteratorMatch = alias.match(forIteratorRE)
  334. if (iteratorMatch) {
  335. el.alias = alias.replace(forIteratorRE, '')
  336. el.iterator1 = iteratorMatch[1].trim()
  337. if (iteratorMatch[2]) {
  338. el.iterator2 = iteratorMatch[2].trim()
  339. }
  340. } else {
  341. el.alias = alias
  342. }
  343. }
  344. }
  345. function processIf (el) {
  346. const exp = getAndRemoveAttr(el, 'v-if')
  347. if (exp) {
  348. el.if = exp
  349. addIfCondition(el, {
  350. exp: exp,
  351. block: el
  352. })
  353. } else {
  354. if (getAndRemoveAttr(el, 'v-else') != null) {
  355. el.else = true
  356. }
  357. const elseif = getAndRemoveAttr(el, 'v-else-if')
  358. if (elseif) {
  359. el.elseif = elseif
  360. }
  361. }
  362. }
  363. function processIfConditions (el, parent) {
  364. const prev = findPrevElement(parent.children)
  365. if (prev && prev.if) {
  366. addIfCondition(prev, {
  367. exp: el.elseif,
  368. block: el
  369. })
  370. } else if (process.env.NODE_ENV !== 'production') {
  371. warn(
  372. `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
  373. `used on element <${el.tag}> without corresponding v-if.`
  374. )
  375. }
  376. }
  377. function findPrevElement (children: Array<any>): ASTElement | void {
  378. let i = children.length
  379. while (i--) {
  380. if (children[i].type === 1) {
  381. return children[i]
  382. } else {
  383. if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
  384. warn(
  385. `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
  386. `will be ignored.`
  387. )
  388. }
  389. children.pop()
  390. }
  391. }
  392. }
  393. export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  394. if (!el.ifConditions) {
  395. el.ifConditions = []
  396. }
  397. el.ifConditions.push(condition)
  398. }
  399. function processOnce (el) {
  400. const once = getAndRemoveAttr(el, 'v-once')
  401. if (once != null) {
  402. el.once = true
  403. }
  404. }
  405. function processSlot (el) {
  406. if (el.tag === 'slot') {
  407. el.slotName = getBindingAttr(el, 'name')
  408. if (process.env.NODE_ENV !== 'production' && el.key) {
  409. warn(
  410. `\`key\` does not work on <slot> because slots are abstract outlets ` +
  411. `and can possibly expand into multiple elements. ` +
  412. `Use the key on a wrapping element instead.`
  413. )
  414. }
  415. } else {
  416. let slotScope
  417. if (el.tag === 'template') {
  418. slotScope = getAndRemoveAttr(el, 'scope')
  419. /* istanbul ignore if */
  420. if (process.env.NODE_ENV !== 'production' && slotScope) {
  421. warn(
  422. `the "scope" attribute for scoped slots have been deprecated and ` +
  423. `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
  424. `can also be used on plain elements in addition to <template> to ` +
  425. `denote scoped slots.`,
  426. true
  427. )
  428. }
  429. el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  430. } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
  431. /* istanbul ignore if */
  432. if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
  433. warn(
  434. `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
  435. `(v-for takes higher priority). Use a wrapper <template> for the ` +
  436. `scoped slot to make it clearer.`,
  437. true
  438. )
  439. }
  440. el.slotScope = slotScope
  441. }
  442. const slotTarget = getBindingAttr(el, 'slot')
  443. if (slotTarget) {
  444. el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
  445. // preserve slot as an attribute for native shadow DOM compat
  446. // only for non-scoped slots.
  447. if (el.tag !== 'template' && !el.slotScope) {
  448. addAttr(el, 'slot', slotTarget)
  449. }
  450. }
  451. }
  452. }
  453. function processComponent (el) {
  454. let binding
  455. if ((binding = getBindingAttr(el, 'is'))) {
  456. el.component = binding
  457. }
  458. if (getAndRemoveAttr(el, 'inline-template') != null) {
  459. el.inlineTemplate = true
  460. }
  461. }
  462. function processAttrs (el) {
  463. const list = el.attrsList
  464. let i, l, name, rawName, value, modifiers, isProp
  465. for (i = 0, l = list.length; i < l; i++) {
  466. name = rawName = list[i].name
  467. value = list[i].value
  468. if (dirRE.test(name)) {
  469. // mark element as dynamic
  470. el.hasBindings = true
  471. // modifiers
  472. modifiers = parseModifiers(name)
  473. if (modifiers) {
  474. name = name.replace(modifierRE, '')
  475. }
  476. if (bindRE.test(name)) { // v-bind
  477. name = name.replace(bindRE, '')
  478. value = parseFilters(value)
  479. isProp = false
  480. if (modifiers) {
  481. if (modifiers.prop) {
  482. isProp = true
  483. name = camelize(name)
  484. if (name === 'innerHtml') name = 'innerHTML'
  485. }
  486. if (modifiers.camel) {
  487. name = camelize(name)
  488. }
  489. if (modifiers.sync) {
  490. addHandler(
  491. el,
  492. `update:${camelize(name)}`,
  493. genAssignmentCode(value, `$event`)
  494. )
  495. }
  496. }
  497. // optimize literal values in component props by wrapping them
  498. // in an inline watcher to avoid unnecessary re-renders
  499. if (
  500. !platformIsReservedTag(el.tag) &&
  501. el.tag !== 'slot' &&
  502. literalValueRE.test(value.trim())
  503. ) {
  504. value = `_a(${literalPropId++},function(){return ${value}})`
  505. }
  506. if (isProp || (
  507. !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
  508. )) {
  509. addProp(el, name, value)
  510. } else {
  511. addAttr(el, name, value)
  512. }
  513. } else if (onRE.test(name)) { // v-on
  514. name = name.replace(onRE, '')
  515. addHandler(el, name, value, modifiers, false, warn)
  516. } else { // normal directives
  517. name = name.replace(dirRE, '')
  518. // parse arg
  519. const argMatch = name.match(argRE)
  520. const arg = argMatch && argMatch[1]
  521. if (arg) {
  522. name = name.slice(0, -(arg.length + 1))
  523. }
  524. addDirective(el, name, rawName, value, arg, modifiers)
  525. if (process.env.NODE_ENV !== 'production' && name === 'model') {
  526. checkForAliasModel(el, value)
  527. }
  528. }
  529. } else {
  530. // literal attribute
  531. if (process.env.NODE_ENV !== 'production') {
  532. const res = parseText(value, delimiters)
  533. if (res) {
  534. warn(
  535. `${name}="${value}": ` +
  536. 'Interpolation inside attributes has been removed. ' +
  537. 'Use v-bind or the colon shorthand instead. For example, ' +
  538. 'instead of <div id="{{ val }}">, use <div :id="val">.'
  539. )
  540. }
  541. }
  542. addAttr(el, name, JSON.stringify(value))
  543. // #6887 firefox doesn't update muted state if set via attribute
  544. // even immediately after element creation
  545. if (!el.component &&
  546. name === 'muted' &&
  547. platformMustUseProp(el.tag, el.attrsMap.type, name)) {
  548. addProp(el, name, 'true')
  549. }
  550. }
  551. }
  552. }
  553. function checkInFor (el: ASTElement): boolean {
  554. let parent = el
  555. while (parent) {
  556. if (parent.for !== undefined) {
  557. return true
  558. }
  559. parent = parent.parent
  560. }
  561. return false
  562. }
  563. function parseModifiers (name: string): Object | void {
  564. const match = name.match(modifierRE)
  565. if (match) {
  566. const ret = {}
  567. match.forEach(m => { ret[m.slice(1)] = true })
  568. return ret
  569. }
  570. }
  571. function makeAttrsMap (attrs: Array<Object>): Object {
  572. const map = {}
  573. for (let i = 0, l = attrs.length; i < l; i++) {
  574. if (
  575. process.env.NODE_ENV !== 'production' &&
  576. map[attrs[i].name] && !isIE && !isEdge
  577. ) {
  578. warn('duplicate attribute: ' + attrs[i].name)
  579. }
  580. map[attrs[i].name] = attrs[i].value
  581. }
  582. return map
  583. }
  584. // for script (e.g. type="x/template") or style, do not decode content
  585. function isTextTag (el): boolean {
  586. return el.tag === 'script' || el.tag === 'style'
  587. }
  588. function isForbiddenTag (el): boolean {
  589. return (
  590. el.tag === 'style' ||
  591. (el.tag === 'script' && (
  592. !el.attrsMap.type ||
  593. el.attrsMap.type === 'text/javascript'
  594. ))
  595. )
  596. }
  597. const ieNSBug = /^xmlns:NS\d+/
  598. const ieNSPrefix = /^NS\d+:/
  599. /* istanbul ignore next */
  600. function guardIESVGBug (attrs) {
  601. const res = []
  602. for (let i = 0; i < attrs.length; i++) {
  603. const attr = attrs[i]
  604. if (!ieNSBug.test(attr.name)) {
  605. attr.name = attr.name.replace(ieNSPrefix, '')
  606. res.push(attr)
  607. }
  608. }
  609. return res
  610. }
  611. function checkForAliasModel (el, value) {
  612. let _el = el
  613. while (_el) {
  614. if (_el.for && _el.alias === value) {
  615. warn(
  616. `<${el.tag} v-model="${value}">: ` +
  617. `You are binding v-model directly to a v-for iteration alias. ` +
  618. `This will not be able to modify the v-for source array because ` +
  619. `writing to the alias is like modifying a function local variable. ` +
  620. `Consider using an array of objects and use v-model on an object property instead.`
  621. )
  622. }
  623. _el = _el.parent
  624. }
  625. }