parse.spec.ts 72 KB

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