parse.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import {
  2. baseParse as parse,
  3. NodeTypes,
  4. ElementNode,
  5. TextNode,
  6. ErrorCodes,
  7. ElementTypes,
  8. InterpolationNode,
  9. AttributeNode,
  10. ConstantTypes
  11. } from '@vue/compiler-core'
  12. import { parserOptions, DOMNamespaces } from '../src/parserOptions'
  13. describe('DOM parser', () => {
  14. describe('Text', () => {
  15. test('textarea handles comments/elements as just text', () => {
  16. const ast = parse(
  17. '<textarea>some<div>text</div>and<!--comment--></textarea>',
  18. parserOptions
  19. )
  20. const element = ast.children[0] as ElementNode
  21. const text = element.children[0] as TextNode
  22. expect(text).toStrictEqual({
  23. type: NodeTypes.TEXT,
  24. content: 'some<div>text</div>and<!--comment-->',
  25. loc: {
  26. start: { offset: 10, line: 1, column: 11 },
  27. end: { offset: 46, line: 1, column: 47 }
  28. }
  29. })
  30. })
  31. test('textarea handles entities', () => {
  32. const ast = parse('<textarea>&amp;</textarea>', parserOptions)
  33. const element = ast.children[0] as ElementNode
  34. const text = element.children[0] as TextNode
  35. expect(text).toStrictEqual({
  36. type: NodeTypes.TEXT,
  37. content: '&',
  38. loc: {
  39. start: { offset: 10, line: 1, column: 11 },
  40. end: { offset: 15, line: 1, column: 16 }
  41. }
  42. })
  43. })
  44. test('textarea support interpolation', () => {
  45. const ast = parse('<textarea><div>{{ foo }}</textarea>', parserOptions)
  46. const element = ast.children[0] as ElementNode
  47. expect(element.children).toMatchObject([
  48. { type: NodeTypes.TEXT, content: `<div>` },
  49. {
  50. type: NodeTypes.INTERPOLATION,
  51. content: {
  52. type: NodeTypes.SIMPLE_EXPRESSION,
  53. content: `foo`,
  54. isStatic: false
  55. }
  56. }
  57. ])
  58. })
  59. test('style handles comments/elements as just a text', () => {
  60. const ast = parse(
  61. '<style>some<div>text</div>and<!--comment--></style>',
  62. parserOptions
  63. )
  64. const element = ast.children[0] as ElementNode
  65. const text = element.children[0] as TextNode
  66. expect(text).toStrictEqual({
  67. type: NodeTypes.TEXT,
  68. content: 'some<div>text</div>and<!--comment-->',
  69. loc: {
  70. start: { offset: 7, line: 1, column: 8 },
  71. end: { offset: 43, line: 1, column: 44 }
  72. }
  73. })
  74. })
  75. test("style doesn't handle character references", () => {
  76. const ast = parse('<style>&amp;</style>', parserOptions)
  77. const element = ast.children[0] as ElementNode
  78. const text = element.children[0] as TextNode
  79. expect(text).toStrictEqual({
  80. type: NodeTypes.TEXT,
  81. content: '&amp;',
  82. loc: {
  83. start: { offset: 7, line: 1, column: 8 },
  84. end: { offset: 12, line: 1, column: 13 }
  85. }
  86. })
  87. })
  88. test('CDATA', () => {
  89. const ast = parse('<svg><![CDATA[some text]]></svg>', parserOptions)
  90. const text = (ast.children[0] as ElementNode).children![0] as TextNode
  91. expect(text).toStrictEqual({
  92. type: NodeTypes.TEXT,
  93. content: 'some text',
  94. loc: {
  95. start: { offset: 14, line: 1, column: 15 },
  96. end: { offset: 23, line: 1, column: 24 }
  97. }
  98. })
  99. })
  100. test('<pre> tag should preserve raw whitespace', () => {
  101. const rawText = ` \na <div>foo \n bar</div> \n c`
  102. const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
  103. expect((ast.children[0] as ElementNode).children).toMatchObject([
  104. {
  105. type: NodeTypes.TEXT,
  106. content: ` \na `
  107. },
  108. {
  109. type: NodeTypes.ELEMENT,
  110. children: [
  111. {
  112. type: NodeTypes.TEXT,
  113. content: `foo \n bar`
  114. }
  115. ]
  116. },
  117. {
  118. type: NodeTypes.TEXT,
  119. content: ` \n c`
  120. }
  121. ])
  122. })
  123. // #908
  124. test('<pre> tag should remove leading newline', () => {
  125. const rawText = `\nhello<div>\nbye</div>`
  126. const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
  127. expect((ast.children[0] as ElementNode).children).toMatchObject([
  128. {
  129. type: NodeTypes.TEXT,
  130. content: `hello`
  131. },
  132. {
  133. type: NodeTypes.ELEMENT,
  134. children: [
  135. {
  136. type: NodeTypes.TEXT,
  137. // should not remove the leading newline for nested elements
  138. content: `\nbye`
  139. }
  140. ]
  141. }
  142. ])
  143. })
  144. // #945
  145. test('&nbsp; should not be condensed', () => {
  146. const nbsp = String.fromCharCode(160)
  147. const ast = parse(`foo&nbsp;&nbsp;bar`, parserOptions)
  148. expect(ast.children[0]).toMatchObject({
  149. type: NodeTypes.TEXT,
  150. content: `foo${nbsp}${nbsp}bar`
  151. })
  152. })
  153. // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
  154. test('HTML entities compatibility in text', () => {
  155. const ast = parse('&ampersand;', parserOptions)
  156. const text = ast.children[0] as TextNode
  157. expect(text).toStrictEqual({
  158. type: NodeTypes.TEXT,
  159. content: '&ersand;',
  160. loc: {
  161. start: { offset: 0, line: 1, column: 1 },
  162. end: { offset: 11, line: 1, column: 12 }
  163. }
  164. })
  165. })
  166. // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
  167. test('HTML entities compatibility in attribute', () => {
  168. const ast = parse(
  169. '<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
  170. parserOptions
  171. )
  172. const element = ast.children[0] as ElementNode
  173. const text1 = (element.props[0] as AttributeNode).value
  174. const text2 = (element.props[1] as AttributeNode).value
  175. const text3 = (element.props[2] as AttributeNode).value
  176. expect(text1).toStrictEqual({
  177. type: NodeTypes.TEXT,
  178. content: '&ampersand;',
  179. loc: {
  180. start: { offset: 7, line: 1, column: 8 },
  181. end: { offset: 20, line: 1, column: 21 }
  182. }
  183. })
  184. expect(text2).toStrictEqual({
  185. type: NodeTypes.TEXT,
  186. content: '&ersand;',
  187. loc: {
  188. start: { offset: 23, line: 1, column: 24 },
  189. end: { offset: 37, line: 1, column: 38 }
  190. }
  191. })
  192. expect(text3).toStrictEqual({
  193. type: NodeTypes.TEXT,
  194. content: '&!',
  195. loc: {
  196. start: { offset: 40, line: 1, column: 41 },
  197. end: { offset: 47, line: 1, column: 48 }
  198. }
  199. })
  200. })
  201. test('Some control character reference should be replaced.', () => {
  202. const ast = parse('&#x86;', parserOptions)
  203. const text = ast.children[0] as TextNode
  204. expect(text).toStrictEqual({
  205. type: NodeTypes.TEXT,
  206. content: '†',
  207. loc: {
  208. start: { offset: 0, line: 1, column: 1 },
  209. end: { offset: 6, line: 1, column: 7 }
  210. }
  211. })
  212. })
  213. })
  214. describe('Interpolation', () => {
  215. test('HTML entities in interpolation should be translated for backward compatibility.', () => {
  216. const ast = parse('<div>{{ a &lt; b }}</div>', parserOptions)
  217. const element = ast.children[0] as ElementNode
  218. const interpolation = element.children[0] as InterpolationNode
  219. expect(interpolation).toStrictEqual({
  220. type: NodeTypes.INTERPOLATION,
  221. content: {
  222. type: NodeTypes.SIMPLE_EXPRESSION,
  223. content: `a < b`,
  224. isStatic: false,
  225. constType: ConstantTypes.NOT_CONSTANT,
  226. loc: {
  227. start: { offset: 8, line: 1, column: 9 },
  228. end: { offset: 16, line: 1, column: 17 }
  229. }
  230. },
  231. loc: {
  232. start: { offset: 5, line: 1, column: 6 },
  233. end: { offset: 19, line: 1, column: 20 }
  234. }
  235. })
  236. })
  237. })
  238. describe('Element', () => {
  239. test('void element', () => {
  240. const ast = parse('<img>after', parserOptions)
  241. const element = ast.children[0] as ElementNode
  242. expect(element).toStrictEqual({
  243. type: NodeTypes.ELEMENT,
  244. ns: DOMNamespaces.HTML,
  245. tag: 'img',
  246. tagType: ElementTypes.ELEMENT,
  247. props: [],
  248. children: [],
  249. loc: {
  250. start: { offset: 0, line: 1, column: 1 },
  251. end: { offset: 5, line: 1, column: 6 }
  252. },
  253. codegenNode: undefined
  254. })
  255. })
  256. test('native element', () => {
  257. const ast = parse('<div></div><comp></comp><Comp></Comp>', parserOptions)
  258. expect(ast.children[0]).toMatchObject({
  259. type: NodeTypes.ELEMENT,
  260. tag: 'div',
  261. tagType: ElementTypes.ELEMENT
  262. })
  263. expect(ast.children[1]).toMatchObject({
  264. type: NodeTypes.ELEMENT,
  265. tag: 'comp',
  266. tagType: ElementTypes.COMPONENT
  267. })
  268. expect(ast.children[2]).toMatchObject({
  269. type: NodeTypes.ELEMENT,
  270. tag: 'Comp',
  271. tagType: ElementTypes.COMPONENT
  272. })
  273. })
  274. test('Strict end tag detection for textarea.', () => {
  275. const ast = parse(
  276. '<textarea>hello</textarea</textarea0></texTArea a="<>">',
  277. {
  278. ...parserOptions,
  279. onError: err => {
  280. if (err.code !== ErrorCodes.END_TAG_WITH_ATTRIBUTES) {
  281. throw err
  282. }
  283. }
  284. }
  285. )
  286. const element = ast.children[0] as ElementNode
  287. const text = element.children[0] as TextNode
  288. expect(ast.children.length).toBe(1)
  289. expect(text).toStrictEqual({
  290. type: NodeTypes.TEXT,
  291. content: 'hello</textarea</textarea0>',
  292. loc: {
  293. start: { offset: 10, line: 1, column: 11 },
  294. end: { offset: 37, line: 1, column: 38 }
  295. }
  296. })
  297. })
  298. })
  299. describe('Namespaces', () => {
  300. test('HTML namespace', () => {
  301. const ast = parse('<html>test</html>', parserOptions)
  302. const element = ast.children[0] as ElementNode
  303. expect(element.ns).toBe(DOMNamespaces.HTML)
  304. })
  305. test('SVG namespace', () => {
  306. const ast = parse('<svg>test</svg>', parserOptions)
  307. const element = ast.children[0] as ElementNode
  308. expect(element.ns).toBe(DOMNamespaces.SVG)
  309. })
  310. test('MATH_ML namespace', () => {
  311. const ast = parse('<math>test</math>', parserOptions)
  312. const element = ast.children[0] as ElementNode
  313. expect(element.ns).toBe(DOMNamespaces.MATH_ML)
  314. })
  315. test('SVG in MATH_ML namespace', () => {
  316. const ast = parse(
  317. '<math><annotation-xml><svg></svg></annotation-xml></math>',
  318. parserOptions
  319. )
  320. const elementMath = ast.children[0] as ElementNode
  321. const elementAnnotation = elementMath.children[0] as ElementNode
  322. const elementSvg = elementAnnotation.children[0] as ElementNode
  323. expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
  324. expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
  325. })
  326. test('html text/html in MATH_ML namespace', () => {
  327. const ast = parse(
  328. '<math><annotation-xml encoding="text/html"><test/></annotation-xml></math>',
  329. parserOptions
  330. )
  331. const elementMath = ast.children[0] as ElementNode
  332. const elementAnnotation = elementMath.children[0] as ElementNode
  333. const element = elementAnnotation.children[0] as ElementNode
  334. expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
  335. expect(element.ns).toBe(DOMNamespaces.HTML)
  336. })
  337. test('html application/xhtml+xml in MATH_ML namespace', () => {
  338. const ast = parse(
  339. '<math><annotation-xml encoding="application/xhtml+xml"><test/></annotation-xml></math>',
  340. parserOptions
  341. )
  342. const elementMath = ast.children[0] as ElementNode
  343. const elementAnnotation = elementMath.children[0] as ElementNode
  344. const element = elementAnnotation.children[0] as ElementNode
  345. expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
  346. expect(element.ns).toBe(DOMNamespaces.HTML)
  347. })
  348. test('mtext malignmark in MATH_ML namespace', () => {
  349. const ast = parse(
  350. '<math><mtext><malignmark/></mtext></math>',
  351. parserOptions
  352. )
  353. const elementMath = ast.children[0] as ElementNode
  354. const elementText = elementMath.children[0] as ElementNode
  355. const element = elementText.children[0] as ElementNode
  356. expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
  357. expect(element.ns).toBe(DOMNamespaces.MATH_ML)
  358. })
  359. test('mtext and not malignmark tag in MATH_ML namespace', () => {
  360. const ast = parse('<math><mtext><test/></mtext></math>', parserOptions)
  361. const elementMath = ast.children[0] as ElementNode
  362. const elementText = elementMath.children[0] as ElementNode
  363. const element = elementText.children[0] as ElementNode
  364. expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
  365. expect(element.ns).toBe(DOMNamespaces.HTML)
  366. })
  367. test('foreignObject tag in SVG namespace', () => {
  368. const ast = parse(
  369. '<svg><foreignObject><test/></foreignObject></svg>',
  370. parserOptions
  371. )
  372. const elementSvg = ast.children[0] as ElementNode
  373. const elementForeignObject = elementSvg.children[0] as ElementNode
  374. const element = elementForeignObject.children[0] as ElementNode
  375. expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
  376. expect(element.ns).toBe(DOMNamespaces.HTML)
  377. })
  378. test('desc tag in SVG namespace', () => {
  379. const ast = parse('<svg><desc><test/></desc></svg>', parserOptions)
  380. const elementSvg = ast.children[0] as ElementNode
  381. const elementDesc = elementSvg.children[0] as ElementNode
  382. const element = elementDesc.children[0] as ElementNode
  383. expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
  384. expect(element.ns).toBe(DOMNamespaces.HTML)
  385. })
  386. test('title tag in SVG namespace', () => {
  387. const ast = parse('<svg><title><test/></title></svg>', parserOptions)
  388. const elementSvg = ast.children[0] as ElementNode
  389. const elementTitle = elementSvg.children[0] as ElementNode
  390. const element = elementTitle.children[0] as ElementNode
  391. expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
  392. expect(element.ns).toBe(DOMNamespaces.HTML)
  393. })
  394. test('SVG in HTML namespace', () => {
  395. const ast = parse('<html><svg></svg></html>', parserOptions)
  396. const elementHtml = ast.children[0] as ElementNode
  397. const element = elementHtml.children[0] as ElementNode
  398. expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
  399. expect(element.ns).toBe(DOMNamespaces.SVG)
  400. })
  401. test('MATH in HTML namespace', () => {
  402. const ast = parse('<html><math></math></html>', parserOptions)
  403. const elementHtml = ast.children[0] as ElementNode
  404. const element = elementHtml.children[0] as ElementNode
  405. expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
  406. expect(element.ns).toBe(DOMNamespaces.MATH_ML)
  407. })
  408. })
  409. })