parse.ts 26 KB

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