index.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import { fromCodePoint } from 'entities/lib/decode.js'
  2. import {
  3. AttributeNode,
  4. ConstantTypes,
  5. DirectiveNode,
  6. ElementNode,
  7. ElementTypes,
  8. Namespaces,
  9. NodeTypes,
  10. RootNode,
  11. SimpleExpressionNode,
  12. SourceLocation,
  13. TemplateChildNode,
  14. createRoot
  15. } from '../ast'
  16. import { ParserOptions } from '../options'
  17. import Tokenizer, {
  18. CharCodes,
  19. ParseMode,
  20. QuoteType,
  21. isWhitespace,
  22. toCharCodes
  23. } from './Tokenizer'
  24. import { CompilerCompatOptions } from '../compat/compatConfig'
  25. import { NO, extend } from '@vue/shared'
  26. import { defaultOnError, defaultOnWarn } from '../errors'
  27. import { isCoreComponent } from '../utils'
  28. type OptionalOptions =
  29. | 'whitespace'
  30. | 'isNativeTag'
  31. | 'isBuiltInComponent'
  32. | 'getTextMode'
  33. | keyof CompilerCompatOptions
  34. type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
  35. Pick<ParserOptions, OptionalOptions>
  36. // The default decoder only provides escapes for characters reserved as part of
  37. // the template syntax, and is only used if the custom renderer did not provide
  38. // a platform-specific decoder.
  39. const decodeRE = /&(gt|lt|amp|apos|quot);/g
  40. const decodeMap: Record<string, string> = {
  41. gt: '>',
  42. lt: '<',
  43. amp: '&',
  44. apos: "'",
  45. quot: '"'
  46. }
  47. export const defaultParserOptions: MergedParserOptions = {
  48. parseMode: 'base',
  49. delimiters: [`{{`, `}}`],
  50. getNamespace: () => Namespaces.HTML,
  51. isVoidTag: NO,
  52. isPreTag: NO,
  53. isCustomElement: NO,
  54. // TODO handle entities
  55. decodeEntities: (rawText: string): string =>
  56. rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
  57. onError: defaultOnError,
  58. onWarn: defaultOnWarn,
  59. comments: __DEV__
  60. }
  61. let currentOptions: MergedParserOptions = defaultParserOptions
  62. let currentRoot: RootNode = createRoot([])
  63. // parser state
  64. let currentInput = ''
  65. let currentElement: ElementNode | null = null
  66. let currentProp: AttributeNode | DirectiveNode | null = null
  67. let currentAttrValue = ''
  68. let currentAttrStartIndex = -1
  69. let currentAttrEndIndex = -1
  70. let currentAttrs: Set<string> = new Set()
  71. let inPre = 0
  72. let inVPre = false
  73. let currentElementIsVPreBoundary = false
  74. const stack: ElementNode[] = []
  75. const tokenizer = new Tokenizer(stack, {
  76. ontext(start, end) {
  77. onText(getSlice(start, end), start, end)
  78. },
  79. ontextentity(cp, end) {
  80. onText(fromCodePoint(cp), end - 1, end)
  81. },
  82. oninterpolation(start, end) {
  83. if (inVPre) {
  84. return onText(getSlice(start, end), start, end)
  85. }
  86. let innerStart = start + tokenizer.delimiterOpen.length
  87. let innerEnd = end - tokenizer.delimiterClose.length
  88. while (isWhitespace(currentInput.charCodeAt(innerStart))) {
  89. innerStart++
  90. }
  91. while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) {
  92. innerEnd--
  93. }
  94. addNode({
  95. type: NodeTypes.INTERPOLATION,
  96. content: {
  97. type: NodeTypes.SIMPLE_EXPRESSION,
  98. isStatic: false,
  99. // Set `isConstant` to false by default and will decide in transformExpression
  100. constType: ConstantTypes.NOT_CONSTANT,
  101. content: getSlice(innerStart, innerEnd),
  102. loc: getLoc(innerStart, innerEnd)
  103. },
  104. loc: getLoc(start, end)
  105. })
  106. },
  107. onopentagname(start, end) {
  108. const name = getSlice(start, end)
  109. currentElement = {
  110. type: NodeTypes.ELEMENT,
  111. tag: name,
  112. ns: currentOptions.getNamespace(name, getParent()),
  113. tagType: ElementTypes.ELEMENT, // will be refined on tag close
  114. props: [],
  115. children: [],
  116. loc: {
  117. start: tokenizer.getPos(start - 1),
  118. // @ts-expect-error to be attached on tag close
  119. end: undefined,
  120. source: ''
  121. },
  122. codegenNode: undefined
  123. }
  124. currentAttrs.clear()
  125. },
  126. onopentagend(end) {
  127. endOpenTag(end)
  128. },
  129. onclosetag(start, end) {
  130. const name = getSlice(start, end)
  131. if (!currentOptions.isVoidTag(name)) {
  132. const pos = stack.findIndex(e => e.tag === name)
  133. if (pos !== -1) {
  134. for (let index = 0; index <= pos; index++) {
  135. onCloseTag(stack.shift()!, end)
  136. }
  137. }
  138. }
  139. },
  140. onselfclosingtag(end) {
  141. closeCurrentTag(end)
  142. },
  143. onattribname(start, end) {
  144. // plain attribute
  145. currentProp = {
  146. type: NodeTypes.ATTRIBUTE,
  147. name: getSlice(start, end),
  148. value: undefined,
  149. loc: getLoc(start)
  150. }
  151. },
  152. ondirname(start, end) {
  153. const raw = getSlice(start, end)
  154. if (inVPre) {
  155. currentProp = {
  156. type: NodeTypes.ATTRIBUTE,
  157. name: raw,
  158. value: undefined,
  159. loc: getLoc(start)
  160. }
  161. } else {
  162. const name =
  163. raw === '.' || raw === ':'
  164. ? 'bind'
  165. : raw === '@'
  166. ? 'on'
  167. : raw === '#'
  168. ? 'slot'
  169. : raw.slice(2)
  170. currentProp = {
  171. type: NodeTypes.DIRECTIVE,
  172. name,
  173. raw,
  174. exp: undefined,
  175. arg: undefined,
  176. modifiers: [],
  177. loc: getLoc(start)
  178. }
  179. if (name === 'pre') {
  180. inVPre = true
  181. currentElementIsVPreBoundary = true
  182. // convert dirs before this one to attributes
  183. const props = currentElement!.props
  184. for (let i = 0; i < props.length; i++) {
  185. if (props[i].type === NodeTypes.DIRECTIVE) {
  186. props[i] = dirToAttr(props[i] as DirectiveNode)
  187. }
  188. }
  189. }
  190. }
  191. },
  192. ondirarg(start, end) {
  193. const arg = getSlice(start, end)
  194. if (inVPre) {
  195. ;(currentProp as AttributeNode).name += arg
  196. } else {
  197. const isStatic = arg[0] !== `[`
  198. ;(currentProp as DirectiveNode).arg = {
  199. type: NodeTypes.SIMPLE_EXPRESSION,
  200. content: arg,
  201. isStatic,
  202. constType: isStatic
  203. ? ConstantTypes.CAN_STRINGIFY
  204. : ConstantTypes.NOT_CONSTANT,
  205. loc: getLoc(start, end)
  206. }
  207. }
  208. },
  209. ondirmodifier(start, end) {
  210. const mod = getSlice(start, end)
  211. if (inVPre) {
  212. ;(currentProp as AttributeNode).name += '.' + mod
  213. } else {
  214. ;(currentProp as DirectiveNode).modifiers.push(mod)
  215. }
  216. },
  217. onattribdata(start, end) {
  218. currentAttrValue += getSlice(start, end)
  219. if (currentAttrStartIndex < 0) currentAttrStartIndex = start
  220. currentAttrEndIndex = end
  221. },
  222. onattribentity(codepoint) {
  223. currentAttrValue += fromCodePoint(codepoint)
  224. },
  225. onattribnameend(end) {
  226. // check duplicate attrs
  227. const start = currentProp!.loc.start.offset
  228. const name = getSlice(start, end)
  229. if (currentProp!.type === NodeTypes.DIRECTIVE) {
  230. currentProp!.raw = name
  231. }
  232. if (currentAttrs.has(name)) {
  233. currentProp = null
  234. // TODO emit error DUPLICATE_ATTRIBUTE
  235. throw new Error(`duplicate attr ${name}`)
  236. } else {
  237. currentAttrs.add(name)
  238. }
  239. },
  240. onattribend(quote, end) {
  241. if (currentElement && currentProp) {
  242. if (currentAttrValue) {
  243. if (currentProp.type === NodeTypes.ATTRIBUTE) {
  244. // assign value
  245. currentProp!.value = {
  246. type: NodeTypes.TEXT,
  247. content: currentAttrValue,
  248. loc:
  249. quote === QuoteType.Unquoted
  250. ? getLoc(currentAttrStartIndex, currentAttrEndIndex)
  251. : getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
  252. }
  253. } else {
  254. // directive
  255. currentProp.exp = {
  256. type: NodeTypes.SIMPLE_EXPRESSION,
  257. content: currentAttrValue,
  258. isStatic: false,
  259. // Treat as non-constant by default. This can be potentially set
  260. // to other values by `transformExpression` to make it eligible
  261. // for hoisting.
  262. constType: ConstantTypes.NOT_CONSTANT,
  263. loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
  264. }
  265. }
  266. }
  267. currentProp.loc.end = tokenizer.getPos(end)
  268. if (
  269. currentProp.type !== NodeTypes.DIRECTIVE ||
  270. currentProp.name !== 'pre'
  271. ) {
  272. currentElement.props.push(currentProp)
  273. }
  274. }
  275. currentAttrValue = ''
  276. currentAttrStartIndex = currentAttrEndIndex = -1
  277. },
  278. oncomment(start, end) {
  279. if (currentOptions.comments) {
  280. addNode({
  281. type: NodeTypes.COMMENT,
  282. content: getSlice(start, end),
  283. loc: getLoc(start - 4, end + 3)
  284. })
  285. }
  286. },
  287. onend() {
  288. const end = currentInput.length - 1
  289. for (let index = 0; index < stack.length; index++) {
  290. onCloseTag(stack[index], end)
  291. }
  292. },
  293. oncdata(start, end) {
  294. // TODO throw error
  295. }
  296. })
  297. function getSlice(start: number, end: number) {
  298. return currentInput.slice(start, end)
  299. }
  300. function endOpenTag(end: number) {
  301. addNode(currentElement!)
  302. const name = currentElement!.tag
  303. if (currentOptions.isPreTag(name)) {
  304. inPre++
  305. }
  306. if (currentOptions.isVoidTag(name)) {
  307. onCloseTag(currentElement!, end)
  308. } else {
  309. stack.unshift(currentElement!)
  310. }
  311. currentElement = null
  312. }
  313. function closeCurrentTag(end: number) {
  314. const name = currentElement!.tag
  315. endOpenTag(end)
  316. if (stack[0].tag === name) {
  317. onCloseTag(stack.shift()!, end)
  318. }
  319. }
  320. function onText(content: string, start: number, end: number) {
  321. const parent = getParent()
  322. const lastNode = parent.children[parent.children.length - 1]
  323. if (lastNode?.type === NodeTypes.TEXT) {
  324. // merge
  325. lastNode.content += content
  326. lastNode.loc.end = tokenizer.getPos(end)
  327. } else {
  328. parent.children.push({
  329. type: NodeTypes.TEXT,
  330. content,
  331. loc: {
  332. start: tokenizer.getPos(start),
  333. end: tokenizer.getPos(end),
  334. source: ''
  335. }
  336. })
  337. }
  338. }
  339. function onCloseTag(el: ElementNode, end: number) {
  340. // attach end position
  341. let offset = 0
  342. while (currentInput.charCodeAt(end + offset) !== CharCodes.Gt) {
  343. offset++
  344. }
  345. el.loc.end = tokenizer.getPos(end + offset + 1)
  346. // refine element type
  347. const tag = el.tag
  348. if (!inVPre) {
  349. if (tag === 'slot') {
  350. el.tagType = ElementTypes.SLOT
  351. } else if (isFragmentTemplate(el)) {
  352. el.tagType = ElementTypes.TEMPLATE
  353. } else if (isComponent(el)) {
  354. el.tagType = ElementTypes.COMPONENT
  355. }
  356. }
  357. // whitepsace management
  358. el.children = condenseWhitespace(el.children)
  359. if (currentOptions.isPreTag(tag)) {
  360. inPre--
  361. }
  362. if (currentElementIsVPreBoundary) {
  363. inVPre = false
  364. currentElementIsVPreBoundary = false
  365. }
  366. }
  367. const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
  368. function isFragmentTemplate({ tag, props }: ElementNode): boolean {
  369. if (tag === 'template') {
  370. for (let i = 0; i < props.length; i++) {
  371. if (
  372. props[i].type === NodeTypes.DIRECTIVE &&
  373. specialTemplateDir.has(props[i].name)
  374. ) {
  375. return true
  376. }
  377. }
  378. }
  379. return false
  380. }
  381. function isComponent({ tag, props }: ElementNode): boolean {
  382. if (currentOptions.isCustomElement(tag)) {
  383. return false
  384. }
  385. if (
  386. tag === 'component' ||
  387. isUpperCase(tag.charCodeAt(0)) ||
  388. isCoreComponent(tag) ||
  389. currentOptions.isBuiltInComponent?.(tag) ||
  390. !currentOptions.isNativeTag?.(tag)
  391. ) {
  392. return true
  393. }
  394. // at this point the tag should be a native tag, but check for potential "is"
  395. // casting
  396. for (let i = 0; i < props.length; i++) {
  397. const p = props[i]
  398. if (p.type === NodeTypes.ATTRIBUTE) {
  399. if (p.name === 'is' && p.value) {
  400. if (p.value.content.startsWith('vue:')) {
  401. return true
  402. }
  403. // TODO else if (
  404. // __COMPAT__ &&
  405. // checkCompatEnabled(
  406. // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  407. // context,
  408. // p.loc
  409. // )
  410. // ) {
  411. // return true
  412. // }
  413. }
  414. }
  415. // TODO else if (
  416. // __COMPAT__ &&
  417. // // :is on plain element - only treat as component in compat mode
  418. // p.name === 'bind' &&
  419. // isStaticArgOf(p.arg, 'is') &&
  420. // checkCompatEnabled(
  421. // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
  422. // context,
  423. // p.loc
  424. // )
  425. // ) {
  426. // return true
  427. // }
  428. }
  429. return false
  430. }
  431. function isUpperCase(c: number) {
  432. return c > 64 && c < 91
  433. }
  434. const windowsNewlineRE = /\r\n/g
  435. function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
  436. const shouldCondense = currentOptions.whitespace !== 'preserve'
  437. let removedWhitespace = false
  438. for (let i = 0; i < nodes.length; i++) {
  439. const node = nodes[i]
  440. if (node.type === NodeTypes.TEXT) {
  441. if (!inPre) {
  442. if (isAllWhitespace(node.content)) {
  443. const prev = nodes[i - 1]?.type
  444. const next = nodes[i + 1]?.type
  445. // Remove if:
  446. // - the whitespace is the first or last node, or:
  447. // - (condense mode) the whitespace is between two comments, or:
  448. // - (condense mode) the whitespace is between comment and element, or:
  449. // - (condense mode) the whitespace is between two elements AND contains newline
  450. if (
  451. !prev ||
  452. !next ||
  453. (shouldCondense &&
  454. ((prev === NodeTypes.COMMENT &&
  455. (next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
  456. (prev === NodeTypes.ELEMENT &&
  457. (next === NodeTypes.COMMENT ||
  458. (next === NodeTypes.ELEMENT &&
  459. hasNewlineChar(node.content))))))
  460. ) {
  461. removedWhitespace = true
  462. nodes[i] = null as any
  463. } else {
  464. // Otherwise, the whitespace is condensed into a single space
  465. node.content = ' '
  466. }
  467. } else if (shouldCondense) {
  468. // in condense mode, consecutive whitespaces in text are condensed
  469. // down to a single space.
  470. node.content = condense(node.content)
  471. }
  472. } else {
  473. // #6410 normalize windows newlines in <pre>:
  474. // in SSR, browsers normalize server-rendered \r\n into a single \n
  475. // in the DOM
  476. node.content = node.content.replace(windowsNewlineRE, '\n')
  477. }
  478. }
  479. }
  480. return removedWhitespace ? nodes.filter(Boolean) : nodes
  481. }
  482. function isAllWhitespace(str: string) {
  483. for (let i = 0; i < str.length; i++) {
  484. if (!isWhitespace(str.charCodeAt(i))) {
  485. return false
  486. }
  487. }
  488. return true
  489. }
  490. function hasNewlineChar(str: string) {
  491. for (let i = 0; i < str.length; i++) {
  492. const c = str.charCodeAt(i)
  493. if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
  494. return true
  495. }
  496. }
  497. return false
  498. }
  499. function condense(str: string) {
  500. let ret = ''
  501. let prevCharIsWhitespace = false
  502. for (let i = 0; i < str.length; i++) {
  503. if (isWhitespace(str.charCodeAt(i))) {
  504. if (!prevCharIsWhitespace) {
  505. ret += ' '
  506. prevCharIsWhitespace = true
  507. }
  508. } else {
  509. ret += str[i]
  510. prevCharIsWhitespace = false
  511. }
  512. }
  513. return ret
  514. }
  515. function addNode(node: TemplateChildNode) {
  516. getParent().children.push(node)
  517. }
  518. function getParent() {
  519. return stack[0] || currentRoot
  520. }
  521. function getLoc(start: number, end?: number): SourceLocation {
  522. return {
  523. start: tokenizer.getPos(start),
  524. // @ts-expect-error allow late attachment
  525. end: end && tokenizer.getPos(end)
  526. }
  527. }
  528. function dirToAttr(dir: DirectiveNode): AttributeNode {
  529. const attr: AttributeNode = {
  530. type: NodeTypes.ATTRIBUTE,
  531. name: dir.raw!,
  532. value: undefined,
  533. loc: dir.loc
  534. }
  535. if (dir.exp) {
  536. // account for quotes
  537. const loc = dir.exp.loc
  538. if (loc.end.offset < dir.loc.end.offset) {
  539. loc.start.offset--
  540. loc.start.column--
  541. loc.end.offset++
  542. loc.end.column++
  543. }
  544. attr.value = {
  545. type: NodeTypes.TEXT,
  546. content: (dir.exp as SimpleExpressionNode).content,
  547. loc
  548. }
  549. }
  550. return attr
  551. }
  552. function reset() {
  553. tokenizer.reset()
  554. currentElement = null
  555. currentProp = null
  556. currentAttrs.clear()
  557. currentAttrValue = ''
  558. currentAttrStartIndex = -1
  559. currentAttrEndIndex = -1
  560. stack.length = 0
  561. }
  562. export function baseParse(input: string, options?: ParserOptions): RootNode {
  563. reset()
  564. currentInput = input
  565. currentOptions = extend({}, defaultParserOptions, options)
  566. tokenizer.mode =
  567. currentOptions.parseMode === 'html'
  568. ? ParseMode.HTML
  569. : currentOptions.parseMode === 'sfc'
  570. ? ParseMode.SFC
  571. : ParseMode.BASE
  572. const delimiters = options?.delimiters
  573. if (delimiters) {
  574. tokenizer.delimiterOpen = toCharCodes(delimiters[0])
  575. tokenizer.delimiterClose = toCharCodes(delimiters[1])
  576. }
  577. const root = (currentRoot = createRoot([]))
  578. tokenizer.parse(currentInput)
  579. root.loc.end = tokenizer.getPos(input.length)
  580. root.children = condenseWhitespace(root.children)
  581. return root
  582. }