parse.spec.ts 84 KB

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