parse.spec.ts 69 KB

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