parse.spec.ts 72 KB

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