parse.ts 29 KB

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