parse.ts 30 KB

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