parse.spec.ts 75 KB

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