parser.ts 29 KB

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