parse.ts 26 KB

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