parse.spec.ts 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722
  1. import { parse, ParserOptions, TextModes } from '../src/parse'
  2. import { ErrorCodes } from '../src/errors'
  3. import {
  4. CommentNode,
  5. ElementNode,
  6. ElementTypes,
  7. Namespaces,
  8. NodeTypes,
  9. Position,
  10. TextNode,
  11. AttributeNode,
  12. InterpolationNode
  13. } from '../src/ast'
  14. describe('compiler: parse', () => {
  15. describe('Text', () => {
  16. test('simple text', () => {
  17. const ast = parse('some text')
  18. const text = ast.children[0] as TextNode
  19. expect(text).toStrictEqual({
  20. type: NodeTypes.TEXT,
  21. content: 'some text',
  22. isEmpty: false,
  23. loc: {
  24. start: { offset: 0, line: 1, column: 1 },
  25. end: { offset: 9, line: 1, column: 10 },
  26. source: 'some text'
  27. }
  28. })
  29. })
  30. test('simple text with invalid end tag', () => {
  31. const ast = parse('some text</div>', {
  32. onError: () => {}
  33. })
  34. const text = ast.children[0] as TextNode
  35. expect(text).toStrictEqual({
  36. type: NodeTypes.TEXT,
  37. content: 'some text',
  38. isEmpty: false,
  39. loc: {
  40. start: { offset: 0, line: 1, column: 1 },
  41. end: { offset: 9, line: 1, column: 10 },
  42. source: 'some text'
  43. }
  44. })
  45. })
  46. test('text with interpolation', () => {
  47. const ast = parse('some {{ foo + bar }} text')
  48. const text1 = ast.children[0] as TextNode
  49. const text2 = ast.children[2] as TextNode
  50. expect(text1).toStrictEqual({
  51. type: NodeTypes.TEXT,
  52. content: 'some ',
  53. isEmpty: false,
  54. loc: {
  55. start: { offset: 0, line: 1, column: 1 },
  56. end: { offset: 5, line: 1, column: 6 },
  57. source: 'some '
  58. }
  59. })
  60. expect(text2).toStrictEqual({
  61. type: NodeTypes.TEXT,
  62. content: ' text',
  63. isEmpty: false,
  64. loc: {
  65. start: { offset: 20, line: 1, column: 21 },
  66. end: { offset: 25, line: 1, column: 26 },
  67. source: ' text'
  68. }
  69. })
  70. })
  71. test('text with interpolation which has `<`', () => {
  72. const ast = parse('some {{ a<b && c>d }} text')
  73. const text1 = ast.children[0] as TextNode
  74. const text2 = ast.children[2] as TextNode
  75. expect(text1).toStrictEqual({
  76. type: NodeTypes.TEXT,
  77. content: 'some ',
  78. isEmpty: false,
  79. loc: {
  80. start: { offset: 0, line: 1, column: 1 },
  81. end: { offset: 5, line: 1, column: 6 },
  82. source: 'some '
  83. }
  84. })
  85. expect(text2).toStrictEqual({
  86. type: NodeTypes.TEXT,
  87. content: ' text',
  88. isEmpty: false,
  89. loc: {
  90. start: { offset: 21, line: 1, column: 22 },
  91. end: { offset: 26, line: 1, column: 27 },
  92. source: ' text'
  93. }
  94. })
  95. })
  96. test('text with mix of tags and interpolations', () => {
  97. const ast = parse('some <span>{{ foo < bar + foo }} text</span>')
  98. const text1 = ast.children[0] as TextNode
  99. const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
  100. expect(text1).toStrictEqual({
  101. type: NodeTypes.TEXT,
  102. content: 'some ',
  103. isEmpty: false,
  104. loc: {
  105. start: { offset: 0, line: 1, column: 1 },
  106. end: { offset: 5, line: 1, column: 6 },
  107. source: 'some '
  108. }
  109. })
  110. expect(text2).toStrictEqual({
  111. type: NodeTypes.TEXT,
  112. content: ' text',
  113. isEmpty: false,
  114. loc: {
  115. start: { offset: 32, line: 1, column: 33 },
  116. end: { offset: 37, line: 1, column: 38 },
  117. source: ' text'
  118. }
  119. })
  120. })
  121. test('lonly "<" don\'t separate nodes', () => {
  122. const ast = parse('a < b', {
  123. onError: err => {
  124. if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
  125. throw err
  126. }
  127. }
  128. })
  129. const text = ast.children[0] as TextNode
  130. expect(text).toStrictEqual({
  131. type: NodeTypes.TEXT,
  132. content: 'a < b',
  133. isEmpty: false,
  134. loc: {
  135. start: { offset: 0, line: 1, column: 1 },
  136. end: { offset: 5, line: 1, column: 6 },
  137. source: 'a < b'
  138. }
  139. })
  140. })
  141. test('lonly "{{" don\'t separate nodes', () => {
  142. const ast = parse('a {{ b', {
  143. onError: error => {
  144. if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
  145. throw error
  146. }
  147. }
  148. })
  149. const text = ast.children[0] as TextNode
  150. expect(text).toStrictEqual({
  151. type: NodeTypes.TEXT,
  152. content: 'a {{ b',
  153. isEmpty: false,
  154. loc: {
  155. start: { offset: 0, line: 1, column: 1 },
  156. end: { offset: 6, line: 1, column: 7 },
  157. source: 'a {{ b'
  158. }
  159. })
  160. })
  161. test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
  162. const spy = jest.fn()
  163. const ast = parse('&ampersand;', {
  164. namedCharacterReferences: { amp: '&' },
  165. onError: spy
  166. })
  167. const text = ast.children[0] as TextNode
  168. expect(text).toStrictEqual({
  169. type: NodeTypes.TEXT,
  170. content: '&ersand;',
  171. isEmpty: false,
  172. loc: {
  173. start: { offset: 0, line: 1, column: 1 },
  174. end: { offset: 11, line: 1, column: 12 },
  175. source: '&ampersand;'
  176. }
  177. })
  178. expect(spy.mock.calls).toMatchObject([
  179. [
  180. {
  181. code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  182. loc: {
  183. start: { offset: 4, line: 1, column: 5 }
  184. }
  185. }
  186. ]
  187. ])
  188. })
  189. test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
  190. const spy = jest.fn()
  191. const ast = parse(
  192. '<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
  193. {
  194. namedCharacterReferences: { amp: '&', 'amp;': '&' },
  195. onError: spy
  196. }
  197. )
  198. const element = ast.children[0] as ElementNode
  199. const text1 = (element.props[0] as AttributeNode).value
  200. const text2 = (element.props[1] as AttributeNode).value
  201. const text3 = (element.props[2] as AttributeNode).value
  202. expect(text1).toStrictEqual({
  203. type: NodeTypes.TEXT,
  204. content: '&ampersand;',
  205. isEmpty: false,
  206. loc: {
  207. start: { offset: 7, line: 1, column: 8 },
  208. end: { offset: 20, line: 1, column: 21 },
  209. source: '"&ampersand;"'
  210. }
  211. })
  212. expect(text2).toStrictEqual({
  213. type: NodeTypes.TEXT,
  214. content: '&ersand;',
  215. isEmpty: false,
  216. loc: {
  217. start: { offset: 23, line: 1, column: 24 },
  218. end: { offset: 37, line: 1, column: 38 },
  219. source: '"&amp;ersand;"'
  220. }
  221. })
  222. expect(text3).toStrictEqual({
  223. type: NodeTypes.TEXT,
  224. content: '&!',
  225. isEmpty: false,
  226. loc: {
  227. start: { offset: 40, line: 1, column: 41 },
  228. end: { offset: 47, line: 1, column: 48 },
  229. source: '"&amp!"'
  230. }
  231. })
  232. expect(spy.mock.calls).toMatchObject([
  233. [
  234. {
  235. code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  236. loc: {
  237. start: { offset: 45, line: 1, column: 46 }
  238. }
  239. }
  240. ]
  241. ])
  242. })
  243. test('Some control character reference should be replaced.', () => {
  244. const spy = jest.fn()
  245. const ast = parse('&#x86;', { onError: spy })
  246. const text = ast.children[0] as TextNode
  247. expect(text).toStrictEqual({
  248. type: NodeTypes.TEXT,
  249. content: '†',
  250. isEmpty: false,
  251. loc: {
  252. start: { offset: 0, line: 1, column: 1 },
  253. end: { offset: 6, line: 1, column: 7 },
  254. source: '&#x86;'
  255. }
  256. })
  257. expect(spy.mock.calls).toMatchObject([
  258. [
  259. {
  260. code: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  261. loc: {
  262. start: { offset: 0, line: 1, column: 1 }
  263. }
  264. }
  265. ]
  266. ])
  267. })
  268. })
  269. describe('Interpolation', () => {
  270. test('simple interpolation', () => {
  271. const ast = parse('{{message}}')
  272. const interpolation = ast.children[0] as InterpolationNode
  273. expect(interpolation).toStrictEqual({
  274. type: NodeTypes.INTERPOLATION,
  275. content: {
  276. type: NodeTypes.SIMPLE_EXPRESSION,
  277. content: `message`,
  278. isStatic: false,
  279. isConstant: false,
  280. loc: {
  281. start: { offset: 2, line: 1, column: 3 },
  282. end: { offset: 9, line: 1, column: 10 },
  283. source: `message`
  284. }
  285. },
  286. loc: {
  287. start: { offset: 0, line: 1, column: 1 },
  288. end: { offset: 11, line: 1, column: 12 },
  289. source: '{{message}}'
  290. }
  291. })
  292. })
  293. test('it can have tag-like notation', () => {
  294. const ast = parse('{{ a<b }}')
  295. const interpolation = ast.children[0] as InterpolationNode
  296. expect(interpolation).toStrictEqual({
  297. type: NodeTypes.INTERPOLATION,
  298. content: {
  299. type: NodeTypes.SIMPLE_EXPRESSION,
  300. content: `a<b`,
  301. isStatic: false,
  302. isConstant: false,
  303. loc: {
  304. start: { offset: 3, line: 1, column: 4 },
  305. end: { offset: 6, line: 1, column: 7 },
  306. source: 'a<b'
  307. }
  308. },
  309. loc: {
  310. start: { offset: 0, line: 1, column: 1 },
  311. end: { offset: 9, line: 1, column: 10 },
  312. source: '{{ a<b }}'
  313. }
  314. })
  315. })
  316. test('it can have tag-like notation (2)', () => {
  317. const ast = parse('{{ a<b }}{{ c>d }}')
  318. const interpolation1 = ast.children[0] as InterpolationNode
  319. const interpolation2 = ast.children[1] as InterpolationNode
  320. expect(interpolation1).toStrictEqual({
  321. type: NodeTypes.INTERPOLATION,
  322. content: {
  323. type: NodeTypes.SIMPLE_EXPRESSION,
  324. content: `a<b`,
  325. isStatic: false,
  326. isConstant: false,
  327. loc: {
  328. start: { offset: 3, line: 1, column: 4 },
  329. end: { offset: 6, line: 1, column: 7 },
  330. source: 'a<b'
  331. }
  332. },
  333. loc: {
  334. start: { offset: 0, line: 1, column: 1 },
  335. end: { offset: 9, line: 1, column: 10 },
  336. source: '{{ a<b }}'
  337. }
  338. })
  339. expect(interpolation2).toStrictEqual({
  340. type: NodeTypes.INTERPOLATION,
  341. content: {
  342. type: NodeTypes.SIMPLE_EXPRESSION,
  343. isStatic: false,
  344. isConstant: false,
  345. content: 'c>d',
  346. loc: {
  347. start: { offset: 12, line: 1, column: 13 },
  348. end: { offset: 15, line: 1, column: 16 },
  349. source: 'c>d'
  350. }
  351. },
  352. loc: {
  353. start: { offset: 9, line: 1, column: 10 },
  354. end: { offset: 18, line: 1, column: 19 },
  355. source: '{{ c>d }}'
  356. }
  357. })
  358. })
  359. test('it can have tag-like notation (3)', () => {
  360. const ast = parse('<div>{{ "</div>" }}</div>')
  361. const element = ast.children[0] as ElementNode
  362. const interpolation = element.children[0] as InterpolationNode
  363. expect(interpolation).toStrictEqual({
  364. type: NodeTypes.INTERPOLATION,
  365. content: {
  366. type: NodeTypes.SIMPLE_EXPRESSION,
  367. isStatic: false,
  368. // The `isConstant` is the default value and will be determined in `transformExpression`.
  369. isConstant: false,
  370. content: '"</div>"',
  371. loc: {
  372. start: { offset: 8, line: 1, column: 9 },
  373. end: { offset: 16, line: 1, column: 17 },
  374. source: '"</div>"'
  375. }
  376. },
  377. loc: {
  378. start: { offset: 5, line: 1, column: 6 },
  379. end: { offset: 19, line: 1, column: 20 },
  380. source: '{{ "</div>" }}'
  381. }
  382. })
  383. })
  384. test('custom delimiters', () => {
  385. const ast = parse('<p>{msg}</p>', {
  386. delimiters: ['{', '}']
  387. })
  388. const element = ast.children[0] as ElementNode
  389. const interpolation = element.children[0] as InterpolationNode
  390. expect(interpolation).toStrictEqual({
  391. type: NodeTypes.INTERPOLATION,
  392. content: {
  393. type: NodeTypes.SIMPLE_EXPRESSION,
  394. content: `msg`,
  395. isStatic: false,
  396. isConstant: false,
  397. loc: {
  398. start: { offset: 4, line: 1, column: 5 },
  399. end: { offset: 7, line: 1, column: 8 },
  400. source: 'msg'
  401. }
  402. },
  403. loc: {
  404. start: { offset: 3, line: 1, column: 4 },
  405. end: { offset: 8, line: 1, column: 9 },
  406. source: '{msg}'
  407. }
  408. })
  409. })
  410. })
  411. describe('Comment', () => {
  412. test('empty comment', () => {
  413. const ast = parse('<!---->')
  414. const comment = ast.children[0] as CommentNode
  415. expect(comment).toStrictEqual({
  416. type: NodeTypes.COMMENT,
  417. content: '',
  418. loc: {
  419. start: { offset: 0, line: 1, column: 1 },
  420. end: { offset: 7, line: 1, column: 8 },
  421. source: '<!---->'
  422. }
  423. })
  424. })
  425. test('simple comment', () => {
  426. const ast = parse('<!--abc-->')
  427. const comment = ast.children[0] as CommentNode
  428. expect(comment).toStrictEqual({
  429. type: NodeTypes.COMMENT,
  430. content: 'abc',
  431. loc: {
  432. start: { offset: 0, line: 1, column: 1 },
  433. end: { offset: 10, line: 1, column: 11 },
  434. source: '<!--abc-->'
  435. }
  436. })
  437. })
  438. test('two comments', () => {
  439. const ast = parse('<!--abc--><!--def-->')
  440. const comment1 = ast.children[0] as CommentNode
  441. const comment2 = ast.children[1] as CommentNode
  442. expect(comment1).toStrictEqual({
  443. type: NodeTypes.COMMENT,
  444. content: 'abc',
  445. loc: {
  446. start: { offset: 0, line: 1, column: 1 },
  447. end: { offset: 10, line: 1, column: 11 },
  448. source: '<!--abc-->'
  449. }
  450. })
  451. expect(comment2).toStrictEqual({
  452. type: NodeTypes.COMMENT,
  453. content: 'def',
  454. loc: {
  455. start: { offset: 10, line: 1, column: 11 },
  456. end: { offset: 20, line: 1, column: 21 },
  457. source: '<!--def-->'
  458. }
  459. })
  460. })
  461. })
  462. describe('Element', () => {
  463. test('simple div', () => {
  464. const ast = parse('<div>hello</div>')
  465. const element = ast.children[0] as ElementNode
  466. expect(element).toStrictEqual({
  467. type: NodeTypes.ELEMENT,
  468. ns: Namespaces.HTML,
  469. tag: 'div',
  470. tagType: ElementTypes.ELEMENT,
  471. codegenNode: undefined,
  472. props: [],
  473. isSelfClosing: false,
  474. children: [
  475. {
  476. type: NodeTypes.TEXT,
  477. content: 'hello',
  478. isEmpty: false,
  479. loc: {
  480. start: { offset: 5, line: 1, column: 6 },
  481. end: { offset: 10, line: 1, column: 11 },
  482. source: 'hello'
  483. }
  484. }
  485. ],
  486. loc: {
  487. start: { offset: 0, line: 1, column: 1 },
  488. end: { offset: 16, line: 1, column: 17 },
  489. source: '<div>hello</div>'
  490. }
  491. })
  492. })
  493. test('empty', () => {
  494. const ast = parse('<div></div>')
  495. const element = ast.children[0] as ElementNode
  496. expect(element).toStrictEqual({
  497. type: NodeTypes.ELEMENT,
  498. ns: Namespaces.HTML,
  499. tag: 'div',
  500. tagType: ElementTypes.ELEMENT,
  501. codegenNode: undefined,
  502. props: [],
  503. isSelfClosing: false,
  504. children: [],
  505. loc: {
  506. start: { offset: 0, line: 1, column: 1 },
  507. end: { offset: 11, line: 1, column: 12 },
  508. source: '<div></div>'
  509. }
  510. })
  511. })
  512. test('self closing', () => {
  513. const ast = parse('<div/>after')
  514. const element = ast.children[0] as ElementNode
  515. expect(element).toStrictEqual({
  516. type: NodeTypes.ELEMENT,
  517. ns: Namespaces.HTML,
  518. tag: 'div',
  519. tagType: ElementTypes.ELEMENT,
  520. codegenNode: undefined,
  521. props: [],
  522. isSelfClosing: true,
  523. children: [],
  524. loc: {
  525. start: { offset: 0, line: 1, column: 1 },
  526. end: { offset: 6, line: 1, column: 7 },
  527. source: '<div/>'
  528. }
  529. })
  530. })
  531. test('void element', () => {
  532. const ast = parse('<img>after', {
  533. isVoidTag: tag => tag === 'img'
  534. })
  535. const element = ast.children[0] as ElementNode
  536. expect(element).toStrictEqual({
  537. type: NodeTypes.ELEMENT,
  538. ns: Namespaces.HTML,
  539. tag: 'img',
  540. tagType: ElementTypes.ELEMENT,
  541. codegenNode: undefined,
  542. props: [],
  543. isSelfClosing: false,
  544. children: [],
  545. loc: {
  546. start: { offset: 0, line: 1, column: 1 },
  547. end: { offset: 5, line: 1, column: 6 },
  548. source: '<img>'
  549. }
  550. })
  551. })
  552. test('native element with `isNativeTag`', () => {
  553. const ast = parse('<div></div><comp></comp><Comp></Comp>', {
  554. isNativeTag: tag => tag === 'div'
  555. })
  556. expect(ast.children[0]).toMatchObject({
  557. type: NodeTypes.ELEMENT,
  558. tag: 'div',
  559. tagType: ElementTypes.ELEMENT
  560. })
  561. expect(ast.children[1]).toMatchObject({
  562. type: NodeTypes.ELEMENT,
  563. tag: 'comp',
  564. tagType: ElementTypes.COMPONENT
  565. })
  566. expect(ast.children[2]).toMatchObject({
  567. type: NodeTypes.ELEMENT,
  568. tag: 'Comp',
  569. tagType: ElementTypes.COMPONENT
  570. })
  571. })
  572. test('native element without `isNativeTag`', () => {
  573. const ast = parse('<div></div><comp></comp><Comp></Comp>')
  574. expect(ast.children[0]).toMatchObject({
  575. type: NodeTypes.ELEMENT,
  576. tag: 'div',
  577. tagType: ElementTypes.ELEMENT
  578. })
  579. expect(ast.children[1]).toMatchObject({
  580. type: NodeTypes.ELEMENT,
  581. tag: 'comp',
  582. tagType: ElementTypes.ELEMENT
  583. })
  584. expect(ast.children[2]).toMatchObject({
  585. type: NodeTypes.ELEMENT,
  586. tag: 'Comp',
  587. tagType: ElementTypes.COMPONENT
  588. })
  589. })
  590. test('custom element', () => {
  591. const ast = parse('<div></div><comp></comp>', {
  592. isNativeTag: tag => tag === 'div',
  593. isCustomElement: tag => tag === 'comp'
  594. })
  595. expect(ast.children[0]).toMatchObject({
  596. type: NodeTypes.ELEMENT,
  597. tag: 'div',
  598. tagType: ElementTypes.ELEMENT
  599. })
  600. expect(ast.children[1]).toMatchObject({
  601. type: NodeTypes.ELEMENT,
  602. tag: 'comp',
  603. tagType: ElementTypes.ELEMENT
  604. })
  605. })
  606. test('attribute with no value', () => {
  607. const ast = parse('<div id></div>')
  608. const element = ast.children[0] as ElementNode
  609. expect(element).toStrictEqual({
  610. type: NodeTypes.ELEMENT,
  611. ns: Namespaces.HTML,
  612. tag: 'div',
  613. tagType: ElementTypes.ELEMENT,
  614. codegenNode: undefined,
  615. props: [
  616. {
  617. type: NodeTypes.ATTRIBUTE,
  618. name: 'id',
  619. value: undefined,
  620. loc: {
  621. start: { offset: 5, line: 1, column: 6 },
  622. end: { offset: 7, line: 1, column: 8 },
  623. source: 'id'
  624. }
  625. }
  626. ],
  627. isSelfClosing: false,
  628. children: [],
  629. loc: {
  630. start: { offset: 0, line: 1, column: 1 },
  631. end: { offset: 14, line: 1, column: 15 },
  632. source: '<div id></div>'
  633. }
  634. })
  635. })
  636. test('attribute with empty value, double quote', () => {
  637. const ast = parse('<div id=""></div>')
  638. const element = ast.children[0] as ElementNode
  639. expect(element).toStrictEqual({
  640. type: NodeTypes.ELEMENT,
  641. ns: Namespaces.HTML,
  642. tag: 'div',
  643. tagType: ElementTypes.ELEMENT,
  644. codegenNode: undefined,
  645. props: [
  646. {
  647. type: NodeTypes.ATTRIBUTE,
  648. name: 'id',
  649. value: {
  650. type: NodeTypes.TEXT,
  651. content: '',
  652. isEmpty: true,
  653. loc: {
  654. start: { offset: 8, line: 1, column: 9 },
  655. end: { offset: 10, line: 1, column: 11 },
  656. source: '""'
  657. }
  658. },
  659. loc: {
  660. start: { offset: 5, line: 1, column: 6 },
  661. end: { offset: 10, line: 1, column: 11 },
  662. source: 'id=""'
  663. }
  664. }
  665. ],
  666. isSelfClosing: false,
  667. children: [],
  668. loc: {
  669. start: { offset: 0, line: 1, column: 1 },
  670. end: { offset: 17, line: 1, column: 18 },
  671. source: '<div id=""></div>'
  672. }
  673. })
  674. })
  675. test('attribute with empty value, single quote', () => {
  676. const ast = parse("<div id=''></div>")
  677. const element = ast.children[0] as ElementNode
  678. expect(element).toStrictEqual({
  679. type: NodeTypes.ELEMENT,
  680. ns: Namespaces.HTML,
  681. tag: 'div',
  682. tagType: ElementTypes.ELEMENT,
  683. codegenNode: undefined,
  684. props: [
  685. {
  686. type: NodeTypes.ATTRIBUTE,
  687. name: 'id',
  688. value: {
  689. type: NodeTypes.TEXT,
  690. content: '',
  691. isEmpty: true,
  692. loc: {
  693. start: { offset: 8, line: 1, column: 9 },
  694. end: { offset: 10, line: 1, column: 11 },
  695. source: "''"
  696. }
  697. },
  698. loc: {
  699. start: { offset: 5, line: 1, column: 6 },
  700. end: { offset: 10, line: 1, column: 11 },
  701. source: "id=''"
  702. }
  703. }
  704. ],
  705. isSelfClosing: false,
  706. children: [],
  707. loc: {
  708. start: { offset: 0, line: 1, column: 1 },
  709. end: { offset: 17, line: 1, column: 18 },
  710. source: "<div id=''></div>"
  711. }
  712. })
  713. })
  714. test('attribute with value, double quote', () => {
  715. const ast = parse('<div id=">\'"></div>')
  716. const element = ast.children[0] as ElementNode
  717. expect(element).toStrictEqual({
  718. type: NodeTypes.ELEMENT,
  719. ns: Namespaces.HTML,
  720. tag: 'div',
  721. tagType: ElementTypes.ELEMENT,
  722. codegenNode: undefined,
  723. props: [
  724. {
  725. type: NodeTypes.ATTRIBUTE,
  726. name: 'id',
  727. value: {
  728. type: NodeTypes.TEXT,
  729. content: ">'",
  730. isEmpty: false,
  731. loc: {
  732. start: { offset: 8, line: 1, column: 9 },
  733. end: { offset: 12, line: 1, column: 13 },
  734. source: '">\'"'
  735. }
  736. },
  737. loc: {
  738. start: { offset: 5, line: 1, column: 6 },
  739. end: { offset: 12, line: 1, column: 13 },
  740. source: 'id=">\'"'
  741. }
  742. }
  743. ],
  744. isSelfClosing: false,
  745. children: [],
  746. loc: {
  747. start: { offset: 0, line: 1, column: 1 },
  748. end: { offset: 19, line: 1, column: 20 },
  749. source: '<div id=">\'"></div>'
  750. }
  751. })
  752. })
  753. test('attribute with value, single quote', () => {
  754. const ast = parse("<div id='>\"'></div>")
  755. const element = ast.children[0] as ElementNode
  756. expect(element).toStrictEqual({
  757. type: NodeTypes.ELEMENT,
  758. ns: Namespaces.HTML,
  759. tag: 'div',
  760. tagType: ElementTypes.ELEMENT,
  761. codegenNode: undefined,
  762. props: [
  763. {
  764. type: NodeTypes.ATTRIBUTE,
  765. name: 'id',
  766. value: {
  767. type: NodeTypes.TEXT,
  768. content: '>"',
  769. isEmpty: false,
  770. loc: {
  771. start: { offset: 8, line: 1, column: 9 },
  772. end: { offset: 12, line: 1, column: 13 },
  773. source: "'>\"'"
  774. }
  775. },
  776. loc: {
  777. start: { offset: 5, line: 1, column: 6 },
  778. end: { offset: 12, line: 1, column: 13 },
  779. source: "id='>\"'"
  780. }
  781. }
  782. ],
  783. isSelfClosing: false,
  784. children: [],
  785. loc: {
  786. start: { offset: 0, line: 1, column: 1 },
  787. end: { offset: 19, line: 1, column: 20 },
  788. source: "<div id='>\"'></div>"
  789. }
  790. })
  791. })
  792. test('attribute with value, unquoted', () => {
  793. const ast = parse('<div id=a/></div>')
  794. const element = ast.children[0] as ElementNode
  795. expect(element).toStrictEqual({
  796. type: NodeTypes.ELEMENT,
  797. ns: Namespaces.HTML,
  798. tag: 'div',
  799. tagType: ElementTypes.ELEMENT,
  800. codegenNode: undefined,
  801. props: [
  802. {
  803. type: NodeTypes.ATTRIBUTE,
  804. name: 'id',
  805. value: {
  806. type: NodeTypes.TEXT,
  807. content: 'a/',
  808. isEmpty: false,
  809. loc: {
  810. start: { offset: 8, line: 1, column: 9 },
  811. end: { offset: 10, line: 1, column: 11 },
  812. source: 'a/'
  813. }
  814. },
  815. loc: {
  816. start: { offset: 5, line: 1, column: 6 },
  817. end: { offset: 10, line: 1, column: 11 },
  818. source: 'id=a/'
  819. }
  820. }
  821. ],
  822. isSelfClosing: false,
  823. children: [],
  824. loc: {
  825. start: { offset: 0, line: 1, column: 1 },
  826. end: { offset: 17, line: 1, column: 18 },
  827. source: '<div id=a/></div>'
  828. }
  829. })
  830. })
  831. test('multiple attributes', () => {
  832. const ast = parse('<div id=a class="c" inert style=\'\'></div>')
  833. const element = ast.children[0] as ElementNode
  834. expect(element).toStrictEqual({
  835. type: NodeTypes.ELEMENT,
  836. ns: Namespaces.HTML,
  837. tag: 'div',
  838. tagType: ElementTypes.ELEMENT,
  839. codegenNode: undefined,
  840. props: [
  841. {
  842. type: NodeTypes.ATTRIBUTE,
  843. name: 'id',
  844. value: {
  845. type: NodeTypes.TEXT,
  846. content: 'a',
  847. isEmpty: false,
  848. loc: {
  849. start: { offset: 8, line: 1, column: 9 },
  850. end: { offset: 9, line: 1, column: 10 },
  851. source: 'a'
  852. }
  853. },
  854. loc: {
  855. start: { offset: 5, line: 1, column: 6 },
  856. end: { offset: 9, line: 1, column: 10 },
  857. source: 'id=a'
  858. }
  859. },
  860. {
  861. type: NodeTypes.ATTRIBUTE,
  862. name: 'class',
  863. value: {
  864. type: NodeTypes.TEXT,
  865. content: 'c',
  866. isEmpty: false,
  867. loc: {
  868. start: { offset: 16, line: 1, column: 17 },
  869. end: { offset: 19, line: 1, column: 20 },
  870. source: '"c"'
  871. }
  872. },
  873. loc: {
  874. start: { offset: 10, line: 1, column: 11 },
  875. end: { offset: 19, line: 1, column: 20 },
  876. source: 'class="c"'
  877. }
  878. },
  879. {
  880. type: NodeTypes.ATTRIBUTE,
  881. name: 'inert',
  882. value: undefined,
  883. loc: {
  884. start: { offset: 20, line: 1, column: 21 },
  885. end: { offset: 25, line: 1, column: 26 },
  886. source: 'inert'
  887. }
  888. },
  889. {
  890. type: NodeTypes.ATTRIBUTE,
  891. name: 'style',
  892. value: {
  893. type: NodeTypes.TEXT,
  894. content: '',
  895. isEmpty: true,
  896. loc: {
  897. start: { offset: 32, line: 1, column: 33 },
  898. end: { offset: 34, line: 1, column: 35 },
  899. source: "''"
  900. }
  901. },
  902. loc: {
  903. start: { offset: 26, line: 1, column: 27 },
  904. end: { offset: 34, line: 1, column: 35 },
  905. source: "style=''"
  906. }
  907. }
  908. ],
  909. isSelfClosing: false,
  910. children: [],
  911. loc: {
  912. start: { offset: 0, line: 1, column: 1 },
  913. end: { offset: 41, line: 1, column: 42 },
  914. source: '<div id=a class="c" inert style=\'\'></div>'
  915. }
  916. })
  917. })
  918. test('directive with no value', () => {
  919. const ast = parse('<div v-if/>')
  920. const directive = (ast.children[0] as ElementNode).props[0]
  921. expect(directive).toStrictEqual({
  922. type: NodeTypes.DIRECTIVE,
  923. name: 'if',
  924. arg: undefined,
  925. modifiers: [],
  926. exp: undefined,
  927. loc: {
  928. start: { offset: 5, line: 1, column: 6 },
  929. end: { offset: 9, line: 1, column: 10 },
  930. source: 'v-if'
  931. }
  932. })
  933. })
  934. test('directive with value', () => {
  935. const ast = parse('<div v-if="a"/>')
  936. const directive = (ast.children[0] as ElementNode).props[0]
  937. expect(directive).toStrictEqual({
  938. type: NodeTypes.DIRECTIVE,
  939. name: 'if',
  940. arg: undefined,
  941. modifiers: [],
  942. exp: {
  943. type: NodeTypes.SIMPLE_EXPRESSION,
  944. content: 'a',
  945. isStatic: false,
  946. isConstant: false,
  947. loc: {
  948. start: { offset: 11, line: 1, column: 12 },
  949. end: { offset: 12, line: 1, column: 13 },
  950. source: 'a'
  951. }
  952. },
  953. loc: {
  954. start: { offset: 5, line: 1, column: 6 },
  955. end: { offset: 13, line: 1, column: 14 },
  956. source: 'v-if="a"'
  957. }
  958. })
  959. })
  960. test('directive with argument', () => {
  961. const ast = parse('<div v-on:click/>')
  962. const directive = (ast.children[0] as ElementNode).props[0]
  963. expect(directive).toStrictEqual({
  964. type: NodeTypes.DIRECTIVE,
  965. name: 'on',
  966. arg: {
  967. type: NodeTypes.SIMPLE_EXPRESSION,
  968. content: 'click',
  969. isStatic: true,
  970. isConstant: true,
  971. loc: {
  972. source: 'click',
  973. start: {
  974. column: 11,
  975. line: 1,
  976. offset: 10
  977. },
  978. end: {
  979. column: 16,
  980. line: 1,
  981. offset: 15
  982. }
  983. }
  984. },
  985. modifiers: [],
  986. exp: undefined,
  987. loc: {
  988. start: { offset: 5, line: 1, column: 6 },
  989. end: { offset: 15, line: 1, column: 16 },
  990. source: 'v-on:click'
  991. }
  992. })
  993. })
  994. test('directive with a modifier', () => {
  995. const ast = parse('<div v-on.enter/>')
  996. const directive = (ast.children[0] as ElementNode).props[0]
  997. expect(directive).toStrictEqual({
  998. type: NodeTypes.DIRECTIVE,
  999. name: 'on',
  1000. arg: undefined,
  1001. modifiers: ['enter'],
  1002. exp: undefined,
  1003. loc: {
  1004. start: { offset: 5, line: 1, column: 6 },
  1005. end: { offset: 15, line: 1, column: 16 },
  1006. source: 'v-on.enter'
  1007. }
  1008. })
  1009. })
  1010. test('directive with two modifiers', () => {
  1011. const ast = parse('<div v-on.enter.exact/>')
  1012. const directive = (ast.children[0] as ElementNode).props[0]
  1013. expect(directive).toStrictEqual({
  1014. type: NodeTypes.DIRECTIVE,
  1015. name: 'on',
  1016. arg: undefined,
  1017. modifiers: ['enter', 'exact'],
  1018. exp: undefined,
  1019. loc: {
  1020. start: { offset: 5, line: 1, column: 6 },
  1021. end: { offset: 21, line: 1, column: 22 },
  1022. source: 'v-on.enter.exact'
  1023. }
  1024. })
  1025. })
  1026. test('directive with argument and modifiers', () => {
  1027. const ast = parse('<div v-on:click.enter.exact/>')
  1028. const directive = (ast.children[0] as ElementNode).props[0]
  1029. expect(directive).toStrictEqual({
  1030. type: NodeTypes.DIRECTIVE,
  1031. name: 'on',
  1032. arg: {
  1033. type: NodeTypes.SIMPLE_EXPRESSION,
  1034. content: 'click',
  1035. isStatic: true,
  1036. isConstant: true,
  1037. loc: {
  1038. source: 'click',
  1039. start: {
  1040. column: 11,
  1041. line: 1,
  1042. offset: 10
  1043. },
  1044. end: {
  1045. column: 16,
  1046. line: 1,
  1047. offset: 15
  1048. }
  1049. }
  1050. },
  1051. modifiers: ['enter', 'exact'],
  1052. exp: undefined,
  1053. loc: {
  1054. start: { offset: 5, line: 1, column: 6 },
  1055. end: { offset: 27, line: 1, column: 28 },
  1056. source: 'v-on:click.enter.exact'
  1057. }
  1058. })
  1059. })
  1060. test('v-bind shorthand', () => {
  1061. const ast = parse('<div :a=b />')
  1062. const directive = (ast.children[0] as ElementNode).props[0]
  1063. expect(directive).toStrictEqual({
  1064. type: NodeTypes.DIRECTIVE,
  1065. name: 'bind',
  1066. arg: {
  1067. type: NodeTypes.SIMPLE_EXPRESSION,
  1068. content: 'a',
  1069. isStatic: true,
  1070. isConstant: true,
  1071. loc: {
  1072. source: 'a',
  1073. start: {
  1074. column: 7,
  1075. line: 1,
  1076. offset: 6
  1077. },
  1078. end: {
  1079. column: 8,
  1080. line: 1,
  1081. offset: 7
  1082. }
  1083. }
  1084. },
  1085. modifiers: [],
  1086. exp: {
  1087. type: NodeTypes.SIMPLE_EXPRESSION,
  1088. content: 'b',
  1089. isStatic: false,
  1090. isConstant: false,
  1091. loc: {
  1092. start: { offset: 8, line: 1, column: 9 },
  1093. end: { offset: 9, line: 1, column: 10 },
  1094. source: 'b'
  1095. }
  1096. },
  1097. loc: {
  1098. start: { offset: 5, line: 1, column: 6 },
  1099. end: { offset: 9, line: 1, column: 10 },
  1100. source: ':a=b'
  1101. }
  1102. })
  1103. })
  1104. test('v-bind shorthand with modifier', () => {
  1105. const ast = parse('<div :a.sync=b />')
  1106. const directive = (ast.children[0] as ElementNode).props[0]
  1107. expect(directive).toStrictEqual({
  1108. type: NodeTypes.DIRECTIVE,
  1109. name: 'bind',
  1110. arg: {
  1111. type: NodeTypes.SIMPLE_EXPRESSION,
  1112. content: 'a',
  1113. isStatic: true,
  1114. isConstant: true,
  1115. loc: {
  1116. source: 'a',
  1117. start: {
  1118. column: 7,
  1119. line: 1,
  1120. offset: 6
  1121. },
  1122. end: {
  1123. column: 8,
  1124. line: 1,
  1125. offset: 7
  1126. }
  1127. }
  1128. },
  1129. modifiers: ['sync'],
  1130. exp: {
  1131. type: NodeTypes.SIMPLE_EXPRESSION,
  1132. content: 'b',
  1133. isStatic: false,
  1134. isConstant: false,
  1135. loc: {
  1136. start: { offset: 13, line: 1, column: 14 },
  1137. end: { offset: 14, line: 1, column: 15 },
  1138. source: 'b'
  1139. }
  1140. },
  1141. loc: {
  1142. start: { offset: 5, line: 1, column: 6 },
  1143. end: { offset: 14, line: 1, column: 15 },
  1144. source: ':a.sync=b'
  1145. }
  1146. })
  1147. })
  1148. test('v-on shorthand', () => {
  1149. const ast = parse('<div @a=b />')
  1150. const directive = (ast.children[0] as ElementNode).props[0]
  1151. expect(directive).toStrictEqual({
  1152. type: NodeTypes.DIRECTIVE,
  1153. name: 'on',
  1154. arg: {
  1155. type: NodeTypes.SIMPLE_EXPRESSION,
  1156. content: 'a',
  1157. isStatic: true,
  1158. isConstant: true,
  1159. loc: {
  1160. source: 'a',
  1161. start: {
  1162. column: 7,
  1163. line: 1,
  1164. offset: 6
  1165. },
  1166. end: {
  1167. column: 8,
  1168. line: 1,
  1169. offset: 7
  1170. }
  1171. }
  1172. },
  1173. modifiers: [],
  1174. exp: {
  1175. type: NodeTypes.SIMPLE_EXPRESSION,
  1176. content: 'b',
  1177. isStatic: false,
  1178. isConstant: false,
  1179. loc: {
  1180. start: { offset: 8, line: 1, column: 9 },
  1181. end: { offset: 9, line: 1, column: 10 },
  1182. source: 'b'
  1183. }
  1184. },
  1185. loc: {
  1186. start: { offset: 5, line: 1, column: 6 },
  1187. end: { offset: 9, line: 1, column: 10 },
  1188. source: '@a=b'
  1189. }
  1190. })
  1191. })
  1192. test('v-on shorthand with modifier', () => {
  1193. const ast = parse('<div @a.enter=b />')
  1194. const directive = (ast.children[0] as ElementNode).props[0]
  1195. expect(directive).toStrictEqual({
  1196. type: NodeTypes.DIRECTIVE,
  1197. name: 'on',
  1198. arg: {
  1199. type: NodeTypes.SIMPLE_EXPRESSION,
  1200. content: 'a',
  1201. isStatic: true,
  1202. isConstant: true,
  1203. loc: {
  1204. source: 'a',
  1205. start: {
  1206. column: 7,
  1207. line: 1,
  1208. offset: 6
  1209. },
  1210. end: {
  1211. column: 8,
  1212. line: 1,
  1213. offset: 7
  1214. }
  1215. }
  1216. },
  1217. modifiers: ['enter'],
  1218. exp: {
  1219. type: NodeTypes.SIMPLE_EXPRESSION,
  1220. content: 'b',
  1221. isStatic: false,
  1222. isConstant: false,
  1223. loc: {
  1224. start: { offset: 14, line: 1, column: 15 },
  1225. end: { offset: 15, line: 1, column: 16 },
  1226. source: 'b'
  1227. }
  1228. },
  1229. loc: {
  1230. start: { offset: 5, line: 1, column: 6 },
  1231. end: { offset: 15, line: 1, column: 16 },
  1232. source: '@a.enter=b'
  1233. }
  1234. })
  1235. })
  1236. test('v-slot shorthand', () => {
  1237. const ast = parse('<Comp #a="{ b }" />')
  1238. const directive = (ast.children[0] as ElementNode).props[0]
  1239. expect(directive).toStrictEqual({
  1240. type: NodeTypes.DIRECTIVE,
  1241. name: 'slot',
  1242. arg: {
  1243. type: NodeTypes.SIMPLE_EXPRESSION,
  1244. content: 'a',
  1245. isStatic: true,
  1246. isConstant: true,
  1247. loc: {
  1248. source: 'a',
  1249. start: {
  1250. column: 8,
  1251. line: 1,
  1252. offset: 7
  1253. },
  1254. end: {
  1255. column: 9,
  1256. line: 1,
  1257. offset: 8
  1258. }
  1259. }
  1260. },
  1261. modifiers: [],
  1262. exp: {
  1263. type: NodeTypes.SIMPLE_EXPRESSION,
  1264. content: '{ b }',
  1265. isStatic: false,
  1266. // The `isConstant` is the default value and will be determined in transformExpression
  1267. isConstant: false,
  1268. loc: {
  1269. start: { offset: 10, line: 1, column: 11 },
  1270. end: { offset: 15, line: 1, column: 16 },
  1271. source: '{ b }'
  1272. }
  1273. },
  1274. loc: {
  1275. start: { offset: 6, line: 1, column: 7 },
  1276. end: { offset: 16, line: 1, column: 17 },
  1277. source: '#a="{ b }"'
  1278. }
  1279. })
  1280. })
  1281. test('v-pre', () => {
  1282. const ast = parse(
  1283. `<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
  1284. `<div :id="foo"><Comp/>{{ bar }}</div>`
  1285. )
  1286. const divWithPre = ast.children[0] as ElementNode
  1287. expect(divWithPre.props).toMatchObject([
  1288. {
  1289. type: NodeTypes.ATTRIBUTE,
  1290. name: `:id`,
  1291. value: {
  1292. type: NodeTypes.TEXT,
  1293. content: `foo`
  1294. },
  1295. loc: {
  1296. source: `:id="foo"`,
  1297. start: {
  1298. line: 1,
  1299. column: 12
  1300. },
  1301. end: {
  1302. line: 1,
  1303. column: 21
  1304. }
  1305. }
  1306. }
  1307. ])
  1308. expect(divWithPre.children[0]).toMatchObject({
  1309. type: NodeTypes.ELEMENT,
  1310. tagType: ElementTypes.ELEMENT,
  1311. tag: `Comp`
  1312. })
  1313. expect(divWithPre.children[1]).toMatchObject({
  1314. type: NodeTypes.TEXT,
  1315. content: `{{ bar }}`
  1316. })
  1317. // should not affect siblings after it
  1318. const divWithoutPre = ast.children[1] as ElementNode
  1319. expect(divWithoutPre.props).toMatchObject([
  1320. {
  1321. type: NodeTypes.DIRECTIVE,
  1322. name: `bind`,
  1323. arg: {
  1324. type: NodeTypes.SIMPLE_EXPRESSION,
  1325. isStatic: true,
  1326. content: `id`
  1327. },
  1328. exp: {
  1329. type: NodeTypes.SIMPLE_EXPRESSION,
  1330. isStatic: false,
  1331. content: `foo`
  1332. },
  1333. loc: {
  1334. source: `:id="foo"`,
  1335. start: {
  1336. line: 2,
  1337. column: 6
  1338. },
  1339. end: {
  1340. line: 2,
  1341. column: 15
  1342. }
  1343. }
  1344. }
  1345. ])
  1346. expect(divWithoutPre.children[0]).toMatchObject({
  1347. type: NodeTypes.ELEMENT,
  1348. tagType: ElementTypes.COMPONENT,
  1349. tag: `Comp`
  1350. })
  1351. expect(divWithoutPre.children[1]).toMatchObject({
  1352. type: NodeTypes.INTERPOLATION,
  1353. content: {
  1354. type: NodeTypes.SIMPLE_EXPRESSION,
  1355. content: `bar`,
  1356. isStatic: false
  1357. }
  1358. })
  1359. })
  1360. test('end tags are case-insensitive.', () => {
  1361. const ast = parse('<div>hello</DIV>after')
  1362. const element = ast.children[0] as ElementNode
  1363. const text = element.children[0] as TextNode
  1364. expect(text).toStrictEqual({
  1365. type: NodeTypes.TEXT,
  1366. content: 'hello',
  1367. isEmpty: false,
  1368. loc: {
  1369. start: { offset: 5, line: 1, column: 6 },
  1370. end: { offset: 10, line: 1, column: 11 },
  1371. source: 'hello'
  1372. }
  1373. })
  1374. })
  1375. })
  1376. test('self closing single tag', () => {
  1377. const ast = parse('<div :class="{ some: condition }" />')
  1378. expect(ast.children).toHaveLength(1)
  1379. expect(ast.children[0]).toMatchObject({ tag: 'div' })
  1380. })
  1381. test('self closing multiple tag', () => {
  1382. const ast = parse(
  1383. `<div :class="{ some: condition }" />\n` +
  1384. `<p v-bind:style="{ color: 'red' }"/>`
  1385. )
  1386. expect(ast).toMatchSnapshot()
  1387. expect(ast.children).toHaveLength(2)
  1388. expect(ast.children[0]).toMatchObject({ tag: 'div' })
  1389. expect(ast.children[1]).toMatchObject({ tag: 'p' })
  1390. })
  1391. test('valid html', () => {
  1392. const ast = parse(
  1393. `<div :class="{ some: condition }">\n` +
  1394. ` <p v-bind:style="{ color: 'red' }"/>\n` +
  1395. ` <!-- a comment with <html> inside it -->\n` +
  1396. `</div>`
  1397. )
  1398. expect(ast).toMatchSnapshot()
  1399. expect(ast.children).toHaveLength(1)
  1400. const el = ast.children[0] as any
  1401. expect(el).toMatchObject({
  1402. tag: 'div'
  1403. })
  1404. expect(el.children).toHaveLength(2)
  1405. expect(el.children[0]).toMatchObject({
  1406. tag: 'p'
  1407. })
  1408. expect(el.children[1]).toMatchObject({
  1409. type: NodeTypes.COMMENT
  1410. })
  1411. })
  1412. test('invalid html', () => {
  1413. expect(() => {
  1414. parse(`<div>\n<span>\n</div>\n</span>`)
  1415. }).toThrow('End tag was not found. (3:1)')
  1416. const spy = jest.fn()
  1417. const ast = parse(`<div>\n<span>\n</div>\n</span>`, {
  1418. onError: spy
  1419. })
  1420. expect(spy.mock.calls).toMatchObject([
  1421. [
  1422. {
  1423. code: ErrorCodes.X_MISSING_END_TAG,
  1424. loc: {
  1425. start: {
  1426. offset: 13,
  1427. line: 3,
  1428. column: 1
  1429. }
  1430. }
  1431. }
  1432. ],
  1433. [
  1434. {
  1435. code: ErrorCodes.X_INVALID_END_TAG,
  1436. loc: {
  1437. start: {
  1438. offset: 20,
  1439. line: 4,
  1440. column: 1
  1441. }
  1442. }
  1443. }
  1444. ]
  1445. ])
  1446. expect(ast).toMatchSnapshot()
  1447. })
  1448. test('parse with correct location info', () => {
  1449. const [foo, bar, but, baz] = parse(
  1450. `
  1451. foo
  1452. is {{ bar }} but {{ baz }}`.trim()
  1453. ).children
  1454. let offset = 0
  1455. expect(foo.loc.start).toEqual({ line: 1, column: 1, offset })
  1456. offset += foo.loc.source.length
  1457. expect(foo.loc.end).toEqual({ line: 2, column: 5, offset })
  1458. expect(bar.loc.start).toEqual({ line: 2, column: 5, offset })
  1459. const barInner = (bar as InterpolationNode).content
  1460. offset += 3
  1461. expect(barInner.loc.start).toEqual({ line: 2, column: 8, offset })
  1462. offset += barInner.loc.source.length
  1463. expect(barInner.loc.end).toEqual({ line: 2, column: 11, offset })
  1464. offset += 3
  1465. expect(bar.loc.end).toEqual({ line: 2, column: 14, offset })
  1466. expect(but.loc.start).toEqual({ line: 2, column: 14, offset })
  1467. offset += but.loc.source.length
  1468. expect(but.loc.end).toEqual({ line: 2, column: 19, offset })
  1469. expect(baz.loc.start).toEqual({ line: 2, column: 19, offset })
  1470. const bazInner = (baz as InterpolationNode).content
  1471. offset += 3
  1472. expect(bazInner.loc.start).toEqual({ line: 2, column: 22, offset })
  1473. offset += bazInner.loc.source.length
  1474. expect(bazInner.loc.end).toEqual({ line: 2, column: 25, offset })
  1475. offset += 3
  1476. expect(baz.loc.end).toEqual({ line: 2, column: 28, offset })
  1477. })
  1478. describe('namedCharacterReferences option', () => {
  1479. test('use the given map', () => {
  1480. const ast: any = parse('&amp;&cups;', {
  1481. namedCharacterReferences: {
  1482. 'cups;': '\u222A\uFE00' // UNION with serifs
  1483. },
  1484. onError: () => {} // Ignore errors
  1485. })
  1486. expect(ast.children.length).toBe(1)
  1487. expect(ast.children[0].type).toBe(NodeTypes.TEXT)
  1488. expect(ast.children[0].content).toBe('&amp;\u222A\uFE00')
  1489. })
  1490. })
  1491. describe('Errors', () => {
  1492. const patterns: {
  1493. [key: string]: Array<{
  1494. code: string
  1495. errors: Array<{ type: ErrorCodes; loc: Position }>
  1496. options?: Partial<ParserOptions>
  1497. }>
  1498. } = {
  1499. ABRUPT_CLOSING_OF_EMPTY_COMMENT: [
  1500. {
  1501. code: '<template><!--></template>',
  1502. errors: [
  1503. {
  1504. type: ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT,
  1505. loc: { offset: 10, line: 1, column: 11 }
  1506. }
  1507. ]
  1508. },
  1509. {
  1510. code: '<template><!---></template>',
  1511. errors: [
  1512. {
  1513. type: ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT,
  1514. loc: { offset: 10, line: 1, column: 11 }
  1515. }
  1516. ]
  1517. },
  1518. {
  1519. code: '<template><!----></template>',
  1520. errors: []
  1521. }
  1522. ],
  1523. ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE: [
  1524. {
  1525. code: '<template>&#a;</template>',
  1526. errors: [
  1527. {
  1528. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1529. loc: { offset: 10, line: 1, column: 11 }
  1530. }
  1531. ]
  1532. },
  1533. {
  1534. code: '<template>&#xg;</template>',
  1535. errors: [
  1536. {
  1537. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1538. loc: { offset: 10, line: 1, column: 11 }
  1539. }
  1540. ]
  1541. },
  1542. {
  1543. code: '<template>&#99;</template>',
  1544. errors: []
  1545. },
  1546. {
  1547. code: '<template>&#xff;</template>',
  1548. errors: []
  1549. },
  1550. {
  1551. code: '<template attr="&#a;"></template>',
  1552. errors: [
  1553. {
  1554. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1555. loc: { offset: 16, line: 1, column: 17 }
  1556. }
  1557. ]
  1558. },
  1559. {
  1560. code: '<template attr="&#xg;"></template>',
  1561. errors: [
  1562. {
  1563. type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
  1564. loc: { offset: 16, line: 1, column: 17 }
  1565. }
  1566. ]
  1567. },
  1568. {
  1569. code: '<template attr="&#99;"></template>',
  1570. errors: []
  1571. },
  1572. {
  1573. code: '<template attr="&#xff;"></template>',
  1574. errors: []
  1575. }
  1576. ],
  1577. CDATA_IN_HTML_CONTENT: [
  1578. {
  1579. code: '<template><![CDATA[cdata]]></template>',
  1580. errors: [
  1581. {
  1582. type: ErrorCodes.CDATA_IN_HTML_CONTENT,
  1583. loc: { offset: 10, line: 1, column: 11 }
  1584. }
  1585. ]
  1586. },
  1587. {
  1588. code: '<template><svg><![CDATA[cdata]]></svg></template>',
  1589. errors: []
  1590. }
  1591. ],
  1592. CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE: [
  1593. {
  1594. code: '<template>&#1234567;</template>',
  1595. errors: [
  1596. {
  1597. type: ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
  1598. loc: { offset: 10, line: 1, column: 11 }
  1599. }
  1600. ]
  1601. }
  1602. ],
  1603. CONTROL_CHARACTER_REFERENCE: [
  1604. {
  1605. code: '<template>&#0003;</template>',
  1606. errors: [
  1607. {
  1608. type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  1609. loc: { offset: 10, line: 1, column: 11 }
  1610. }
  1611. ]
  1612. },
  1613. {
  1614. code: '<template>&#x7F;</template>',
  1615. errors: [
  1616. {
  1617. type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
  1618. loc: { offset: 10, line: 1, column: 11 }
  1619. }
  1620. ]
  1621. }
  1622. ],
  1623. DUPLICATE_ATTRIBUTE: [
  1624. {
  1625. code: '<template><div id="" id=""></div></template>',
  1626. errors: [
  1627. {
  1628. type: ErrorCodes.DUPLICATE_ATTRIBUTE,
  1629. loc: { offset: 21, line: 1, column: 22 }
  1630. }
  1631. ]
  1632. }
  1633. ],
  1634. END_TAG_WITH_ATTRIBUTES: [
  1635. {
  1636. code: '<template><div></div id=""></template>',
  1637. errors: [
  1638. {
  1639. type: ErrorCodes.END_TAG_WITH_ATTRIBUTES,
  1640. loc: { offset: 21, line: 1, column: 22 }
  1641. }
  1642. ]
  1643. }
  1644. ],
  1645. END_TAG_WITH_TRAILING_SOLIDUS: [
  1646. {
  1647. code: '<template><div></div/></template>',
  1648. errors: [
  1649. {
  1650. type: ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS,
  1651. loc: { offset: 20, line: 1, column: 21 }
  1652. }
  1653. ]
  1654. }
  1655. ],
  1656. EOF_BEFORE_TAG_NAME: [
  1657. {
  1658. code: '<template><',
  1659. errors: [
  1660. {
  1661. type: ErrorCodes.EOF_BEFORE_TAG_NAME,
  1662. loc: { offset: 11, line: 1, column: 12 }
  1663. },
  1664. {
  1665. type: ErrorCodes.X_MISSING_END_TAG,
  1666. loc: { offset: 11, line: 1, column: 12 }
  1667. }
  1668. ]
  1669. },
  1670. {
  1671. code: '<template></',
  1672. errors: [
  1673. {
  1674. type: ErrorCodes.EOF_BEFORE_TAG_NAME,
  1675. loc: { offset: 12, line: 1, column: 13 }
  1676. },
  1677. {
  1678. type: ErrorCodes.X_MISSING_END_TAG,
  1679. loc: { offset: 12, line: 1, column: 13 }
  1680. }
  1681. ]
  1682. }
  1683. ],
  1684. EOF_IN_CDATA: [
  1685. {
  1686. code: '<template><svg><![CDATA[cdata',
  1687. errors: [
  1688. {
  1689. type: ErrorCodes.EOF_IN_CDATA,
  1690. loc: { offset: 29, line: 1, column: 30 }
  1691. },
  1692. {
  1693. type: ErrorCodes.X_MISSING_END_TAG,
  1694. loc: { offset: 29, line: 1, column: 30 }
  1695. },
  1696. {
  1697. type: ErrorCodes.X_MISSING_END_TAG,
  1698. loc: { offset: 29, line: 1, column: 30 }
  1699. }
  1700. ]
  1701. },
  1702. {
  1703. code: '<template><svg><![CDATA[',
  1704. errors: [
  1705. {
  1706. type: ErrorCodes.EOF_IN_CDATA,
  1707. loc: { offset: 24, line: 1, column: 25 }
  1708. },
  1709. {
  1710. type: ErrorCodes.X_MISSING_END_TAG,
  1711. loc: { offset: 24, line: 1, column: 25 }
  1712. },
  1713. {
  1714. type: ErrorCodes.X_MISSING_END_TAG,
  1715. loc: { offset: 24, line: 1, column: 25 }
  1716. }
  1717. ]
  1718. }
  1719. ],
  1720. EOF_IN_COMMENT: [
  1721. {
  1722. code: '<template><!--comment',
  1723. errors: [
  1724. {
  1725. type: ErrorCodes.EOF_IN_COMMENT,
  1726. loc: { offset: 21, line: 1, column: 22 }
  1727. },
  1728. {
  1729. type: ErrorCodes.X_MISSING_END_TAG,
  1730. loc: { offset: 21, line: 1, column: 22 }
  1731. }
  1732. ]
  1733. },
  1734. {
  1735. code: '<template><!--',
  1736. errors: [
  1737. {
  1738. type: ErrorCodes.EOF_IN_COMMENT,
  1739. loc: { offset: 14, line: 1, column: 15 }
  1740. },
  1741. {
  1742. type: ErrorCodes.X_MISSING_END_TAG,
  1743. loc: { offset: 14, line: 1, column: 15 }
  1744. }
  1745. ]
  1746. },
  1747. // Bogus comments don't throw eof-in-comment error.
  1748. // https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state
  1749. {
  1750. code: '<template><!',
  1751. errors: [
  1752. {
  1753. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1754. loc: { offset: 10, line: 1, column: 11 }
  1755. },
  1756. {
  1757. type: ErrorCodes.X_MISSING_END_TAG,
  1758. loc: { offset: 12, line: 1, column: 13 }
  1759. }
  1760. ]
  1761. },
  1762. {
  1763. code: '<template><!-',
  1764. errors: [
  1765. {
  1766. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1767. loc: { offset: 10, line: 1, column: 11 }
  1768. },
  1769. {
  1770. type: ErrorCodes.X_MISSING_END_TAG,
  1771. loc: { offset: 13, line: 1, column: 14 }
  1772. }
  1773. ]
  1774. },
  1775. {
  1776. code: '<template><!abc',
  1777. errors: [
  1778. {
  1779. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  1780. loc: { offset: 10, line: 1, column: 11 }
  1781. },
  1782. {
  1783. type: ErrorCodes.X_MISSING_END_TAG,
  1784. loc: { offset: 15, line: 1, column: 16 }
  1785. }
  1786. ]
  1787. }
  1788. ],
  1789. EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT: [
  1790. {
  1791. code: "<script><!--console.log('hello')",
  1792. errors: [
  1793. {
  1794. type: ErrorCodes.X_MISSING_END_TAG,
  1795. loc: { offset: 32, line: 1, column: 33 }
  1796. },
  1797. {
  1798. type: ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
  1799. loc: { offset: 32, line: 1, column: 33 }
  1800. }
  1801. ]
  1802. },
  1803. {
  1804. code: "<script>console.log('hello')",
  1805. errors: [
  1806. {
  1807. type: ErrorCodes.X_MISSING_END_TAG,
  1808. loc: { offset: 28, line: 1, column: 29 }
  1809. }
  1810. ]
  1811. }
  1812. ],
  1813. EOF_IN_TAG: [
  1814. {
  1815. code: '<template><div',
  1816. errors: [
  1817. {
  1818. type: ErrorCodes.EOF_IN_TAG,
  1819. loc: { offset: 14, line: 1, column: 15 }
  1820. },
  1821. {
  1822. type: ErrorCodes.X_MISSING_END_TAG,
  1823. loc: { offset: 14, line: 1, column: 15 }
  1824. },
  1825. {
  1826. type: ErrorCodes.X_MISSING_END_TAG,
  1827. loc: { offset: 14, line: 1, column: 15 }
  1828. }
  1829. ]
  1830. },
  1831. {
  1832. code: '<template><div ',
  1833. errors: [
  1834. {
  1835. type: ErrorCodes.EOF_IN_TAG,
  1836. loc: { offset: 15, line: 1, column: 16 }
  1837. },
  1838. {
  1839. type: ErrorCodes.X_MISSING_END_TAG,
  1840. loc: { offset: 15, line: 1, column: 16 }
  1841. },
  1842. {
  1843. type: ErrorCodes.X_MISSING_END_TAG,
  1844. loc: { offset: 15, line: 1, column: 16 }
  1845. }
  1846. ]
  1847. },
  1848. {
  1849. code: '<template><div id',
  1850. errors: [
  1851. {
  1852. type: ErrorCodes.EOF_IN_TAG,
  1853. loc: { offset: 17, line: 1, column: 18 }
  1854. },
  1855. {
  1856. type: ErrorCodes.X_MISSING_END_TAG,
  1857. loc: { offset: 17, line: 1, column: 18 }
  1858. },
  1859. {
  1860. type: ErrorCodes.X_MISSING_END_TAG,
  1861. loc: { offset: 17, line: 1, column: 18 }
  1862. }
  1863. ]
  1864. },
  1865. {
  1866. code: '<template><div id ',
  1867. errors: [
  1868. {
  1869. type: ErrorCodes.EOF_IN_TAG,
  1870. loc: { offset: 18, line: 1, column: 19 }
  1871. },
  1872. {
  1873. type: ErrorCodes.X_MISSING_END_TAG,
  1874. loc: { offset: 18, line: 1, column: 19 }
  1875. },
  1876. {
  1877. type: ErrorCodes.X_MISSING_END_TAG,
  1878. loc: { offset: 18, line: 1, column: 19 }
  1879. }
  1880. ]
  1881. },
  1882. {
  1883. code: '<template><div id =',
  1884. errors: [
  1885. {
  1886. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  1887. loc: { offset: 19, line: 1, column: 20 }
  1888. },
  1889. {
  1890. type: ErrorCodes.EOF_IN_TAG,
  1891. loc: { offset: 19, line: 1, column: 20 }
  1892. },
  1893. {
  1894. type: ErrorCodes.X_MISSING_END_TAG,
  1895. loc: { offset: 19, line: 1, column: 20 }
  1896. },
  1897. {
  1898. type: ErrorCodes.X_MISSING_END_TAG,
  1899. loc: { offset: 19, line: 1, column: 20 }
  1900. }
  1901. ]
  1902. },
  1903. {
  1904. code: "<template><div id='abc",
  1905. errors: [
  1906. {
  1907. type: ErrorCodes.EOF_IN_TAG,
  1908. loc: { offset: 22, line: 1, column: 23 }
  1909. },
  1910. {
  1911. type: ErrorCodes.X_MISSING_END_TAG,
  1912. loc: { offset: 22, line: 1, column: 23 }
  1913. },
  1914. {
  1915. type: ErrorCodes.X_MISSING_END_TAG,
  1916. loc: { offset: 22, line: 1, column: 23 }
  1917. }
  1918. ]
  1919. },
  1920. {
  1921. code: '<template><div id="abc',
  1922. errors: [
  1923. {
  1924. type: ErrorCodes.EOF_IN_TAG,
  1925. loc: { offset: 22, line: 1, column: 23 }
  1926. },
  1927. {
  1928. type: ErrorCodes.X_MISSING_END_TAG,
  1929. loc: { offset: 22, line: 1, column: 23 }
  1930. },
  1931. {
  1932. type: ErrorCodes.X_MISSING_END_TAG,
  1933. loc: { offset: 22, line: 1, column: 23 }
  1934. }
  1935. ]
  1936. },
  1937. {
  1938. code: "<template><div id='abc'",
  1939. errors: [
  1940. {
  1941. type: ErrorCodes.EOF_IN_TAG,
  1942. loc: { offset: 23, line: 1, column: 24 }
  1943. },
  1944. {
  1945. type: ErrorCodes.X_MISSING_END_TAG,
  1946. loc: { offset: 23, line: 1, column: 24 }
  1947. },
  1948. {
  1949. type: ErrorCodes.X_MISSING_END_TAG,
  1950. loc: { offset: 23, line: 1, column: 24 }
  1951. }
  1952. ]
  1953. },
  1954. {
  1955. code: '<template><div id="abc"',
  1956. errors: [
  1957. {
  1958. type: ErrorCodes.EOF_IN_TAG,
  1959. loc: { offset: 23, line: 1, column: 24 }
  1960. },
  1961. {
  1962. type: ErrorCodes.X_MISSING_END_TAG,
  1963. loc: { offset: 23, line: 1, column: 24 }
  1964. },
  1965. {
  1966. type: ErrorCodes.X_MISSING_END_TAG,
  1967. loc: { offset: 23, line: 1, column: 24 }
  1968. }
  1969. ]
  1970. },
  1971. {
  1972. code: '<template><div id=abc',
  1973. errors: [
  1974. {
  1975. type: ErrorCodes.EOF_IN_TAG,
  1976. loc: { offset: 21, line: 1, column: 22 }
  1977. },
  1978. {
  1979. type: ErrorCodes.X_MISSING_END_TAG,
  1980. loc: { offset: 21, line: 1, column: 22 }
  1981. },
  1982. {
  1983. type: ErrorCodes.X_MISSING_END_TAG,
  1984. loc: { offset: 21, line: 1, column: 22 }
  1985. }
  1986. ]
  1987. },
  1988. {
  1989. code: "<template><div id='abc'/",
  1990. errors: [
  1991. {
  1992. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  1993. loc: { offset: 23, line: 1, column: 24 }
  1994. },
  1995. {
  1996. type: ErrorCodes.EOF_IN_TAG,
  1997. loc: { offset: 24, line: 1, column: 25 }
  1998. },
  1999. {
  2000. type: ErrorCodes.X_MISSING_END_TAG,
  2001. loc: { offset: 24, line: 1, column: 25 }
  2002. },
  2003. {
  2004. type: ErrorCodes.X_MISSING_END_TAG,
  2005. loc: { offset: 24, line: 1, column: 25 }
  2006. }
  2007. ]
  2008. },
  2009. {
  2010. code: '<template><div id="abc"/',
  2011. errors: [
  2012. {
  2013. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2014. loc: { offset: 23, line: 1, column: 24 }
  2015. },
  2016. {
  2017. type: ErrorCodes.EOF_IN_TAG,
  2018. loc: { offset: 24, line: 1, column: 25 }
  2019. },
  2020. {
  2021. type: ErrorCodes.X_MISSING_END_TAG,
  2022. loc: { offset: 24, line: 1, column: 25 }
  2023. },
  2024. {
  2025. type: ErrorCodes.X_MISSING_END_TAG,
  2026. loc: { offset: 24, line: 1, column: 25 }
  2027. }
  2028. ]
  2029. },
  2030. {
  2031. code: '<template><div id=abc /',
  2032. errors: [
  2033. {
  2034. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2035. loc: { offset: 22, line: 1, column: 23 }
  2036. },
  2037. {
  2038. type: ErrorCodes.EOF_IN_TAG,
  2039. loc: { offset: 23, line: 1, column: 24 }
  2040. },
  2041. {
  2042. type: ErrorCodes.X_MISSING_END_TAG,
  2043. loc: { offset: 23, line: 1, column: 24 }
  2044. },
  2045. {
  2046. type: ErrorCodes.X_MISSING_END_TAG,
  2047. loc: { offset: 23, line: 1, column: 24 }
  2048. }
  2049. ]
  2050. }
  2051. ],
  2052. INCORRECTLY_CLOSED_COMMENT: [
  2053. {
  2054. code: '<template><!--comment--!></template>',
  2055. errors: [
  2056. {
  2057. type: ErrorCodes.INCORRECTLY_CLOSED_COMMENT,
  2058. loc: { offset: 10, line: 1, column: 11 }
  2059. }
  2060. ]
  2061. }
  2062. ],
  2063. INCORRECTLY_OPENED_COMMENT: [
  2064. {
  2065. code: '<template><!></template>',
  2066. errors: [
  2067. {
  2068. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2069. loc: { offset: 10, line: 1, column: 11 }
  2070. }
  2071. ]
  2072. },
  2073. {
  2074. code: '<template><!-></template>',
  2075. errors: [
  2076. {
  2077. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2078. loc: { offset: 10, line: 1, column: 11 }
  2079. }
  2080. ]
  2081. },
  2082. {
  2083. code: '<template><!ELEMENT br EMPTY></template>',
  2084. errors: [
  2085. {
  2086. type: ErrorCodes.INCORRECTLY_OPENED_COMMENT,
  2087. loc: { offset: 10, line: 1, column: 11 }
  2088. }
  2089. ]
  2090. },
  2091. // Just ignore doctype.
  2092. {
  2093. code: '<!DOCTYPE html>',
  2094. errors: []
  2095. }
  2096. ],
  2097. INVALID_FIRST_CHARACTER_OF_TAG_NAME: [
  2098. {
  2099. code: '<template>a < b</template>',
  2100. errors: [
  2101. {
  2102. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2103. loc: { offset: 13, line: 1, column: 14 }
  2104. }
  2105. ]
  2106. },
  2107. {
  2108. code: '<template><�></template>',
  2109. errors: [
  2110. {
  2111. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2112. loc: { offset: 11, line: 1, column: 12 }
  2113. }
  2114. ]
  2115. },
  2116. {
  2117. code: '<template>a </ b</template>',
  2118. errors: [
  2119. {
  2120. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2121. loc: { offset: 14, line: 1, column: 15 }
  2122. },
  2123. {
  2124. type: ErrorCodes.X_MISSING_END_TAG,
  2125. loc: { offset: 27, line: 1, column: 28 }
  2126. }
  2127. ]
  2128. },
  2129. {
  2130. code: '<template></�></template>',
  2131. errors: [
  2132. {
  2133. type: ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  2134. loc: { offset: 12, line: 1, column: 13 }
  2135. }
  2136. ]
  2137. },
  2138. // Don't throw invalid-first-character-of-tag-name in interpolation
  2139. {
  2140. code: '<template>{{a < b}}</template>',
  2141. errors: []
  2142. }
  2143. ],
  2144. MISSING_ATTRIBUTE_VALUE: [
  2145. {
  2146. code: '<template><div id=></div></template>',
  2147. errors: [
  2148. {
  2149. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  2150. loc: { offset: 18, line: 1, column: 19 }
  2151. }
  2152. ]
  2153. },
  2154. {
  2155. code: '<template><div id= ></div></template>',
  2156. errors: [
  2157. {
  2158. type: ErrorCodes.MISSING_ATTRIBUTE_VALUE,
  2159. loc: { offset: 19, line: 1, column: 20 }
  2160. }
  2161. ]
  2162. },
  2163. {
  2164. code: '<template><div id= /></div></template>',
  2165. errors: []
  2166. }
  2167. ],
  2168. MISSING_END_TAG_NAME: [
  2169. {
  2170. code: '<template></></template>',
  2171. errors: [
  2172. {
  2173. type: ErrorCodes.MISSING_END_TAG_NAME,
  2174. loc: { offset: 12, line: 1, column: 13 }
  2175. }
  2176. ]
  2177. }
  2178. ],
  2179. MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE: [
  2180. {
  2181. code: '<template>&amp</template>',
  2182. options: { namedCharacterReferences: { amp: '&' } },
  2183. errors: [
  2184. {
  2185. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2186. loc: { offset: 14, line: 1, column: 15 }
  2187. }
  2188. ]
  2189. },
  2190. {
  2191. code: '<template>&#40</template>',
  2192. errors: [
  2193. {
  2194. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2195. loc: { offset: 14, line: 1, column: 15 }
  2196. }
  2197. ]
  2198. },
  2199. {
  2200. code: '<template>&#x40</template>',
  2201. errors: [
  2202. {
  2203. type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
  2204. loc: { offset: 15, line: 1, column: 16 }
  2205. }
  2206. ]
  2207. }
  2208. ],
  2209. MISSING_WHITESPACE_BETWEEN_ATTRIBUTES: [
  2210. {
  2211. code: '<template><div id="foo"class="bar"></div></template>',
  2212. errors: [
  2213. {
  2214. type: ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
  2215. loc: { offset: 23, line: 1, column: 24 }
  2216. }
  2217. ]
  2218. },
  2219. // CR doesn't appear in tokenization phase, but all CR are removed in preprocessing.
  2220. // https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
  2221. {
  2222. code: '<template><div id="foo"\r\nclass="bar"></div></template>',
  2223. errors: []
  2224. }
  2225. ],
  2226. NESTED_COMMENT: [
  2227. {
  2228. code: '<template><!--a<!--b--></template>',
  2229. errors: [
  2230. {
  2231. type: ErrorCodes.NESTED_COMMENT,
  2232. loc: { offset: 15, line: 1, column: 16 }
  2233. }
  2234. ]
  2235. },
  2236. {
  2237. code: '<template><!--a<!--b<!--c--></template>',
  2238. errors: [
  2239. {
  2240. type: ErrorCodes.NESTED_COMMENT,
  2241. loc: { offset: 15, line: 1, column: 16 }
  2242. },
  2243. {
  2244. type: ErrorCodes.NESTED_COMMENT,
  2245. loc: { offset: 20, line: 1, column: 21 }
  2246. }
  2247. ]
  2248. },
  2249. {
  2250. code: '<template><!--a<!--></template>',
  2251. errors: []
  2252. },
  2253. {
  2254. code: '<template><!--a<!--',
  2255. errors: [
  2256. {
  2257. type: ErrorCodes.EOF_IN_COMMENT,
  2258. loc: { offset: 19, line: 1, column: 20 }
  2259. },
  2260. {
  2261. type: ErrorCodes.X_MISSING_END_TAG,
  2262. loc: { offset: 19, line: 1, column: 20 }
  2263. }
  2264. ]
  2265. }
  2266. ],
  2267. NONCHARACTER_CHARACTER_REFERENCE: [
  2268. {
  2269. code: '<template>&#xFFFE;</template>',
  2270. errors: [
  2271. {
  2272. type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
  2273. loc: { offset: 10, line: 1, column: 11 }
  2274. }
  2275. ]
  2276. },
  2277. {
  2278. code: '<template>&#x1FFFF;</template>',
  2279. errors: [
  2280. {
  2281. type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
  2282. loc: { offset: 10, line: 1, column: 11 }
  2283. }
  2284. ]
  2285. }
  2286. ],
  2287. NULL_CHARACTER_REFERENCE: [
  2288. {
  2289. code: '<template>&#0000;</template>',
  2290. errors: [
  2291. {
  2292. type: ErrorCodes.NULL_CHARACTER_REFERENCE,
  2293. loc: { offset: 10, line: 1, column: 11 }
  2294. }
  2295. ]
  2296. }
  2297. ],
  2298. SURROGATE_CHARACTER_REFERENCE: [
  2299. {
  2300. code: '<template>&#xD800;</template>',
  2301. errors: [
  2302. {
  2303. type: ErrorCodes.SURROGATE_CHARACTER_REFERENCE,
  2304. loc: { offset: 10, line: 1, column: 11 }
  2305. }
  2306. ]
  2307. }
  2308. ],
  2309. UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME: [
  2310. {
  2311. code: "<template><div a\"bc=''></div></template>",
  2312. errors: [
  2313. {
  2314. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2315. loc: { offset: 16, line: 1, column: 17 }
  2316. }
  2317. ]
  2318. },
  2319. {
  2320. code: "<template><div a'bc=''></div></template>",
  2321. errors: [
  2322. {
  2323. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2324. loc: { offset: 16, line: 1, column: 17 }
  2325. }
  2326. ]
  2327. },
  2328. {
  2329. code: "<template><div a<bc=''></div></template>",
  2330. errors: [
  2331. {
  2332. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
  2333. loc: { offset: 16, line: 1, column: 17 }
  2334. }
  2335. ]
  2336. }
  2337. ],
  2338. UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE: [
  2339. {
  2340. code: '<template><div foo=bar"></div></template>',
  2341. errors: [
  2342. {
  2343. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2344. loc: { offset: 22, line: 1, column: 23 }
  2345. }
  2346. ]
  2347. },
  2348. {
  2349. code: "<template><div foo=bar'></div></template>",
  2350. errors: [
  2351. {
  2352. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2353. loc: { offset: 22, line: 1, column: 23 }
  2354. }
  2355. ]
  2356. },
  2357. {
  2358. code: '<template><div foo=bar<div></div></template>',
  2359. errors: [
  2360. {
  2361. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2362. loc: { offset: 22, line: 1, column: 23 }
  2363. }
  2364. ]
  2365. },
  2366. {
  2367. code: '<template><div foo=bar=baz></div></template>',
  2368. errors: [
  2369. {
  2370. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2371. loc: { offset: 22, line: 1, column: 23 }
  2372. }
  2373. ]
  2374. },
  2375. {
  2376. code: '<template><div foo=bar`></div></template>',
  2377. errors: [
  2378. {
  2379. type: ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
  2380. loc: { offset: 22, line: 1, column: 23 }
  2381. }
  2382. ]
  2383. }
  2384. ],
  2385. UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME: [
  2386. {
  2387. code: '<template><div =foo=bar></div></template>',
  2388. errors: [
  2389. {
  2390. type: ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
  2391. loc: { offset: 15, line: 1, column: 16 }
  2392. }
  2393. ]
  2394. },
  2395. {
  2396. code: '<template><div =></div></template>',
  2397. errors: [
  2398. {
  2399. type: ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
  2400. loc: { offset: 15, line: 1, column: 16 }
  2401. }
  2402. ]
  2403. }
  2404. ],
  2405. UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME: [
  2406. {
  2407. code: '<template><?xml?></template>',
  2408. errors: [
  2409. {
  2410. type: ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
  2411. loc: { offset: 11, line: 1, column: 12 }
  2412. }
  2413. ]
  2414. }
  2415. ],
  2416. UNEXPECTED_SOLIDUS_IN_TAG: [
  2417. {
  2418. code: '<template><div a/b></div></template>',
  2419. errors: [
  2420. {
  2421. type: ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG,
  2422. loc: { offset: 16, line: 1, column: 17 }
  2423. }
  2424. ]
  2425. }
  2426. ],
  2427. UNKNOWN_NAMED_CHARACTER_REFERENCE: [
  2428. {
  2429. code: '<template>&unknown;</template>',
  2430. errors: [
  2431. {
  2432. type: ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE,
  2433. loc: { offset: 10, line: 1, column: 11 }
  2434. }
  2435. ]
  2436. }
  2437. ],
  2438. X_INVALID_END_TAG: [
  2439. {
  2440. code: '<template></div></template>',
  2441. errors: [
  2442. {
  2443. type: ErrorCodes.X_INVALID_END_TAG,
  2444. loc: { offset: 10, line: 1, column: 11 }
  2445. }
  2446. ]
  2447. },
  2448. {
  2449. code: '<template></div></div></template>',
  2450. errors: [
  2451. {
  2452. type: ErrorCodes.X_INVALID_END_TAG,
  2453. loc: { offset: 10, line: 1, column: 11 }
  2454. },
  2455. {
  2456. type: ErrorCodes.X_INVALID_END_TAG,
  2457. loc: { offset: 16, line: 1, column: 17 }
  2458. }
  2459. ]
  2460. },
  2461. {
  2462. code: "<template>{{'</div>'}}</template>",
  2463. errors: []
  2464. },
  2465. {
  2466. code: '<textarea></div></textarea>',
  2467. errors: []
  2468. },
  2469. {
  2470. code: '<svg><![CDATA[</div>]]></svg>',
  2471. errors: []
  2472. },
  2473. {
  2474. code: '<svg><!--</div>--></svg>',
  2475. errors: []
  2476. }
  2477. ],
  2478. X_MISSING_END_TAG: [
  2479. {
  2480. code: '<template><div></template>',
  2481. errors: [
  2482. {
  2483. type: ErrorCodes.X_MISSING_END_TAG,
  2484. loc: { offset: 15, line: 1, column: 16 }
  2485. }
  2486. ]
  2487. },
  2488. {
  2489. code: '<template><div>',
  2490. errors: [
  2491. {
  2492. type: ErrorCodes.X_MISSING_END_TAG,
  2493. loc: { offset: 15, line: 1, column: 16 }
  2494. },
  2495. {
  2496. type: ErrorCodes.X_MISSING_END_TAG,
  2497. loc: { offset: 15, line: 1, column: 16 }
  2498. }
  2499. ]
  2500. }
  2501. ],
  2502. X_MISSING_INTERPOLATION_END: [
  2503. {
  2504. code: '{{ foo',
  2505. errors: [
  2506. {
  2507. type: ErrorCodes.X_MISSING_INTERPOLATION_END,
  2508. loc: { offset: 0, line: 1, column: 1 }
  2509. }
  2510. ]
  2511. },
  2512. {
  2513. code: '{{',
  2514. errors: [
  2515. {
  2516. type: ErrorCodes.X_MISSING_INTERPOLATION_END,
  2517. loc: { offset: 0, line: 1, column: 1 }
  2518. }
  2519. ]
  2520. },
  2521. {
  2522. code: '{{}}',
  2523. errors: []
  2524. }
  2525. ],
  2526. X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END: [
  2527. {
  2528. code: `<div v-foo:[sef fsef] />`,
  2529. errors: [
  2530. {
  2531. type: ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
  2532. loc: { offset: 15, line: 1, column: 16 }
  2533. }
  2534. ]
  2535. }
  2536. ]
  2537. }
  2538. for (const key of Object.keys(patterns) as (keyof (typeof patterns))[]) {
  2539. describe(key, () => {
  2540. for (const { code, errors, options } of patterns[key]) {
  2541. test(
  2542. code.replace(
  2543. /[\r\n]/g,
  2544. c => `\\x0${c.codePointAt(0)!.toString(16)};`
  2545. ),
  2546. () => {
  2547. const spy = jest.fn()
  2548. const ast = parse(code, {
  2549. getNamespace: (tag, parent) => {
  2550. const ns = parent ? parent.ns : Namespaces.HTML
  2551. if (ns === Namespaces.HTML) {
  2552. if (tag === 'svg') {
  2553. return (Namespaces.HTML + 1) as any
  2554. }
  2555. }
  2556. return ns
  2557. },
  2558. getTextMode: tag => {
  2559. if (tag === 'textarea') {
  2560. return TextModes.RCDATA
  2561. }
  2562. if (tag === 'script') {
  2563. return TextModes.RAWTEXT
  2564. }
  2565. return TextModes.DATA
  2566. },
  2567. ...options,
  2568. onError: spy
  2569. })
  2570. expect(
  2571. spy.mock.calls.map(([err]) => ({
  2572. type: err.code,
  2573. loc: err.loc.start
  2574. }))
  2575. ).toMatchObject(errors)
  2576. expect(ast).toMatchSnapshot()
  2577. }
  2578. )
  2579. }
  2580. })
  2581. }
  2582. })
  2583. })