parse.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  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. isBindKey
  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: number // HTML <pre> tag, preserve whitespaces
  95. inVPre: number // 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: 0,
  130. inVPre: 0,
  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 (!context.inPre && node.type === NodeTypes.TEXT) {
  247. if (!/[^\t\r\n\f ]/.test(node.content)) {
  248. const prev = nodes[i - 1]
  249. const next = nodes[i + 1]
  250. // Remove if:
  251. // - the whitespace is the first or last node, or:
  252. // - (condense mode) the whitespace is adjacent to a comment, or:
  253. // - (condense mode) the whitespace is between two elements AND contains newline
  254. if (
  255. !prev ||
  256. !next ||
  257. (shouldCondense &&
  258. (prev.type === NodeTypes.COMMENT ||
  259. next.type === NodeTypes.COMMENT ||
  260. (prev.type === NodeTypes.ELEMENT &&
  261. next.type === NodeTypes.ELEMENT &&
  262. /[\r\n]/.test(node.content))))
  263. ) {
  264. removedWhitespace = true
  265. nodes[i] = null as any
  266. } else {
  267. // Otherwise, the whitespace is condensed into a single space
  268. node.content = ' '
  269. }
  270. } else if (shouldCondense) {
  271. // in condense mode, consecutive whitespaces in text are condensed
  272. // down to a single space.
  273. node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
  274. }
  275. }
  276. // Remove comment nodes if desired by configuration.
  277. else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
  278. removedWhitespace = true
  279. nodes[i] = null as any
  280. }
  281. }
  282. if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
  283. // remove leading newline per html spec
  284. // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
  285. const first = nodes[0]
  286. if (first && first.type === NodeTypes.TEXT) {
  287. first.content = first.content.replace(/^\r?\n/, '')
  288. }
  289. }
  290. }
  291. return removedWhitespace ? nodes.filter(Boolean) : nodes
  292. }
  293. function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
  294. if (node.type === NodeTypes.TEXT) {
  295. const prev = last(nodes)
  296. // Merge if both this and the previous node are text and those are
  297. // consecutive. This happens for cases like "a < b".
  298. if (
  299. prev &&
  300. prev.type === NodeTypes.TEXT &&
  301. prev.loc.end.offset === node.loc.start.offset
  302. ) {
  303. prev.content += node.content
  304. prev.loc.end = node.loc.end
  305. prev.loc.source += node.loc.source
  306. return
  307. }
  308. }
  309. nodes.push(node)
  310. }
  311. function parseCDATA(
  312. context: ParserContext,
  313. ancestors: ElementNode[]
  314. ): TemplateChildNode[] {
  315. __TEST__ &&
  316. assert(last(ancestors) == null || last(ancestors)!.ns !== Namespaces.HTML)
  317. __TEST__ && assert(startsWith(context.source, '<![CDATA['))
  318. advanceBy(context, 9)
  319. const nodes = parseChildren(context, TextModes.CDATA, ancestors)
  320. if (context.source.length === 0) {
  321. emitError(context, ErrorCodes.EOF_IN_CDATA)
  322. } else {
  323. __TEST__ && assert(startsWith(context.source, ']]>'))
  324. advanceBy(context, 3)
  325. }
  326. return nodes
  327. }
  328. function parseComment(context: ParserContext): CommentNode {
  329. __TEST__ && assert(startsWith(context.source, '<!--'))
  330. const start = getCursor(context)
  331. let content: string
  332. // Regular comment.
  333. const match = /--(\!)?>/.exec(context.source)
  334. if (!match) {
  335. content = context.source.slice(4)
  336. advanceBy(context, context.source.length)
  337. emitError(context, ErrorCodes.EOF_IN_COMMENT)
  338. } else {
  339. if (match.index <= 3) {
  340. emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
  341. }
  342. if (match[1]) {
  343. emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
  344. }
  345. content = context.source.slice(4, match.index)
  346. // Advancing with reporting nested comments.
  347. const s = context.source.slice(0, match.index)
  348. let prevIndex = 1,
  349. nestedIndex = 0
  350. while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
  351. advanceBy(context, nestedIndex - prevIndex + 1)
  352. if (nestedIndex + 4 < s.length) {
  353. emitError(context, ErrorCodes.NESTED_COMMENT)
  354. }
  355. prevIndex = nestedIndex + 1
  356. }
  357. advanceBy(context, match.index + match[0].length - prevIndex + 1)
  358. }
  359. return {
  360. type: NodeTypes.COMMENT,
  361. content,
  362. loc: getSelection(context, start)
  363. }
  364. }
  365. function parseBogusComment(context: ParserContext): CommentNode | undefined {
  366. __TEST__ && assert(/^<(?:[\!\?]|\/[^a-z>])/i.test(context.source))
  367. const start = getCursor(context)
  368. const contentStart = context.source[1] === '?' ? 1 : 2
  369. let content: string
  370. const closeIndex = context.source.indexOf('>')
  371. if (closeIndex === -1) {
  372. content = context.source.slice(contentStart)
  373. advanceBy(context, context.source.length)
  374. } else {
  375. content = context.source.slice(contentStart, closeIndex)
  376. advanceBy(context, closeIndex + 1)
  377. }
  378. return {
  379. type: NodeTypes.COMMENT,
  380. content,
  381. loc: getSelection(context, start)
  382. }
  383. }
  384. function parseElement(
  385. context: ParserContext,
  386. ancestors: ElementNode[]
  387. ): ElementNode | undefined {
  388. __TEST__ && assert(/^<[a-z]/i.test(context.source))
  389. // Start tag.
  390. const wasInPre = context.inPre
  391. const wasInVPre = context.inVPre
  392. const parent = last(ancestors)
  393. const element = parseTag(context, TagType.Start, parent)
  394. const isPreBoundary = context.inPre && !wasInPre
  395. const isVPreBoundary = context.inVPre && !wasInVPre
  396. if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
  397. // #4030 self-closing <pre> tag
  398. if (context.options.isPreTag(element.tag)) {
  399. context.inPre--
  400. }
  401. return element
  402. }
  403. // Children.
  404. ancestors.push(element)
  405. const mode = context.options.getTextMode(element, parent)
  406. const children = parseChildren(context, mode, ancestors)
  407. ancestors.pop()
  408. // 2.x inline-template compat
  409. if (__COMPAT__) {
  410. const inlineTemplateProp = element.props.find(
  411. p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
  412. ) as AttributeNode
  413. if (
  414. inlineTemplateProp &&
  415. checkCompatEnabled(
  416. CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
  417. context,
  418. inlineTemplateProp.loc
  419. )
  420. ) {
  421. const loc = getSelection(context, element.loc.end)
  422. inlineTemplateProp.value = {
  423. type: NodeTypes.TEXT,
  424. content: loc.source,
  425. loc
  426. }
  427. }
  428. }
  429. element.children = children
  430. // End tag.
  431. if (startsWithEndTagOpen(context.source, element.tag)) {
  432. parseTag(context, TagType.End, parent)
  433. } else {
  434. emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
  435. if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
  436. const first = children[0]
  437. if (first && startsWith(first.loc.source, '<!--')) {
  438. emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
  439. }
  440. }
  441. }
  442. element.loc = getSelection(context, element.loc.start)
  443. if (isPreBoundary) {
  444. context.inPre--
  445. }
  446. if (isVPreBoundary) {
  447. context.inVPre--
  448. }
  449. return element
  450. }
  451. const enum TagType {
  452. Start,
  453. End
  454. }
  455. const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
  456. `if,else,else-if,for,slot`
  457. )
  458. /**
  459. * Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
  460. */
  461. function parseTag(
  462. context: ParserContext,
  463. type: TagType.Start,
  464. parent: ElementNode | undefined
  465. ): ElementNode
  466. function parseTag(
  467. context: ParserContext,
  468. type: TagType.End,
  469. parent: ElementNode | undefined
  470. ): void
  471. function parseTag(
  472. context: ParserContext,
  473. type: TagType,
  474. parent: ElementNode | undefined
  475. ): ElementNode | undefined {
  476. __TEST__ && assert(/^<\/?[a-z]/i.test(context.source))
  477. __TEST__ &&
  478. assert(
  479. type === (startsWith(context.source, '</') ? TagType.End : TagType.Start)
  480. )
  481. // Tag open.
  482. const start = getCursor(context)
  483. const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  484. const tag = match[1]
  485. const ns = context.options.getNamespace(tag, parent)
  486. advanceBy(context, match[0].length)
  487. advanceSpaces(context)
  488. // save current state in case we need to re-parse attributes with v-pre
  489. const cursor = getCursor(context)
  490. const currentSource = context.source
  491. // check <pre> tag
  492. if (context.options.isPreTag(tag)) {
  493. context.inPre++
  494. }
  495. // Attributes.
  496. let props = parseAttributes(context, type)
  497. // check v-pre
  498. if (
  499. type === TagType.Start &&
  500. !context.inVPre &&
  501. props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
  502. ) {
  503. context.inVPre++
  504. // reset context
  505. extend(context, cursor)
  506. context.source = currentSource
  507. // re-parse attrs and filter out v-pre itself
  508. props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
  509. }
  510. // Tag close.
  511. let isSelfClosing = false
  512. if (context.source.length === 0) {
  513. emitError(context, ErrorCodes.EOF_IN_TAG)
  514. } else {
  515. isSelfClosing = startsWith(context.source, '/>')
  516. if (type === TagType.End && isSelfClosing) {
  517. emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
  518. }
  519. advanceBy(context, isSelfClosing ? 2 : 1)
  520. }
  521. if (type === TagType.End) {
  522. return
  523. }
  524. // 2.x deprecation checks
  525. if (
  526. __COMPAT__ &&
  527. __DEV__ &&
  528. isCompatEnabled(
  529. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  530. context
  531. )
  532. ) {
  533. let hasIf = false
  534. let hasFor = false
  535. for (let i = 0; i < props.length; i++) {
  536. const p = props[i]
  537. if (p.type === NodeTypes.DIRECTIVE) {
  538. if (p.name === 'if') {
  539. hasIf = true
  540. } else if (p.name === 'for') {
  541. hasFor = true
  542. }
  543. }
  544. if (hasIf && hasFor) {
  545. warnDeprecation(
  546. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  547. context,
  548. getSelection(context, start)
  549. )
  550. }
  551. }
  552. }
  553. let tagType = ElementTypes.ELEMENT
  554. if (!context.inVPre) {
  555. if (tag === 'slot') {
  556. tagType = ElementTypes.SLOT
  557. } else if (tag === 'template') {
  558. if (
  559. props.some(
  560. p =>
  561. p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
  562. )
  563. ) {
  564. tagType = ElementTypes.TEMPLATE
  565. }
  566. } else if (isComponent(tag, props, context)) {
  567. tagType = ElementTypes.COMPONENT
  568. }
  569. }
  570. return {
  571. type: NodeTypes.ELEMENT,
  572. ns,
  573. tag,
  574. tagType,
  575. props,
  576. isSelfClosing,
  577. children: [],
  578. loc: getSelection(context, start),
  579. codegenNode: undefined // to be created during transform phase
  580. }
  581. }
  582. function isComponent(
  583. tag: string,
  584. props: (AttributeNode | DirectiveNode)[],
  585. context: ParserContext
  586. ) {
  587. const options = context.options
  588. if (options.isCustomElement(tag)) {
  589. return false
  590. }
  591. if (
  592. tag === 'component' ||
  593. /^[A-Z]/.test(tag) ||
  594. isCoreComponent(tag) ||
  595. (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
  596. (options.isNativeTag && !options.isNativeTag(tag))
  597. ) {
  598. return true
  599. }
  600. // at this point the tag should be a native tag, but check for potential "is"
  601. // casting
  602. for (let i = 0; i < props.length; i++) {
  603. const p = props[i]
  604. if (p.type === NodeTypes.ATTRIBUTE) {
  605. if (p.name === 'is' && p.value) {
  606. if (p.value.content.startsWith('vue:')) {
  607. return true
  608. } else if (
  609. __COMPAT__ &&
  610. checkCompatEnabled(
  611. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  612. context,
  613. p.loc
  614. )
  615. ) {
  616. return true
  617. }
  618. }
  619. } else {
  620. // directive
  621. // v-is (TODO Deprecate)
  622. if (p.name === 'is') {
  623. return true
  624. } else if (
  625. // :is on plain element - only treat as component in compat mode
  626. p.name === 'bind' &&
  627. isBindKey(p.arg, 'is') &&
  628. __COMPAT__ &&
  629. checkCompatEnabled(
  630. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  631. context,
  632. p.loc
  633. )
  634. ) {
  635. return true
  636. }
  637. }
  638. }
  639. }
  640. function parseAttributes(
  641. context: ParserContext,
  642. type: TagType
  643. ): (AttributeNode | DirectiveNode)[] {
  644. const props = []
  645. const attributeNames = new Set<string>()
  646. while (
  647. context.source.length > 0 &&
  648. !startsWith(context.source, '>') &&
  649. !startsWith(context.source, '/>')
  650. ) {
  651. if (startsWith(context.source, '/')) {
  652. emitError(context, ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG)
  653. advanceBy(context, 1)
  654. advanceSpaces(context)
  655. continue
  656. }
  657. if (type === TagType.End) {
  658. emitError(context, ErrorCodes.END_TAG_WITH_ATTRIBUTES)
  659. }
  660. const attr = parseAttribute(context, attributeNames)
  661. if (type === TagType.Start) {
  662. props.push(attr)
  663. }
  664. if (/^[^\t\r\n\f />]/.test(context.source)) {
  665. emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
  666. }
  667. advanceSpaces(context)
  668. }
  669. return props
  670. }
  671. function parseAttribute(
  672. context: ParserContext,
  673. nameSet: Set<string>
  674. ): AttributeNode | DirectiveNode {
  675. __TEST__ && assert(/^[^\t\r\n\f />]/.test(context.source))
  676. // Name.
  677. const start = getCursor(context)
  678. const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  679. const name = match[0]
  680. if (nameSet.has(name)) {
  681. emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
  682. }
  683. nameSet.add(name)
  684. if (name[0] === '=') {
  685. emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
  686. }
  687. {
  688. const pattern = /["'<]/g
  689. let m: RegExpExecArray | null
  690. while ((m = pattern.exec(name))) {
  691. emitError(
  692. context,
  693. ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  694. m.index
  695. )
  696. }
  697. }
  698. advanceBy(context, name.length)
  699. // Value
  700. let value: AttributeValue = undefined
  701. if (/^[\t\r\n\f ]*=/.test(context.source)) {
  702. advanceSpaces(context)
  703. advanceBy(context, 1)
  704. advanceSpaces(context)
  705. value = parseAttributeValue(context)
  706. if (!value) {
  707. emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
  708. }
  709. }
  710. const loc = getSelection(context, start)
  711. if (!context.inVPre && /^(v-|:|\.|@|#)/.test(name)) {
  712. const match =
  713. /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
  714. name
  715. )!
  716. let isPropShorthand = startsWith(name, '.')
  717. let dirName =
  718. match[1] ||
  719. (isPropShorthand || startsWith(name, ':')
  720. ? 'bind'
  721. : startsWith(name, '@')
  722. ? 'on'
  723. : 'slot')
  724. let arg: ExpressionNode | undefined
  725. if (match[2]) {
  726. const isSlot = dirName === 'slot'
  727. const startOffset = name.lastIndexOf(match[2])
  728. const loc = getSelection(
  729. context,
  730. getNewPosition(context, start, startOffset),
  731. getNewPosition(
  732. context,
  733. start,
  734. startOffset + match[2].length + ((isSlot && match[3]) || '').length
  735. )
  736. )
  737. let content = match[2]
  738. let isStatic = true
  739. if (content.startsWith('[')) {
  740. isStatic = false
  741. if (!content.endsWith(']')) {
  742. emitError(
  743. context,
  744. ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
  745. )
  746. }
  747. content = content.substr(1, content.length - 2)
  748. } else if (isSlot) {
  749. // #1241 special case for v-slot: vuetify relies extensively on slot
  750. // names containing dots. v-slot doesn't have any modifiers and Vue 2.x
  751. // supports such usage so we are keeping it consistent with 2.x.
  752. content += match[3] || ''
  753. }
  754. arg = {
  755. type: NodeTypes.SIMPLE_EXPRESSION,
  756. content,
  757. isStatic,
  758. constType: isStatic
  759. ? ConstantTypes.CAN_STRINGIFY
  760. : ConstantTypes.NOT_CONSTANT,
  761. loc
  762. }
  763. }
  764. if (value && value.isQuoted) {
  765. const valueLoc = value.loc
  766. valueLoc.start.offset++
  767. valueLoc.start.column++
  768. valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
  769. valueLoc.source = valueLoc.source.slice(1, -1)
  770. }
  771. const modifiers = match[3] ? match[3].substr(1).split('.') : []
  772. if (isPropShorthand) modifiers.push('prop')
  773. // 2.x compat v-bind:foo.sync -> v-model:foo
  774. if (__COMPAT__ && dirName === 'bind' && arg) {
  775. if (
  776. modifiers.includes('sync') &&
  777. checkCompatEnabled(
  778. CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
  779. context,
  780. loc,
  781. arg.loc.source
  782. )
  783. ) {
  784. dirName = 'model'
  785. modifiers.splice(modifiers.indexOf('sync'), 1)
  786. }
  787. if (__DEV__ && modifiers.includes('prop')) {
  788. checkCompatEnabled(
  789. CompilerDeprecationTypes.COMPILER_V_BIND_PROP,
  790. context,
  791. loc
  792. )
  793. }
  794. }
  795. return {
  796. type: NodeTypes.DIRECTIVE,
  797. name: dirName,
  798. exp: value && {
  799. type: NodeTypes.SIMPLE_EXPRESSION,
  800. content: value.content,
  801. isStatic: false,
  802. // Treat as non-constant by default. This can be potentially set to
  803. // other values by `transformExpression` to make it eligible for hoisting.
  804. constType: ConstantTypes.NOT_CONSTANT,
  805. loc: value.loc
  806. },
  807. arg,
  808. modifiers,
  809. loc
  810. }
  811. }
  812. return {
  813. type: NodeTypes.ATTRIBUTE,
  814. name,
  815. value: value && {
  816. type: NodeTypes.TEXT,
  817. content: value.content,
  818. loc: value.loc
  819. },
  820. loc
  821. }
  822. }
  823. function parseAttributeValue(context: ParserContext): AttributeValue {
  824. const start = getCursor(context)
  825. let content: string
  826. const quote = context.source[0]
  827. const isQuoted = quote === `"` || quote === `'`
  828. if (isQuoted) {
  829. // Quoted value.
  830. advanceBy(context, 1)
  831. const endIndex = context.source.indexOf(quote)
  832. if (endIndex === -1) {
  833. content = parseTextData(
  834. context,
  835. context.source.length,
  836. TextModes.ATTRIBUTE_VALUE
  837. )
  838. } else {
  839. content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
  840. advanceBy(context, 1)
  841. }
  842. } else {
  843. // Unquoted
  844. const match = /^[^\t\r\n\f >]+/.exec(context.source)
  845. if (!match) {
  846. return undefined
  847. }
  848. const unexpectedChars = /["'<=`]/g
  849. let m: RegExpExecArray | null
  850. while ((m = unexpectedChars.exec(match[0]))) {
  851. emitError(
  852. context,
  853. ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  854. m.index
  855. )
  856. }
  857. content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
  858. }
  859. return { content, isQuoted, loc: getSelection(context, start) }
  860. }
  861. function parseInterpolation(
  862. context: ParserContext,
  863. mode: TextModes
  864. ): InterpolationNode | undefined {
  865. const [open, close] = context.options.delimiters
  866. __TEST__ && assert(startsWith(context.source, open))
  867. const closeIndex = context.source.indexOf(close, open.length)
  868. if (closeIndex === -1) {
  869. emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
  870. return undefined
  871. }
  872. const start = getCursor(context)
  873. advanceBy(context, open.length)
  874. const innerStart = getCursor(context)
  875. const innerEnd = getCursor(context)
  876. const rawContentLength = closeIndex - open.length
  877. const rawContent = context.source.slice(0, rawContentLength)
  878. const preTrimContent = parseTextData(context, rawContentLength, mode)
  879. const content = preTrimContent.trim()
  880. const startOffset = preTrimContent.indexOf(content)
  881. if (startOffset > 0) {
  882. advancePositionWithMutation(innerStart, rawContent, startOffset)
  883. }
  884. const endOffset =
  885. rawContentLength - (preTrimContent.length - content.length - startOffset)
  886. advancePositionWithMutation(innerEnd, rawContent, endOffset)
  887. advanceBy(context, close.length)
  888. return {
  889. type: NodeTypes.INTERPOLATION,
  890. content: {
  891. type: NodeTypes.SIMPLE_EXPRESSION,
  892. isStatic: false,
  893. // Set `isConstant` to false by default and will decide in transformExpression
  894. constType: ConstantTypes.NOT_CONSTANT,
  895. content,
  896. loc: getSelection(context, innerStart, innerEnd)
  897. },
  898. loc: getSelection(context, start)
  899. }
  900. }
  901. function parseText(context: ParserContext, mode: TextModes): TextNode {
  902. __TEST__ && assert(context.source.length > 0)
  903. const endTokens = ['<', context.options.delimiters[0]]
  904. if (mode === TextModes.CDATA) {
  905. endTokens.push(']]>')
  906. }
  907. let endIndex = context.source.length
  908. for (let i = 0; i < endTokens.length; i++) {
  909. const index = context.source.indexOf(endTokens[i], 1)
  910. if (index !== -1 && endIndex > index) {
  911. endIndex = index
  912. }
  913. }
  914. __TEST__ && assert(endIndex > 0)
  915. const start = getCursor(context)
  916. const content = parseTextData(context, endIndex, mode)
  917. return {
  918. type: NodeTypes.TEXT,
  919. content,
  920. loc: getSelection(context, start)
  921. }
  922. }
  923. /**
  924. * Get text data with a given length from the current location.
  925. * This translates HTML entities in the text data.
  926. */
  927. function parseTextData(
  928. context: ParserContext,
  929. length: number,
  930. mode: TextModes
  931. ): string {
  932. const rawText = context.source.slice(0, length)
  933. advanceBy(context, length)
  934. if (
  935. mode === TextModes.RAWTEXT ||
  936. mode === TextModes.CDATA ||
  937. rawText.indexOf('&') === -1
  938. ) {
  939. return rawText
  940. } else {
  941. // DATA or RCDATA containing "&"". Entity decoding required.
  942. return context.options.decodeEntities(
  943. rawText,
  944. mode === TextModes.ATTRIBUTE_VALUE
  945. )
  946. }
  947. }
  948. function getCursor(context: ParserContext): Position {
  949. const { column, line, offset } = context
  950. return { column, line, offset }
  951. }
  952. function getSelection(
  953. context: ParserContext,
  954. start: Position,
  955. end?: Position
  956. ): SourceLocation {
  957. end = end || getCursor(context)
  958. return {
  959. start,
  960. end,
  961. source: context.originalSource.slice(start.offset, end.offset)
  962. }
  963. }
  964. function last<T>(xs: T[]): T | undefined {
  965. return xs[xs.length - 1]
  966. }
  967. function startsWith(source: string, searchString: string): boolean {
  968. return source.startsWith(searchString)
  969. }
  970. function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  971. const { source } = context
  972. __TEST__ && assert(numberOfCharacters <= source.length)
  973. advancePositionWithMutation(context, source, numberOfCharacters)
  974. context.source = source.slice(numberOfCharacters)
  975. }
  976. function advanceSpaces(context: ParserContext): void {
  977. const match = /^[\t\r\n\f ]+/.exec(context.source)
  978. if (match) {
  979. advanceBy(context, match[0].length)
  980. }
  981. }
  982. function getNewPosition(
  983. context: ParserContext,
  984. start: Position,
  985. numberOfCharacters: number
  986. ): Position {
  987. return advancePositionWithClone(
  988. start,
  989. context.originalSource.slice(start.offset, numberOfCharacters),
  990. numberOfCharacters
  991. )
  992. }
  993. function emitError(
  994. context: ParserContext,
  995. code: ErrorCodes,
  996. offset?: number,
  997. loc: Position = getCursor(context)
  998. ): void {
  999. if (offset) {
  1000. loc.offset += offset
  1001. loc.column += offset
  1002. }
  1003. context.options.onError(
  1004. createCompilerError(code, {
  1005. start: loc,
  1006. end: loc,
  1007. source: ''
  1008. })
  1009. )
  1010. }
  1011. function isEnd(
  1012. context: ParserContext,
  1013. mode: TextModes,
  1014. ancestors: ElementNode[]
  1015. ): boolean {
  1016. const s = context.source
  1017. switch (mode) {
  1018. case TextModes.DATA:
  1019. if (startsWith(s, '</')) {
  1020. // TODO: probably bad performance
  1021. for (let i = ancestors.length - 1; i >= 0; --i) {
  1022. if (startsWithEndTagOpen(s, ancestors[i].tag)) {
  1023. return true
  1024. }
  1025. }
  1026. }
  1027. break
  1028. case TextModes.RCDATA:
  1029. case TextModes.RAWTEXT: {
  1030. const parent = last(ancestors)
  1031. if (parent && startsWithEndTagOpen(s, parent.tag)) {
  1032. return true
  1033. }
  1034. break
  1035. }
  1036. case TextModes.CDATA:
  1037. if (startsWith(s, ']]>')) {
  1038. return true
  1039. }
  1040. break
  1041. }
  1042. return !s
  1043. }
  1044. function startsWithEndTagOpen(source: string, tag: string): boolean {
  1045. return (
  1046. startsWith(source, '</') &&
  1047. source.substr(2, tag.length).toLowerCase() === tag.toLowerCase() &&
  1048. /[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
  1049. )
  1050. }