parse.spec.ts 94 KB

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