parse.spec.ts 66 KB

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