index.js 15 KB

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