parse.spec.ts 70 KB

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