body-parser-spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. const BodyParser = require('../lib/snippet-body-parser');
  2. function expectMatch (input, tree) {
  3. expect(BodyParser.parse(input)).toEqual(tree);
  4. }
  5. describe("Snippet Body Parser", () => {
  6. it("parses a snippet with no special behavior", () => {
  7. const bodyTree = BodyParser.parse('${} $ n $}1} ${/upcase/} \n world ${||}');
  8. expect(bodyTree).toEqual([
  9. '${} $ n $}1} ${/upcase/} \n world ${||}'
  10. ]);
  11. });
  12. describe('for snippets with variables', () => {
  13. it('parses simple variables', () => {
  14. expectMatch('$f_o_0', [{variable: 'f_o_0'}]);
  15. expectMatch('$_FOO', [{variable: '_FOO'}]);
  16. });
  17. it('parses verbose variables', () => {
  18. expectMatch('${foo}', [{variable: 'foo'}]);
  19. expectMatch('${FOO}', [{variable: 'FOO'}]);
  20. });
  21. it('parses variables with placeholders', () => {
  22. expectMatch(
  23. '${f:placeholder}',
  24. [{variable: 'f', content: ['placeholder']}]
  25. );
  26. expectMatch(
  27. '${f:foo$1 $VAR}',
  28. [
  29. {
  30. variable: 'f',
  31. content: [
  32. 'foo',
  33. {index: 1, content: []},
  34. ' ',
  35. {variable: 'VAR'}
  36. ]
  37. }
  38. ]
  39. );
  40. // Allows a colon as part of the placeholder value.
  41. expectMatch(
  42. '${TM_SELECTED_TEXT:foo:bar}',
  43. [
  44. {
  45. variable: 'TM_SELECTED_TEXT',
  46. content: [
  47. 'foo:bar'
  48. ]
  49. }
  50. ]
  51. );
  52. });
  53. it('parses simple transformations like /upcase', () => {
  54. const bodyTree = BodyParser.parse("lorem ipsum ${CLIPBOARD:/upcase} dolor sit amet");
  55. expectMatch(
  56. "lorem ipsum ${CLIPBOARD:/upcase} dolor sit amet",
  57. [
  58. "lorem ipsum ",
  59. {
  60. variable: 'CLIPBOARD',
  61. substitution: {flag: 'upcase'}
  62. },
  63. " dolor sit amet"
  64. ]
  65. );
  66. });
  67. it('parses variables with transforms', () => {
  68. expectMatch('${f/.*/$0/}', [
  69. {
  70. variable: 'f',
  71. substitution: {
  72. find: /.*/,
  73. replace: [
  74. {backreference: 0}
  75. ]
  76. }
  77. }
  78. ]);
  79. });
  80. });
  81. describe('for snippets with tabstops', () => {
  82. it('parses simple tabstops', () => {
  83. expectMatch('hello$1world$2', [
  84. 'hello',
  85. {index: 1, content: []},
  86. 'world',
  87. {index: 2, content: []}
  88. ]);
  89. });
  90. it('parses verbose tabstops', () => {
  91. expectMatch('hello${1}world${2}', [
  92. 'hello',
  93. {index: 1, content: []},
  94. 'world',
  95. {index: 2, content: []}
  96. ]);
  97. });
  98. it('skips escaped tabstops', () => {
  99. expectMatch('$1 \\$2 $3 \\\\$4 \\\\\\$5 $6', [
  100. {index: 1, content: []},
  101. ' $2 ',
  102. {index: 3, content: []},
  103. ' \\',
  104. {index: 4, content: []},
  105. ' \\$5 ',
  106. {index: 6, content: []}
  107. ]);
  108. });
  109. describe('for tabstops with placeholders', () => {
  110. it('parses them', () => {
  111. expectMatch('hello${1:placeholder}world', [
  112. 'hello',
  113. {index: 1, content: ['placeholder']},
  114. 'world'
  115. ]);
  116. });
  117. it('allows escaped back braces', () => {
  118. expectMatch('${1:{}}', [
  119. {index: 1, content: ['{']},
  120. '}'
  121. ]);
  122. expectMatch('${1:{\\}}', [
  123. {index: 1, content: ['{}']}
  124. ]);
  125. });
  126. });
  127. it('parses tabstops with transforms', () => {
  128. expectMatch('${1/.*/$0/}', [
  129. {
  130. index: 1,
  131. content: [],
  132. substitution: {
  133. find: /.*/,
  134. replace: [{backreference: 0}]
  135. }
  136. }
  137. ]);
  138. });
  139. it('parses tabstops with choices', () => {
  140. expectMatch('${1|on}e,t\\|wo,th\\,ree|}', [
  141. {index: 1, content: ['on}e'], choice: ['on}e', 't|wo', 'th,ree']}
  142. ]);
  143. });
  144. it('parses if-else syntax', () => {
  145. expectMatch(
  146. '$1 ${1/(?:(wat)|^.*$)$/${1:+hey}/}',
  147. [
  148. {index: 1, content: []},
  149. " ",
  150. {
  151. index: 1,
  152. content: [],
  153. substitution: {
  154. find: /(?:(wat)|^.*$)$/,
  155. replace: [
  156. {
  157. backreference: 1,
  158. iftext: "hey",
  159. elsetext: ""
  160. }
  161. ],
  162. },
  163. },
  164. ]
  165. );
  166. expectMatch(
  167. '$1 ${1/(?:(wat)|^.*$)$/${1:?hey:nah}/}',
  168. [
  169. {index: 1, content: []},
  170. " ",
  171. {
  172. index: 1,
  173. content: [],
  174. substitution: {
  175. find: /(?:(wat)|^.*$)$/,
  176. replace: [
  177. {
  178. backreference: 1,
  179. iftext: "hey",
  180. elsetext: "nah"
  181. }
  182. ],
  183. },
  184. },
  185. ]
  186. );
  187. // else with `:` syntax
  188. expectMatch(
  189. '$1 ${1/(?:(wat)|^.*$)$/${1:fallback}/}',
  190. [
  191. {index: 1, content: []},
  192. " ",
  193. {
  194. index: 1,
  195. content: [],
  196. substitution: {
  197. find: /(?:(wat)|^.*$)$/,
  198. replace: [
  199. {
  200. backreference: 1,
  201. iftext: "",
  202. elsetext: "fallback"
  203. }
  204. ],
  205. },
  206. },
  207. ]
  208. );
  209. // else with `:-` syntax; should be same as above
  210. expectMatch(
  211. '$1 ${1/(?:(wat)|^.*$)$/${1:-fallback}/}',
  212. [
  213. {index: 1, content: []},
  214. " ",
  215. {
  216. index: 1,
  217. content: [],
  218. substitution: {
  219. find: /(?:(wat)|^.*$)$/,
  220. replace: [
  221. {
  222. backreference: 1,
  223. iftext: "",
  224. elsetext: "fallback"
  225. }
  226. ],
  227. },
  228. },
  229. ]
  230. );
  231. });
  232. it('parses alternative if-else syntax', () => {
  233. expectMatch(
  234. '$1 ${1/(?:(wat)|^.*$)$/(?1:hey:)/}',
  235. [
  236. {index: 1, content: []},
  237. " ",
  238. {
  239. index: 1,
  240. content: [],
  241. substitution: {
  242. find: /(?:(wat)|^.*$)$/,
  243. replace: [
  244. {
  245. backreference: 1,
  246. iftext: ["hey"],
  247. elsetext: ""
  248. }
  249. ],
  250. },
  251. },
  252. ]
  253. );
  254. expectMatch(
  255. '$1 ${1/(?:(wat)|^.*$)$/(?1:\\u$1:)/}',
  256. [
  257. {index: 1, content: []},
  258. " ",
  259. {
  260. index: 1,
  261. content: [],
  262. substitution: {
  263. find: /(?:(wat)|^.*$)$/,
  264. replace: [
  265. {
  266. backreference: 1,
  267. iftext: [
  268. {escape: 'u'},
  269. {backreference: 1}
  270. ],
  271. elsetext: ""
  272. }
  273. ],
  274. },
  275. },
  276. ]
  277. );
  278. expectMatch(
  279. '$1 ${1/(?:(wat)|^.*$)$/(?1::hey)/}',
  280. [
  281. {index: 1, content: []},
  282. " ",
  283. {
  284. index: 1,
  285. content: [],
  286. substitution: {
  287. find: /(?:(wat)|^.*$)$/,
  288. replace: [
  289. {
  290. backreference: 1,
  291. iftext: "",
  292. elsetext: ["hey"]
  293. }
  294. ],
  295. },
  296. },
  297. ]
  298. );
  299. expectMatch(
  300. 'class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend',
  301. [
  302. 'class ',
  303. {
  304. index: 1,
  305. content: [
  306. {
  307. variable: 'TM_FILENAME',
  308. substitution: {
  309. find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g,
  310. replace: [
  311. {
  312. backreference: 2,
  313. iftext: '',
  314. elsetext: [
  315. {escape: 'u'},
  316. {backreference: 1}
  317. ]
  318. }
  319. ]
  320. }
  321. }
  322. ]
  323. },
  324. ' < ',
  325. {
  326. index: 2,
  327. content: ['Application']
  328. },
  329. 'Controller\n ',
  330. {index: 3, content : []},
  331. '\nend'
  332. ]
  333. );
  334. });
  335. it('recognizes escape characters in if/else syntax', () => {
  336. expectMatch(
  337. '$1 ${1/(?:(wat)|^.*$)$/${1:?hey\\:hey:nah}/}',
  338. [
  339. {index: 1, content: []},
  340. " ",
  341. {
  342. index: 1,
  343. content: [],
  344. substitution: {
  345. find: /(?:(wat)|^.*$)$/,
  346. replace: [
  347. {
  348. backreference: 1,
  349. iftext: "hey:hey",
  350. elsetext: "nah"
  351. }
  352. ],
  353. },
  354. },
  355. ]
  356. );
  357. expectMatch(
  358. '$1 ${1/(?:(wat)|^.*$)$/${1:?hey:n\\}ah}/}',
  359. [
  360. {index: 1, content: []},
  361. " ",
  362. {
  363. index: 1,
  364. content: [],
  365. substitution: {
  366. find: /(?:(wat)|^.*$)$/,
  367. replace: [
  368. {
  369. backreference: 1,
  370. iftext: "hey",
  371. elsetext: "n}ah"
  372. }
  373. ],
  374. },
  375. },
  376. ]
  377. );
  378. });
  379. it('parses nested tabstops', () => {
  380. expectMatch(
  381. '${1:place${2:hol${3:der}}}',
  382. [
  383. {
  384. index: 1,
  385. content: [
  386. 'place',
  387. {index: 2, content: [
  388. 'hol',
  389. {index: 3, content: ['der']}
  390. ]}
  391. ]
  392. }
  393. ]
  394. );
  395. expectMatch(
  396. '${1:${foo:${1}}}',
  397. [
  398. {
  399. index: 1,
  400. content: [
  401. {
  402. variable: 'foo',
  403. content: [
  404. {
  405. index: 1,
  406. content: []
  407. }
  408. ]
  409. }
  410. ]
  411. }
  412. ]
  413. );
  414. });
  415. });
  416. it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => {
  417. const bodyTree = BodyParser.parse(`\
  418. the quick brown $1fox \${2:jumped \${3:over}
  419. }the \${4:lazy} dog\
  420. `
  421. );
  422. expect(bodyTree).toEqual([
  423. "the quick brown ",
  424. {index: 1, content: []},
  425. "fox ",
  426. {
  427. index: 2,
  428. content: [
  429. "jumped ",
  430. {index: 3, content: ["over"]},
  431. "\n"
  432. ],
  433. },
  434. "the ",
  435. {index: 4, content: ["lazy"]},
  436. " dog"
  437. ]);
  438. });
  439. it('handles a snippet with a transformed variable', () => {
  440. expectMatch(
  441. 'module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/\\u$1/g}}',
  442. [
  443. 'module ',
  444. {
  445. index: 1,
  446. content: [
  447. 'ActiveRecord::',
  448. {
  449. variable: 'TM_FILENAME',
  450. substitution: {
  451. find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g,
  452. replace: [
  453. {escape: 'u'},
  454. {backreference: 1}
  455. ]
  456. }
  457. }
  458. ]
  459. }
  460. ]
  461. );
  462. });
  463. it("skips escaped tabstops", () => {
  464. const bodyTree = BodyParser.parse("snippet $1 escaped \\$2 \\\\$3");
  465. expect(bodyTree).toEqual([
  466. "snippet ",
  467. {
  468. index: 1,
  469. content: []
  470. },
  471. " escaped $2 \\",
  472. {
  473. index: 3,
  474. content: []
  475. }
  476. ]);
  477. });
  478. it("includes escaped right-braces", () => {
  479. const bodyTree = BodyParser.parse("snippet ${1:{\\}}");
  480. expect(bodyTree).toEqual([
  481. "snippet ",
  482. {
  483. index: 1,
  484. content: ["{}"]
  485. }
  486. ]);
  487. });
  488. it("parses a snippet with transformations", () => {
  489. const bodyTree = BodyParser.parse("<${1:p}>$0</${1/f/F/}>");
  490. expect(bodyTree).toEqual([
  491. '<',
  492. {index: 1, content: ['p']},
  493. '>',
  494. {index: 0, content: []},
  495. '</',
  496. {index: 1, content: [], substitution: {find: /f/, replace: ['F']}},
  497. '>'
  498. ]);
  499. });
  500. it("parses a snippet with transformations and a global flag", () => {
  501. const bodyTree = BodyParser.parse("<${1:p}>$0</${1/f/F/g}>");
  502. expect(bodyTree).toEqual([
  503. '<',
  504. {index: 1, content: ['p']},
  505. '>',
  506. {index: 0, content: []},
  507. '</',
  508. {index: 1, content: [], substitution: {find: /f/g, replace: ['F']}},
  509. '>'
  510. ]);
  511. });
  512. it("parses a snippet with multiple tab stops with transformations", () => {
  513. const bodyTree = BodyParser.parse("${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2");
  514. expect(bodyTree).toEqual([
  515. {index: 1, content: ['placeholder']},
  516. ' ',
  517. {
  518. index: 1,
  519. content: [],
  520. substitution: {
  521. find: /(.)/g,
  522. replace: [
  523. {escape: 'u'},
  524. {backreference: 1}
  525. ]
  526. }
  527. },
  528. ' ',
  529. {index: 1, content: []},
  530. ' ',
  531. {index: 2, content: ['ANOTHER']},
  532. ' ',
  533. {
  534. index: 2,
  535. content: [],
  536. substitution: {
  537. find: /^(.*)$/,
  538. replace: [
  539. {escape: 'L'},
  540. {backreference: 1}
  541. ]
  542. }
  543. },
  544. ' ',
  545. {index: 2, content: []},
  546. ]);
  547. });
  548. it("parses a snippet with transformations and mirrors", () => {
  549. const bodyTree = BodyParser.parse("${1:placeholder}\n${1/(.)/\\u$1/g}\n$1");
  550. expect(bodyTree).toEqual([
  551. {index: 1, content: ['placeholder']},
  552. '\n',
  553. {
  554. index: 1,
  555. content: [],
  556. substitution: {
  557. find: /(.)/g,
  558. replace: [
  559. {escape: 'u'},
  560. {backreference: 1}
  561. ]
  562. }
  563. },
  564. '\n',
  565. {index: 1, content: []}
  566. ]);
  567. });
  568. it("parses a snippet with a format string and case-control flags", () => {
  569. const bodyTree = BodyParser.parse("<${1:p}>$0</${1/(.)(.*)/\\u$1$2/g}>");
  570. expect(bodyTree).toEqual([
  571. '<',
  572. {index: 1, content: ['p']},
  573. '>',
  574. {index: 0, content: []},
  575. '</',
  576. {
  577. index: 1,
  578. content: [],
  579. substitution: {
  580. find: /(.)(.*)/g,
  581. replace: [
  582. {escape: 'u'},
  583. {backreference: 1},
  584. {backreference: 2}
  585. ]
  586. }
  587. },
  588. '>'
  589. ]);
  590. });
  591. it("parses a snippet with an escaped forward slash in a transform", () => {
  592. // Annoyingly, a forward slash needs to be double-backslashed just like the
  593. // other escapes.
  594. const bodyTree = BodyParser.parse("<${1:p}>$0</${1/(.)\\/(.*)/\\u$1$2/g}>");
  595. expect(bodyTree).toEqual([
  596. '<',
  597. {index: 1, content: ['p']},
  598. '>',
  599. {index: 0, content: []},
  600. '</',
  601. {
  602. index: 1,
  603. content: [],
  604. substitution: {
  605. find: /(.)\/(.*)/g,
  606. replace: [
  607. {escape: 'u'},
  608. {backreference: 1},
  609. {backreference: 2}
  610. ]
  611. }
  612. },
  613. '>'
  614. ]);
  615. });
  616. it("parses a snippet with a placeholder that mirrors another tab stop's content", () => {
  617. const bodyTree = BodyParser.parse("$4console.${3:log}('${2:$1}', $1);$0");
  618. expect(bodyTree).toEqual([
  619. {index: 4, content: []},
  620. 'console.',
  621. {index: 3, content: ['log']},
  622. '(\'',
  623. {
  624. index: 2, content: [
  625. {index: 1, content: []}
  626. ]
  627. },
  628. '\', ',
  629. {index: 1, content: []},
  630. ');',
  631. {index: 0, content: []}
  632. ]);
  633. });
  634. it("parses a snippet with a placeholder that mixes text and tab stop references", () => {
  635. const bodyTree = BodyParser.parse("$4console.${3:log}('${2:uh $1}', $1);$0");
  636. expect(bodyTree).toEqual([
  637. {index: 4, content: []},
  638. 'console.',
  639. {index: 3, content: ['log']},
  640. '(\'',
  641. {
  642. index: 2, content: [
  643. 'uh ',
  644. {index: 1, content: []}
  645. ]
  646. },
  647. '\', ',
  648. {index: 1, content: []},
  649. ');',
  650. {index: 0, content: []}
  651. ]);
  652. });
  653. });