parse.spec.ts 17 KB

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