parse.ts 32 KB

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