parser.ts 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. import {
  2. type AttributeNode,
  3. ConstantTypes,
  4. type DirectiveNode,
  5. type ElementNode,
  6. ElementTypes,
  7. type ForParseResult,
  8. Namespaces,
  9. NodeTypes,
  10. type RootNode,
  11. type SimpleExpressionNode,
  12. type SourceLocation,
  13. type TemplateChildNode,
  14. createRoot,
  15. createSimpleExpression,
  16. } from './ast'
  17. import type { ParserOptions } from './options'
  18. import Tokenizer, {
  19. CharCodes,
  20. ParseMode,
  21. QuoteType,
  22. Sequences,
  23. State,
  24. isWhitespace,
  25. toCharCodes,
  26. } from './tokenizer'
  27. import {
  28. type CompilerCompatOptions,
  29. CompilerDeprecationTypes,
  30. checkCompatEnabled,
  31. isCompatEnabled,
  32. warnDeprecation,
  33. } from './compat/compatConfig'
  34. import { NO, extend } from '@vue/shared'
  35. import {
  36. ErrorCodes,
  37. createCompilerError,
  38. defaultOnError,
  39. defaultOnWarn,
  40. } from './errors'
  41. import {
  42. forAliasRE,
  43. isCoreComponent,
  44. isSimpleIdentifier,
  45. isStaticArgOf,
  46. } from './utils'
  47. import { decodeHTML } from 'entities/dist/decode.js'
  48. import {
  49. type ParserOptions as BabelOptions,
  50. parse,
  51. parseExpression,
  52. } from '@babel/parser'
  53. type OptionalOptions =
  54. | 'decodeEntities'
  55. | 'whitespace'
  56. | 'isNativeTag'
  57. | 'isBuiltInComponent'
  58. | 'expressionPlugins'
  59. | keyof CompilerCompatOptions
  60. export type MergedParserOptions = Omit<
  61. Required<ParserOptions>,
  62. OptionalOptions
  63. > &
  64. Pick<ParserOptions, OptionalOptions>
  65. export const defaultParserOptions: MergedParserOptions = {
  66. parseMode: 'base',
  67. ns: Namespaces.HTML,
  68. delimiters: [`{{`, `}}`],
  69. getNamespace: () => Namespaces.HTML,
  70. isVoidTag: NO,
  71. isPreTag: NO,
  72. isCustomElement: NO,
  73. onError: defaultOnError,
  74. onWarn: defaultOnWarn,
  75. comments: __DEV__,
  76. prefixIdentifiers: false,
  77. }
  78. let currentOptions: MergedParserOptions = defaultParserOptions
  79. let currentRoot: RootNode | null = null
  80. // parser state
  81. let currentInput = ''
  82. let currentOpenTag: ElementNode | null = null
  83. let currentProp: AttributeNode | DirectiveNode | null = null
  84. let currentAttrValue = ''
  85. let currentAttrStartIndex = -1
  86. let currentAttrEndIndex = -1
  87. let inPre = 0
  88. let inVPre = false
  89. let currentVPreBoundary: ElementNode | null = null
  90. const stack: ElementNode[] = []
  91. const tokenizer = new Tokenizer(stack, {
  92. onerr: emitError,
  93. ontext(start, end) {
  94. onText(getSlice(start, end), start, end)
  95. },
  96. ontextentity(char, start, end) {
  97. onText(char, start, end)
  98. },
  99. oninterpolation(start, end) {
  100. if (inVPre) {
  101. return onText(getSlice(start, end), start, end)
  102. }
  103. let innerStart = start + tokenizer.delimiterOpen.length
  104. let innerEnd = end - tokenizer.delimiterClose.length
  105. while (isWhitespace(currentInput.charCodeAt(innerStart))) {
  106. innerStart++
  107. }
  108. while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) {
  109. innerEnd--
  110. }
  111. let exp = getSlice(innerStart, innerEnd)
  112. // decode entities for backwards compat
  113. if (exp.includes('&')) {
  114. if (__BROWSER__) {
  115. exp = currentOptions.decodeEntities!(exp, false)
  116. } else {
  117. exp = decodeHTML(exp)
  118. }
  119. }
  120. addNode({
  121. type: NodeTypes.INTERPOLATION,
  122. content: createExp(exp, false, getLoc(innerStart, innerEnd)),
  123. loc: getLoc(start, end),
  124. })
  125. },
  126. onopentagname(start, end) {
  127. const name = getSlice(start, end)
  128. currentOpenTag = {
  129. type: NodeTypes.ELEMENT,
  130. tag: name,
  131. ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
  132. tagType: ElementTypes.ELEMENT, // will be refined on tag close
  133. props: [],
  134. children: [],
  135. loc: getLoc(start - 1, end),
  136. codegenNode: undefined,
  137. }
  138. },
  139. onopentagend(end) {
  140. endOpenTag(end)
  141. },
  142. onclosetag(start, end) {
  143. const name = getSlice(start, end)
  144. if (!currentOptions.isVoidTag(name)) {
  145. let found = false
  146. for (let i = 0; i < stack.length; i++) {
  147. const e = stack[i]
  148. if (e.tag.toLowerCase() === name.toLowerCase()) {
  149. found = true
  150. if (i > 0) {
  151. emitError(ErrorCodes.X_MISSING_END_TAG, stack[0].loc.start.offset)
  152. }
  153. for (let j = 0; j <= i; j++) {
  154. const el = stack.shift()!
  155. onCloseTag(el, end, j < i)
  156. }
  157. break
  158. }
  159. }
  160. if (!found) {
  161. emitError(ErrorCodes.X_INVALID_END_TAG, backTrack(start, CharCodes.Lt))
  162. }
  163. }
  164. },
  165. onselfclosingtag(end) {
  166. const name = currentOpenTag!.tag
  167. currentOpenTag!.isSelfClosing = true
  168. endOpenTag(end)
  169. if (stack[0] && stack[0].tag === name) {
  170. onCloseTag(stack.shift()!, end)
  171. }
  172. },
  173. onattribname(start, end) {
  174. // plain attribute
  175. currentProp = {
  176. type: NodeTypes.ATTRIBUTE,
  177. name: getSlice(start, end),
  178. nameLoc: getLoc(start, end),
  179. value: undefined,
  180. loc: getLoc(start),
  181. }
  182. },
  183. ondirname(start, end) {
  184. const raw = getSlice(start, end)
  185. const name =
  186. raw === '.' || raw === ':'
  187. ? 'bind'
  188. : raw === '@'
  189. ? 'on'
  190. : raw === '#'
  191. ? 'slot'
  192. : raw.slice(2)
  193. if (!inVPre && name === '') {
  194. emitError(ErrorCodes.X_MISSING_DIRECTIVE_NAME, start)
  195. }
  196. if (inVPre || name === '') {
  197. currentProp = {
  198. type: NodeTypes.ATTRIBUTE,
  199. name: raw,
  200. nameLoc: getLoc(start, end),
  201. value: undefined,
  202. loc: getLoc(start),
  203. }
  204. } else {
  205. currentProp = {
  206. type: NodeTypes.DIRECTIVE,
  207. name,
  208. rawName: raw,
  209. exp: undefined,
  210. arg: undefined,
  211. modifiers: raw === '.' ? ['prop'] : [],
  212. loc: getLoc(start),
  213. }
  214. if (name === 'pre') {
  215. inVPre = tokenizer.inVPre = true
  216. currentVPreBoundary = currentOpenTag
  217. // convert dirs before this one to attributes
  218. const props = currentOpenTag!.props
  219. for (let i = 0; i < props.length; i++) {
  220. if (props[i].type === NodeTypes.DIRECTIVE) {
  221. props[i] = dirToAttr(props[i] as DirectiveNode)
  222. }
  223. }
  224. }
  225. }
  226. },
  227. ondirarg(start, end) {
  228. if (start === end) return
  229. const arg = getSlice(start, end)
  230. if (inVPre) {
  231. ;(currentProp as AttributeNode).name += arg
  232. setLocEnd((currentProp as AttributeNode).nameLoc, end)
  233. } else {
  234. const isStatic = arg[0] !== `[`
  235. ;(currentProp as DirectiveNode).arg = createExp(
  236. isStatic ? arg : arg.slice(1, -1),
  237. isStatic,
  238. getLoc(start, end),
  239. isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT,
  240. )
  241. }
  242. },
  243. ondirmodifier(start, end) {
  244. const mod = getSlice(start, end)
  245. if (inVPre) {
  246. ;(currentProp as AttributeNode).name += '.' + mod
  247. setLocEnd((currentProp as AttributeNode).nameLoc, end)
  248. } else if ((currentProp as DirectiveNode).name === 'slot') {
  249. // slot has no modifiers, special case for edge cases like
  250. // https://github.com/vuejs/language-tools/issues/2710
  251. const arg = (currentProp as DirectiveNode).arg
  252. if (arg) {
  253. ;(arg as SimpleExpressionNode).content += '.' + mod
  254. setLocEnd(arg.loc, end)
  255. }
  256. } else {
  257. ;(currentProp as DirectiveNode).modifiers.push(mod)
  258. }
  259. },
  260. onattribdata(start, end) {
  261. currentAttrValue += getSlice(start, end)
  262. if (currentAttrStartIndex < 0) currentAttrStartIndex = start
  263. currentAttrEndIndex = end
  264. },
  265. onattribentity(char, start, end) {
  266. currentAttrValue += char
  267. if (currentAttrStartIndex < 0) currentAttrStartIndex = start
  268. currentAttrEndIndex = end
  269. },
  270. onattribnameend(end) {
  271. const start = currentProp!.loc.start.offset
  272. const name = getSlice(start, end)
  273. if (currentProp!.type === NodeTypes.DIRECTIVE) {
  274. currentProp!.rawName = name
  275. }
  276. // check duplicate attrs
  277. if (
  278. currentOpenTag!.props.some(
  279. p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name,
  280. )
  281. ) {
  282. emitError(ErrorCodes.DUPLICATE_ATTRIBUTE, start)
  283. }
  284. },
  285. onattribend(quote, end) {
  286. if (currentOpenTag && currentProp) {
  287. // finalize end pos
  288. setLocEnd(currentProp.loc, end)
  289. if (quote !== QuoteType.NoValue) {
  290. if (__BROWSER__ && currentAttrValue.includes('&')) {
  291. currentAttrValue = currentOptions.decodeEntities!(
  292. currentAttrValue,
  293. true,
  294. )
  295. }
  296. if (currentProp.type === NodeTypes.ATTRIBUTE) {
  297. // assign value
  298. // condense whitespaces in class
  299. if (currentProp!.name === 'class') {
  300. currentAttrValue = condense(currentAttrValue).trim()
  301. }
  302. if (quote === QuoteType.Unquoted && !currentAttrValue) {
  303. emitError(ErrorCodes.MISSING_ATTRIBUTE_VALUE, end)
  304. }
  305. currentProp!.value = {
  306. type: NodeTypes.TEXT,
  307. content: currentAttrValue,
  308. loc:
  309. quote === QuoteType.Unquoted
  310. ? getLoc(currentAttrStartIndex, currentAttrEndIndex)
  311. : getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1),
  312. }
  313. if (
  314. tokenizer.inSFCRoot &&
  315. currentOpenTag.tag === 'template' &&
  316. currentProp.name === 'lang' &&
  317. currentAttrValue &&
  318. currentAttrValue !== 'html'
  319. ) {
  320. // SFC root template with preprocessor lang, force tokenizer to
  321. // RCDATA mode
  322. tokenizer.enterRCDATA(toCharCodes(`</template`), 0)
  323. }
  324. } else {
  325. // directive
  326. let expParseMode = ExpParseMode.Normal
  327. if (!__BROWSER__) {
  328. if (currentProp.name === 'for') {
  329. expParseMode = ExpParseMode.Skip
  330. } else if (currentProp.name === 'slot') {
  331. expParseMode = ExpParseMode.Params
  332. } else if (
  333. currentProp.name === 'on' &&
  334. currentAttrValue.includes(';')
  335. ) {
  336. expParseMode = ExpParseMode.Statements
  337. }
  338. }
  339. currentProp.exp = createExp(
  340. currentAttrValue,
  341. false,
  342. getLoc(currentAttrStartIndex, currentAttrEndIndex),
  343. ConstantTypes.NOT_CONSTANT,
  344. expParseMode,
  345. )
  346. if (currentProp.name === 'for') {
  347. currentProp.forParseResult = parseForExpression(currentProp.exp)
  348. }
  349. // 2.x compat v-bind:foo.sync -> v-model:foo
  350. let syncIndex = -1
  351. if (
  352. __COMPAT__ &&
  353. currentProp.name === 'bind' &&
  354. (syncIndex = currentProp.modifiers.indexOf('sync')) > -1 &&
  355. checkCompatEnabled(
  356. CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
  357. currentOptions,
  358. currentProp.loc,
  359. currentProp.rawName,
  360. )
  361. ) {
  362. currentProp.name = 'model'
  363. currentProp.modifiers.splice(syncIndex, 1)
  364. }
  365. }
  366. }
  367. if (
  368. currentProp.type !== NodeTypes.DIRECTIVE ||
  369. currentProp.name !== 'pre'
  370. ) {
  371. currentOpenTag.props.push(currentProp)
  372. }
  373. }
  374. currentAttrValue = ''
  375. currentAttrStartIndex = currentAttrEndIndex = -1
  376. },
  377. oncomment(start, end) {
  378. if (currentOptions.comments) {
  379. addNode({
  380. type: NodeTypes.COMMENT,
  381. content: getSlice(start, end),
  382. loc: getLoc(start - 4, end + 3),
  383. })
  384. }
  385. },
  386. onend() {
  387. const end = currentInput.length
  388. // EOF ERRORS
  389. if ((__DEV__ || !__BROWSER__) && tokenizer.state !== State.Text) {
  390. switch (tokenizer.state) {
  391. case State.BeforeTagName:
  392. case State.BeforeClosingTagName:
  393. emitError(ErrorCodes.EOF_BEFORE_TAG_NAME, end)
  394. break
  395. case State.Interpolation:
  396. case State.InterpolationClose:
  397. emitError(
  398. ErrorCodes.X_MISSING_INTERPOLATION_END,
  399. tokenizer.sectionStart,
  400. )
  401. break
  402. case State.InCommentLike:
  403. if (tokenizer.currentSequence === Sequences.CdataEnd) {
  404. emitError(ErrorCodes.EOF_IN_CDATA, end)
  405. } else {
  406. emitError(ErrorCodes.EOF_IN_COMMENT, end)
  407. }
  408. break
  409. case State.InTagName:
  410. case State.InSelfClosingTag:
  411. case State.InClosingTagName:
  412. case State.BeforeAttrName:
  413. case State.InAttrName:
  414. case State.InDirName:
  415. case State.InDirArg:
  416. case State.InDirDynamicArg:
  417. case State.InDirModifier:
  418. case State.AfterAttrName:
  419. case State.BeforeAttrValue:
  420. case State.InAttrValueDq: // "
  421. case State.InAttrValueSq: // '
  422. case State.InAttrValueNq:
  423. emitError(ErrorCodes.EOF_IN_TAG, end)
  424. break
  425. default:
  426. // console.log(tokenizer.state)
  427. break
  428. }
  429. }
  430. for (let index = 0; index < stack.length; index++) {
  431. onCloseTag(stack[index], end - 1)
  432. emitError(ErrorCodes.X_MISSING_END_TAG, stack[index].loc.start.offset)
  433. }
  434. },
  435. oncdata(start, end) {
  436. if (stack[0].ns !== Namespaces.HTML) {
  437. onText(getSlice(start, end), start, end)
  438. } else {
  439. emitError(ErrorCodes.CDATA_IN_HTML_CONTENT, start - 9)
  440. }
  441. },
  442. onprocessinginstruction(start) {
  443. // ignore as we do not have runtime handling for this, only check error
  444. if ((stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML) {
  445. emitError(
  446. ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
  447. start - 1,
  448. )
  449. }
  450. },
  451. })
  452. // This regex doesn't cover the case if key or index aliases have destructuring,
  453. // but those do not make sense in the first place, so this works in practice.
  454. const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
  455. const stripParensRE = /^\(|\)$/g
  456. function parseForExpression(
  457. input: SimpleExpressionNode,
  458. ): ForParseResult | undefined {
  459. const loc = input.loc
  460. const exp = input.content
  461. const inMatch = exp.match(forAliasRE)
  462. if (!inMatch) return
  463. const [, LHS, RHS] = inMatch
  464. const createAliasExpression = (
  465. content: string,
  466. offset: number,
  467. asParam = false,
  468. ) => {
  469. const start = loc.start.offset + offset
  470. const end = start + content.length
  471. return createExp(
  472. content,
  473. false,
  474. getLoc(start, end),
  475. ConstantTypes.NOT_CONSTANT,
  476. asParam ? ExpParseMode.Params : ExpParseMode.Normal,
  477. )
  478. }
  479. const result: ForParseResult = {
  480. source: createAliasExpression(RHS.trim(), exp.indexOf(RHS, LHS.length)),
  481. value: undefined,
  482. key: undefined,
  483. index: undefined,
  484. finalized: false,
  485. }
  486. let valueContent = LHS.trim().replace(stripParensRE, '').trim()
  487. const trimmedOffset = LHS.indexOf(valueContent)
  488. const iteratorMatch = valueContent.match(forIteratorRE)
  489. if (iteratorMatch) {
  490. valueContent = valueContent.replace(forIteratorRE, '').trim()
  491. const keyContent = iteratorMatch[1].trim()
  492. let keyOffset: number | undefined
  493. if (keyContent) {
  494. keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
  495. result.key = createAliasExpression(keyContent, keyOffset, true)
  496. }
  497. if (iteratorMatch[2]) {
  498. const indexContent = iteratorMatch[2].trim()
  499. if (indexContent) {
  500. result.index = createAliasExpression(
  501. indexContent,
  502. exp.indexOf(
  503. indexContent,
  504. result.key
  505. ? keyOffset! + keyContent.length
  506. : trimmedOffset + valueContent.length,
  507. ),
  508. true,
  509. )
  510. }
  511. }
  512. }
  513. if (valueContent) {
  514. result.value = createAliasExpression(valueContent, trimmedOffset, true)
  515. }
  516. return result
  517. }
  518. function getSlice(start: number, end: number) {
  519. return currentInput.slice(start, end)
  520. }
  521. function endOpenTag(end: number) {
  522. if (tokenizer.inSFCRoot) {
  523. // in SFC mode, generate locations for root-level tags' inner content.
  524. currentOpenTag!.innerLoc = getLoc(end + 1, end + 1)
  525. }
  526. addNode(currentOpenTag!)
  527. const { tag, ns } = currentOpenTag!
  528. if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
  529. inPre++
  530. }
  531. if (currentOptions.isVoidTag(tag)) {
  532. onCloseTag(currentOpenTag!, end)
  533. } else {
  534. stack.unshift(currentOpenTag!)
  535. if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
  536. tokenizer.inXML = true
  537. }
  538. }
  539. currentOpenTag = null
  540. }
  541. function onText(content: string, start: number, end: number) {
  542. if (__BROWSER__) {
  543. const tag = stack[0] && stack[0].tag
  544. if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
  545. content = currentOptions.decodeEntities!(content, false)
  546. }
  547. }
  548. const parent = stack[0] || currentRoot
  549. const lastNode = parent.children[parent.children.length - 1]
  550. if (lastNode && lastNode.type === NodeTypes.TEXT) {
  551. // merge
  552. lastNode.content += content
  553. setLocEnd(lastNode.loc, end)
  554. } else {
  555. parent.children.push({
  556. type: NodeTypes.TEXT,
  557. content,
  558. loc: getLoc(start, end),
  559. })
  560. }
  561. }
  562. function onCloseTag(el: ElementNode, end: number, isImplied = false) {
  563. // attach end position
  564. if (isImplied) {
  565. // implied close, end should be backtracked to close
  566. setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
  567. } else {
  568. setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
  569. }
  570. if (tokenizer.inSFCRoot) {
  571. // SFC root tag, resolve inner end
  572. if (el.children.length) {
  573. el.innerLoc!.end = extend({}, el.children[el.children.length - 1].loc.end)
  574. } else {
  575. el.innerLoc!.end = extend({}, el.innerLoc!.start)
  576. }
  577. el.innerLoc!.source = getSlice(
  578. el.innerLoc!.start.offset,
  579. el.innerLoc!.end.offset,
  580. )
  581. }
  582. // refine element type
  583. const { tag, ns } = el
  584. if (!inVPre) {
  585. if (tag === 'slot') {
  586. el.tagType = ElementTypes.SLOT
  587. } else if (isFragmentTemplate(el)) {
  588. el.tagType = ElementTypes.TEMPLATE
  589. } else if (isComponent(el)) {
  590. el.tagType = ElementTypes.COMPONENT
  591. }
  592. }
  593. // whitespace management
  594. if (!tokenizer.inRCDATA) {
  595. el.children = condenseWhitespace(el.children, el.tag)
  596. }
  597. if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
  598. inPre--
  599. }
  600. if (currentVPreBoundary === el) {
  601. inVPre = tokenizer.inVPre = false
  602. currentVPreBoundary = null
  603. }
  604. if (
  605. tokenizer.inXML &&
  606. (stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML
  607. ) {
  608. tokenizer.inXML = false
  609. }
  610. // 2.x compat / deprecation checks
  611. if (__COMPAT__) {
  612. const props = el.props
  613. if (
  614. __DEV__ &&
  615. isCompatEnabled(
  616. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  617. currentOptions,
  618. )
  619. ) {
  620. let hasIf = false
  621. let hasFor = false
  622. for (let i = 0; i < props.length; i++) {
  623. const p = props[i]
  624. if (p.type === NodeTypes.DIRECTIVE) {
  625. if (p.name === 'if') {
  626. hasIf = true
  627. } else if (p.name === 'for') {
  628. hasFor = true
  629. }
  630. }
  631. if (hasIf && hasFor) {
  632. warnDeprecation(
  633. CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
  634. currentOptions,
  635. el.loc,
  636. )
  637. break
  638. }
  639. }
  640. }
  641. if (
  642. !tokenizer.inSFCRoot &&
  643. isCompatEnabled(
  644. CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
  645. currentOptions,
  646. ) &&
  647. el.tag === 'template' &&
  648. !isFragmentTemplate(el)
  649. ) {
  650. __DEV__ &&
  651. warnDeprecation(
  652. CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
  653. currentOptions,
  654. el.loc,
  655. )
  656. // unwrap
  657. const parent = stack[0] || currentRoot
  658. const index = parent.children.indexOf(el)
  659. parent.children.splice(index, 1, ...el.children)
  660. }
  661. const inlineTemplateProp = props.find(
  662. p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template',
  663. ) as AttributeNode
  664. if (
  665. inlineTemplateProp &&
  666. checkCompatEnabled(
  667. CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
  668. currentOptions,
  669. inlineTemplateProp.loc,
  670. ) &&
  671. el.children.length
  672. ) {
  673. inlineTemplateProp.value = {
  674. type: NodeTypes.TEXT,
  675. content: getSlice(
  676. el.children[0].loc.start.offset,
  677. el.children[el.children.length - 1].loc.end.offset,
  678. ),
  679. loc: inlineTemplateProp.loc,
  680. }
  681. }
  682. }
  683. }
  684. function lookAhead(index: number, c: number) {
  685. let i = index
  686. while (currentInput.charCodeAt(i) !== c && i < currentInput.length - 1) i++
  687. return i
  688. }
  689. function backTrack(index: number, c: number) {
  690. let i = index
  691. while (currentInput.charCodeAt(i) !== c && i >= 0) i--
  692. return i
  693. }
  694. const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
  695. function isFragmentTemplate({ tag, props }: ElementNode): boolean {
  696. if (tag === 'template') {
  697. for (let i = 0; i < props.length; i++) {
  698. if (
  699. props[i].type === NodeTypes.DIRECTIVE &&
  700. specialTemplateDir.has((props[i] as DirectiveNode).name)
  701. ) {
  702. return true
  703. }
  704. }
  705. }
  706. return false
  707. }
  708. function isComponent({ tag, props }: ElementNode): boolean {
  709. if (currentOptions.isCustomElement(tag)) {
  710. return false
  711. }
  712. if (
  713. tag === 'component' ||
  714. isUpperCase(tag.charCodeAt(0)) ||
  715. isCoreComponent(tag) ||
  716. (currentOptions.isBuiltInComponent &&
  717. currentOptions.isBuiltInComponent(tag)) ||
  718. (currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
  719. ) {
  720. return true
  721. }
  722. // at this point the tag should be a native tag, but check for potential "is"
  723. // casting
  724. for (let i = 0; i < props.length; i++) {
  725. const p = props[i]
  726. if (p.type === NodeTypes.ATTRIBUTE) {
  727. if (p.name === 'is' && p.value) {
  728. if (p.value.content.startsWith('vue:')) {
  729. return true
  730. } else if (
  731. __COMPAT__ &&
  732. checkCompatEnabled(
  733. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  734. currentOptions,
  735. p.loc,
  736. )
  737. ) {
  738. return true
  739. }
  740. }
  741. } else if (
  742. __COMPAT__ &&
  743. // :is on plain element - only treat as component in compat mode
  744. p.name === 'bind' &&
  745. isStaticArgOf(p.arg, 'is') &&
  746. checkCompatEnabled(
  747. CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  748. currentOptions,
  749. p.loc,
  750. )
  751. ) {
  752. return true
  753. }
  754. }
  755. return false
  756. }
  757. function isUpperCase(c: number) {
  758. return c > 64 && c < 91
  759. }
  760. const windowsNewlineRE = /\r\n/g
  761. function condenseWhitespace(
  762. nodes: TemplateChildNode[],
  763. tag?: string,
  764. ): TemplateChildNode[] {
  765. const shouldCondense = currentOptions.whitespace !== 'preserve'
  766. let removedWhitespace = false
  767. for (let i = 0; i < nodes.length; i++) {
  768. const node = nodes[i]
  769. if (node.type === NodeTypes.TEXT) {
  770. if (!inPre) {
  771. if (isAllWhitespace(node.content)) {
  772. const prev = nodes[i - 1] && nodes[i - 1].type
  773. const next = nodes[i + 1] && nodes[i + 1].type
  774. // Remove if:
  775. // - the whitespace is the first or last node, or:
  776. // - (condense mode) the whitespace is between two comments, or:
  777. // - (condense mode) the whitespace is between comment and element, or:
  778. // - (condense mode) the whitespace is between two elements AND contains newline
  779. if (
  780. !prev ||
  781. !next ||
  782. (shouldCondense &&
  783. ((prev === NodeTypes.COMMENT &&
  784. (next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
  785. (prev === NodeTypes.ELEMENT &&
  786. (next === NodeTypes.COMMENT ||
  787. (next === NodeTypes.ELEMENT &&
  788. hasNewlineChar(node.content))))))
  789. ) {
  790. removedWhitespace = true
  791. nodes[i] = null as any
  792. } else {
  793. // Otherwise, the whitespace is condensed into a single space
  794. node.content = ' '
  795. }
  796. } else if (shouldCondense) {
  797. // in condense mode, consecutive whitespaces in text are condensed
  798. // down to a single space.
  799. node.content = condense(node.content)
  800. }
  801. } else {
  802. // #6410 normalize windows newlines in <pre>:
  803. // in SSR, browsers normalize server-rendered \r\n into a single \n
  804. // in the DOM
  805. node.content = node.content.replace(windowsNewlineRE, '\n')
  806. }
  807. }
  808. }
  809. if (inPre && tag && currentOptions.isPreTag(tag)) {
  810. // remove leading newline per html spec
  811. // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
  812. const first = nodes[0]
  813. if (first && first.type === NodeTypes.TEXT) {
  814. first.content = first.content.replace(/^\r?\n/, '')
  815. }
  816. }
  817. return removedWhitespace ? nodes.filter(Boolean) : nodes
  818. }
  819. function isAllWhitespace(str: string) {
  820. for (let i = 0; i < str.length; i++) {
  821. if (!isWhitespace(str.charCodeAt(i))) {
  822. return false
  823. }
  824. }
  825. return true
  826. }
  827. function hasNewlineChar(str: string) {
  828. for (let i = 0; i < str.length; i++) {
  829. const c = str.charCodeAt(i)
  830. if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
  831. return true
  832. }
  833. }
  834. return false
  835. }
  836. function condense(str: string) {
  837. let ret = ''
  838. let prevCharIsWhitespace = false
  839. for (let i = 0; i < str.length; i++) {
  840. if (isWhitespace(str.charCodeAt(i))) {
  841. if (!prevCharIsWhitespace) {
  842. ret += ' '
  843. prevCharIsWhitespace = true
  844. }
  845. } else {
  846. ret += str[i]
  847. prevCharIsWhitespace = false
  848. }
  849. }
  850. return ret
  851. }
  852. function addNode(node: TemplateChildNode) {
  853. ;(stack[0] || currentRoot).children.push(node)
  854. }
  855. function getLoc(start: number, end?: number): SourceLocation {
  856. return {
  857. start: tokenizer.getPos(start),
  858. // @ts-expect-error allow late attachment
  859. end: end == null ? end : tokenizer.getPos(end),
  860. // @ts-expect-error allow late attachment
  861. source: end == null ? end : getSlice(start, end),
  862. }
  863. }
  864. function setLocEnd(loc: SourceLocation, end: number) {
  865. loc.end = tokenizer.getPos(end)
  866. loc.source = getSlice(loc.start.offset, end)
  867. }
  868. function dirToAttr(dir: DirectiveNode): AttributeNode {
  869. const attr: AttributeNode = {
  870. type: NodeTypes.ATTRIBUTE,
  871. name: dir.rawName!,
  872. nameLoc: getLoc(
  873. dir.loc.start.offset,
  874. dir.loc.start.offset + dir.rawName!.length,
  875. ),
  876. value: undefined,
  877. loc: dir.loc,
  878. }
  879. if (dir.exp) {
  880. // account for quotes
  881. const loc = dir.exp.loc
  882. if (loc.end.offset < dir.loc.end.offset) {
  883. loc.start.offset--
  884. loc.start.column--
  885. loc.end.offset++
  886. loc.end.column++
  887. }
  888. attr.value = {
  889. type: NodeTypes.TEXT,
  890. content: (dir.exp as SimpleExpressionNode).content,
  891. loc,
  892. }
  893. }
  894. return attr
  895. }
  896. enum ExpParseMode {
  897. Normal,
  898. Params,
  899. Statements,
  900. Skip,
  901. }
  902. function createExp(
  903. content: SimpleExpressionNode['content'],
  904. isStatic: SimpleExpressionNode['isStatic'] = false,
  905. loc: SourceLocation,
  906. constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
  907. parseMode = ExpParseMode.Normal,
  908. ) {
  909. const exp = createSimpleExpression(content, isStatic, loc, constType)
  910. if (
  911. !__BROWSER__ &&
  912. !isStatic &&
  913. currentOptions.prefixIdentifiers &&
  914. parseMode !== ExpParseMode.Skip &&
  915. content.trim()
  916. ) {
  917. if (isSimpleIdentifier(content)) {
  918. exp.ast = null // fast path
  919. return exp
  920. }
  921. try {
  922. const plugins = currentOptions.expressionPlugins
  923. const options: BabelOptions = {
  924. plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
  925. }
  926. if (parseMode === ExpParseMode.Statements) {
  927. // v-on with multi-inline-statements, pad 1 char
  928. exp.ast = parse(` ${content} `, options).program
  929. } else if (parseMode === ExpParseMode.Params) {
  930. exp.ast = parseExpression(`(${content})=>{}`, options)
  931. } else {
  932. // normal exp, wrap with parens
  933. exp.ast = parseExpression(`(${content})`, options)
  934. }
  935. } catch (e: any) {
  936. exp.ast = false // indicate an error
  937. emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
  938. }
  939. }
  940. return exp
  941. }
  942. function emitError(code: ErrorCodes, index: number, message?: string) {
  943. currentOptions.onError(
  944. createCompilerError(code, getLoc(index, index), undefined, message),
  945. )
  946. }
  947. function reset() {
  948. tokenizer.reset()
  949. currentOpenTag = null
  950. currentProp = null
  951. currentAttrValue = ''
  952. currentAttrStartIndex = -1
  953. currentAttrEndIndex = -1
  954. stack.length = 0
  955. }
  956. export function baseParse(input: string, options?: ParserOptions): RootNode {
  957. reset()
  958. currentInput = input
  959. currentOptions = extend({}, defaultParserOptions)
  960. if (options) {
  961. let key: keyof ParserOptions
  962. for (key in options) {
  963. if (options[key] != null) {
  964. // @ts-expect-error
  965. currentOptions[key] = options[key]
  966. }
  967. }
  968. }
  969. if (__DEV__) {
  970. if (!__BROWSER__ && currentOptions.decodeEntities) {
  971. console.warn(
  972. `[@vue/compiler-core] decodeEntities option is passed but will be ` +
  973. `ignored in non-browser builds.`,
  974. )
  975. } else if (__BROWSER__ && !currentOptions.decodeEntities) {
  976. throw new Error(
  977. `[@vue/compiler-core] decodeEntities option is required in browser builds.`,
  978. )
  979. }
  980. }
  981. tokenizer.mode =
  982. currentOptions.parseMode === 'html'
  983. ? ParseMode.HTML
  984. : currentOptions.parseMode === 'sfc'
  985. ? ParseMode.SFC
  986. : ParseMode.BASE
  987. tokenizer.inXML =
  988. currentOptions.ns === Namespaces.SVG ||
  989. currentOptions.ns === Namespaces.MATH_ML
  990. const delimiters = options && options.delimiters
  991. if (delimiters) {
  992. tokenizer.delimiterOpen = toCharCodes(delimiters[0])
  993. tokenizer.delimiterClose = toCharCodes(delimiters[1])
  994. }
  995. const root = (currentRoot = createRoot([], input))
  996. tokenizer.parse(currentInput)
  997. root.loc = getLoc(0, input.length)
  998. root.children = condenseWhitespace(root.children)
  999. currentRoot = null
  1000. return root
  1001. }