parse.spec.ts 85 KB

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