parse.spec.ts 73 KB

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