parse.ts 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. import { ErrorHandlingOptions, ParserOptions } from './options'
  2. import { NO, isArray, makeMap, extend } from '@vue/shared'
  3. import {
  4. ErrorCodes,
  5. createCompilerError,
  6. defaultOnError,
  7. defaultOnWarn
  8. } from './errors'
  9. import {
  10. assert,
  11. advancePositionWithMutation,
  12. advancePositionWithClone,
  13. isCoreComponent,
  14. isStaticArgOf
  15. } from './utils'
  16. import {
  17. Namespaces,
  18. AttributeNode,
  19. CommentNode,
  20. DirectiveNode,
  21. ElementNode,
  22. ElementTypes,
  23. ExpressionNode,
  24. NodeTypes,
  25. Position,
  26. RootNode,
  27. SourceLocation,
  28. TextNode,
  29. TemplateChildNode,
  30. InterpolationNode,
  31. createRoot,
  32. ConstantTypes
  33. } from './ast'
  34. import {
  35. checkCompatEnabled,
  36. CompilerCompatOptions,
  37. CompilerDeprecationTypes,
  38. isCompatEnabled,
  39. warnDeprecation
  40. } from './compat/compatConfig'
  41. type OptionalOptions =
  42. | 'whitespace'
  43. | 'isNativeTag'
  44. | 'isBuiltInComponent'
  45. | keyof CompilerCompatOptions
  46. type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
  47. Pick<ParserOptions, OptionalOptions>
  48. type AttributeValue =
  49. | {
  50. content: string
  51. isQuoted: boolean
  52. loc: SourceLocation
  53. }
  54. | undefined
  55. // The default decoder only provides escapes for characters reserved as part of
  56. // the template syntax, and is only used if the custom renderer did not provide
  57. // a platform-specific decoder.
  58. const decodeRE = /&(gt|lt|amp|apos|quot);/g
  59. const decodeMap: Record<string, string> = {
  60. gt: '>',
  61. lt: '<',
  62. amp: '&',
  63. apos: "'",
  64. quot: '"'
  65. }
  66. export const defaultParserOptions: MergedParserOptions = {
  67. delimiters: [`{{`, `}}`],
  68. getNamespace: () => Namespaces.HTML,
  69. getTextMode: () => TextModes.DATA,
  70. isVoidTag: NO,
  71. isPreTag: NO,
  72. isCustomElement: NO,
  73. decodeEntities: (rawText: string): string =>
  74. rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
  75. onError: defaultOnError,
  76. onWarn: defaultOnWarn,
  77. comments: __DEV__
  78. }
  79. export const enum TextModes {
  80. // | Elements | Entities | End sign | Inside of
  81. DATA, // | ✔ | ✔ | End tags of ancestors |
  82. RCDATA, // | ✘ | ✔ | End tag of the parent | <textarea>
  83. RAWTEXT, // | ✘ | ✘ | End tag of the parent | <style>,<script>
  84. CDATA,
  85. ATTRIBUTE_VALUE
  86. }
  87. export interface ParserContext {
  88. options: MergedParserOptions
  89. readonly originalSource: string
  90. source: string
  91. offset: number
  92. line: number
  93. column: number
  94. inPre: boolean // HTML <pre> tag, preserve whitespaces
  95. inVPre: boolean // v-pre, do not process directives and interpolations
  96. onWarn: NonNullable<ErrorHandlingOptions['onWarn']>
  97. }
  98. export function baseParse(
  99. content: string,
  100. options: ParserOptions = {}
  101. ): RootNode {
  102. const context = createParserContext(content, options)
  103. const start = getCursor(context)
  104. return createRoot(
  105. parseChildren(context, TextModes.DATA, []),
  106. getSelection(context, start)
  107. )
  108. }
  109. function createParserContext(
  110. content: string,
  111. rawOptions: ParserOptions
  112. ): ParserContext {
  113. const options = extend({}, defaultParserOptions)
  114. let key: keyof ParserOptions
  115. for (key in rawOptions) {
  116. // @ts-ignore
  117. options[key] =
  118. rawOptions[key] === undefined
  119. ? defaultParserOptions[key]
  120. : rawOptions[key]
  121. }
  122. return {
  123. options,
  124. column: 1,
  125. line: 1,
  126. offset: 0,
  127. originalSource: content,
  128. source: content,
  129. inPre: false,
  130. inVPre: false,
  131. onWarn: options.onWarn
  132. }
  133. }
  134. function parseChildren(
  135. context: ParserContext,
  136. mode: TextModes,
  137. ancestors: ElementNode[]
  138. ): TemplateChildNode[] {
  139. const parent = last(ancestors)
  140. const ns = parent ? parent.ns : Namespaces.HTML
  141. const nodes: TemplateChildNode[] = []
  142. while (!isEnd(context, mode, ancestors)) {
  143. __TEST__ && assert(context.source.length > 0)
  144. const s = context.source
  145. let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
  146. if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
  147. if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
  148. // '{{'
  149. node = parseInterpolation(context, mode)
  150. } else if (mode === TextModes.DATA && s[0] === '<') {
  151. // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
  152. if (s.length === 1) {
  153. emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
  154. } else if (s[1] === '!') {
  155. // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
  156. if (startsWith(s, '<!--')) {
  157. node = parseComment(context)
  158. } else if (startsWith(s, '<!DOCTYPE')) {
  159. // Ignore DOCTYPE by a limitation.
  160. node = parseBogusComment(context)
  161. } else if (startsWith(s, '<![CDATA[')) {
  162. if (ns !== Namespaces.HTML) {
  163. node = parseCDATA(context, ancestors)
  164. } else {
  165. emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
  166. node = parseBogusComment(context)
  167. }
  168. } else {
  169. emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
  170. node = parseBogusComment(context)
  171. }
  172. } else if (s[1] === '/') {
  173. // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
  174. if (s.length === 2) {
  175. emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
  176. } else if (s[2] === '>') {
  177. emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
  178. advanceBy(context, 3)
  179. continue
  180. } else if (/[a-z]/i.test(s[2])) {
  181. emitError(context, ErrorCodes.X_INVALID_END_TAG)
  182. parseTag(context, TagType.End, parent)
  183. continue
  184. } else {
  185. emitError(
  186. context,
  187. ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  188. 2
  189. )
  190. node = parseBogusComment(context)
  191. }
  192. } else if (/[a-z]/i.test(s[1])) {
  193. node = parseElement(context, ancestors)
  194. // 2.x <template> with no directive compat
  195. if (
  196. __COMPAT__ &&
  197. isCompatEnabled(
  198. CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
  199. context
  200. ) &&
  201. node &&
  202. node.tag === 'template' &&
  203. !node.props.some(
  204. p =>
  205. p.type === NodeTypes.DIRECTIVE &&
  206. isSpecialTemplateDirective(p.name)
  207. )
  208. ) {
  209. __DEV__ &&
  210. warnDeprecation(
  211. CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
  212. context,
  213. node.loc
  214. )
  215. node = node.children
  216. }
  217. } else if (s[1] === '?') {
  218. emitError(
  219. context,
  220. ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
  221. 1
  222. )
  223. node = parseBogusComment(context)
  224. } else {
  225. emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
  226. }
  227. }
  228. }
  229. if (!node) {
  230. node = parseText(context, mode)
  231. }
  232. if (isArray(node)) {
  233. for (let i = 0; i < node.length; i++) {
  234. pushNode(nodes, node[i])
  235. }
  236. } else {
  237. pushNode(nodes, node)
  238. }
  239. }
  240. // Whitespace handling strategy like v2
  241. let removedWhitespace = false
  242. if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
  243. const shouldCondense = context.options.whitespace !== 'preserve'
  244. for (let i = 0; i < nodes.length; i++) {
  245. const node = nodes[i]
  246. if (node.type === NodeTypes.TEXT) {
  247. if (!context.inPre) {
  248. if (!/[^\t\r\n\f ]/.test(node.content)) {
  249. const prev = nodes[i - 1]
  250. const next = nodes[i + 1]
  251. // Remove if:
  252. // - the whitespace is the first or last node, or:
  253. // - (condense mode) the whitespace is between twos comments, or:
  254. // - (condense mode) the whitespace is between comment and element, or:
  255. // - (condense mode) the whitespace is between two elements AND contains newline
  256. if (
  257. !prev ||
  258. !next ||
  259. (shouldCondense &&
  260. ((prev.type === NodeTypes.COMMENT &&
  261. next.type === NodeTypes.COMMENT) ||
  262. (prev.type === NodeTypes.COMMENT &&
  263. next.type === NodeTypes.ELEMENT) ||
  264. (prev.type === NodeTypes.ELEMENT &&
  265. next.type === NodeTypes.COMMENT) ||
  266. (prev.type === NodeTypes.ELEMENT &&
  267. next.type === NodeTypes.ELEMENT &&
  268. /[\r\n]/.test(node.content))))
  269. ) {
  270. removedWhitespace = true
  271. nodes[i] = null as any
  272. } else {
  273. // Otherwise, the whitespace is condensed into a single space
  274. node.content = ' '
  275. }
  276. } else if (shouldCondense) {
  277. // in condense mode, consecutive whitespaces in text are condensed
  278. // down to a single space.
  279. node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
  280. }
  281. } else {
  282. // #6410 normalize windows newlines in <pre>:
  283. // in SSR, browsers normalize server-rendered \r\n into a single \n
  284. // in the DOM
  285. node.content = node.content.replace(/\r\n/g, '\n')
  286. }
  287. }
  288. // Remove comment nodes if desired by configuration.
  289. else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
  290. removedWhitespace = true
  291. nodes[i] = null as any
  292. }
  293. }
  294. if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
  295. // remove leading newline per html spec
  296. // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
  297. const first = nodes[0]
  298. if (first && first.type === NodeTypes.TEXT) {
  299. first.content = first.content.replace(/^\r?\n/, '')
  300. }
  301. }
  302. }
  303. return removedWhitespace ? nodes.filter(Boolean) : nodes
  304. }
  305. function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
  306. if (node.type === NodeTypes.TEXT) {
  307. const prev = last(nodes)
  308. // Merge if both this and the previous node are text and those are
  309. // consecutive. This happens for cases like "a < b".
  310. if (
  311. prev &&
  312. prev.type === NodeTypes.TEXT &&
  313. prev.loc.end.offset === node.loc.start.offset
  314. ) {
  315. prev.content += node.content
  316. prev.loc.end = node.loc.end
  317. prev.loc.source += node.loc.source
  318. return
  319. }
  320. }
  321. nodes.push(node)
  322. }
  323. function parseCDATA(
  324. context: ParserContext,
  325. ancestors: ElementNode[]
  326. ): TemplateChildNode[] {
  327. __TEST__ &&
  328. assert(last(ancestors) == null || last(ancestors)!.ns !== Namespaces.HTML)
  329. __TEST__ && assert(startsWith(context.source, '<![CDATA['))
  330. advanceBy(context, 9)
  331. const nodes = parseChildren(context, TextModes.CDATA, ancestors)
  332. if (context.source.length === 0) {
  333. emitError(context, ErrorCodes.EOF_IN_CDATA)
  334. } else {
  335. __TEST__ && assert(startsWith(context.source, ']]>'))
  336. advanceBy(context, 3)
  337. }
  338. return nodes
  339. }
  340. function parseComment(context: ParserContext): CommentNode {
  341. __TEST__ && assert(startsWith(context.source, '<!--'))
  342. const start = getCursor(context)
  343. let content: string
  344. // Regular comment.
  345. const match = /--(\!)?>/.exec(context.source)
  346. if (!match) {
  347. content = context.source.slice(4)
  348. advanceBy(context, context.source.length)
  349. emitError(context, ErrorCodes.EOF_IN_COMMENT)
  350. } else {
  351. if (match.index <= 3) {
  352. emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
  353. }
  354. if (match[1]) {
  355. emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
  356. }
  357. content = context.source.slice(4, match.index)
  358. // Advancing with reporting nested comments.
  359. const s = context.source.slice(0, match.index)
  360. let prevIndex = 1,
  361. nestedIndex = 0
  362. while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
  363. advanceBy(context, nestedIndex - prevIndex + 1)
  364. if (nestedIndex + 4 < s.length) {
  365. emitError(context, ErrorCodes.NESTED_COMMENT)
  366. }
  367. prevIndex = nestedIndex + 1
  368. }
  369. advanceBy(context, match.index + match[0].length - prevIndex + 1)
  370. }
  371. return {
  372. type: NodeTypes.COMMENT,
  373. content,
  374. loc: getSelection(context, start)
  375. }
  376. }
  377. function parseBogusComment(context: ParserContext): CommentNode | undefined {
  378. __TEST__ && assert(/^<(?:[\!\?]|\/[^a-z>])/i.test(context.source))
  379. const start = getCursor(context)
  380. const contentStart = context.source[1] === '?' ? 1 : 2
  381. let content: string
  382. const closeIndex = context.source.indexOf('>')
  383. if (closeIndex === -1) {
  384. content = context.source.slice(contentStart)
  385. advanceBy(context, context.source.length)
  386. } else {
  387. content = context.source.slice(contentStart, closeIndex)
  388. advanceBy(context, closeIndex + 1)
  389. }
  390. return {
  391. type: NodeTypes.COMMENT,
  392. content,
  393. loc: getSelection(context, start)
  394. }
  395. }
  396. function parseElement(
  397. context: ParserContext,
  398. ancestors: ElementNode[]
  399. ): ElementNode | undefined {
  400. __TEST__ && assert(/^<[a-z]/i.test(context.source))
  401. // Start tag.
  402. const wasInPre = context.inPre
  403. const wasInVPre = context.inVPre
  404. const parent = last(ancestors)
  405. const element = parseTag(context, TagType.Start, parent)
  406. const isPreBoundary = context.inPre && !wasInPre
  407. const isVPreBoundary = context.inVPre && !wasInVPre
  408. if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
  409. // #4030 self-closing <pre> tag
  410. if (isPreBoundary) {
  411. context.inPre = false
  412. }
  413. if (isVPreBoundary) {
  414. context.inVPre = false
  415. }
  416. return element
  417. }
  418. // Children.
  419. ancestors.push(element)
  420. const mode = context.options.getTextMode(element, parent)
  421. const children = parseChildren(context, mode, ancestors)
  422. ancestors.pop()
  423. // 2.x inline-template compat
  424. if (__COMPAT__) {
  425. const inlineTemplateProp = element.props.find(
  426. p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
  427. ) as AttributeNode
  428. if (
  429. inlineTemplateProp &&
  430. checkCompatEnabled(
  431. CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
  432. context,
  433. inlineTemplateProp.loc
  434. )
  435. ) {
  436. const loc = getSelection(context, element.loc.end)
  437. inlineTemplateProp.value = {
  438. type: NodeTypes.TEXT,
  439. content: loc.source,
  440. loc
  441. }
  442. }
  443. }
  444. element.children = children
  445. // End tag.
  446. if (startsWithEndTagOpen(context.source, element.tag)) {
  447. parseTag(context, TagType.End, parent)
  448. } else {
  449. emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
  450. if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
  451. const first = children[0]
  452. if (first && startsWith(first.loc.source, '<!--')) {
  453. emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
  454. }
  455. }
  456. }
  457. element.loc = getSelection(context, element.loc.start)
  458. if (isPreBoundary) {
  459. context.inPre = false
  460. }
  461. if (isVPreBoundary) {
  462. context.inVPre = false
  463. }
  464. return element
  465. }
  466. const enum TagType {
  467. Start,
  468. End
  469. }
  470. const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
  471. `if,else,else-if,for,slot`
  472. )
  473. /**
  474. * Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
  475. */
  476. function parseTag(
  477. context: ParserContext,
  478. type: TagType.Start,
  479. parent: ElementNode | undefined
  480. ): ElementNode
  481. function parseTag(
  482. context: ParserContext,
  483. type: TagType.End,
  484. parent: ElementNode | undefined
  485. ): void
  486. function parseTag(
  487. context: ParserContext,
  488. type: TagType,
  489. parent: ElementNode | undefined
  490. ): ElementNode | undefined {
  491. __TEST__ && assert(/^<\/?[a-z]/i.test(context.source))
  492. __TEST__ &&
  493. assert(
  494. type === (startsWith(context.source, '</') ? TagType.End : TagType.Start)
  495. )
  496. // Tag open.
  497. const start = getCursor(context)
  498. const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  499. const tag = match[1]
  500. const ns = context.options.getNamespace(tag, parent)
  501. advanceBy(context, match[0].length)
  502. advanceSpaces(context)
  503. // save current state in case we need to re-parse attributes with v-pre
  504. const cursor = getCursor(context)
  505. const currentSource = context.source
  506. // check <pre> tag
  507. if (context.options.isPreTag(tag)) {
  508. context.inPre = true
  509. }
  510. // Attributes.
  511. let props = parseAttributes(context, type)
  512. // check v-pre
  513. if (
  514. type === TagType.Start &&
  515. !context.inVPre &&
  516. props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
  517. ) {
  518. context.inVPre = true
  519. // reset context
  520. extend(context, cursor)
  521. context.source = currentSource
  522. // re-parse attrs and filter out v-pre itself
  523. props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
  524. }
  525. // Tag close.
  526. let isSelfClosing = false
  527. if (context.source.length === 0) {
  528. emitError(context, ErrorCodes.EOF_IN_TAG)
  529. } else {
  530. isSelfClosing = startsWith(context.source, '/>')
  531. if (type === TagType.End && isSelfClosing) {
  532. emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
  533. }
  534. advanceBy(context, isSelfClosing ? 2 : 1)
  535. }
  536. if (type === TagType.End) {
  537. return
  538. }
  539. // 2.x deprecation checks
  540. if (
  541. __COMPAT__ &&
  542. __DEV__ &&
  543. isCompatEnabled(
  544. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  545. context
  546. )
  547. ) {
  548. let hasIf = false
  549. let hasFor = false
  550. for (let i = 0; i < props.length; i++) {
  551. const p = props[i]
  552. if (p.type === NodeTypes.DIRECTIVE) {
  553. if (p.name === 'if') {
  554. hasIf = true
  555. } else if (p.name === 'for') {
  556. hasFor = true
  557. }
  558. }
  559. if (hasIf && hasFor) {
  560. warnDeprecation(
  561. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  562. context,
  563. getSelection(context, start)
  564. )
  565. break
  566. }
  567. }
  568. }
  569. let tagType = ElementTypes.ELEMENT
  570. if (!context.inVPre) {
  571. if (tag === 'slot') {
  572. tagType = ElementTypes.SLOT
  573. } else if (tag === 'template') {
  574. if (
  575. props.some(
  576. p =>
  577. p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
  578. )
  579. ) {
  580. tagType = ElementTypes.TEMPLATE
  581. }
  582. } else if (isComponent(tag, props, context)) {
  583. tagType = ElementTypes.COMPONENT
  584. }
  585. }
  586. return {
  587. type: NodeTypes.ELEMENT,
  588. ns,
  589. tag,
  590. tagType,
  591. props,
  592. isSelfClosing,
  593. children: [],
  594. loc: getSelection(context, start),
  595. codegenNode: undefined // to be created during transform phase
  596. }
  597. }
  598. function isComponent(
  599. tag: string,
  600. props: (AttributeNode | DirectiveNode)[],
  601. context: ParserContext
  602. ) {
  603. const options = context.options
  604. if (options.isCustomElement(tag)) {
  605. return false
  606. }
  607. if (
  608. tag === 'component' ||
  609. /^[A-Z]/.test(tag) ||
  610. isCoreComponent(tag) ||
  611. (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
  612. (options.isNativeTag && !options.isNativeTag(tag))
  613. ) {
  614. return true
  615. }
  616. // at this point the tag should be a native tag, but check for potential "is"
  617. // casting
  618. for (let i = 0; i < props.length; i++) {
  619. const p = props[i]
  620. if (p.type === NodeTypes.ATTRIBUTE) {
  621. if (p.name === 'is' && p.value) {
  622. if (p.value.content.startsWith('vue:')) {
  623. return true
  624. } else if (
  625. __COMPAT__ &&
  626. checkCompatEnabled(
  627. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  628. context,
  629. p.loc
  630. )
  631. ) {
  632. return true
  633. }
  634. }
  635. } else {
  636. // directive
  637. // v-is (TODO Deprecate)
  638. if (p.name === 'is') {
  639. return true
  640. } else if (
  641. // :is on plain element - only treat as component in compat mode
  642. p.name === 'bind' &&
  643. isStaticArgOf(p.arg, 'is') &&
  644. __COMPAT__ &&
  645. checkCompatEnabled(
  646. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  647. context,
  648. p.loc
  649. )
  650. ) {
  651. return true
  652. }
  653. }
  654. }
  655. }
  656. function parseAttributes(
  657. context: ParserContext,
  658. type: TagType
  659. ): (AttributeNode | DirectiveNode)[] {
  660. const props = []
  661. const attributeNames = new Set<string>()
  662. while (
  663. context.source.length > 0 &&
  664. !startsWith(context.source, '>') &&
  665. !startsWith(context.source, '/>')
  666. ) {
  667. if (startsWith(context.source, '/')) {
  668. emitError(context, ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG)
  669. advanceBy(context, 1)
  670. advanceSpaces(context)
  671. continue
  672. }
  673. if (type === TagType.End) {
  674. emitError(context, ErrorCodes.END_TAG_WITH_ATTRIBUTES)
  675. }
  676. const attr = parseAttribute(context, attributeNames)
  677. // Trim whitespace between class
  678. // https://github.com/vuejs/core/issues/4251
  679. if (
  680. attr.type === NodeTypes.ATTRIBUTE &&
  681. attr.value &&
  682. attr.name === 'class'
  683. ) {
  684. attr.value.content = attr.value.content.replace(/\s+/g, ' ').trim()
  685. }
  686. if (type === TagType.Start) {
  687. props.push(attr)
  688. }
  689. if (/^[^\t\r\n\f />]/.test(context.source)) {
  690. emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
  691. }
  692. advanceSpaces(context)
  693. }
  694. return props
  695. }
  696. function parseAttribute(
  697. context: ParserContext,
  698. nameSet: Set<string>
  699. ): AttributeNode | DirectiveNode {
  700. __TEST__ && assert(/^[^\t\r\n\f />]/.test(context.source))
  701. // Name.
  702. const start = getCursor(context)
  703. const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  704. const name = match[0]
  705. if (nameSet.has(name)) {
  706. emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
  707. }
  708. nameSet.add(name)
  709. if (name[0] === '=') {
  710. emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
  711. }
  712. {
  713. const pattern = /["'<]/g
  714. let m: RegExpExecArray | null
  715. while ((m = pattern.exec(name))) {
  716. emitError(
  717. context,
  718. ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  719. m.index
  720. )
  721. }
  722. }
  723. advanceBy(context, name.length)
  724. // Value
  725. let value: AttributeValue = undefined
  726. if (/^[\t\r\n\f ]*=/.test(context.source)) {
  727. advanceSpaces(context)
  728. advanceBy(context, 1)
  729. advanceSpaces(context)
  730. value = parseAttributeValue(context)
  731. if (!value) {
  732. emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
  733. }
  734. }
  735. const loc = getSelection(context, start)
  736. if (!context.inVPre && /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
  737. const match =
  738. /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
  739. name
  740. )!
  741. let isPropShorthand = startsWith(name, '.')
  742. let dirName =
  743. match[1] ||
  744. (isPropShorthand || startsWith(name, ':')
  745. ? 'bind'
  746. : startsWith(name, '@')
  747. ? 'on'
  748. : 'slot')
  749. let arg: ExpressionNode | undefined
  750. if (match[2]) {
  751. const isSlot = dirName === 'slot'
  752. const startOffset = name.lastIndexOf(match[2])
  753. const loc = getSelection(
  754. context,
  755. getNewPosition(context, start, startOffset),
  756. getNewPosition(
  757. context,
  758. start,
  759. startOffset + match[2].length + ((isSlot && match[3]) || '').length
  760. )
  761. )
  762. let content = match[2]
  763. let isStatic = true
  764. if (content.startsWith('[')) {
  765. isStatic = false
  766. if (!content.endsWith(']')) {
  767. emitError(
  768. context,
  769. ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
  770. )
  771. content = content.slice(1)
  772. } else {
  773. content = content.slice(1, content.length - 1)
  774. }
  775. } else if (isSlot) {
  776. // #1241 special case for v-slot: vuetify relies extensively on slot
  777. // names containing dots. v-slot doesn't have any modifiers and Vue 2.x
  778. // supports such usage so we are keeping it consistent with 2.x.
  779. content += match[3] || ''
  780. }
  781. arg = {
  782. type: NodeTypes.SIMPLE_EXPRESSION,
  783. content,
  784. isStatic,
  785. constType: isStatic
  786. ? ConstantTypes.CAN_STRINGIFY
  787. : ConstantTypes.NOT_CONSTANT,
  788. loc
  789. }
  790. }
  791. if (value && value.isQuoted) {
  792. const valueLoc = value.loc
  793. valueLoc.start.offset++
  794. valueLoc.start.column++
  795. valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
  796. valueLoc.source = valueLoc.source.slice(1, -1)
  797. }
  798. const modifiers = match[3] ? match[3].slice(1).split('.') : []
  799. if (isPropShorthand) modifiers.push('prop')
  800. // 2.x compat v-bind:foo.sync -> v-model:foo
  801. if (__COMPAT__ && dirName === 'bind' && arg) {
  802. if (
  803. modifiers.includes('sync') &&
  804. checkCompatEnabled(
  805. CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
  806. context,
  807. loc,
  808. arg.loc.source
  809. )
  810. ) {
  811. dirName = 'model'
  812. modifiers.splice(modifiers.indexOf('sync'), 1)
  813. }
  814. if (__DEV__ && modifiers.includes('prop')) {
  815. checkCompatEnabled(
  816. CompilerDeprecationTypes.COMPILER_V_BIND_PROP,
  817. context,
  818. loc
  819. )
  820. }
  821. }
  822. return {
  823. type: NodeTypes.DIRECTIVE,
  824. name: dirName,
  825. exp: value && {
  826. type: NodeTypes.SIMPLE_EXPRESSION,
  827. content: value.content,
  828. isStatic: false,
  829. // Treat as non-constant by default. This can be potentially set to
  830. // other values by `transformExpression` to make it eligible for hoisting.
  831. constType: ConstantTypes.NOT_CONSTANT,
  832. loc: value.loc
  833. },
  834. arg,
  835. modifiers,
  836. loc
  837. }
  838. }
  839. // missing directive name or illegal directive name
  840. if (!context.inVPre && startsWith(name, 'v-')) {
  841. emitError(context, ErrorCodes.X_MISSING_DIRECTIVE_NAME)
  842. }
  843. return {
  844. type: NodeTypes.ATTRIBUTE,
  845. name,
  846. value: value && {
  847. type: NodeTypes.TEXT,
  848. content: value.content,
  849. loc: value.loc
  850. },
  851. loc
  852. }
  853. }
  854. function parseAttributeValue(context: ParserContext): AttributeValue {
  855. const start = getCursor(context)
  856. let content: string
  857. const quote = context.source[0]
  858. const isQuoted = quote === `"` || quote === `'`
  859. if (isQuoted) {
  860. // Quoted value.
  861. advanceBy(context, 1)
  862. const endIndex = context.source.indexOf(quote)
  863. if (endIndex === -1) {
  864. content = parseTextData(
  865. context,
  866. context.source.length,
  867. TextModes.ATTRIBUTE_VALUE
  868. )
  869. } else {
  870. content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
  871. advanceBy(context, 1)
  872. }
  873. } else {
  874. // Unquoted
  875. const match = /^[^\t\r\n\f >]+/.exec(context.source)
  876. if (!match) {
  877. return undefined
  878. }
  879. const unexpectedChars = /["'<=`]/g
  880. let m: RegExpExecArray | null
  881. while ((m = unexpectedChars.exec(match[0]))) {
  882. emitError(
  883. context,
  884. ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  885. m.index
  886. )
  887. }
  888. content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
  889. }
  890. return { content, isQuoted, loc: getSelection(context, start) }
  891. }
  892. function parseInterpolation(
  893. context: ParserContext,
  894. mode: TextModes
  895. ): InterpolationNode | undefined {
  896. const [open, close] = context.options.delimiters
  897. __TEST__ && assert(startsWith(context.source, open))
  898. const closeIndex = context.source.indexOf(close, open.length)
  899. if (closeIndex === -1) {
  900. emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
  901. return undefined
  902. }
  903. const start = getCursor(context)
  904. advanceBy(context, open.length)
  905. const innerStart = getCursor(context)
  906. const innerEnd = getCursor(context)
  907. const rawContentLength = closeIndex - open.length
  908. const rawContent = context.source.slice(0, rawContentLength)
  909. const preTrimContent = parseTextData(context, rawContentLength, mode)
  910. const content = preTrimContent.trim()
  911. const startOffset = preTrimContent.indexOf(content)
  912. if (startOffset > 0) {
  913. advancePositionWithMutation(innerStart, rawContent, startOffset)
  914. }
  915. const endOffset =
  916. rawContentLength - (preTrimContent.length - content.length - startOffset)
  917. advancePositionWithMutation(innerEnd, rawContent, endOffset)
  918. advanceBy(context, close.length)
  919. return {
  920. type: NodeTypes.INTERPOLATION,
  921. content: {
  922. type: NodeTypes.SIMPLE_EXPRESSION,
  923. isStatic: false,
  924. // Set `isConstant` to false by default and will decide in transformExpression
  925. constType: ConstantTypes.NOT_CONSTANT,
  926. content,
  927. loc: getSelection(context, innerStart, innerEnd)
  928. },
  929. loc: getSelection(context, start)
  930. }
  931. }
  932. function parseText(context: ParserContext, mode: TextModes): TextNode {
  933. __TEST__ && assert(context.source.length > 0)
  934. const endTokens =
  935. mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
  936. let endIndex = context.source.length
  937. for (let i = 0; i < endTokens.length; i++) {
  938. const index = context.source.indexOf(endTokens[i], 1)
  939. if (index !== -1 && endIndex > index) {
  940. endIndex = index
  941. }
  942. }
  943. __TEST__ && assert(endIndex > 0)
  944. const start = getCursor(context)
  945. const content = parseTextData(context, endIndex, mode)
  946. return {
  947. type: NodeTypes.TEXT,
  948. content,
  949. loc: getSelection(context, start)
  950. }
  951. }
  952. /**
  953. * Get text data with a given length from the current location.
  954. * This translates HTML entities in the text data.
  955. */
  956. function parseTextData(
  957. context: ParserContext,
  958. length: number,
  959. mode: TextModes
  960. ): string {
  961. const rawText = context.source.slice(0, length)
  962. advanceBy(context, length)
  963. if (
  964. mode === TextModes.RAWTEXT ||
  965. mode === TextModes.CDATA ||
  966. !rawText.includes('&')
  967. ) {
  968. return rawText
  969. } else {
  970. // DATA or RCDATA containing "&"". Entity decoding required.
  971. return context.options.decodeEntities(
  972. rawText,
  973. mode === TextModes.ATTRIBUTE_VALUE
  974. )
  975. }
  976. }
  977. function getCursor(context: ParserContext): Position {
  978. const { column, line, offset } = context
  979. return { column, line, offset }
  980. }
  981. function getSelection(
  982. context: ParserContext,
  983. start: Position,
  984. end?: Position
  985. ): SourceLocation {
  986. end = end || getCursor(context)
  987. return {
  988. start,
  989. end,
  990. source: context.originalSource.slice(start.offset, end.offset)
  991. }
  992. }
  993. function last<T>(xs: T[]): T | undefined {
  994. return xs[xs.length - 1]
  995. }
  996. function startsWith(source: string, searchString: string): boolean {
  997. return source.startsWith(searchString)
  998. }
  999. function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  1000. const { source } = context
  1001. __TEST__ && assert(numberOfCharacters <= source.length)
  1002. advancePositionWithMutation(context, source, numberOfCharacters)
  1003. context.source = source.slice(numberOfCharacters)
  1004. }
  1005. function advanceSpaces(context: ParserContext): void {
  1006. const match = /^[\t\r\n\f ]+/.exec(context.source)
  1007. if (match) {
  1008. advanceBy(context, match[0].length)
  1009. }
  1010. }
  1011. function getNewPosition(
  1012. context: ParserContext,
  1013. start: Position,
  1014. numberOfCharacters: number
  1015. ): Position {
  1016. return advancePositionWithClone(
  1017. start,
  1018. context.originalSource.slice(start.offset, numberOfCharacters),
  1019. numberOfCharacters
  1020. )
  1021. }
  1022. function emitError(
  1023. context: ParserContext,
  1024. code: ErrorCodes,
  1025. offset?: number,
  1026. loc: Position = getCursor(context)
  1027. ): void {
  1028. if (offset) {
  1029. loc.offset += offset
  1030. loc.column += offset
  1031. }
  1032. context.options.onError(
  1033. createCompilerError(code, {
  1034. start: loc,
  1035. end: loc,
  1036. source: ''
  1037. })
  1038. )
  1039. }
  1040. function isEnd(
  1041. context: ParserContext,
  1042. mode: TextModes,
  1043. ancestors: ElementNode[]
  1044. ): boolean {
  1045. const s = context.source
  1046. switch (mode) {
  1047. case TextModes.DATA:
  1048. if (startsWith(s, '</')) {
  1049. // TODO: probably bad performance
  1050. for (let i = ancestors.length - 1; i >= 0; --i) {
  1051. if (startsWithEndTagOpen(s, ancestors[i].tag)) {
  1052. return true
  1053. }
  1054. }
  1055. }
  1056. break
  1057. case TextModes.RCDATA:
  1058. case TextModes.RAWTEXT: {
  1059. const parent = last(ancestors)
  1060. if (parent && startsWithEndTagOpen(s, parent.tag)) {
  1061. return true
  1062. }
  1063. break
  1064. }
  1065. case TextModes.CDATA:
  1066. if (startsWith(s, ']]>')) {
  1067. return true
  1068. }
  1069. break
  1070. }
  1071. return !s
  1072. }
  1073. function startsWithEndTagOpen(source: string, tag: string): boolean {
  1074. return (
  1075. startsWith(source, '</') &&
  1076. source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
  1077. /[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
  1078. )
  1079. }