parser.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import { parse, print } from '../dist/esm';
  2. import { equals, shouldThrow } from './utils';
  3. describe('parser', function() {
  4. function astFor(template) {
  5. let ast = parse(template);
  6. return print(ast);
  7. }
  8. it('parses simple mustaches', function() {
  9. equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
  10. equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
  11. equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
  12. equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
  13. equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
  14. equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
  15. equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
  16. equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
  17. equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
  18. });
  19. it('parses simple mustaches with data', function() {
  20. equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
  21. });
  22. it('parses simple mustaches with data paths', function() {
  23. equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
  24. });
  25. it('parses mustaches with paths', function() {
  26. equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
  27. });
  28. it('parses mustaches with this/foo', function() {
  29. equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
  30. });
  31. it('parses mustaches with - in a path', function() {
  32. equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
  33. });
  34. it('parses mustaches with escaped [] in a path', function() {
  35. equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n');
  36. });
  37. it('parses escaped \\\\ in path', function() {
  38. equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n');
  39. });
  40. it('parses mustaches with parameters', function() {
  41. equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
  42. });
  43. it('parses mustaches with string parameters', function() {
  44. equals(astFor('{{foo bar "baz" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
  45. });
  46. it('parses mustaches with NUMBER parameters', function() {
  47. equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
  48. });
  49. it('parses mustaches with BOOLEAN parameters', function() {
  50. equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
  51. equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
  52. });
  53. it('parses mustaches with undefined and null paths', function() {
  54. equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
  55. equals(astFor('{{null}}'), '{{ NULL [] }}\n');
  56. });
  57. it('parses mustaches with undefined and null parameters', function() {
  58. equals(
  59. astFor('{{foo undefined null}}'),
  60. '{{ PATH:foo [UNDEFINED, NULL] }}\n'
  61. );
  62. });
  63. it('parses mustaches with DATA parameters', function() {
  64. equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
  65. });
  66. it('parses mustaches with hash arguments', function() {
  67. equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
  68. equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
  69. equals(
  70. astFor('{{foo bar=true}}'),
  71. '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n'
  72. );
  73. equals(
  74. astFor('{{foo bar=false}}'),
  75. '{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n'
  76. );
  77. equals(
  78. astFor('{{foo bar=@baz}}'),
  79. '{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n'
  80. );
  81. equals(
  82. astFor('{{foo bar=baz bat=bam}}'),
  83. '{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n'
  84. );
  85. equals(
  86. astFor('{{foo bar=baz bat="bam"}}'),
  87. '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'
  88. );
  89. equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
  90. equals(
  91. astFor('{{foo omg bar=baz bat="bam"}}'),
  92. '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'
  93. );
  94. equals(
  95. astFor('{{foo omg bar=baz bat="bam" baz=1}}'),
  96. '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'
  97. );
  98. equals(
  99. astFor('{{foo omg bar=baz bat="bam" baz=true}}'),
  100. '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'
  101. );
  102. equals(
  103. astFor('{{foo omg bar=baz bat="bam" baz=false}}'),
  104. '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'
  105. );
  106. });
  107. it('parses contents followed by a mustache', function() {
  108. equals(
  109. astFor('foo bar {{baz}}'),
  110. "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"
  111. );
  112. });
  113. it('parses a partial', function() {
  114. equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
  115. equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
  116. equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
  117. });
  118. it('parses a partial with context', function() {
  119. equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
  120. });
  121. it('parses a partial with hash', function() {
  122. equals(
  123. astFor('{{> foo bar=bat}}'),
  124. '{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n'
  125. );
  126. });
  127. it('parses a partial with context and hash', function() {
  128. equals(
  129. astFor('{{> foo bar bat=baz}}'),
  130. '{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n'
  131. );
  132. });
  133. it('parses a partial with a complex name', function() {
  134. equals(
  135. astFor('{{> shared/partial?.bar}}'),
  136. '{{> PARTIAL:shared/partial?.bar }}\n'
  137. );
  138. });
  139. it('parsers partial blocks', function() {
  140. equals(
  141. astFor('{{#> foo}}bar{{/foo}}'),
  142. "{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
  143. );
  144. });
  145. it('should handle parser block mismatch', function() {
  146. shouldThrow(
  147. function() {
  148. astFor('{{#> goodbyes}}{{/hellos}}');
  149. },
  150. Error,
  151. /goodbyes doesn't match hellos/
  152. );
  153. });
  154. it('parsers partial blocks with arguments', function() {
  155. equals(
  156. astFor('{{#> foo context hash=value}}bar{{/foo}}'),
  157. "{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
  158. );
  159. });
  160. it('parses a comment', function() {
  161. equals(
  162. astFor('{{! this is a comment }}'),
  163. "{{! ' this is a comment ' }}\n"
  164. );
  165. });
  166. it('parses a multi-line comment', function() {
  167. equals(
  168. astFor('{{!\nthis is a multi-line comment\n}}'),
  169. "{{! '\nthis is a multi-line comment\n' }}\n"
  170. );
  171. });
  172. it('parses an inverse section', function() {
  173. equals(
  174. astFor('{{#foo}} bar {{^}} baz {{/foo}}'),
  175. "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
  176. );
  177. });
  178. it('parses an inverse (else-style) section', function() {
  179. equals(
  180. astFor('{{#foo}} bar {{else}} baz {{/foo}}'),
  181. "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
  182. );
  183. });
  184. it('parses multiple inverse sections', function() {
  185. equals(
  186. astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'),
  187. "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"
  188. );
  189. });
  190. it('parses empty blocks', function() {
  191. equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n');
  192. });
  193. it('parses empty blocks with empty inverse section', function() {
  194. equals(
  195. astFor('{{#foo}}{{^}}{{/foo}}'),
  196. 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
  197. );
  198. });
  199. it('parses empty blocks with empty inverse (else-style) section', function() {
  200. equals(
  201. astFor('{{#foo}}{{else}}{{/foo}}'),
  202. 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
  203. );
  204. });
  205. it('parses non-empty blocks with empty inverse section', function() {
  206. equals(
  207. astFor('{{#foo}} bar {{^}}{{/foo}}'),
  208. "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
  209. );
  210. });
  211. it('parses non-empty blocks with empty inverse (else-style) section', function() {
  212. equals(
  213. astFor('{{#foo}} bar {{else}}{{/foo}}'),
  214. "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
  215. );
  216. });
  217. it('parses empty blocks with non-empty inverse section', function() {
  218. equals(
  219. astFor('{{#foo}}{{^}} bar {{/foo}}'),
  220. "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
  221. );
  222. });
  223. it('parses empty blocks with non-empty inverse (else-style) section', function() {
  224. equals(
  225. astFor('{{#foo}}{{else}} bar {{/foo}}'),
  226. "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
  227. );
  228. });
  229. it('parses a standalone inverse section', function() {
  230. equals(
  231. astFor('{{^foo}}bar{{/foo}}'),
  232. "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"
  233. );
  234. });
  235. it('throws on old inverse section', function() {
  236. shouldThrow(function() {
  237. astFor('{{else foo}}bar{{/foo}}');
  238. }, Error);
  239. });
  240. it('parses block with block params', function() {
  241. equals(
  242. astFor('{{#foo as |bar baz|}}content{{/foo}}'),
  243. "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
  244. );
  245. });
  246. it('parses mustaches with sub-expressions as the callable', function() {
  247. equals(
  248. astFor('{{(my-helper foo)}}'),
  249. '{{ PATH:my-helper [PATH:foo] [] }}\n'
  250. );
  251. });
  252. it('parses mustaches with sub-expressions as the callable (with args)', function() {
  253. equals(
  254. astFor('{{(my-helper foo) bar}}'),
  255. '{{ PATH:my-helper [PATH:foo] [PATH:bar] }}\n'
  256. );
  257. });
  258. it('parses sub-expressions with a sub-expression as the callable', function() {
  259. equals(
  260. astFor('{{((my-helper foo))}}'),
  261. '{{ PATH:my-helper [PATH:foo] [] [] }}\n'
  262. );
  263. });
  264. it('parses sub-expressions with a sub-expression as the callable (with args)', function() {
  265. equals(
  266. astFor('{{((my-helper foo) bar)}}'),
  267. '{{ PATH:my-helper [PATH:foo] [PATH:bar] [] }}\n'
  268. );
  269. });
  270. it('parses arguments with a sub-expression as the callable (with args)', function() {
  271. equals(
  272. astFor('{{my-helper ((foo) bar) baz=((foo bar))}}'),
  273. '{{ PATH:my-helper [PATH:foo [] [PATH:bar]] HASH{baz=PATH:foo [PATH:bar] []} }}\n'
  274. );
  275. });
  276. it('parses inverse block with block params', function() {
  277. equals(
  278. astFor('{{^foo as |bar baz|}}content{{/foo}}'),
  279. "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
  280. );
  281. });
  282. it('parses chained inverse block with block params', function() {
  283. equals(
  284. astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'),
  285. "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
  286. );
  287. });
  288. it("raises if there's a Parse error", function() {
  289. shouldThrow(
  290. function() {
  291. astFor('foo{{^}}bar');
  292. },
  293. Error,
  294. /Parse error on line 1/
  295. );
  296. shouldThrow(
  297. function() {
  298. astFor('{{foo}');
  299. },
  300. Error,
  301. /Parse error on line 1/
  302. );
  303. shouldThrow(
  304. function() {
  305. astFor('{{foo &}}');
  306. },
  307. Error,
  308. /Parse error on line 1/
  309. );
  310. shouldThrow(
  311. function() {
  312. astFor('{{#goodbyes}}{{/hellos}}');
  313. },
  314. Error,
  315. /goodbyes doesn't match hellos/
  316. );
  317. shouldThrow(
  318. function() {
  319. astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
  320. },
  321. Error,
  322. /goodbyes doesn't match hellos/
  323. );
  324. });
  325. it('should handle invalid paths', function() {
  326. shouldThrow(
  327. function() {
  328. astFor('{{foo/../bar}}');
  329. },
  330. Error,
  331. /Invalid path: foo\/\.\. - 1:2/
  332. );
  333. shouldThrow(
  334. function() {
  335. astFor('{{foo/./bar}}');
  336. },
  337. Error,
  338. /Invalid path: foo\/\. - 1:2/
  339. );
  340. shouldThrow(
  341. function() {
  342. astFor('{{foo/this/bar}}');
  343. },
  344. Error,
  345. /Invalid path: foo\/this - 1:2/
  346. );
  347. });
  348. it('knows how to report the correct line number in errors', function() {
  349. shouldThrow(
  350. function() {
  351. astFor('hello\nmy\n{{foo}');
  352. },
  353. Error,
  354. /Parse error on line 3/
  355. );
  356. shouldThrow(
  357. function() {
  358. astFor('hello\n\nmy\n\n{{foo}');
  359. },
  360. Error,
  361. /Parse error on line 5/
  362. );
  363. });
  364. it('knows how to report the correct line number in errors when the first character is a newline', function() {
  365. shouldThrow(
  366. function() {
  367. astFor('\n\nhello\n\nmy\n\n{{foo}');
  368. },
  369. Error,
  370. /Parse error on line 7/
  371. );
  372. });
  373. describe('externally compiled AST', function() {
  374. it('can pass through an already-compiled AST', function() {
  375. equals(
  376. astFor({
  377. type: 'Program',
  378. body: [{ type: 'ContentStatement', value: 'Hello' }]
  379. }),
  380. "CONTENT[ 'Hello' ]\n"
  381. );
  382. });
  383. });
  384. describe('directives', function() {
  385. it('should parse block directives', function() {
  386. equals(
  387. astFor('{{#* foo}}{{/foo}}'),
  388. 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'
  389. );
  390. });
  391. it('should parse directives', function() {
  392. equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
  393. });
  394. it('should fail if directives have inverse', function() {
  395. shouldThrow(
  396. function() {
  397. astFor('{{#* foo}}{{^}}{{/foo}}');
  398. },
  399. Error,
  400. /Unexpected inverse/
  401. );
  402. });
  403. });
  404. it('GH1024 - should track program location properly', function() {
  405. let p = parse(
  406. '\n' +
  407. ' {{#if foo}}\n' +
  408. ' {{bar}}\n' +
  409. ' {{else}} {{baz}}\n' +
  410. '\n' +
  411. ' {{/if}}\n' +
  412. ' '
  413. );
  414. // We really need a deep equals but for now this should be stable...
  415. equals(
  416. JSON.stringify(p.loc),
  417. JSON.stringify({
  418. start: { line: 1, column: 0 },
  419. end: { line: 7, column: 4 }
  420. })
  421. );
  422. equals(
  423. JSON.stringify(p.body[1].program.loc),
  424. JSON.stringify({
  425. start: { line: 2, column: 13 },
  426. end: { line: 4, column: 7 }
  427. })
  428. );
  429. equals(
  430. JSON.stringify(p.body[1].inverse.loc),
  431. JSON.stringify({
  432. start: { line: 4, column: 15 },
  433. end: { line: 6, column: 5 }
  434. })
  435. );
  436. });
  437. });