Compare commits

..
238 Commits
Author SHA1 Message Date
semantic-release-bot c8de8bcde2 chore(release): 6.4.3 [skip ci]
## [6.4.3](https://github.com/harttle/liquidjs/compare/v6.4.2...v6.4.3) (2019-02-13)

### Bug Fixes

* better index.d.ts and a demo ([2015f68](https://github.com/harttle/liquidjs/commit/2015f68)), closes [#98](https://github.com/harttle/liquidjs/issues/98)
2019-02-13 12:13:27 +00:00
harttle 2015f683b8 fix: better index.d.ts and a demo
The demo is located at demo/typescript/index.ts , working on #98
2019-02-13 20:07:13 +08:00
semantic-release-bot d14f18520b chore(release): 6.4.2 [skip ci]
## [6.4.2](https://github.com/harttle/liquidjs/compare/v6.4.1...v6.4.2) (2019-01-28)

### Bug Fixes

* **CI:** e2e not building cjs ([dde7b3b](https://github.com/harttle/liquidjs/commit/dde7b3b))
2019-01-28 17:34:02 +00:00
Jun Yang c0524a83f3 chore: remove build from e2e, check es5 after published 2019-01-29 01:28:02 +08:00
Jun Yang dde7b3b389 fix(CI): e2e not building cjs 2019-01-29 01:08:52 +08:00
semantic-release-bot 84bbb3432f chore(release): 6.4.1 [skip ci]
## [6.4.1](https://github.com/harttle/liquidjs/compare/v6.4.0...v6.4.1) (2019-01-28)

### Bug Fixes

* regenerator undefined ([a2caeb5](https://github.com/harttle/liquidjs/commit/a2caeb5))
2019-01-28 16:44:56 +00:00
Jun Yang 1c5b934250 chore(CI): update package-lock.json 2019-01-29 00:39:19 +08:00
Jun Yang a2caeb559b fix: regenerator undefined 2019-01-29 00:26:08 +08:00
harttle 81e6861b73 chore(Changelog): add semantic release plugins 2019-01-28 20:33:33 +08:00
harttle 9038d56ce7 chore(Changelog): add @semantic-release/changelog as dev dependency 2019-01-28 20:26:50 +08:00
harttle bad72005fe chore(CI): add @semantic-release/changelog plugin
working on #100
2019-01-28 20:21:16 +08:00
harttle 4dffb27e95 feat(Drop): liquid_method_missing and to_s
close #97
2019-01-28 19:53:04 +08:00
harttle f38ea63c14 chore(CI): mv lint and covereage to test phase 2019-01-28 18:57:25 +08:00
harttle 57bdf17dea chore(CI): downgrade rollup-plugin-babel 2019-01-28 18:46:41 +08:00
harttle 0040485188 chore(CI): skip_cleanup for node_modules 2019-01-28 18:22:20 +08:00
harttle 42d799055a chore(CI): use nodejs_8 for linting 2019-01-28 18:11:25 +08:00
harttle 8a881ac69a style: align to [email protected] 2019-01-28 18:06:47 +08:00
harttle 7a2e036c79 chore(package): update package-lock.json 2019-01-28 17:30:34 +08:00
harttle a31e0c6275 chore(CI): test use build matrix 2019-01-28 17:27:00 +08:00
harttle 7df4d5d62c chore(CI): adjust build matrix 2019-01-28 16:29:59 +08:00
harttle 22b7e55fc9 chore(CI): assign node_js for each script 2019-01-28 15:40:32 +08:00
harttle 0238cf2c44 fix(CI): scirpt for the first stage not defined 2019-01-28 15:26:40 +08:00
harttle 601f2e48fc chore(CI): enable semantic release 2019-01-28 15:18:21 +08:00
harttle 8cb0137362 6.3.0 2019-01-21 15:25:11 +08:00
harttle dee4303fad feature: plugin API, working on #86 2019-01-21 15:24:13 +08:00
harttle 9ba352ef04 6.2.1 2019-01-21 14:54:07 +08:00
harttle 4793a83bd6 fix #101: Slice filter does not work on arrays 2019-01-21 14:52:08 +08:00
harttle af4fe1b330 6.2.0 2019-01-10 13:30:05 +08:00
harttle 5f5c62a51e fix banner link 2019-01-10 13:29:39 +08:00
oott123andJun Yang 21f4d6bf08 export types variable 2019-01-10 13:28:13 +08:00
oott123andJun Yang ba54bec82f feat: add typescript definitions 2019-01-10 13:28:13 +08:00
Jun Yang c2e1bc7055 6.1.1 2018-11-22 09:36:18 +08:00
Andrew BarclayandJun Yang 2055258193 fix assign tag regex to include linebreaks in value 2018-11-22 09:31:22 +08:00
harttle d65ea14b9f 6.1.0 2018-10-14 00:53:43 +08:00
steven anthonyandJun Yang f1b4c4fe14 Added react-liquidjs-demo 2018-10-10 19:59:43 +08:00
harttle 6af631b77b feature: url_decode, working on #94 2018-10-08 10:37:24 +08:00
Jun Yang d504c8107e 6.0.1 2018-10-06 12:20:13 +08:00
Jun Yang c0181aec31 remove dependency from 'util' 2018-10-01 23:08:39 +08:00
Jun Yang 22a7a85d50 test case for Regexp rendering 2018-10-01 23:00:49 +08:00
Jun Yang 044be8253d fixes #91 2018-10-01 22:51:40 +08:00
Jun Yang 8a629980d9 refactor: move fs util into template.js 2018-10-01 22:06:03 +08:00
harttle 5412c5a39f fixes #88 2018-09-29 16:45:11 +08:00
harttle b7445f26d6 6.0.0 2018-08-28 00:37:00 +08:00
harttle 3213c6d23a fix: ci 2018-08-28 00:27:44 +08:00
harttle 829afc24a7 fix: ci 2018-08-28 00:17:26 +08:00
harttle 0049a56864 fix: ci 2018-08-28 00:05:50 +08:00
harttle f8e0587542 fix: ci 2018-08-27 23:54:43 +08:00
harttle c3c57a560b fix: travis 2018-08-27 23:49:11 +08:00
harttle 5378f2709b test: cover all 2018-08-27 23:27:42 +08:00
harttle d6876bd9ec refactor: bundle template.js respectively 2018-08-27 21:37:39 +08:00
harttle 31c39561c9 feature: get nyc, babel and coveralls working 2018-08-26 21:54:35 +08:00
harttle d828c4021e fix: unit test on node6.x 2018-08-21 01:21:30 +08:00
harttle 4d4a2d04ec refactor 2018-08-21 01:18:13 +08:00
harttle bc9d8f92af refactor: import rollup 2018-08-20 23:58:41 +08:00
harttle 4ece4e208f 5.3.0-0 2018-08-17 01:34:57 +08:00
harttle 4ad3d2a52b modified: .eslintignore 2018-08-17 01:34:07 +08:00
harttle fa7cd8f4ff refactor: import babelify 2018-08-17 01:32:16 +08:00
harttle 87360d5207 refactor: mv es5 files to build/ 2018-08-16 23:20:13 +08:00
harttle 19891b2df0 refactor: es6 building and linting 2018-08-16 23:14:12 +08:00
harttle 5f634b0b5e refactor: moving all ES6 files to src/, working on #83 2018-08-16 00:22:46 +08:00
harttle 9f597843c9 5.2.0 2018-08-14 00:12:42 +08:00
harttle 887b736f39 test cases for stringify 2018-08-14 00:11:50 +08:00
harttle 7a3689067b feature: support toLiquid, working on #81 2018-08-14 00:11:50 +08:00
harttle 5b43520d78 feature: variable lookup respects to to_liquid, #81 2018-08-14 00:11:50 +08:00
harttle ea38a9e1f5 refactor: test/render.js use local scope 2018-08-14 00:11:50 +08:00
harttle fa5b731daf fix string type checking and circular reference, working on #81 2018-08-14 00:11:50 +08:00
harttle 96829b5293 fix: express demo, fixes #79 2018-07-25 21:51:42 +08:00
harttle 4cf7215a67 5.1.1 2018-07-24 19:39:12 +08:00
harttle 50006db26d fix: test/util/strftime.js 2018-07-24 19:38:47 +08:00
harttle 2d81223d99 update outdated packages 2018-07-24 19:37:21 +08:00
harttle 3038115c21 5.1.0 2018-07-24 14:53:47 +08:00
harttle fdbf4b838a add package.lock 2018-07-24 14:53:28 +08:00
harttle 7ab0e96419 feature: template string for {%include%} argument, #72 2018-07-24 14:49:23 +08:00
harttle 48484fe52f 5.0.2 2018-07-21 23:42:18 +08:00
harttle 14d1956232 test case for _.forOwn 2018-07-21 23:23:25 +08:00
harttle c3e7859cdd fix: increment/decrement independent from capture, #76 2018-07-21 23:19:28 +08:00
harttle c3de3fbb19 fix: post-increment, pre-decrement working on #76 2018-07-21 22:59:52 +08:00
harttle b5cfd2d2c5 fix: {%assign%} now adds a shading scope 2018-07-21 21:30:40 +08:00
harttle 36f84e4b3c refactor: pop the same context as the pushed one 2018-07-21 17:12:46 +08:00
harttle 63b5658604 refactor: src/scope.js 2018-07-21 17:02:18 +08:00
Ryan KennedyandJun Yang c79700ef1a fix: correctly understand variables with question marks
Fixes #77
2018-07-18 10:15:43 +08:00
harttle 6755462056 5.0.1 2018-07-16 15:04:40 +08:00
harttle 02806b2257 fix #75 2018-07-16 15:02:31 +08:00
harttle d0fb5ccece 5.0.0 2018-07-16 13:41:57 +08:00
harttle b7144f9b75 fix: linting 2018-07-07 01:22:40 +08:00
harttle 88fc05732f fix: tablerow cols defaults to 0, #74 2018-07-07 01:18:11 +08:00
Jun Yang 44b1aac096 remove any-promise, working on #73 2018-06-23 21:55:23 +08:00
Jun Yang 42043b6944 4.0.0 2018-05-12 23:10:36 +08:00
Jun Yang 61f146432d fix #59, divide numbers as it is 2018-05-12 22:58:43 +08:00
Jun Yang 220b705b73 3.1.3 2018-05-12 22:06:35 +08:00
Jun Yang ef85bd9192 fix #67: forloop.last, forloop.rindex, forloop.rindex0 incorrect if loop has a limit 2018-05-12 22:05:16 +08:00
Paul Robert LloydandJun Yang 8e9e10fdb4 Ensure strip_html filter uses same regex as Ruby Liquid 2018-05-09 10:41:25 +08:00
Jun Yang f668a5cc41 3.1.2 2018-04-07 19:49:46 +08:00
Jun Yang 098f5164e7 fix: linting 2018-04-07 19:48:46 +08:00
Jun Yang 432f878bb6 fix: support comma-separated arguments for static partials, fixes #62 2018-04-07 19:45:57 +08:00
Cory MawhorterandJun Yang aa54dad36e fix dupe name bug 2018-04-07 19:36:50 +08:00
Jun Yang c6ff1f7911 3.1.1 2018-03-05 01:34:00 +08:00
Jun Yang 11355cbf09 refactor: remove makefile 2018-03-05 01:31:12 +08:00
Jun Yang 7ccd0cb1e5 fix: branch coverage 2018-03-05 01:20:43 +08:00
Jun Yang 4326c223f3 fix #61: static filename not parsed for subdirectories 2018-03-05 00:58:26 +08:00
Jun Yang 78b6e35fbf fix: file path asserts for windows 2018-03-03 20:01:24 +08:00
Jun YangandGitHub e275368cb3 Update README.md 2018-01-17 11:01:58 +08:00
Jun YangandGitHub ca2b16a583 Update README.md 2018-01-17 10:48:05 +08:00
Jun YangandGitHub 86d0529a5e Update README.md 2018-01-17 10:47:11 +08:00
Jun YangandGitHub 400b93c88d Update README.md 2018-01-17 10:44:11 +08:00
harttle 22b41fd1a8 3.1.0 2018-01-12 10:55:38 +08:00
ChenLandJun Yang d7c7f32d4b make dist 2018-01-12 10:53:43 +08:00
ChenLandJun Yang 925df9a9b5 100% coverage 2018-01-12 10:53:43 +08:00
ChenLandJun Yang 7b696ed0d2 low version, skip tests 2018-01-12 10:53:43 +08:00
ChenLandJun Yang c55f0234eb Add xhr support for render file in browser. 2018-01-12 10:53:43 +08:00
harttle eee5649a47 fix package.json syntax 2017-12-31 12:32:13 +08:00
harttle 1048f4e0ab add engine spec 2017-12-31 12:29:32 +08:00
harttle 5c6d05092f fix: remove let for nodejs 4.x 2017-12-29 12:33:10 +08:00
harttle 900664a26d test cases for #53 2017-12-29 12:17:52 +08:00
harttle 757a06ee83 fix linting 2017-12-29 12:14:01 +08:00
harttle 9f6e9f2f3f 3.0.0 2017-12-23 21:36:37 +08:00
harttle 0f9adfb067 change: remove deprecated features, coverall 2017-12-23 21:31:55 +08:00
harttle dabde0e9e4 2.2.1 2017-12-23 13:01:28 +08:00
harttle bd4e851b3c update babelify 2017-12-23 12:57:19 +08:00
harttle 1d2aece687 2.2.0 2017-12-21 14:10:42 +08:00
harttle ace271c7f0 feature: support static filepath resolving by options.dynamicPartials,
fix #51
2017-12-21 14:09:30 +08:00
Sami KukkonenandJun Yang 4cbfa6d204 Ignore "fs" module in browser environments 2017-12-21 13:09:21 +08:00
harttle 3153f1d4bb 2.1.4 2017-12-10 01:35:21 +08:00
harttle 9cd4f3d904 modified: README.md 2017-12-10 01:35:06 +08:00
harttle 8dd1b3ba99 2.1.3 2017-12-10 01:26:46 +08:00
harttle 11de73a7b1 update doc 2017-12-10 00:30:13 +08:00
harttle 0325eacddf 2.1.2 2017-12-08 10:42:27 +08:00
harttle a21fcf6f87 fix: compatible to nodejs<=4 2017-12-08 10:42:00 +08:00
harttle 7bb2ee2213 2.1.1 2017-12-08 00:29:09 +08:00
harttle b24582c2c4 remove debug message, working on #50 2017-12-08 00:28:51 +08:00
harttle 49cb7a8c36 2.1.0 2017-12-08 00:25:44 +08:00
harttle a4126e7f10 fix: typo in scope.get("liquid") error message, fixes #50 2017-12-08 00:24:23 +08:00
harttle 5196d6ad56 feature: date filter supports utc string now, fixes #49 2017-12-08 00:22:56 +08:00
harttle 6082bf4757 2.0.3 2017-11-27 00:00:09 +08:00
harttle 8efb41a631 use isTruthy for and/or operators, fixes #47 2017-11-26 23:59:50 +08:00
harttle d348edac14 2.0.2 2017-11-06 00:16:51 +08:00
harttle be00f1d385 feature: treat top-level content as anonymous block, fixes #45 2017-11-06 00:14:33 +08:00
harttle 4e132ca1c5 tags: use scope.opts insteadof scope.get('liquid') 2017-11-04 10:46:15 +08:00
harttle 329aea6bd6 2.0.1 2017-10-31 00:11:35 +08:00
harttle 23e7424f9d change: default extname to "", working for #41 2017-10-31 00:10:45 +08:00
harttle 8241625be4 remove publish from version script 2017-10-30 02:24:08 +08:00
harttle d36ec17621 2.0.0 2017-10-30 02:23:36 +08:00
harttle 485424cc89 feature: concat filter 2017-10-30 02:22:56 +08:00
harttle a239389044 docs for #41 2017-10-30 02:07:07 +08:00
harttle e8fa8d32f5 feature: separate options for value/tag trimming, working on #17 2017-10-30 01:46:31 +08:00
harttle 502426f621 mv whiteSpaceCtrl to a separate file 2017-10-30 00:36:21 +08:00
harttle 8c77fc5a3d fix: whiteSpace Ctrl with compliance to shopify/liquid, working on #17 2017-10-30 00:09:29 +08:00
harttle be17d7443c 1.9.7 2017-10-21 10:50:44 +08:00
Mehdi JafferyandJun Yang 118624ca7d Fix to make for tag iterate objects like Shopify liquid.
Ref: https://github.com/Shopify/liquid/wiki/Liquid-for-Designers#allowed-collection-types
2017-10-18 05:41:43 -05:00
harttle d08ad6dc97 fix: error.stack assertion in v8.5 2017-10-06 12:32:06 +08:00
harttle e22870f5d7 fix: broken unit test by UTC time parsing behaviour change 2017-10-06 12:16:03 +08:00
harttle bd6706cf4d update outdated 2017-10-05 08:32:32 +08:00
harttle b71928ffd4 refactor: format 2017-10-05 08:21:32 +08:00
harttle 6f69cd774d 1.9.6 2017-09-15 10:39:18 +08:00
harttle 1b662d374d update outdated dependencies 2017-09-15 10:38:54 +08:00
harttle 6dd6ed176c 1.9.5 2017-08-26 17:58:14 +08:00
harttle 21af8c4d7a fix read/write variable in parent's scope, fix #39 2017-08-26 17:57:43 +08:00
harttle 942a3224a1 1.9.4 2017-08-16 00:07:47 +08:00
harttle 11259fae62 fix stack matching 2017-08-16 00:04:31 +08:00
harttle 030037eedc cover all 2017-08-15 23:52:05 +08:00
harttle 6d59f34fc8 cover all 2017-08-14 22:43:49 +08:00
harttle 72597c52e5 cover all 2017-08-14 22:07:08 +08:00
harttle 7904f823a8 1.9.3 2017-08-04 11:28:02 +08:00
harttle e993118e87 Merge branch 'jaswrks-patch-1' 2017-08-04 11:27:37 +08:00
harttle 4ae63b2899 test cases for #36 2017-08-04 11:27:12 +08:00
jaswrksandGitHub 9f446ad024 Support {{- and -}} for whitespace control.
With this PR it now matches the intended functionality; i.e., https://github.com/harttle/liquidjs/wiki/Whitespace-Control
2017-08-03 06:06:35 -08:00
harttle 6f82b914cb 1.9.2 2017-08-03 13:56:09 +08:00
harttle 31397a4862 update uglify-js 2017-08-03 13:55:46 +08:00
harttle a2a076d038 refactor access seq parsing 2017-08-03 13:46:00 +08:00
jaswrksandGitHub 3dc37bf980 Bug fix for foo.bar[0].foo that is not working.
Corrects a bug where `foo.bar[0].foo` results in error: 'Cannot read property `foo` of undefined', because the sequence contains an empty string; i.e., `seq.push('')` should be avoided.

Another possible solution would be to strip empty strings from the final `seq` variable, but this PR fixes the immediate problem.
2017-08-02 10:37:11 -08:00
harttle 4ee2bc964c upgrade outdated dependencies 2017-07-28 21:25:24 +08:00
wojtask9andJun Yang ea2a5f5c3b add identation value to token (#34)
* add identation value to token

* fix lint problems

* use var instead of let
2017-07-21 12:44:27 +08:00
harttle 52e2be68ab 1.9.1 2017-07-17 01:09:22 +08:00
harttle cbde48971a rename to liquidjs 2017-07-17 01:08:51 +08:00
harttle e91cc8c950 update: auto publish after version 2017-07-14 10:42:01 +08:00
harttle 314aa6b467 1.9.0 2017-07-14 00:37:08 +08:00
harttle 14e53ed3a9 test cases for #32 2017-07-14 00:34:12 +08:00
harttle cf6d3e78fe Merge branch 'object-for-loop' of https://github.com/aleclarson/liquid-node 2017-07-13 23:24:48 +08:00
aleclarson fb52c864b1 Add isPlainObject to 'util/underscore.js' 2017-07-13 10:33:01 -04:00
harttle 9f82aec569 add test case for truncate 2017-07-13 16:28:32 +08:00
Jun YangandGitHub 29fe8dcb4b Merge pull request #31 from aleclarson/remove-stop-skip
Remove unused `stop` and `skip` from `forloop` object
2017-07-13 16:10:10 +08:00
aleclarson 2a16800305 Iterate keys of object when used with for...in loop 2017-07-12 15:33:48 -04:00
aleclarson 154cf379a8 Remove unused stop and skip from forloop object 2017-07-12 15:33:09 -04:00
harttle ed20fe6072 1.8.0 2017-04-24 21:31:56 +08:00
harttle 2632e3e97e eslint 2017-04-24 21:31:30 +08:00
harttle 82f84d96c1 1.7.9 2017-04-20 10:33:49 +08:00
harttle 1126dffd7d fix default filter use isTruthy, working on #30 2017-04-20 10:33:10 +08:00
harttle 07ec997ffd tests for truthy 2017-04-20 10:31:02 +08:00
harttle 79815784c1 1.7.8 2017-04-17 22:56:16 +08:00
harttle 2d793a63fe fix #29 2017-04-17 22:49:53 +08:00
harttle cc2b2ea5d1 1.7.7 2017-04-10 19:07:51 +08:00
Jun YangandGitHub 8ff9eeb51d Merge pull request #28 from wojtask9/master
support multiline tags
2017-04-10 19:06:16 +08:00
wojtask9 3bc7caa387 support multiline tags 2017-04-10 11:58:45 +02:00
harttle 20044c4256 1.7.6 2017-03-21 18:00:27 +08:00
chenosandJun Yang e72f295bd0 Comparison on NULL is always false (#27)
* All values in Liquid are truthy except nil and false.

* The falsy values are null, undefined and false.

* comparison on null
2017-03-21 14:54:26 +08:00
chenosandJun Yang 66774865f4 All values in Liquid are truthy except nil and false. (#26)
* All values in Liquid are truthy except nil and false.

* The falsy values are null, undefined and false.
2017-03-21 12:25:11 +08:00
harttle 2ae6874397 1.7.5 2017-03-20 15:07:10 +08:00
chenosandJun Yang b307f6ffd0 Size can also be used with dot notation (#25)
* Size can also be used with dot notation (for example, {{ my_string.size }}).

* Fixed test failed.

* Optimization
2017-03-20 15:06:32 +08:00
harttle 5b5db616e2 1.7.4 2017-03-16 19:29:16 +08:00
harttle 551a15f373 import value declaration syntax 2017-03-16 19:27:54 +08:00
ChenL 45e0fb21dc Fixed array arguments bug 2017-03-16 19:02:30 +08:00
ChenL 4fc977268f Support create variables on the filter line. 2017-03-16 18:26:10 +08:00
harttle 8231727428 update README 2017-03-16 10:44:21 +08:00
harttle 0a068ac51c 1.7.3 2017-03-16 10:40:27 +08:00
harttle 6f1d6f3d05 Merge branch 'chenos-master' 2017-03-16 10:40:09 +08:00
harttle aa52a31e90 fix: contains return false when left side equals null 2017-03-16 10:39:10 +08:00
ChenL e4056a79ee Test if an array contains a specific number/string. 2017-03-16 10:05:50 +08:00
ChenL f1d68409b1 Do not throw error in contains when variable is illegal. 2017-03-15 21:20:00 +08:00
ChenL 7ad9ce95e9 Illegal expression in contains 2017-03-15 20:38:08 +08:00
harttle ea7e334d87 1.7.2 2017-03-15 17:14:21 +08:00
harttle 4f123c1d6b Merge branch 'chenos-master' 2017-03-15 17:13:58 +08:00
harttle 0f4edc5b53 add unit test for #21 2017-03-15 17:13:05 +08:00
chenosandGitHub 48bee428d5 reversed position
eg:

- reversed limit:4 offset:2
- limit:4 reversed offset:2
- limit:4 offset:2 reversed
2017-03-15 16:59:24 +08:00
Jun YangandGitHub 2d1f05799f Update README.md 2017-02-18 17:14:51 +08:00
harttle dbc18539a0 modified: README.md 2017-02-14 10:57:57 +08:00
harttle 492f56cbfb unit test: multiple hash 2017-02-01 13:49:01 +08:00
harttle 4018ffe68b 1.7.1 2017-01-31 23:17:08 +08:00
harttle 21eefe1d7b do not trim output tags and previous \n, #19 2017-01-31 23:15:23 +08:00
harttle 0c8148c12b 1.7.0 2017-01-31 00:48:26 +08:00
harttle 7f336ded5c fix badges 2017-01-31 00:48:11 +08:00
harttle 5ecca1b761 feature: greedy trim disabled by default, working on #19 2017-01-31 00:07:46 +08:00
harttle af284daf34 1.6.2 2017-01-21 19:41:59 +08:00
harttle 21a5ac7e08 RenderError extends original error 2017-01-21 19:41:19 +08:00
harttle 7d22214cb5 remove strict_* from express arguments 2017-01-02 13:44:33 +08:00
harttle d6ca12f3bd 1.6.1 2016-12-27 00:46:53 +08:00
harttle 84f7a399c5 update README and unit test 2016-12-27 00:33:11 +08:00
harttle 5b5dfd5c00 documents for whitespace control (#17) 2016-12-27 00:09:39 +08:00
harttle d032f7f4d4 1.6.0 2016-12-26 23:38:15 +08:00
harttle 044035b8ed unit tests for #17 2016-12-26 23:37:33 +08:00
harttle 564d9cd9f4 feature: whitespace control, fixes #17 2016-12-26 23:23:58 +08:00
harttle fbb1dc8a36 test cases for filters: string->number convertion, #18 2016-12-15 23:14:49 +08:00
harttle c99dfd69e7 1.5.4 2016-12-15 22:39:52 +08:00
harttle 4719544d49 fix: number convertion for plus filter, closes #18 2016-12-15 22:38:48 +08:00
harttle 2c0ba9bfd9 1.5.3 2016-12-08 23:26:00 +08:00
harttle 89636736a4 robust date filter 2016-12-08 23:25:38 +08:00
harttle deca9677dd modified: README.md 2016-12-05 23:49:07 +08:00
harttle 543368ed89 unit test for original error for {%layout%} 2016-12-01 00:50:36 +08:00
harttle dcd7b01fa4 update tag test 2016-11-16 02:53:30 +08:00
170 changed files with 54086 additions and 6993 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
build
coverage
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "standard",
"env": {
"es6": true,
"browser": true,
"node": true
},
"plugins": [
"standard",
"promise"
],
"rules": {
"no-var": 2,
"prefer-const": 2
}
}
+9 -34
View File
@@ -1,40 +1,15 @@
# Logs
logs
# logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# tests
coverage/
.nyc_output/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# modules
node_modules/
build/
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# vim
# editors
.*.swp
.vscode
+3 -1
View File
@@ -1 +1,3 @@
test/
/coverage
/test
.*
+16 -4
View File
@@ -1,7 +1,19 @@
language: node_js
node_js:
- "8"
- "6"
- "4"
before_script:
- npm install -g mocha
after_script:
- NODE_ENV=test ./node_modules/.bin/istanbul cover --report lcovonly ./node_modules/mocha/bin/_mocha -- -R spec --recursive && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
jobs:
include:
- stage: test
name: 'Linting'
script: npm run lint
- name: 'Coverage'
script: npm run coverage && npm run coveralls
- stage: release
if: branch = master
script: skip
deploy:
provider: script
skip_cleanup: true
script: npx semantic-release
+20
View File
@@ -0,0 +1,20 @@
## [6.4.3](https://github.com/harttle/liquidjs/compare/v6.4.2...v6.4.3) (2019-02-13)
### Bug Fixes
* better index.d.ts and a demo ([2015f68](https://github.com/harttle/liquidjs/commit/2015f68)), closes [#98](https://github.com/harttle/liquidjs/issues/98)
## [6.4.2](https://github.com/harttle/liquidjs/compare/v6.4.1...v6.4.2) (2019-01-28)
### Bug Fixes
* **CI:** e2e not building cjs ([dde7b3b](https://github.com/harttle/liquidjs/commit/dde7b3b))
## [6.4.1](https://github.com/harttle/liquidjs/compare/v6.4.0...v6.4.1) (2019-01-28)
### Bug Fixes
* regenerator undefined ([a2caeb5](https://github.com/harttle/liquidjs/commit/a2caeb5))
+157 -100
View File
@@ -1,135 +1,129 @@
# shopify-liquid
# liquidjs
[![NPM version](https://img.shields.io/npm/v/shopify-liquid.svg?style=flat)](https://www.npmjs.org/package/shopify-liquid)
[![Build Status](https://travis-ci.org/harttle/shopify-liquid.svg?branch=master)](https://travis-ci.org/harttle/shopify-liquid)
[![Coverage Status](https://img.shields.io/coveralls/harttle/shopify-liquid/master.svg)](https://coveralls.io/github/harttle/shopify-liquid?branch=master)
[![Dependency manager](https://img.shields.io/david/harttle/shopify-liquid.svg?style=flat)](https://david-dm.org/harttle/shopify-liquid)
[![npm](https://img.shields.io/npm/v/liquidjs.svg)](https://www.npmjs.org/package/liquidjs)
[![npm](https://img.shields.io/npm/dm/liquidjs.svg)](https://www.npmjs.org/package/liquidjs)
[![Build Status](https://travis-ci.org/harttle/liquidjs.svg?branch=master)](https://travis-ci.org/harttle/liquidjs)
[![Coveralls](https://img.shields.io/coveralls/harttle/liquidjs.svg)](https://coveralls.io/github/harttle/liquidjs?branch=master)
[![GitHub issues](https://img.shields.io/github/issues-closed/harttle/liquidjs.svg)](https://github.com/harttle/liquidjs/issues)
[![GitHub contributors](https://img.shields.io/github/contributors/harttle/liquidjs.svg)](https://github.com/harttle/liquidjs/graphs/contributors)
[![David](https://img.shields.io/david/harttle/liquidjs.svg)](https://david-dm.org/harttle/liquidjs)
[![David Dev](https://img.shields.io/david/dev/harttle/liquidjs.svg)](https://david-dm.org/harttle/liquidjs?type=dev)
[![DUB](https://img.shields.io/dub/l/vibe-d.svg)](https://github.com/harttle/liquidjs/blob/master/LICENSE)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://github.com/harttle/liquidjs)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/harttle/liquidjs)
A feature-rich Liquid template engine for Node.js and browsers,
with compliance with [the Ruby version][shopify-liquid].
New to Liquid? Here's a live demo: <http://harttle.com/shopify-liquid/>
This is a liquid implementation for both Node.js and browsers. Website: <http://harttle.github.io/liquidjs/>, Live Demo: <https://jsfiddle.net/6u40xbzs/>
> The Liquid template engine is implemented in Ruby originally,
> which is used by [Jekyll][jekyll] and [Github Pages][gh].
**Features**
Features:
* Fully compatible to [shopify][shopify/liquid], with all [tags][tags] and [filters][filters] implemented
* Support layout(extend) and include syntax
* In pure JavaScript with Promise-based API
* Pretty much [builtin filters](https://github.com/harttle/shopify-liquid/wiki/Builtin-Filters) and [builtin tags](https://github.com/harttle/shopify-liquid/wiki/Builtin-Tags)
* Async rendering, especially for tags
* [any-promise][any-promise]
**Differences**
Installation:
Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is one of our priorities, there're still certain differences. You may need some configuration to get it compatible in these senarios:
```bash
npm install --save shopify-liquid
```
* Dynamic file locating (enabled by default), which means layout/partial name can be an variable in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) has a `.toLiquid()` alias and and the JavaScript `.toString()` is aliased to `.to_s()`.
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) uses `JSON.prototype.stringify` as default, rather than Ruby's inspect.
## TOC
* Usage
* [Render from String](#render-from-string)
* [Render from File](#render-from-file)
* [Use with Express.js](#use-with-expressjs)
* [Use in Browser](#use-in-browser)
* [Include Partials](#include-partials)
* [Layout Templates (Extends)](#layout-templates-extends)
* API Spec
* [Constructor Options](#options)
* [Register Filters](#register-filters), [Builtin Filters](https://github.com/harttle/liquidjs/wiki/Builtin-Filters)
* [Register Tags](#register-tags), [Builtin Tags](https://github.com/harttle/liquidjs/wiki/Builtin-Tags)
* [Operators](https://github.com/harttle/liquidjs/wiki/Operators)
* [Whitespace Control](https://github.com/harttle/liquidjs/wiki/Whitespace-Control)
* [Contribute Guidelines](#contribute-guidelines)
## Render from String
Install as Node.js dependency:
```bash
# You'll need a promise-polyfill for Node.js < 4
npm install --save liquidjs
```
Parse and Render:
```javascript
var Liquid = require('shopify-liquid');
var Liquid = require('liquidjs');
var engine = Liquid();
engine.parseAndRender('{{name | capitalize}}', {name: 'alice'})
.then(function(html){
// html === 'Alice'
});
engine
.parseAndRender('{{name | capitalize}}', {name: 'alice'})
.then(console.log);
// outputs 'Alice'
```
Caching templates:
```javascript
var tpl = engine.parse('{{name | capitalize}}');
engine.render(tpl, {name: 'alice'})
.then(function(html){
// html === 'Alice'
});
engine
.render(tpl, {name: 'alice'})
.then(console.log);
// outputs 'Alice'
```
## Render from File
```javascript
var engine = Liquid({
root: path.resolve(__dirname, 'views/'), // for layouts and includes
extname: '.liquid',
cache: false,
strict_filters: false, // default: false
strict_variables: false // default: false
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
extname: '.liquid' // the extname used for layouts/includes, defaults ""
});
engine.renderFile("hello.liquid", {name: 'alice'})
.then(function(html){
// html === 'Alice'
});
// equivalent to:
engine.renderFile("hello", {name: 'alice'})
.then(function(html){
// html === 'Alice'
});
.then(console.log) // outputs "Alice"
// which is equivalent to:
engine
.renderFile("hello", {name: 'alice'})
.then(console.log) // outputs "Alice"
```
* `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`.
If an array, the files are looked up in the order they occur in the array.
Defaults to `process.cwd()`
* `extname` is used to lookup the template file when filepath doesn't include an extension name. Defaults to `.liquid`
* `cache` indicates whether or not to cache resolved templates. Defaults to `false`.
* `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
* `strict_variables` is used to enable strict variable derivation.
If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`.
## Use with Express.js
```javascript
// register liquid engine
app.engine('liquid', engine.express({
strict_variables: true, // Default: fasle
strict_filters: true // Default: false
}));
app.engine('liquid', engine.express());
app.set('views', './views'); // specify the views directory
app.set('view engine', 'liquid'); // set to default
```
> There's an Express demo [here](demo/express/).
When using with Express.js, partials(includes and layouts) will be looked up in
both Liquid `root` and Express `views` directories.
[Here](demo/express/)'s an Express demo. When used with Express.js,
Express [`views`][express-views] will be included when looking up
partials(includes and layouts).
## Use in Browser
[Download][releases] the dist files and import into your HTML.
And `window.Liquid` is what you want. There's also a [demo](demo/browser/).
You can get a dist file for browsers from
```html
<html lang="en">
<head>
<script src="dist/liquid.min.js"></script>
</head>
<body>
<script>
var engine = window.Liquid();
var src = '{{ name | capitalize}}';
var ctx = {
name: 'welcome to Shopify Liquid'
};
engine.parseAndRender(src, ctx)
.then(function(html) {
// html === Welcome to Shopify Liquid
});
</script>
</body>
</html>
```
* [Releases][releases] page for liquidjs, or
* unpkg.com: <https://unpkg.com/liquidjs/dist/liquid.min.js>
Note: In [IE and Android UC][caniuse-promises] browser, you need a Promise implementation
registered to [any-promise][any-promise].
Here's the demo:
## Includes
* JSFiddle: <https://jsfiddle.net/6u40xbzs/>
* Demo directory: [/demo/browser/](demo/browser/).
Note: For [IE and Android UC][caniuse-promises] browser, you will need a [Promise polyfill][pp].
## Include Partials
```
// file: color.liquid
@@ -150,7 +144,7 @@ color: 'red' shape: 'circle'
color: 'yellow' shape: 'square'
```
## Layouts
## Layout Templates (Extends)
```
// file: default-layout.liquid
@@ -174,16 +168,51 @@ Footer
* It's possible to define multiple blocks.
* block name is optional when there's only one block.
## Options
The full list of options for `Liquid()` is listed as following:
* `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`.
If an array, the files are looked up in the order they occur in the array.
Defaults to `["."]`
* `extname` is used to lookup the template file when filepath doesn't include an extension name. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`.
* `cache` indicates whether or not to cache resolved templates. Defaults to `false`.
* `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`.
* `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
* `strict_variables` is used to enable strict variable derivation.
If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`.
* `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
* `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`.
* `trim_value_left` is similiar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`.
## Register Filters
```javascript
// Usage: {{ name | uppper }}
engine.registerFilter('upper', function(v){
return v.toUpperCase();
});
engine.registerFilter('upper', v => v.toUpperCase())
```
> See existing filter implementations: <https://github.com/harttle/shopify-liquid/blob/master/filters.js>
Filter arguments will be passed to the registered filter function, for example:
```javascript
// Usage: {{ 1 | add: 2, 3 }}
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
```
See existing filter implementations here: <https://github.com/harttle/liquidjs/blob/master/filters.js>
## Register Tags
@@ -200,20 +229,48 @@ engine.registerTag('upper', {
});
```
> See existing tag implementations: <https://github.com/harttle/shopify-liquid/blob/master/tags/>
* `parse`: Read tokens from `remainTokens` until your end token.
* `render`: Combine scope data with your parsed tokens into HTML string.
## Contribution Guide
See existing tag implementations here: <https://github.com/harttle/liquidjs/blob/master/tags/>
1. Write a [test][test] to define the feature you want.
2. Just initiate an issue, or optionally:
3. Get your test pass and make a pull request.
## Plugin API
A pack of tags or filters can be encapsulated into a **plugin**, which will be typically installed via npm.
```javascript
engine.plugin(require('./some-plugin'));
// some-plugin.js
module.exports = function (Liquid) {
// here `this` refers to the engine instance
// `Liquid` provides facilities to implement tags and filters
this.registerFilter('foo', x => x);
}
```
Plugin List:
* To add your plugin, contact me or simply send a PR.
## Contribute Guidelines
This repo uses [eslint](https://eslint.org/) to check code style, [semantic-release](https://github.com/semantic-release/semantic-release) to generate changelog and publish to npm and Github Releases.
* Code Style: <https://github.com/standard/eslint-config-standard>, `npm run lint` to check locally.
* Commit Message: <https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits>
[nunjucks]: http://mozilla.github.io/nunjucks/
[liquid-node]: https://github.com/sirlantis/liquid-node
[shopify-liquid]: https://shopify.github.io/liquid/
[shopify/liquid]: https://shopify.github.io/liquid/
[jekyll]: http://jekyllrb.com/
[gh]: https://pages.github.com/
[releases]: https://github.com/harttle/shopify-liquid/releases
[releases]: https://github.com/harttle/liquidjs/releases
[any-promise]: https://github.com/kevinbeaty/any-promise
[test]: https://github.com/harttle/shopify-liquid/tree/master/test
[test]: https://github.com/harttle/liquidjs/tree/master/test
[caniuse-promises]: http://caniuse.com/#feat=promises
[whitespace control]: https://github.com/harttle/liquidjs/wiki/Whitespace-Control
[tags]: https://github.com/harttle/liquidjs/wiki/Builtin-Tags
[filters]: https://github.com/harttle/liquidjs/wiki/Builtin-Filters
[express-views]: http://expressjs.com/en/guide/using-template-engines.html
[pp]: https://github.com/taylorhakes/promise-polyfill
+1
View File
@@ -0,0 +1 @@
access time: {{date|date: "%Y-%m-%d %H:%M:%S"}}
+1
View File
@@ -0,0 +1 @@
<h2>Welcome to {{ name | capitalize}}, {% include 'date.html' %}</h2>
+12 -9
View File
@@ -1,26 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<title></title>
<title>liquidjs for the browser</title>
<script src="../../dist/liquid.js"></script>
</head>
<body>
<h2 id="header"></h2>
<script>
var engine = window.Liquid();
var src = 'Welcome to {{ name | capitalize}}, ' +
'access time: {{date|date: "%Y-%m-%d %H:%M:%S"}}';
var engine = window.Liquid({
extname: '.html',
cache: true
});
var src = '<h2>Welcome to {{ name | capitalize}}, ' +
'access time: {{date|date: "%Y-%m-%d %H:%M:%S"}}&lt;/h2>';
var ctx = {
name: 'Liquid',
date: new Date()
};
engine.parseAndRender(src, ctx)
.then(function(html) {
document.getElementById('header').innerHTML = html;
return engine.renderFile('hello', ctx);
})
.then(function(html) {
document.body.innerHTML += html
});
</script>
</body>
+17 -17
View File
@@ -1,22 +1,22 @@
var express = require('express');
var app = express();
var Liquid = require('../..');
const express = require('express')
const Liquid = require('../..')
var engine = Liquid({
root: __dirname, // for layouts and partials
extname: '.liquid'
});
let app = express()
let engine = Liquid({
root: __dirname, // for layouts and partials
extname: '.liquid'
})
app.engine('liquid', engine.express()); // register liquid engine
app.set('views', ['./partials', './views']); // specify the views directory
app.set('view engine', 'liquid'); // set to default
app.engine('liquid', engine.express()) // register liquid engine
app.set('views', ['./partials', './views']) // specify the views directory
app.set('view engine', 'liquid') // set to default
app.get('/', function (req, res) {
var todos = ['fork and clone', 'make it better', 'make a pull request'];
res.render('todolist', {
todos: todos,
title: 'Welcome to shopify-liquid!'
});
});
let todos = ['fork and clone', 'make it better', 'make a pull request']
res.render('todolist', {
todos: todos,
title: 'Welcome to liquidjs!'
})
})
module.exports = app;
module.exports = app
+3 -4
View File
@@ -1,6 +1,5 @@
const app = require('./app.js');
const app = require('./app.js')
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
console.log('Express running: http://localhost:3000')
})
+2 -2
View File
@@ -1,14 +1,14 @@
{
"name": "express-demo",
"version": "1.0.0",
"description": "Express Demo Using Shopify-Liquid",
"description": "Express Demo Using liquidjs",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "harttle",
"license": "ISC",
"license": "MIT",
"dependencies": {
"express": "^4.14.0"
}
+2 -2
View File
@@ -1,8 +1,8 @@
{% layout 'layout' %}
<ul>
{% for todo in todolist %}
<li>{% include 'todo' id=forloop.index %}</li>
{% for todo in todos %}
<li>{% include 'todo', id:forloop.index %}</li>
{% endfor %}
</ul>
+12
View File
@@ -0,0 +1,12 @@
const Liquid = require('liquidjs')
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
engine.renderFile('todolist', ctx).then(console.log)
+5
View File
@@ -0,0 +1,5 @@
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
+21
View File
@@ -0,0 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
File diff suppressed because it is too large Load Diff
+16737
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "react-liquidjs",
"version": "0.1.0",
"private": true,
"dependencies": {
"dns": "^0.2.2",
"html-react-parser": "^0.4.7",
"liquidjs": "^6.0.1",
"path": "^0.12.7",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-promise": "^2.0.3",
"react-scripts": "2.0.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="result"></div>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+32
View File
@@ -0,0 +1,32 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+46
View File
@@ -0,0 +1,46 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
let path = require('path')
let Liquid = require('liquidjs');
let config = require('./views/demo.liquid');
let pageConfig = config.toString()
let Parser = require('html-react-parser');
class App extends Component {
componentDidMount() {
let engine = Liquid({
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
extname: '.liquid' // the extname used for layouts/includes, defaults
});
engine.registerFilter('image', d => {
let img = `<img src="${d}" class="App-logo" alt="logo"></img>`;
return img
})
engine.renderFile(pageConfig, {name: 'alice', logo: logo })
.then((htmlTemp) => {
this.setState({ html: htmlTemp })
}) // outputs "Alice"
}
state = {
html: []
}
render() {
return (
<div className="App">
{Parser(`${this.state.html}`)}
</div>
);
}
}
export default App;
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
+14
View File
@@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+127
View File
@@ -0,0 +1,127 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
// Execute callback
if (config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
+18
View File
@@ -0,0 +1,18 @@
<header className="App-header">
{{ logo | image }}
<h2>Welcome {{name | capitalize}}</h2>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<p>
You are using liquidjs & Reactjs
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
import Liquid, {isTruthy} from 'liquidjs'
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
// console.log('isTruthy:', isTruthy('a string here'));
engine.renderFile('todolist', ctx).then(console.log)
+5
View File
@@ -0,0 +1,5 @@
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
View File
+3645
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+3279 -2184
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
-127
View File
@@ -1,127 +0,0 @@
const strftime = require('./src/util/strftime.js');
const _ = require('./src/util/underscore.js');
var escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;',
};
var unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'",
};
var filters = {
'abs': v => Math.abs(v),
'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v),
'date': (v, arg) => {
if (v === 'now') v = new Date();
return strftime(v, arg);
},
'default': (v, arg) => v || arg,
'divided_by': (v, arg) => Math.floor(v / arg),
'downcase': v => v.toLowerCase(),
'escape': escape,
'escape_once': str => escape(unescape(str)),
'first': v => v[0],
'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg),
'last': v => v[v.length - 1],
'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]),
'minus': bindFixed((v, arg) => v - arg),
'modulo': bindFixed((v, arg) => v % arg),
'newline_to_br': v => v.replace(/\n/g, '<br />'),
'plus': bindFixed((v, arg) => v + arg),
'prepend': (v, arg) => arg + v,
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
'replace': (v, pattern, replacement) =>
stringify(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(),
'round': (v, arg) => {
var amp = Math.pow(10, arg || 0);
return Math.round(v * amp, arg) / amp;
},
'rstrip': str => stringify(str).replace(/\s+$/, ''),
'size': v => v.length,
'slice': (v, begin, length) =>
v.substr(begin, length === undefined ? 1 : length),
'sort': (v, arg) => v.sort(arg),
'split': (v, arg) => stringify(v).split(arg),
'strip': (v) => stringify(v).trim(),
'strip_html': v => stringify(v).replace(/<\/?\s*\w+\s*\/?>/g, ''),
'strip_newlines': v => stringify(v).replace(/\n/g, ''),
'times': (v, arg) => v * arg,
'truncate': (v, l, o) => {
v = stringify(v);
o = (o === undefined) ? '...' : o;
l = l || 16;
if (v.length <= l) return v;
return v.substr(0, l - o.length) + o;
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...';
var arr = v.split(' ');
var ret = arr.slice(0, l).join(' ');
if (arr.length > l) ret += o;
return ret;
},
'uniq': function(arr) {
var u = {};
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false;
}
u[val] = true;
return true;
});
},
'upcase': str => stringify(str).toUpperCase(),
'url_encode': encodeURIComponent
};
function escape(str) {
return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
}
function unescape(str) {
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
}
function getFixed(v) {
var p = (v + "").split(".");
return (p.length > 1) ? p[1].length : 0;
}
function getMaxFixed(l, r) {
return Math.max(getFixed(l), getFixed(r));
}
function stringify(obj) {
obj = obj || "";
return obj + '';
}
function bindFixed(cb) {
return (l, r) => {
var f = getMaxFixed(l, r);
return cb(l, r).toFixed(f);
};
}
function registerAll(liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func));
}
registerAll.filters = filters;
module.exports = registerAll;
-148
View File
@@ -1,148 +0,0 @@
const Scope = require('./src/scope');
const _ = require('./src/util/underscore.js');
const assert = require('./src/util/assert.js');
const tokenizer = require('./src/tokenizer.js');
const statFileAsync = require('./src/util/fs.js').statFileAsync;
const readFileAsync = require('./src/util/fs.js').readFileAsync;
const path = require('path');
const Render = require('./src/render.js');
const lexical = require('./src/lexical.js');
const Tag = require('./src/tag.js');
const Filter = require('./src/filter.js');
const Parser = require('./src/parser');
const Syntax = require('./src/syntax.js');
const tags = require('./tags');
const filters = require('./filters');
const Promise = require('any-promise');
const anySeries = require('./src/util/promise.js').anySeries;
const Errors = require('./src/util/error.js');
var _engine = {
init: function(tag, filter, options) {
if (options.cache) {
this.cache = {};
}
this.options = options;
this.tag = tag;
this.filter = filter;
this.parser = Parser(tag, filter);
this.renderer = Render();
tags(this);
filters(this);
return this;
},
parse: function(html, filepath) {
var tokens = tokenizer.parse(html, filepath);
return this.parser.parse(tokens);
},
render: function(tpl, ctx, opts) {
opts = _.assign({}, this.options, opts);
var scope = Scope.factory(ctx, opts);
return this.renderer.renderTemplates(tpl, scope);
},
parseAndRender: function(html, ctx, opts) {
return Promise.resolve()
.then(() => this.parse(html))
.then(tpl => this.render(tpl, ctx, opts))
.catch(e => {
if (e instanceof Errors.RenderBreakError) {
return e.html;
}
throw e;
});
},
renderFile: function(filepath, ctx, opts) {
opts = _.assign({}, opts);
return this.getTemplate(filepath, opts.root)
.then(templates => this.render(templates, ctx, opts));
},
evalOutput: function(str, scope) {
var tpl = this.parser.parseOutput(str.trim());
return this.renderer.evalOutput(tpl, scope);
},
registerFilter: function(name, filter) {
return this.filter.register(name, filter);
},
registerTag: function(name, tag) {
return this.tag.register(name, tag);
},
lookup: function(filepath, root) {
root = this.options.root.concat(root || []);
root = _.uniq(root);
var paths = root.map(root => path.resolve(root, filepath));
return anySeries(paths, path => statFileAsync(path).then(() => path))
.catch((e) => {
if (e.code === 'ENOENT') {
e.message = `Failed to lookup ${filepath} in: ${root}`;
}
throw e;
});
},
getTemplate: function(filepath, root) {
if(!path.extname(filepath)){
filepath += this.options.extname;
}
return this
.lookup(filepath, root)
.then(filepath => {
if (this.options.cache) {
var tpl = this.cache[filepath];
if (tpl) {
return Promise.resolve(tpl);
}
return readFileAsync(filepath)
.then(str => this.parse(str))
.then(tpl => this.cache[filepath] = tpl);
} else {
return readFileAsync(filepath).then(str => this.parse(str, filepath));
}
});
},
express: function(opts) {
opts = opts || {};
var self = this;
return function(filePath, ctx, callback) {
assert(_.isArray(this.root) || _.isString(this.root),
'illegal views root, are you using express.js?');
opts.root = this.root;
self.renderFile(filePath, ctx, opts)
.then(html => callback(null, html))
.catch(e => callback(e));
};
}
};
function factory(options) {
options = _.assign({}, options);
options.root = normalizeStringArray(options.root);
if (!options.root.length) options.root = ['.'];
options.extname = options.extname || '.liquid';
var engine = Object.create(_engine);
engine.init(Tag(), Filter(options), options);
return engine;
}
function normalizeStringArray(value) {
if (_.isArray(value)) return value;
if (_.isString(value)) return [value];
return [];
}
factory.lexical = lexical;
factory.isTruthy = Syntax.isTruthy;
factory.isFalsy = Syntax.isFalsy;
factory.evalExp = Syntax.evalExp;
factory.evalValue = Syntax.evalValue;
factory.Types = {
ParseError: Errors.ParseError,
TokenizationEroor: Errors.TokenizationError,
RenderBreakError: Errors.RenderBreakError,
AssertionError: Errors.AssertionError
};
module.exports = factory;
-18
View File
@@ -1,18 +0,0 @@
MINIFY = ./node_modules/.bin/uglifyjs
BROWSERIFY = ./node_modules/.bin/browserify
.PHONY: dist default clean
default:
dist:
[ -d dist/ ] || mkdir dist/
$(BROWSERIFY) index.js -s Liquid \
--ignore path \
-t [ babelify --global true --presets [ es2015 ] ] \
> dist/liquid.js
$(MINIFY) dist/liquid.js --compress warnings=false --mangle --output dist/liquid.min.js
ls -lh dist/
clean:
rm -rf dist/
+11986
View File
File diff suppressed because it is too large Load Diff
+103 -29
View File
@@ -1,18 +1,30 @@
{
"name": "shopify-liquid",
"version": "1.5.2",
"description": "A feature-rich Liquid template engine for Node.js and browsers, with compliance with the Ruby version.",
"main": "index.js",
"name": "liquidjs",
"version": "6.4.3",
"description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.",
"main": "dist/liquid.common.js",
"module": "src/index.js",
"browser": "dist/liquid.js",
"types": "src/index.d.ts",
"scripts": {
"test": "mocha --recursive",
"dist": "make dist",
"preversion": "npm test",
"version": "npm run dist && git add -A dist",
"postversion": "git push && git push --tags"
"lint": "eslint src/ test/ *.js",
"unit": "mocha test/unit",
"e2e": "mocha test/e2e",
"test": "npm run unit && npm run e2e",
"coverage": "cross-env NODE_ENV=test nyc --reporter=html npm run unit",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"build": "rollup -c && ls -lh dist",
"version": "npm run build && git add -A dist",
"demo:browser": "echo open http://localhost:8080/demo/browser && http-server -c-1",
"demo:nodejs": "node ./demo/nodejs/index.js",
"demo:express": "cd ./demo/express/ && npm start"
},
"repository": {
"type": "git",
"url": "git+https://github.com/harttle/shopify-liquid.git"
"url": "git+https://github.com/harttle/liquidjs.git"
},
"engines": {
"node": ">=4.8.7"
},
"keywords": [
"liquid",
@@ -24,26 +36,88 @@
"author": "Harttle",
"license": "MIT",
"bugs": {
"url": "https://github.com/harttle/shopify-liquid/issues"
},
"homepage": "https://github.com/harttle/shopify-liquid#readme",
"dependencies": {
"any-promise": "^1.3.0"
"url": "https://github.com/harttle/liquidjs/issues"
},
"homepage": "https://github.com/harttle/liquidjs#readme",
"devDependencies": {
"babel-preset-es2015": "^6.14.0",
"babelify": "^7.3.0",
"browserify": "^13.1.0",
"chai": "^3.5.0",
"chai-as-promised": "^5.3.0",
"coveralls": "^2.11.9",
"express": "^4.14.0",
"istanbul": "^0.4.3",
"mocha": "^3.0.2",
"mock-fs": "^3.9.0",
"sinon": "^1.17.4",
"sinon-chai": "^2.8.0",
"supertest": "^2.0.0",
"uglifyjs": "^2.4.10"
"@semantic-release/changelog": "^3.0.2",
"@semantic-release/commit-analyzer": "^6.1.0",
"@semantic-release/git": "^7.0.8",
"@semantic-release/npm": "^5.1.4",
"@semantic-release/release-notes-generator": "^7.1.4",
"babel-core": "^6.26.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-istanbul": "^5.1.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-runtime": "^6.26.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"coveralls": "^3.0.2",
"cross-env": "^5.2.0",
"eslint": "^5.12.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.15.0",
"eslint-plugin-mocha": "^5.2.1",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"express": "^4.16.4",
"http-server": "^0.11.1",
"jsdom": "^13.2.0",
"mocha": "^5.2.0",
"mock-fs": "^4.7.0",
"nyc": "^13.1.0",
"regenerator-runtime": "^0.12.1",
"rollup": "^1.1.2",
"rollup-plugin-alias": "^1.5.1",
"rollup-plugin-babel": "^3.0.7",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-shim": "^1.0.0",
"rollup-plugin-uglify": "^6.0.2",
"semantic-release": "^15.13.3",
"sinon": "^7.2.3",
"sinon-chai": "^3.3.0",
"supertest": "^3.4.2"
},
"release": {
"branch": "master",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git"
]
},
"nyc": {
"require": [
"babel-core/register"
],
"sourceMap": false,
"instrument": false
},
"babel": {
"presets": [
"env"
],
"plugins": [
[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}
]
],
"env": {
"test": {
"plugins": [
"istanbul"
]
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import shim from 'rollup-plugin-shim'
import alias from 'rollup-plugin-alias'
import babel from 'rollup-plugin-babel'
import { uglify } from 'rollup-plugin-uglify'
import pkg from './package.json'
import nodeResolve from 'rollup-plugin-node-resolve'
const fake = { fs: `export default {}`, path: `export default {}` }
const version = process.env.VERSION || pkg.version
const sourcemap = true
const banner = `/*
* liquidjs@${version}, https://github.com/harttle/liquidjs
* (c) 2016-${new Date().getFullYear()} harttle
* Released under the MIT License.
*/`
const treeshake = {
propertyReadSideEffects: false
}
const input = 'src/index.js'
const babelConf = {
babelrc: false,
'presets': [['env', { 'modules': false }]],
'plugins': ['external-helpers']
}
export default [{
output: [{
file: 'dist/liquid.common.js',
name: 'Liquid',
format: 'cjs',
sourcemap,
banner
}],
external: ['path', 'fs'],
plugins: [
nodeResolve(),
babel(babelConf)
],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.js',
name: 'Liquid',
format: 'umd',
sourcemap,
banner
}],
plugins: [
shim(fake),
alias({
'./template': './template-browser'
}),
nodeResolve(),
babel(babelConf)
],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.min.js',
name: 'Liquid',
format: 'umd',
sourcemap
}],
plugins: [
shim(fake),
nodeResolve(),
babel(babelConf),
uglify()
],
treeshake,
input
}]
+58 -56
View File
@@ -1,66 +1,68 @@
const lexical = require('./lexical.js');
const Syntax = require('./syntax.js');
const assert = require('./util/assert.js');
const _ = require('./util/underscore.js');
import * as lexical from './lexical.js'
import { evalValue } from './syntax.js'
import assert from './util/assert.js'
import { assign, create } from './util/underscore.js'
var valueRE = new RegExp(`${lexical.value.source}`, 'g');
const valueRE = new RegExp(`${lexical.value.source}`, 'g')
module.exports = function(options) {
options = _.assign({}, options);
var filters = {};
export default function (options) {
options = assign({}, options)
let filters = {}
var _filterInstance = {
render: function(output, scope) {
var args = this.args.map(arg => Syntax.evalValue(arg, scope));
args.unshift(output);
return this.filter.apply(null, args);
},
parse: function(str) {
var match = lexical.filterLine.exec(str);
assert(match, 'illegal filter: ' + str);
const _filterInstance = {
render: function (output, scope) {
const args = this.args.map(arg => evalValue(arg, scope))
args.unshift(output)
return this.filter.apply(null, args)
},
parse: function (str) {
let match = lexical.filterLine.exec(str)
assert(match, 'illegal filter: ' + str)
var name = match[1], argList = match[2] || '', filter = filters[name];
if (typeof filter !== 'function'){
if(options.strict_filters){
throw new TypeError(`undefined filter: ${name}`);
}
this.name= name;
this.filter= x => x;
this.args= [];
return this;
//return {
//name: name,
//error: new TypeError(`undefined filter: ${name}`)
//};
}
var args = [];
while(match = valueRE.exec(argList.trim())){
args.push(match[0]);
}
this.name = name;
this.filter = filter;
this.args = args;
return this;
const name = match[1]
const argList = match[2] || ''
const filter = filters[name]
if (typeof filter !== 'function') {
if (options.strict_filters) {
throw new TypeError(`undefined filter: ${name}`)
}
};
this.name = name
this.filter = x => x
this.args = []
return this
}
function construct(str) {
var instance = Object.create(_filterInstance);
return instance.parse(str);
const args = []
while ((match = valueRE.exec(argList.trim()))) {
const v = match[0]
const re = new RegExp(`${v}\\s*:`, 'g')
const keyMatch = re.exec(match.input)
const currentMatchIsKey = keyMatch && keyMatch.index === match.index
currentMatchIsKey ? args.push(`'${v}'`) : args.push(v)
}
this.name = name
this.filter = filter
this.args = args
return this
}
}
function register(name, filter) {
filters[name] = filter;
}
function construct (str) {
const instance = create(_filterInstance)
return instance.parse(str)
}
function clear() {
filters = {};
}
function register (name, filter) {
filters[name] = filter
}
return {
construct, register, clear
};
};
function clear () {
filters = {}
}
return {
construct, register, clear
}
}
+139
View File
@@ -0,0 +1,139 @@
import strftime from './util/strftime.js'
import * as _ from './util/underscore.js'
import { isTruthy } from './syntax.js'
const escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
}
const unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'"
}
const filters = {
'abs': v => Math.abs(v),
'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v),
'concat': (v, arg) => Array.prototype.concat.call(v, arg),
'date': (v, arg) => {
let date = v
if (v === 'now') {
date = new Date()
} else if (_.isString(v)) {
date = new Date(v)
}
return isValidDate(date) ? strftime(date, arg) : v
},
'default': (v, arg) => isTruthy(v) ? v : arg,
'divided_by': (v, arg) => v / arg,
'downcase': v => v.toLowerCase(),
'escape': escape,
'escape_once': str => escape(unescape(str)),
'first': v => v[0],
'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg === undefined ? ' ' : arg),
'last': v => _.last(v),
'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]),
'minus': bindFixed((v, arg) => v - arg),
'modulo': bindFixed((v, arg) => v % arg),
'newline_to_br': v => v.replace(/\n/g, '<br />'),
'plus': bindFixed((v, arg) => Number(v) + Number(arg)),
'prepend': (v, arg) => arg + v,
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
'replace': (v, pattern, replacement) =>
stringify(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(),
'round': (v, arg) => {
const amp = Math.pow(10, arg || 0)
return Math.round(v * amp, arg) / amp
},
'rstrip': str => stringify(str).replace(/\s+$/, ''),
'size': v => v.length,
'slice': (v, begin, length) => {
if (length === undefined) length = 1
return v.slice(begin, begin + length)
},
'sort': (v, arg) => v.sort(arg),
'split': (v, arg) => stringify(v).split(arg),
'strip': (v) => stringify(v).trim(),
'strip_html': v => stringify(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, ''),
'strip_newlines': v => stringify(v).replace(/\n/g, ''),
'times': (v, arg) => v * arg,
'truncate': (v, l, o) => {
v = stringify(v)
o = (o === undefined) ? '...' : o
l = l || 16
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...'
const arr = v.split(' ')
let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o
return ret
},
'uniq': function (arr) {
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false
}
u[val] = true
return true
})
},
'upcase': str => stringify(str).toUpperCase(),
'url_decode': x => x.split('+').map(decodeURIComponent).join(' '),
'url_encode': x => x.split(' ').map(encodeURIComponent).join('+')
}
function escape (str) {
return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
}
function unescape (str) {
return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
}
function getFixed (v) {
const p = (v + '').split('.')
return (p.length > 1) ? p[1].length : 0
}
function getMaxFixed (l, r) {
return Math.max(getFixed(l), getFixed(r))
}
function stringify (obj) {
return obj + ''
}
function bindFixed (cb) {
return (l, r) => {
const f = getMaxFixed(l, r)
return cb(l, r).toFixed(f)
}
}
function isValidDate (date) {
return date instanceof Date && !isNaN(date.getTime())
}
export default function registerAll (liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func))
}
registerAll.filters = filters
+69
View File
@@ -0,0 +1,69 @@
export as namespace Liquid;
export function isTruthy(val: any): boolean;
export function isFalsy(val: any): boolean;
export function evalExp(exp: string, scope: any): any;
export function evalValue(str: string, scope: any): any;
export default class Liquid {
constructor(options?: Options);
private init(tag, filter, options): Liquid;
private respectCache(key, getter): Promise<any>
parse(html: string, filepath?: string): Liquid.Template
render(tpl: Template, ctx: any, opts?: Options): Promise<string>
parseAndRender(html: string, ctx: any, opts?: Options): Promise<string>
renderFile(file: string, ctx: any, opts?: Options): Promise<string>
getTemplate(file: string, root: string): Promise<Liquid.Template>
registerFilter(name: string, filter: Filter): void
registerTag(name: string, tag: Tag): void
express(opts: Options): any
}
export interface Options {
/** `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`. If an array, the files are looked up in the order they occur in the array. Defaults to `["."]`*/
root?: string | string[]
/** `extname` is used to lookup the template file when filepath doesn't include an extension name. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
extname?: string
/** `cache` indicates whether or not to cache resolved templates. Defaults to `false`. */
cache?: boolean
/** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
dynamicPartials?: boolean
/** `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
strict_filters?: boolean
/** `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
trim_tag_right?: boolean
/** `trim_tag_left` is similar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_tag_left?: boolean
/** ``trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
trim_value_right?: boolean
/** `trim_value_left` is similar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_value_left?: boolean
/** `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
greedy?: boolean
}
export interface Template { }
export interface Tag {
parse(this: any, tagToken: any, remainTokens: any): void
render(this: any, scope: any, hash: any): void
}
export type Filter = (...args: any) => string
declare namespace Types {
class LiquidError extends Error {
input: string
line: number
file: string
}
class ParseError extends LiquidError {
originalError: Error
}
class TokenizationError extends LiquidError {}
class RenderBreakError extends LiquidError {}
class AssertionError extends LiquidError {}
class AssignScope {}
class CaptureScope {}
class IncrementScope {}
class DecrementScope {}
}
+134
View File
@@ -0,0 +1,134 @@
import 'regenerator-runtime/runtime'
import * as Scope from './scope'
import * as template from './template'
import * as _ from './util/underscore.js'
import assert from './util/assert.js'
import * as tokenizer from './tokenizer.js'
import Render from './render.js'
import Tag from './tag.js'
import Filter from './filter.js'
import Parser from './parser'
import { isTruthy, isFalsy, evalExp, evalValue } from './syntax.js'
import { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error.js'
import tags from './tags/index.js'
import filters from './filters.js'
const _engine = {
init: function (tag, filter, options) {
if (options.cache) {
this.cache = {}
}
this.options = options
this.tag = tag
this.filter = filter
this.parser = Parser(tag, filter)
this.renderer = Render()
tags(this, Liquid)
filters(this, Liquid)
return this
},
parse: function (html, filepath) {
const tokens = tokenizer.parse(html, filepath, this.options)
return this.parser.parse(tokens)
},
render: function (tpl, ctx, opts) {
opts = _.assign({}, this.options, opts)
const scope = Scope.factory(ctx, opts)
return this.renderer.renderTemplates(tpl, scope)
},
parseAndRender: async function (html, ctx, opts) {
const tpl = await this.parse(html)
return this.render(tpl, ctx, opts)
},
getTemplate: async function (file, root) {
const filepath = await template.resolve(file, root, this.options)
return this.respectCache(filepath, async () => {
const str = await template.read(filepath)
return this.parse(str, filepath)
})
},
renderFile: async function (file, ctx, opts) {
opts = _.assign({}, opts)
const templates = await this.getTemplate(file, opts.root)
return this.render(templates, ctx, opts)
},
respectCache: async function (key, getter) {
const cacheEnabled = this.options.cache
if (cacheEnabled && this.cache[key]) {
return this.cache[key]
}
const value = await getter()
if (cacheEnabled) {
this.cache[key] = value
}
return value
},
evalValue: function (str, scope) {
const tpl = this.parser.parseValue(str.trim())
return this.renderer.evalValue(tpl, scope)
},
registerFilter: function (name, filter) {
return this.filter.register(name, filter)
},
registerTag: function (name, tag) {
return this.tag.register(name, tag)
},
plugin: function (plugin) {
return plugin.call(this, Liquid)
},
express: function (opts) {
opts = opts || {}
const self = this
return function (filePath, ctx, cb) {
assert(_.isArray(this.root) || _.isString(this.root),
'illegal views root, are you using express.js?')
opts.root = this.root
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
}
}
function normalizeStringArray (value) {
if (_.isArray(value)) return value
if (_.isString(value)) return [value]
throw new TypeError('illegal root: ' + value)
}
export default function Liquid (options) {
options = _.assign({
root: ['.'],
cache: false,
extname: '',
dynamicPartials: true,
trim_tag_right: false,
trim_tag_left: false,
trim_value_right: false,
trim_value_left: false,
greedy: true,
strict_filters: false,
strict_variables: false
}, options)
options.root = normalizeStringArray(options.root)
const engine = _.create(_engine)
engine.init(Tag(), Filter(options), options)
return engine
}
Liquid.default = Liquid
Liquid.isTruthy = isTruthy
Liquid.isFalsy = isFalsy
Liquid.evalExp = evalExp
Liquid.evalValue = evalValue
Liquid.Types = {
ParseError,
TokenizationError,
RenderBreakError,
AssertionError,
AssignScope: {},
CaptureScope: {},
IncrementScope: {},
DecrementScope: {}
}
+60 -65
View File
@@ -1,91 +1,86 @@
// quote related
var singleQuoted = /'[^']*'/;
var doubleQuoted = /"[^"]*"/;
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`);
var quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`);
const singleQuoted = /'[^']*'/
const doubleQuoted = /"[^"]*"/
export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
// basic types
var integer = /-?\d+/;
var number = /-?\d+\.?\d*|\.?\d+/;
var bool = /true|false/;
export const integer = /-?\d+/
export const number = /-?\d+\.?\d*|\.?\d+/
export const bool = /true|false/
// peoperty access
var identifier = /[\w-]+/;
var subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`);
var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`);
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`);
// property access
export const identifier = /[\w-]+[?]?/
export const subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`)
export const literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`)
export const variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`)
// range related
var rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`);
var range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`);
var rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`);
export const rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`)
export const range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`)
export const rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`)
var value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`);
export const value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
// hash related
var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`);
var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g');
export const hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`)
export const hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g')
// full match
var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`);
var literalLine = new RegExp(`^${literal.source}$`, 'i');
var variableLine = new RegExp(`^${variable.source}$`);
var numberLine = new RegExp(`^${number.source}$`);
var boolLine = new RegExp(`^${bool.source}$`, 'i');
var quotedLine = new RegExp(`^${quoted.source}$`);
var rangeLine = new RegExp(`^${rangeCapture.source}$`);
var integerLine = new RegExp(`^${integer.source}$`);
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*)\\s*$`)
export const literalLine = new RegExp(`^${literal.source}$`, 'i')
export const variableLine = new RegExp(`^${variable.source}$`)
export const numberLine = new RegExp(`^${number.source}$`)
export const boolLine = new RegExp(`^${bool.source}$`, 'i')
export const quotedLine = new RegExp(`^${quoted.source}$`)
export const rangeLine = new RegExp(`^${rangeCapture.source}$`)
export const integerLine = new RegExp(`^${integer.source}$`)
// filter related
var valueList = new RegExp(`${value.source}(\\s*,\\s*${value.source})*`);
var filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g');
var filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`);
var filterLine = new RegExp(`^${filterCapture.source}$`);
export const valueDeclaration = new RegExp(`(?:${identifier.source}\\s*:\\s*)?${value.source}`)
export const valueList = new RegExp(`${valueDeclaration.source}(\\s*,\\s*${valueDeclaration.source})*`)
export const filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g')
export const filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`)
export const filterLine = new RegExp(`^${filterCapture.source}$`)
var operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
];
export const operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
]
function isInteger(str){
return integerLine.test(str);
export function isInteger (str) {
return integerLine.test(str)
}
function isLiteral(str) {
return literalLine.test(str);
export function isLiteral (str) {
return literalLine.test(str)
}
function isRange(str) {
return rangeLine.test(str);
export function isRange (str) {
return rangeLine.test(str)
}
function isVariable(str) {
return variableLine.test(str);
export function isVariable (str) {
return variableLine.test(str)
}
function matchValue(str) {
return value.exec(str);
export function matchValue (str) {
return value.exec(str)
}
function parseLiteral(str) {
var res;
if (res = str.match(numberLine)) {
return Number(str);
}
if (res = str.match(boolLine)) {
return str.toLowerCase() === 'true';
}
if (res = str.match(quotedLine)) {
return str.slice(1, -1);
}
export function parseLiteral (str) {
let res = str.match(numberLine)
if (res) {
return Number(str)
}
res = str.match(boolLine)
if (res) {
return str.toLowerCase() === 'true'
}
res = str.match(quotedLine)
if (res) {
return str.slice(1, -1)
}
throw new TypeError(`cannot parse '${str}' as literal`)
}
module.exports = {
quoted, number, bool, literal, filter, integer,
hash, hashCapture,
range, rangeCapture,
identifier, value, quoteBalanced, operators,
quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine,
isLiteral, isVariable, parseLiteral, isRange, matchValue, isInteger
};
+17 -13
View File
@@ -1,13 +1,17 @@
var operators = {
'==': (l, r) => l == r,
'!=': (l, r) => l != r,
'>': (l, r) => l > r,
'<': (l, r) => l < r,
'>=': (l, r) => l >= r,
'<=': (l, r) => l <= r,
'contains': (l, r) => l.indexOf(r) > -1,
'and': (l, r) => l && r,
'or': (l, r) => l || r
};
module.exports = operators;
export default function (isTruthy) {
return {
'==': (l, r) => l === r,
'!=': (l, r) => l !== r,
'>': (l, r) => l !== null && r !== null && l > r,
'<': (l, r) => l !== null && r !== null && l < r,
'>=': (l, r) => l !== null && r !== null && l >= r,
'<=': (l, r) => l !== null && r !== null && l <= r,
'contains': (l, r) => {
if (!l) return false
if (typeof l.indexOf !== 'function') return false
return l.indexOf(r) > -1
},
'and': (l, r) => isTruthy(l) && isTruthy(r),
'or': (l, r) => isTruthy(l) || isTruthy(r)
}
}
+92 -91
View File
@@ -1,105 +1,106 @@
const lexical = require('./lexical.js');
const ParseError = require('./util/error.js').ParseError;
const assert = require('./util/assert.js');
import * as lexical from './lexical.js'
import { create } from './util/underscore.js'
import { ParseError } from './util/error.js'
import assert from './util/assert.js'
module.exports = function(Tag, Filter) {
var stream = {
init: function(tokens) {
this.tokens = tokens;
this.handlers = {};
return this;
},
on: function(name, cb) {
this.handlers[name] = cb;
return this;
},
trigger: function(event, arg) {
var h = this.handlers[event];
if (typeof h === 'function') {
h(arg);
return true;
}
},
start: function() {
this.trigger('start');
var token;
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue;
if (token.type == 'tag' &&
this.trigger(`tag:${token.name}`, token)) {
continue;
}
var template = parseToken(token, this.tokens);
this.trigger('template', template);
}
if (!this.stopRequested) this.trigger('end');
return this;
},
stop: function() {
this.stopRequested = true;
return this;
export default function (Tag, Filter) {
const stream = {
init: function (tokens) {
this.tokens = tokens
this.handlers = {}
return this
},
on: function (name, cb) {
this.handlers[name] = cb
return this
},
trigger: function (event, arg) {
const h = this.handlers[event]
if (typeof h === 'function') {
h(arg)
return true
}
},
start: function () {
this.trigger('start')
let token
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue
if (token.type === 'tag' &&
this.trigger(`tag:${token.name}`, token)) {
continue
}
};
function parse(tokens) {
var token, templates = [];
while (token = tokens.shift()) {
templates.push(parseToken(token, tokens));
}
return templates;
const template = parseToken(token, this.tokens)
this.trigger('template', template)
}
if (!this.stopRequested) this.trigger('end')
return this
},
stop: function () {
this.stopRequested = true
return this
}
}
function parseToken(token, tokens) {
try {
var tpl = null;
if (token.type === 'tag') {
tpl = parseTag(token, tokens);
} else if (token.type === 'output') {
tpl = parseOutput(token.value);
} else { // token.type === 'html'
tpl = token;
}
tpl.token = token;
return tpl;
} catch (e) {
throw new ParseError(e, token);
}
function parse (tokens) {
let token
const templates = []
while ((token = tokens.shift())) {
templates.push(parseToken(token, tokens))
}
return templates
}
function parseTag(token, tokens) {
if (token.name === 'continue' || token.name === 'break') return token;
return Tag.construct(token, tokens);
function parseToken (token, tokens) {
try {
let tpl = null
if (token.type === 'tag') {
tpl = parseTag(token, tokens)
} else if (token.type === 'value') {
tpl = parseValue(token.value)
} else { // token.type === 'html'
tpl = token
}
tpl.token = token
return tpl
} catch (e) {
throw new ParseError(e, token)
}
}
function parseOutput(str) {
var match = lexical.matchValue(str);
assert(match, `illegal output string: ${str}`);
function parseTag (token, tokens) {
if (token.name === 'continue' || token.name === 'break') return token
return Tag.construct(token, tokens)
}
var initial = match[0];
str = str.substr(match.index + match[0].length);
function parseValue (str) {
let match = lexical.matchValue(str)
assert(match, `illegal value string: ${str}`)
var filters = [];
while (match = lexical.filter.exec(str)) {
filters.push([match[0].trim()]);
}
const initial = match[0]
str = str.substr(match.index + match[0].length)
return {
type: 'output',
initial: initial,
filters: filters.map(str => Filter.construct(str))
};
}
function parseStream(tokens) {
var s = Object.create(stream);
return s.init(tokens);
const filters = []
while ((match = lexical.filter.exec(str))) {
filters.push([match[0].trim()])
}
return {
parse,
parseTag,
parseStream,
parseOutput
};
};
type: 'value',
initial: initial,
filters: filters.map(str => Filter.construct(str))
}
}
function parseStream (tokens) {
const s = create(stream)
return s.init(tokens)
}
return {
parse,
parseTag,
parseStream,
parseValue
}
}
+54 -60
View File
@@ -1,68 +1,62 @@
const Syntax = require('./syntax.js');
const Promise = require('any-promise');
const mapSeries = require('./util/promise.js').mapSeries;
const RenderBreakError = require('./util/error.js').RenderBreakError;
const RenderError = require('./util/error.js').RenderError;
const assert = require('./util/assert.js');
import { evalExp } from './syntax.js'
import { RenderBreakError, RenderError } from './util/error.js'
import { stringify, create } from './util/underscore.js'
import assert from './util/assert.js'
var render = {
const render = {
renderTemplates: async function (templates, scope) {
assert(scope, 'unable to evalTemplates: scope undefined')
renderTemplates: function(templates, scope) {
assert(scope, 'unable to evalTemplates: scope undefined');
var html = '';
return mapSeries(templates, (tpl) => {
return renderTemplate.call(this, tpl)
.then(partial => html += partial)
.catch(e => {
if (e instanceof RenderBreakError) {
e.resolvedHTML = html;
throw e;
}
throw new RenderError(e, tpl);
});
}).then(() => html);
function renderTemplate(template) {
if (template.type === 'tag') {
return this.renderTag(template, scope)
.then(partial => partial === undefined ? '' : partial);
} else if (template.type === 'output') {
return Promise.resolve()
.then(() => this.evalOutput(template, scope))
.then(partial => partial === undefined ? '' : stringify(partial));
} else { // template.type === 'html'
return Promise.resolve(template.value);
}
let html = ''
for (const tpl of templates) {
try {
html += await renderTemplate.call(this, tpl)
} catch (e) {
if (e instanceof RenderBreakError) {
e.resolvedHTML = html
throw e
}
},
renderTag: function(template, scope) {
if (template.name === 'continue') {
return Promise.reject(new RenderBreakError('continue'));
}
if (template.name === 'break') {
return Promise.reject(new RenderBreakError('break'));
}
return template.render(scope);
},
evalOutput: function(template, scope) {
assert(scope, 'unable to evalOutput: scope undefined');
return template.filters.reduce(
(prev, filter) => filter.render(prev, scope),
Syntax.evalExp(template.initial, scope));
throw new RenderError(e, tpl)
}
}
};
return html
function factory() {
var instance = Object.create(render);
return instance;
async function renderTemplate (template) {
if (template.type === 'tag') {
const partial = await this.renderTag(template, scope)
return partial === undefined ? '' : partial
}
if (template.type === 'value') {
return this.renderValue(template, scope)
}
return template.value
}
},
renderTag: async function (template, scope) {
if (template.name === 'continue') {
throw new RenderBreakError('continue')
}
if (template.name === 'break') {
throw new RenderBreakError('break')
}
return template.render(scope)
},
renderValue: async function (template, scope) {
const partial = this.evalValue(template, scope)
return partial === undefined ? '' : stringify(partial)
},
evalValue: function (template, scope) {
assert(scope, 'unable to evalValue: scope undefined')
return template.filters.reduce(
(prev, filter) => filter.render(prev, scope),
evalExp(template.initial, scope))
}
}
function stringify(val) {
if (typeof val === 'string') return val;
return JSON.stringify(val);
export default function () {
const instance = create(render)
return instance
}
module.exports = factory;
+172 -165
View File
@@ -1,172 +1,179 @@
const _ = require('./util/underscore.js');
const lexical = require('./lexical.js');
const assert = require('./util/assert.js');
import * as _ from './util/underscore.js'
import * as lexical from './lexical.js'
import assert from './util/assert.js'
var Scope = {
getAll: function() {
var ctx = {};
for (var i = this.scopes.length - 1; i >= 0; i--) {
_.assign(ctx, this.scopes[i]);
}
return ctx;
},
get: function(str) {
for (var i = this.scopes.length - 1; i >= 0; i--) {
try {
return this.getPropertyByPath(this.scopes[i], str);
} catch (e) {
if (/undefined variable/.test(e.message)) {
continue;
}
if (/Cannot read property/.test(e.message)) {
if (this.opts.strict_variables) {
e.message += ': ' + str;
throw e;
} else {
continue;
}
} else {
e.message += ': ' + str;
throw e;
}
}
}
if (this.opts.strict_variables) {
throw new TypeError('undefined variable: ' + str);
}
},
set: function(k, v) {
this.setPropertyByPath(this.scopes[this.scopes.length - 1], k, v);
return this;
},
push: function(ctx) {
assert(ctx, `trying to push ${ctx} into scopes`);
return this.scopes.push(ctx);
},
pop: function() {
return this.scopes.pop();
},
unshift: function(ctx) {
assert(ctx, `trying to push ${ctx} into scopes`);
return this.scopes.unshift(ctx);
},
shift: function() {
return this.scopes.shift();
},
setPropertyByPath: function(obj, path, val) {
if (_.isString(path)) {
var paths = path.replace(/\[/g, '.').replace(/\]/g, '').split('.');
for (var i = 0; i < paths.length; i++) {
var key = paths[i];
if (i === paths.length - 1) {
return obj[key] = val;
}
if (undefined === obj[key]) obj[key] = {};
// case for readonly objects
obj = obj[key] || {};
}
}
},
getPropertyByPath: function(obj, path) {
var paths = this.propertyAccessSeq(path + '');
var varName = paths.shift();
if (!obj.hasOwnProperty(varName)) {
throw new TypeError('undefined variable');
}
var variable = obj[varName];
paths.forEach(p => variable = variable[p]);
return variable;
},
/*
* Parse property access sequence from access string
* @example
* accessSeq("foo.bar") // ['foo', 'bar']
* accessSeq("foo['bar']") // ['foo', 'bar']
* accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
propertyAccessSeq: function(str) {
var seq = [],
name = '';
for (var i = 0; i < str.length; i++) {
if (str[i] === '[') {
seq.push(name);
name = '';
var delemiter = str[i + 1];
// foo[bar.coo]
if (delemiter !== "'" && delemiter !== '"') {
var j = matchRightBracket(str, i + 1);
assert(j !== -1, `unbalanced []: ${str}`);
name = str.slice(i + 1, j);
// foo[1]
if (lexical.isInteger(name)) {
seq.push(name);
}
// foo["bar"]
else {
seq.push(this.get(name));
}
name = '';
i = j;
}
// foo["bar"]
else {
j = str.indexOf(delemiter, i + 2);
assert(j !== -1, `unbalanced ${delemiter}: ${str}`);
name = str.slice(i + 2, j);
seq.push(name);
name = '';
i = j + 1;
}
}
// foo.bar
else if (str[i] === ".") {
seq.push(name);
name = '';
}
//foo.bar
else {
name += str[i];
}
}
if (name.length) seq.push(name);
return seq;
const Scope = {
getAll: function () {
return this.contexts.reduce((ctx, val) => _.assign(ctx, val), _.create(null))
},
get: function (path) {
const paths = this.propertyAccessSeq(path)
const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
return paths.reduce((value, key) => this.readProperty(value, key), scope)
},
set: function (path, v) {
const paths = this.propertyAccessSeq(path)
let scope = this.findContextFor(paths[0]) || _.last(this.contexts)
paths.some((key, i) => {
if (!_.isObject(scope)) {
return true
}
if (i === paths.length - 1) {
scope[key] = v
return true
}
if (undefined === scope[key]) {
scope[key] = {}
}
scope = scope[key]
})
},
unshift: function (ctx) {
return this.contexts.unshift(ctx)
},
push: function (ctx) {
return this.contexts.push(ctx)
},
pop: function (ctx) {
if (!arguments.length) {
return this.contexts.pop()
}
};
function matchRightBracket(str, begin) {
var stack = 1; // count of '[' - count of ']'
for (var i = begin; i < str.length; i++) {
if (str[i] === '[') {
stack++;
}
if (str[i] === ']') {
stack--;
if (stack === 0) {
return i;
}
}
const i = this.contexts.findIndex(scope => scope === ctx)
if (i === -1) {
throw new TypeError('scope not found, cannot pop')
}
return -1;
return this.contexts.splice(i, 1)[0]
},
findContextFor: function (key, filter) {
filter = filter || (() => true)
for (let i = this.contexts.length - 1; i >= 0; i--) {
const candidate = this.contexts[i]
if (!filter(candidate)) continue
if (key in candidate) {
return candidate
}
}
return null
},
readProperty: function (obj, key) {
let val
if (_.isNil(obj)) {
val = undefined
} else {
obj = toLiquid(obj)
val = key === 'size' ? readSize(obj) : obj[key]
if (_.isFunction(obj.liquid_method_missing)) {
val = obj.liquid_method_missing(key)
}
}
if (_.isNil(val) && this.opts.strict_variables) {
throw new TypeError(`undefined variable: ${key}`)
}
return val
},
/*
* Parse property access sequence from access string
* @example
* accessSeq("foo.bar") // ['foo', 'bar']
* accessSeq("foo['bar']") // ['foo', 'bar']
* accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
propertyAccessSeq: function (str) {
str = String(str)
const seq = []
let name = ''
let j
let i = 0
while (i < str.length) {
switch (str[i]) {
case '[':
push()
const delemiter = str[i + 1]
if (/['"]/.test(delemiter)) { // foo["bar"]
j = str.indexOf(delemiter, i + 2)
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
name = str.slice(i + 2, j)
push()
i = j + 2
} else { // foo[bar.coo]
j = matchRightBracket(str, i + 1)
assert(j !== -1, `unbalanced []: ${str}`)
name = str.slice(i + 1, j)
if (!lexical.isInteger(name)) { // foo[bar] vs. foo[1]
name = String(this.get(name))
}
push()
i = j + 1
}
break
case '.':// foo.bar, foo[0].bar
push()
i++
break
default:// foo.bar
name += str[i]
i++
}
}
push()
if (!seq.length) {
throw new TypeError(`invalid path:"${str}"`)
}
return seq
function push () {
if (name.length) seq.push(name)
name = ''
}
}
}
exports.factory = function(ctx, opts) {
opts = _.assign({
strict_variables: false,
strict_filters: false,
blocks: {},
root: []
}, opts);
function toLiquid (obj) {
if (_.isFunction(obj.to_liquid)) {
return obj.to_liquid()
}
if (_.isFunction(obj.toLiquid)) {
return obj.toLiquid()
}
return obj
}
ctx = _.assign(ctx, {
liquid: opts
});
function readSize (obj) {
if (!_.isNil(obj.size)) return obj.size
if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj.size
}
var scope = Object.create(Scope);
scope.opts = opts;
scope.scopes = [ctx];
return scope;
};
function matchRightBracket (str, begin) {
let stack = 1 // count of '[' - count of ']'
for (let i = begin; i < str.length; i++) {
if (str[i] === '[') {
stack++
}
if (str[i] === ']') {
stack--
if (stack === 0) {
return i
}
}
}
return -1
}
export function factory (ctx, opts) {
const defaultOptions = {
dynamicPartials: true,
strict_variables: false,
strict_filters: false,
blocks: {},
root: []
}
const scope = _.create(Scope)
scope.opts = _.assign(defaultOptions, opts)
scope.contexts = [ctx || {}]
return scope
}
+41 -43
View File
@@ -1,56 +1,54 @@
const operators = require('./operators.js');
const lexical = require('./lexical.js');
const assert = require('../src/util/assert.js');
import Operators from './operators.js'
import * as lexical from './lexical.js'
import assert from './util/assert.js'
function evalExp(exp, scope) {
assert(scope, 'unable to evalExp: scope undefined');
var operatorREs = lexical.operators,
match;
for (var i = 0; i < operatorREs.length; i++) {
var operatorRE = operatorREs[i];
var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`);
if (match = exp.match(expRE)) {
var l = evalExp(match[1], scope);
var op = operators[match[2].trim()];
var r = evalExp(match[3], scope);
return op(l, r);
}
const operators = Operators(isTruthy)
export function evalExp (exp, scope) {
assert(scope, 'unable to evalExp: scope undefined')
const operatorREs = lexical.operators
let match
for (let i = 0; i < operatorREs.length; i++) {
const operatorRE = operatorREs[i]
const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
if ((match = exp.match(expRE))) {
const l = evalExp(match[1], scope)
const op = operators[match[2].trim()]
const r = evalExp(match[3], scope)
return op(l, r)
}
}
if (match = exp.match(lexical.rangeLine)) {
var low = evalValue(match[1], scope),
high = evalValue(match[2], scope);
var range = [];
for (var j = low; j <= high; j++) {
range.push(j);
}
return range;
if ((match = exp.match(lexical.rangeLine))) {
const low = evalValue(match[1], scope)
const high = evalValue(match[2], scope)
const range = []
for (let j = low; j <= high; j++) {
range.push(j)
}
return range
}
return evalValue(exp, scope);
return evalValue(exp, scope)
}
function evalValue(str, scope) {
str = str && str.trim();
if (!str) return undefined;
export function evalValue (str, scope) {
str = str && str.trim()
if (!str) return undefined
if (lexical.isLiteral(str)) {
return lexical.parseLiteral(str);
}
if (lexical.isVariable(str)) {
return scope.get(str);
}
if (lexical.isLiteral(str)) {
return lexical.parseLiteral(str)
}
if (lexical.isVariable(str)) {
return scope.get(str)
}
throw new TypeError(`cannot eval '${str}' as value`)
}
function isTruthy(val) {
if (val instanceof Array) return !!val.length;
return !!val;
export function isTruthy (val) {
return !isFalsy(val)
}
function isFalsy(val) {
return !isTruthy(val);
export function isFalsy (val) {
return val === false || undefined === val || val === null
}
module.exports = {
evalExp, evalValue, isTruthy, isFalsy
};
+54 -64
View File
@@ -1,73 +1,63 @@
const lexical = require('./lexical.js');
const _ = require('./util/underscore.js');
const Promise = require('any-promise');
const Syntax = require('./syntax.js');
const assert = require('./util/assert.js');
import { hashCapture } from './lexical.js'
import { create } from './util/underscore.js'
import { evalValue } from './syntax.js'
import assert from './util/assert.js'
function hash(markup, scope) {
var obj = {},
match;
lexical.hashCapture.lastIndex = 0;
while (match = lexical.hashCapture.exec(markup)) {
var k = match[1],
v = match[2];
obj[k] = Syntax.evalValue(v, scope);
}
return obj;
function hash (markup, scope) {
const obj = {}
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
const k = match[1]
const v = match[2]
obj[k] = evalValue(v, scope)
}
return obj
}
module.exports = function() {
var tagImpls = {};
export default function () {
let tagImpls = {}
var _tagInstance = {
render: function(scope) {
var obj = hash(this.token.args, scope);
var impl = this.tagImpl;
if (typeof impl.render !== 'function') {
return Promise.resolve('');
}
return Promise.resolve()
.then(() => typeof impl.render === 'function' ?
impl.render(scope, obj) : '')
.catch(function(e) {
if (_.isError(e)) {
throw e;
}
var msg = `Please reject with an Error in ${impl.render}, got ${e}`;
throw new Error(msg);
});
},
parse: function(token, tokens) {
this.type = 'tag';
this.token = token;
this.name = token.name;
const _tagInstance = {
render: async function (scope) {
const obj = hash(this.token.args, scope)
const impl = this.tagImpl
if (typeof impl.render !== 'function') {
return ''
}
return impl.render(scope, obj)
},
parse: function (token, tokens) {
this.type = 'tag'
this.token = token
this.name = token.name
var tagImpl = tagImpls[this.name];
assert(tagImpl, `tag ${this.name} not found`);
this.tagImpl = Object.create(tagImpl);
if (this.tagImpl.parse) {
this.tagImpl.parse(token, tokens);
}
}
};
function register(name, tag) {
tagImpls[name] = tag;
const tagImpl = tagImpls[this.name]
assert(tagImpl, `tag ${this.name} not found`)
this.tagImpl = create(tagImpl)
if (this.tagImpl.parse) {
this.tagImpl.parse(token, tokens)
}
}
}
function construct(token, tokens) {
var instance = Object.create(_tagInstance);
instance.parse(token, tokens);
return instance;
}
function register (name, tag) {
tagImpls[name] = tag
}
function clear() {
tagImpls = {};
}
function construct (token, tokens) {
const instance = create(_tagInstance)
instance.parse(token, tokens)
return instance
}
return {
construct,
register,
clear
};
};
function clear () {
tagImpls = {}
}
return {
construct,
register,
clear
}
}
+23
View File
@@ -0,0 +1,23 @@
import assert from '../util/assert.js'
import { identifier } from '../lexical.js'
import { create } from '../util/underscore.js'
export default function (liquid, Liquid) {
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
const { AssignScope } = Liquid.Types
liquid.registerTag('assign', {
parse: function (token) {
const match = token.args.match(re)
assert(match, `illegal token ${token.raw}`)
this.key = match[1]
this.value = match[2]
},
render: function (scope) {
const ctx = create(AssignScope)
ctx[this.key] = liquid.evalValue(this.value, scope)
scope.push(ctx)
return Promise.resolve('')
}
})
}
+32
View File
@@ -0,0 +1,32 @@
import assert from '../util/assert.js'
import { create } from '../util/underscore.js'
import { identifier } from '../lexical.js'
export default function (liquid, Liquid) {
const re = new RegExp(`(${identifier.source})`)
const { CaptureScope } = Liquid.Types
liquid.registerTag('capture', {
parse: function (tagToken, remainTokens) {
const match = tagToken.args.match(re)
assert(match, `${tagToken.args} not valid identifier`)
this.variable = match[1]
this.templates = []
const stream = liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', token => stream.stop())
.on('template', tpl => this.templates.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope, hash) {
const html = await liquid.renderer.renderTemplates(this.templates, scope)
const ctx = create(CaptureScope)
ctx[this.variable] = html
scope.push(ctx)
}
})
}
+39
View File
@@ -0,0 +1,39 @@
export default function (liquid, Liquid) {
liquid.registerTag('case', {
parse: function (tagToken, remainTokens) {
this.cond = tagToken.args
this.cases = []
this.elseTemplates = []
let p = []
const stream = liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
this.cases.push({
val: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = Liquid.evalExp(branch.val, scope)
const cond = Liquid.evalExp(this.cond, scope)
if (val === cond) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
})
}
+15
View File
@@ -0,0 +1,15 @@
export default function (liquid) {
liquid.registerTag('comment', {
parse: function (tagToken, remainTokens) {
const stream = liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
if (token.name === 'endcomment') stream.stop()
})
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
}
})
}
+43
View File
@@ -0,0 +1,43 @@
import assert from '../util/assert.js'
import { value as rValue } from '../lexical.js'
export default function (liquid, Liquid) {
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(rValue.source, 'g')
liquid.registerTag('cycle', {
parse: function (tagToken, remainTokens) {
let match = groupRE.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.group = match[1] || ''
const candidates = match[2]
this.candidates = []
while ((match = candidatesRE.exec(candidates))) {
this.candidates.push(match[0])
}
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: function (scope, hash) {
const group = Liquid.evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.opts.groups = scope.opts.groups || {}
let idx = groups[fingerprint]
if (idx === undefined) {
idx = groups[fingerprint] = 0
}
const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
return Liquid.evalValue(candidate, scope)
}
})
}
+32
View File
@@ -0,0 +1,32 @@
import { create } from '../util/underscore.js'
import assert from '../util/assert.js'
import { identifier } from '../lexical.js'
export default function (liquid, Liquid) {
const { CaptureScope, AssignScope, DecrementScope } = Liquid.Types
liquid.registerTag('decrement', {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
let context = scope.findContextFor(
this.variable,
ctx => {
const proto = Object.getPrototypeOf(ctx)
return proto !== CaptureScope && proto !== AssignScope
}
)
if (!context) {
context = create(DecrementScope)
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
return --context[this.variable]
}
})
}
+93
View File
@@ -0,0 +1,93 @@
import { mapSeries } from '../util/promise.js'
import { isString, isObject, isArray } from '../util/underscore.js'
import assert from '../util/assert.js'
import { identifier, value, hash } from '../lexical.js'
export default function (liquid, Liquid) {
const RenderBreakError = Liquid.Types.RenderBreakError
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*` +
`(?:\\s+(reversed))?` +
`(?:\\s+${hash.source})*$`)
liquid.registerTag('for', { parse, render })
function parse (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.reversed = !!match[3]
this.templates = []
this.elseTemplates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
}
async function render (scope, hash) {
let collection = Liquid.evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
collection = [collection]
} else if (isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]])
}
}
if (!isArray(collection) || !collection.length) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
const contexts = collection.map((item, i) => {
const ctx = {}
ctx[this.variable] = item
ctx.forloop = {
first: i === 0,
index: i + 1,
index0: i,
last: i === collection.length - 1,
length: collection.length,
rindex: collection.length - i,
rindex0: collection.length - i - 1
}
return ctx
})
let html = ''
let finished = false
await mapSeries(contexts, async context => {
if (finished) return
scope.push(context)
try {
html += await liquid.renderer.renderTemplates(this.templates, scope)
} catch (e) {
if (e instanceof RenderBreakError) {
html += e.resolvedHTML
if (e.message === 'break') {
finished = true
}
} else throw e
}
scope.pop(context)
})
return html
}
}
+40
View File
@@ -0,0 +1,40 @@
export default function (liquid, Liquid) {
liquid.registerTag('if', {
parse: function (tagToken, remainTokens) {
this.branches = []
this.elseTemplates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
cond: tagToken.args,
templates: (p = [])
}))
.on('tag:elsif', token => {
this.branches.push({
cond: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
for (const branch of this.branches) {
const cond = Liquid.evalExp(branch.cond, scope)
if (Liquid.isTruthy(cond)) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
})
}
+57
View File
@@ -0,0 +1,57 @@
import assert from '../util/assert.js'
import { value, quotedLine } from '../lexical.js'
const staticFileRE = /[^\s,]+/
export default function (liquid, Liquid) {
const withRE = new RegExp(`with\\s+(${value.source})`)
liquid.registerTag('include', {
parse: function (token) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticValue = match[0]
}
match = value.exec(token.args)
if (match) {
this.value = match[0]
}
match = withRE.exec(token.args)
if (match) {
this.with = match[1]
}
},
render: async function (scope, hash) {
let filepath
if (scope.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await liquid.parseAndRender(template, scope.getAll(), scope.opts)
} else {
filepath = Liquid.evalValue(this.value, scope)
}
} else {
filepath = this.staticValue
}
assert(filepath, `cannot include with empty filename`)
const originBlocks = scope.opts.blocks
const originBlockMode = scope.opts.blockMode
scope.opts.blocks = {}
scope.opts.blockMode = 'output'
if (this.with) {
hash[filepath] = Liquid.evalValue(this.with, scope)
}
const templates = await liquid.getTemplate(filepath, scope.opts.root)
scope.push(hash)
const html = await liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
scope.opts.blocks = originBlocks
scope.opts.blockMode = originBlockMode
return html
}
})
}
+34
View File
@@ -0,0 +1,34 @@
import assert from '../util/assert.js'
import { create } from '../util/underscore.js'
import { identifier } from '../lexical.js'
export default function (liquid, Liquid) {
const { CaptureScope, AssignScope, IncrementScope } = Liquid.Types
liquid.registerTag('increment', {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
let context = scope.findContextFor(
this.variable,
ctx => {
const proto = Object.getPrototypeOf(ctx)
return proto !== CaptureScope && proto !== AssignScope
}
)
if (!context) {
context = create(IncrementScope)
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
const val = context[this.variable]
context[this.variable]++
return val
}
})
}
+31
View File
@@ -0,0 +1,31 @@
import For from './for.js'
import Assign from './assign.js'
import Capture from './capture.js'
import Case from './case.js'
import Comment from './comment.js'
import Include from './include.js'
import Decrement from './decrement.js'
import Cycle from './cycle.js'
import If from './if.js'
import Increment from './increment.js'
import Layout from './layout.js'
import Raw from './raw.js'
import Tablerow from './tablerow.js'
import Unless from './unless.js'
export default function (engine, Liquid) {
Assign(engine, Liquid)
Capture(engine, Liquid)
Case(engine, Liquid)
Comment(engine, Liquid)
Cycle(engine, Liquid)
Decrement(engine, Liquid)
For(engine, Liquid)
If(engine, Liquid)
Include(engine, Liquid)
Increment(engine, Liquid)
Layout(engine, Liquid)
Raw(engine, Liquid)
Tablerow(engine, Liquid)
Unless(engine, Liquid)
}
+75
View File
@@ -0,0 +1,75 @@
import assert from '../util/assert.js'
import { value as rValue } from '../lexical.js'
/*
* blockMode:
* * "store": store rendered html into blocks
* * "output": output rendered html
*/
export default function (liquid, Liquid) {
const staticFileRE = /\S+/
liquid.registerTag('layout', {
parse: function (token, remainTokens) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticLayout = match[0]
}
match = rValue.exec(token.args)
if (match) {
this.layout = match[0]
}
this.tpls = liquid.parser.parse(remainTokens)
},
render: async function (scope, hash) {
const layout = scope.opts.dynamicPartials
? Liquid.evalValue(this.layout, scope)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
scope.opts.blockMode = 'store'
const html = await liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.opts.blocks[''] === undefined) {
scope.opts.blocks[''] = html
}
const templates = await liquid.getTemplate(layout, scope.opts.root)
scope.push(hash)
scope.opts.blockMode = 'output'
const partial = await liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
return partial
}
})
liquid.registerTag('block', {
parse: function (token, remainTokens) {
const match = /\w+/.exec(token.args)
this.block = match ? match[0] : ''
this.tpls = []
const stream = liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`)
})
stream.start()
},
render: async function (scope) {
const childDefined = scope.opts.blocks[this.block]
const html = childDefined !== undefined
? childDefined
: await liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.opts.blockMode === 'store') {
scope.opts.blocks[this.block] = html
return ''
}
return html
}
})
}
+21
View File
@@ -0,0 +1,21 @@
export default function (liquid) {
liquid.registerTag('raw', {
parse: function (tagToken, remainTokens) {
this.tokens = []
const stream = liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
if (token.name === 'endraw') stream.stop()
else this.tokens.push(token)
})
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
return this.tokens.map(token => token.raw).join('')
}
})
}
+70
View File
@@ -0,0 +1,70 @@
import { mapSeries } from '../util/promise.js'
import assert from '../util/assert.js'
import { identifier, value, hash } from '../lexical.js'
export default function (liquid, Liquid) {
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*$`)
liquid.registerTag('tablerow', {
parse: function (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.templates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope, hash) {
let collection = Liquid.evalExp(this.collection, scope) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
const contexts = collection.map((item, i) => {
const ctx = {}
ctx[this.variable] = item
return ctx
})
let row
let html = ''
await mapSeries(contexts, async (context, idx) => {
row = Math.floor(idx / cols) + 1
const col = (idx % cols) + 1
if (col === 1) {
if (row !== 1) {
html += '</tr>'
}
html += `<tr class="row${row}">`
}
html += `<td class="col${col}">`
scope.push(context)
html += await liquid.renderer.renderTemplates(this.templates, scope)
html += '</td>'
scope.pop(context)
return html
})
if (row > 0) {
html += '</tr>'
}
return html
}
})
}
+29
View File
@@ -0,0 +1,29 @@
export default function (liquid, Liquid) {
liquid.registerTag('unless', {
parse: function (tagToken, remainTokens) {
this.templates = []
this.elseTemplates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', x => {
p = this.templates
this.cond = tagToken.args
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endunless', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
const cond = Liquid.evalExp(this.cond, scope)
return Liquid.isFalsy(cond)
? liquid.renderer.renderTemplates(this.templates, scope)
: liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
})
}
+52
View File
@@ -0,0 +1,52 @@
import { last, isArray } from './util/underscore'
function domResolve (root, path) {
const base = document.createElement('base')
base.href = root
const head = document.getElementsByTagName('head')[0]
head.insertBefore(base, head.firstChild)
const a = document.createElement('a')
a.href = path
const resolved = a.href
head.removeChild(base)
return resolved
}
export function resolve (filepath, root, options) {
root = root || options.root
if (isArray(root)) {
root = root[0]
}
if (root.length && last(root) !== '/') {
root += '/'
}
const url = domResolve(root, filepath)
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
const last = path.split('/').pop()
if (/\.\w+$/.test(last)) {
return str
}
return origin + path + options.extname
})
}
export async function read (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.onerror = () => {
reject(new Error('An error occurred whilst receiving the response.'))
}
xhr.open('GET', url)
xhr.send()
})
}
+29
View File
@@ -0,0 +1,29 @@
import * as _ from './util/underscore.js'
import path from 'path'
import { anySeries } from './util/promise.js'
import fs from 'fs'
const statFileAsync = _.promisify(fs.stat)
const readFileAsync = _.promisify(fs.readFile)
export async function resolve (filepath, root, options) {
if (!path.extname(filepath)) {
filepath += options.extname
}
root = options.root.concat(root || [])
root = _.uniq(root)
const paths = root.map(root => path.resolve(root, filepath))
return anySeries(paths, async path => {
try {
await statFileAsync(path)
return path
} catch (e) {
e.message = `${e.code}: Failed to lookup ${filepath} in: ${root}`
throw e
}
})
}
export async function read (filepath) {
return readFileAsync(filepath, 'utf8')
}
+77 -61
View File
@@ -1,74 +1,90 @@
const lexical = require('./lexical.js');
const TokenizationError = require('./util/error.js').TokenizationError;
const _ = require('./util/underscore.js');
const assert = require('../src/util/assert.js');
import * as lexical from './lexical.js'
import { TokenizationError } from './util/error.js'
import * as _ from './util/underscore.js'
import assert from './util/assert.js'
import whiteSpaceCtrl from './whitespace-ctrl.js'
function parse(html, filepath) {
assert(_.isString(html), 'illegal input type');
export { default as whiteSpaceCtrl } from './whitespace-ctrl.js'
var tokens = [];
var syntax = /({%(.*?)%})|({{(.*?)}})/g;
var result, htmlFragment, token;
var lastMatchEnd = 0, lastMatchBegin = -1, parsedLinesCount = 0;
export function parse (input, file, options) {
assert(_.isString(input), 'illegal input')
while ((result = syntax.exec(html)) !== null) {
// passed html fragments
if (result.index > lastMatchEnd) {
htmlFragment = html.slice(lastMatchEnd, result.index);
tokens.push({
type: 'html',
raw: htmlFragment,
value: htmlFragment
});
}
// tag appeared
if (result[1]) {
token = factory('tag', 1, result);
const rLiquid = /({%-?([\s\S]*?)-?%})|({{-?([\s\S]*?)-?}})/g
let currIndent = 0
const lineNumber = LineNumber(input)
let lastMatchEnd = 0
const tokens = []
var match = token.value.match(lexical.tagLine);
if (!match) {
throw new TokenizationError(`illegal tag syntax`, token);
}
token.name = match[1];
token.args = match[2];
tokens.push(token);
}
// output
else { token = factory('output', 3, result);
tokens.push(token);
}
lastMatchEnd = syntax.lastIndex;
for (let match; (match = rLiquid.exec(input)); lastMatchEnd = rLiquid.lastIndex) {
if (match.index > lastMatchEnd) {
tokens.push(parseHTMLToken(lastMatchEnd, match.index))
}
tokens.push(match[1]
? parseTagToken(match[1], match[2].trim(), match.index)
: parseValueToken(match[3], match[4].trim(), match.index))
}
if (input.length > lastMatchEnd) {
tokens.push(parseHTMLToken(lastMatchEnd, input.length))
}
whiteSpaceCtrl(tokens, options)
return tokens
// remaining html
if (html.length > lastMatchEnd) {
htmlFragment = html.slice(lastMatchEnd, html.length);
tokens.push({
type: 'html',
raw: htmlFragment,
value: htmlFragment
});
function parseTagToken (raw, value, pos) {
const match = value.match(lexical.tagLine)
const token = {
type: 'tag',
indent: currIndent,
line: lineNumber.get(pos),
trim_left: raw.slice(0, 3) === '{%-',
trim_right: raw.slice(-3) === '-%}',
raw,
value,
input,
file
}
return tokens;
if (!match) {
throw new TokenizationError(`illegal tag syntax`, token)
}
token.name = match[1]
token.args = match[2]
return token
}
function factory(type, offset, match) {
return {
type: type,
raw: match[offset],
value: match[offset + 1].trim(),
line: getLineNum(match),
input: html,
file: filepath
};
function parseValueToken (raw, value, pos) {
return {
type: 'value',
line: lineNumber.get(pos),
trim_left: raw.slice(0, 3) === '{{-',
trim_right: raw.slice(-3) === '-}}',
raw,
value,
input,
file
}
}
function getLineNum(match) {
var lines = match.input.slice(lastMatchBegin + 1, match.index).split('\n');
parsedLinesCount += lines.length - 1;
lastMatchBegin = match.index;
return parsedLinesCount + 1;
function parseHTMLToken (begin, end) {
const htmlFragment = input.slice(begin, end)
currIndent = _.last((htmlFragment).split('\n')).length
return {
type: 'html',
raw: htmlFragment,
value: htmlFragment
}
}
}
exports.parse = parse;
function LineNumber (html) {
let parsedLinesCount = 0
let lastMatchBegin = -1
return {
get: function (pos) {
const lines = html.slice(lastMatchBegin + 1, pos).split('\n')
parsedLinesCount += lines.length - 1
lastMatchBegin = pos
return parsedLinesCount + 1
}
}
}
+6 -11
View File
@@ -1,13 +1,8 @@
const AssertionError = require('./error.js').AssertionError;
import { AssertionError } from './error.js'
function assert(predicate, message) {
if (!predicate) {
if (message instanceof Error) {
throw message;
}
var message = message || `expect ${predicate} to be true`;
throw new AssertionError(message);
}
export default function (predicate, message) {
if (!predicate) {
message = message || `expect ${predicate} to be true`
throw new AssertionError(message)
}
}
module.exports = assert;
+93 -109
View File
@@ -1,116 +1,100 @@
const _ = require('./underscore.js');
import * as _ from './underscore.js'
function TokenizationError(message, token) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = mkMessage(message, token);
this.stack = context + '\n' + (this.stack || '');
}
TokenizationError.prototype = Object.create(Error.prototype);
TokenizationError.prototype.constructor = TokenizationError;
function ParseError(e, token) {
this.name = this.constructor.name;
this.stack = e.stack;
this.input = token.input;
this.line = token.line;
this.file = token.file;
var context = mkContext(token.input, token.line);
this.message = mkMessage(e.message, token);
this.stack = context + '\n' + (this.stack || '');
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;
function RenderError(e, tpl) {
// return the original render error
if(e instanceof RenderError){
return e;
}
this.name = this.constructor.name;
this.stack = e.stack;
this.input = tpl.token.input;
this.line = tpl.token.line;
this.file = tpl.token.file;
var context = mkContext(tpl.token.input, tpl.token.line);
this.message = mkMessage(e.message, tpl.token);
this.stack = context + '\n' + (e.stack || '');
}
RenderError.prototype = Object.create(Error.prototype);
RenderError.prototype.constructor = RenderError;
function RenderBreakError(message) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.message = message || '';
}
RenderBreakError.prototype = Object.create(Error.prototype);
RenderBreakError.prototype.constructor = RenderBreakError;
function AssertionError(message) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.name = this.constructor.name;
this.message = message;
}
AssertionError.prototype = Object.create(Error.prototype);
AssertionError.prototype.constructor = AssertionError;
function mkContext(input, line) {
var lines = input.split('\n');
var begin = Math.max(line - 2, 1);
var end = Math.min(line + 3, lines.length);
var context = _
.range(begin, end + 1)
.map(l => [
(l === line) ? '>> ' : ' ',
align(l, end),
'| ',
lines[l - 1]
].join(''))
.join('\n');
return context;
function initError () {
this.name = this.constructor.name
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
function align(n, max) {
var length = (max + '').length;
var str = n + '';
var blank = Array(length - str.length).join(' ');
return blank + str;
function initLiquidError (err, token) {
initError.call(this)
this.input = token.input
this.line = token.line
this.file = token.file
const context = mkContext(token.input, token.line)
this.message = mkMessage(err.message, token)
this.stack = context +
'\n' + (this.stack || this.message) +
(err.stack ? '\nFrom ' + err.stack : '')
}
function mkMessage(msg, token){
msg = msg || '';
if(token.file){
msg += ', file:' + token.file;
}
if(token.line){
msg += ', line:' + token.line;
}
return msg;
export function TokenizationError (message, token) {
initLiquidError.call(this, { message: message }, token)
}
TokenizationError.prototype = _.create(Error.prototype)
TokenizationError.prototype.constructor = TokenizationError
export function ParseError (e, token) {
_.assign(this, e)
this.originalError = e
initLiquidError.call(this, e, token)
}
ParseError.prototype = _.create(Error.prototype)
ParseError.prototype.constructor = ParseError
export function RenderError (e, tpl) {
// return the original render error
if (e instanceof RenderError) {
return e
}
_.assign(this, e)
this.originalError = e
initLiquidError.call(this, e, tpl.token)
}
RenderError.prototype = _.create(Error.prototype)
RenderError.prototype.constructor = RenderError
export function RenderBreakError (message) {
initError.call(this)
this.message = message + ''
}
RenderBreakError.prototype = _.create(Error.prototype)
RenderBreakError.prototype.constructor = RenderBreakError
export function AssertionError (message) {
initError.call(this)
this.message = message + ''
}
AssertionError.prototype = _.create(Error.prototype)
AssertionError.prototype.constructor = AssertionError
function mkContext (input, line) {
const lines = input.split('\n')
const begin = Math.max(line - 2, 1)
const end = Math.min(line + 3, lines.length)
const context = _
.range(begin, end + 1)
.map(l => [
(l === line) ? '>> ' : ' ',
align(l, end),
'| ',
lines[l - 1]
].join(''))
.join('\n')
return context
}
module.exports = {
TokenizationError,
ParseError,
RenderBreakError,
AssertionError,
RenderError
};
function align (n, max) {
const length = (max + '').length
const str = n + ''
const blank = Array(length - str.length).join(' ')
return blank + str
}
function mkMessage (msg, token) {
msg = msg || ''
if (token.file) {
msg += ', file:' + token.file
}
if (token.line) {
msg += ', line:' + token.line
}
return msg
}
-20
View File
@@ -1,20 +0,0 @@
const fs = require('fs');
function readFileAsync(filepath) {
return new Promise(function(resolve, reject) {
fs.readFile(filepath, 'utf8', function(err, content) {
err ? reject(err) : resolve(content);
});
});
};
function statFileAsync(path) {
return new Promise(function(resolve, reject) {
fs.stat(path, (err, stat) => err ? reject(err) : resolve(stat))
});
};
module.exports = {
readFileAsync,
statFileAsync
};
+17 -22
View File
@@ -1,35 +1,30 @@
const Promise = require('any-promise');
/*
* Call functions in serial until someone resolved.
* @param {Array} iterable the array to iterate with.
* @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
function anySeries(iterable, iteratee) {
var ret = Promise.reject(new Error('init'));
iterable.forEach(function(item, idx) {
ret = ret.catch(e => iteratee(item, idx, iterable));
});
return ret;
export function anySeries (iterable, iteratee) {
let ret = Promise.reject(new Error('init'))
iterable.forEach(function (item, idx) {
ret = ret.catch(e => iteratee(item, idx, iterable))
})
return ret
}
/*
* Call functions in serial until someone rejected.
* @param {Array} iterable the array to iterate with.
* @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
function mapSeries(iterable, iteratee) {
var ret = Promise.resolve('init');
var result = [];
iterable.forEach(function(item, idx) {
ret = ret
.then(() => iteratee(item, idx, iterable))
.then(x => result.push(x));
});
return ret.then(() => result);
export function mapSeries (iterable, iteratee) {
let ret = Promise.resolve('init')
const result = []
iterable.forEach(function (item, idx) {
ret = ret
.then(() => iteratee(item, idx, iterable))
.then(x => result.push(x))
})
return ret.then(() => result)
}
exports.anySeries = anySeries;
exports.mapSeries = mapSeries;
+185 -187
View File
@@ -1,200 +1,198 @@
var monthNames = [
"January", "February", "March", "April", "May", "June", "July", "August",
"September", "October", "November", "December"
];
var monthNamesShort = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
"Nov", "Dec"
];
var dayNames = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
];
var dayNamesShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var suffixes = {
1: 'st',
2: 'nd',
3: 'rd',
'default': 'th'
};
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
]
const dayNames = [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
]
const monthNamesShort = monthNames.map(abbr)
const dayNamesShort = dayNames.map(abbr)
const suffixes = {
1: 'st',
2: 'nd',
3: 'rd',
'default': 'th'
}
function abbr (str) {
return str.slice(0, 3)
}
// prototype extensions
var _date = {
daysInMonth: function(d) {
var feb = _date.isLeapYear(d) ? 29 : 28;
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
},
const _date = {
daysInMonth: function (d) {
const feb = _date.isLeapYear(d) ? 29 : 28
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
},
getDayOfYear: function(d) {
var num = 0;
for (var i = 0; i < d.getMonth(); ++i) {
num += _date.daysInMonth(d)[i];
}
return num + d.getDate();
},
// Startday is an integer of which day to start the week measuring from
// TODO: that comment was retarted. fix it.
getWeekOfYear: function(d, startDay) {
// Skip to startDay of this week
var now = this.getDayOfYear(d) + (startDay - d.getDay());
// Find the first startDay of the year
var jan1 = new Date(d.getFullYear(), 0, 1);
var then = (7 - jan1.getDay() + startDay);
return _number.pad(Math.floor((now - then) / 7) + 1, 2);
},
isLeapYear: function(d) {
var year = d.getFullYear();
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)));
},
getSuffix: function(d) {
var str = d.getDate().toString();
var index = parseInt(str.slice(-1));
return suffixes[index] || suffixes['default'];
},
century: function(d) {
return parseInt(d.getFullYear().toString().substring(0, 2), 10);
getDayOfYear: function (d) {
let num = 0
for (let i = 0; i < d.getMonth(); ++i) {
num += _date.daysInMonth(d)[i]
}
};
return num + d.getDate()
},
var _number = {
pad: function(value, size, ch) {
if (!ch) ch = '0';
var result = value.toString();
var pad = size - result.length;
// Startday is an integer of which day to start the week measuring from
// TODO: that comment was retarted. fix it.
getWeekOfYear: function (d, startDay) {
// Skip to startDay of this week
const now = this.getDayOfYear(d) + (startDay - d.getDay())
// Find the first startDay of the year
const jan1 = new Date(d.getFullYear(), 0, 1)
const then = (7 - jan1.getDay() + startDay)
return _number.pad(Math.floor((now - then) / 7) + 1, 2)
},
while (pad-- > 0) {
result = ch + result;
}
isLeapYear: function (d) {
const year = d.getFullYear()
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
},
return result;
getSuffix: function (d) {
const str = d.getDate().toString()
const index = parseInt(str.slice(-1))
return suffixes[index] || suffixes['default']
},
century: function (d) {
return parseInt(d.getFullYear().toString().substring(0, 2), 10)
}
}
const _number = {
pad: function (value, size, ch) {
if (!ch) ch = '0'
let result = value.toString()
let pad = size - result.length
while (pad-- > 0) {
result = ch + result
}
};
return result
}
}
var format_codes = {
a: function(d) {
return dayNamesShort[d.getDay()];
},
A: function(d) {
return dayNames[d.getDay()];
},
b: function(d) {
return monthNamesShort[d.getMonth()];
},
B: function(d) {
return monthNames[d.getMonth()];
},
c: function(d) {
return d.toLocaleString();
},
C: function(d) {
return _date.century(d);
},
d: function(d) {
return _number.pad(d.getDate(), 2);
},
e: function(d) {
return _number.pad(d.getDate(), 2, ' ');
},
H: function(d) {
return _number.pad(d.getHours(), 2);
},
I: function(d) {
return _number.pad(d.getHours() % 12 || 12, 2);
},
j: function(d) {
return _number.pad(_date.getDayOfYear(d), 3);
},
k: function(d) {
return _number.pad(d.getHours(), 2, ' ');
},
l: function(d) {
return _number.pad(d.getHours() % 12 || 12, 2, ' ');
},
L: function(d) {
return _number.pad(d.getMilliseconds(), 3);
},
m: function(d) {
return _number.pad(d.getMonth() + 1, 2);
},
M: function(d) {
return _number.pad(d.getMinutes(), 2);
},
p: function(d) {
return (d.getHours() < 12 ? 'AM' : 'PM');
},
P: function(d) {
return (d.getHours() < 12 ? 'am' : 'pm');
},
q: function(d) {
return _date.getSuffix(d);
},
s: function(d) {
return Math.round(d.valueOf() / 1000);
},
S: function(d) {
return _number.pad(d.getSeconds(), 2);
},
u: function(d) {
return d.getDay() || 7;
},
U: function(d) {
return _date.getWeekOfYear(d, 0);
},
w: function(d) {
return d.getDay();
},
W: function(d) {
return _date.getWeekOfYear(d, 1);
},
x: function(d) {
return d.toLocaleDateString();
},
X: function(d) {
return d.toLocaleTimeString();
},
y: function(d) {
return d.getFullYear().toString().substring(2, 4);
},
Y: function(d) {
return d.getFullYear();
},
z: function(d) {
var tz = d.getTimezoneOffset() / 60 * 100;
return (tz > 0 ? '-' : '+') + _number.pad(Math.abs(tz), 4);
},
"%": function() {
return '%';
const formatCodes = {
a: function (d) {
return dayNamesShort[d.getDay()]
},
A: function (d) {
return dayNames[d.getDay()]
},
b: function (d) {
return monthNamesShort[d.getMonth()]
},
B: function (d) {
return monthNames[d.getMonth()]
},
c: function (d) {
return d.toLocaleString()
},
C: function (d) {
return _date.century(d)
},
d: function (d) {
return _number.pad(d.getDate(), 2)
},
e: function (d) {
return _number.pad(d.getDate(), 2, ' ')
},
H: function (d) {
return _number.pad(d.getHours(), 2)
},
I: function (d) {
return _number.pad(d.getHours() % 12 || 12, 2)
},
j: function (d) {
return _number.pad(_date.getDayOfYear(d), 3)
},
k: function (d) {
return _number.pad(d.getHours(), 2, ' ')
},
l: function (d) {
return _number.pad(d.getHours() % 12 || 12, 2, ' ')
},
L: function (d) {
return _number.pad(d.getMilliseconds(), 3)
},
m: function (d) {
return _number.pad(d.getMonth() + 1, 2)
},
M: function (d) {
return _number.pad(d.getMinutes(), 2)
},
p: function (d) {
return (d.getHours() < 12 ? 'AM' : 'PM')
},
P: function (d) {
return (d.getHours() < 12 ? 'am' : 'pm')
},
q: function (d) {
return _date.getSuffix(d)
},
s: function (d) {
return Math.round(d.valueOf() / 1000)
},
S: function (d) {
return _number.pad(d.getSeconds(), 2)
},
u: function (d) {
return d.getDay() || 7
},
U: function (d) {
return _date.getWeekOfYear(d, 0)
},
w: function (d) {
return d.getDay()
},
W: function (d) {
return _date.getWeekOfYear(d, 1)
},
x: function (d) {
return d.toLocaleDateString()
},
X: function (d) {
return d.toLocaleTimeString()
},
y: function (d) {
return d.getFullYear().toString().substring(2, 4)
},
Y: function (d) {
return d.getFullYear()
},
z: function (d) {
const tz = d.getTimezoneOffset() / 60 * 100
return (tz > 0 ? '-' : '+') + _number.pad(Math.abs(tz), 4)
},
'%': function () {
return '%'
}
}
formatCodes.h = formatCodes.b
formatCodes.N = formatCodes.L
export default function (d, format) {
let output = ''
let remaining = format
while (true) {
const r = /%./g
const results = r.exec(remaining)
// No more format codes. Add the remaining text and return
if (!results) {
return output + remaining
}
};
format_codes.h = format_codes.b;
format_codes.N = format_codes.L;
var strftime = function(d, format) {
var output = '';
var remaining = format;
// Add the preceding text
output += remaining.slice(0, r.lastIndex - 2)
remaining = remaining.slice(r.lastIndex)
while (true) {
var r = /%./g;
var results = r.exec(remaining);
// No more format codes. Add the remaining text and return
if (!results) {
return output + remaining;
}
// Add the preceding text
output += remaining.slice(0, r.lastIndex - 2);
remaining = remaining.slice(r.lastIndex);
// Add the format code
var ch = results[0].charAt(1);
var func = format_codes[ch];
output += func ? func.call(this, d) : '%' + ch;
}
};
module.exports = strftime;
// Add the format code
const ch = results[0].charAt(1)
const func = formatCodes[ch]
output += func ? func.call(this, d) : '%' + ch
}
}
+109 -86
View File
@@ -1,41 +1,94 @@
const toStr = Object.prototype.toString
const arrToStr = Array.prototype.toString
/*
* Checks if value is classified as a String primitive or object.
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is a string, else false.
*/
function isString(value) {
return value instanceof String || typeof value === 'string';
export function isString (value) {
return toStr.call(value) === '[object String]'
}
function isError(value) {
var signature = Object.prototype.toString.call(value);
// [object XXXError]
return signature.substr(-6, 5) === 'Error' ||
(typeof value.message == 'string' && typeof value.name == 'string');
export function isFunction (value) {
return typeof value === 'function'
}
export function promisify (fn) {
return function () {
return new Promise((resolve, reject) => {
fn(...arguments, (err, result) => {
err ? reject(err) : resolve(result)
})
})
}
}
export function stringify (value) {
if (isNil(value)) return String(value)
if (isFunction(value.to_liquid)) return stringify(value.to_liquid())
if (isFunction(value.toLiquid)) return stringify(value.toLiquid())
if (isFunction(value.to_s)) return value.to_s()
if ([toStr, arrToStr].indexOf(value.toString) > -1) return defaultToString(value)
if (isFunction(value.toString)) return value.toString()
return toStr.call(value)
}
function defaultToString (value) {
const cache = []
return JSON.stringify(value, (key, value) => {
if (isObject(value)) {
if (cache.indexOf(value) !== -1) {
return
}
cache.push(value)
}
return value
})
}
export function create (proto) {
return Object.create(proto)
}
export function isNil (value) {
return value === null || value === undefined
}
export function isArray (value) {
// be compatible with IE 8
return toStr.call(value) === '[object Array]'
}
export function isError (value) {
const signature = toStr.call(value)
// [object XXXError]
return signature.substr(-6, 5) === 'Error' ||
(typeof value.message === 'string' && typeof value.name === 'string')
}
/*
* Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
* The iteratee is invoked with three arguments: (value, key, object).
* Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
* The iteratee is invoked with three arguments: (value, key, object).
* Iteratee functions may exit iteration early by explicitly returning false.
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @return {Object} Returns object.
*/
function forOwn(object, iteratee) {
object = object || {};
for (var k in object) {
if (object.hasOwnProperty(k)) {
if (iteratee(object[k], k, object) === false) break;
}
export function forOwn (object, iteratee) {
object = object || {}
for (const k in object) {
if (object.hasOwnProperty(k)) {
if (iteratee(object[k], k, object) === false) break
}
return object;
}
return object
}
/*
* Assigns own enumerable string keyed properties of source objects to the destination object.
* Source objects are applied from left to right.
* Subsequent sources overwrite property assignments of previous sources.
* Assigns own enumerable string keyed properties of source objects to the destination object.
* Source objects are applied from left to right.
* Subsequent sources overwrite property assignments of previous sources.
*
* Note: This method mutates object and is loosely based on Object.assign.
*
@@ -43,89 +96,59 @@ function forOwn(object, iteratee) {
* @param {...Object} sources The source objects.
* @return {Object} Returns object.
*/
function assign(object) {
object = isObject(object) ? object : {};
var srcs = Array.prototype.slice.call(arguments, 1);
srcs.forEach(function(src) {
_assignBinary(object, src);
});
return object;
export function assign (object) {
object = isObject(object) ? object : {}
const srcs = Array.prototype.slice.call(arguments, 1)
srcs.forEach((src) => Object.assign(object, src))
return object
}
function _assignBinary(dst, src) {
if (!dst) return dst;
forOwn(src, function(v, k) {
dst[k] = v;
});
return dst;
export function last (arr) {
return arr[arr.length - 1]
}
function isArray(value) {
return value instanceof Array;
}
function echo(prefix) {
return v => {
console.log('[' + prefix + ']', v);
return v;
};
}
function uniq(arr) {
var u = {},
a = [];
for (var i = 0, l = arr.length; i < l; ++i) {
if (u.hasOwnProperty(arr[i])) {
continue;
}
a.push(arr[i]);
u[arr[i]] = 1;
export function uniq (arr) {
const u = {}
const a = []
for (let i = 0, l = arr.length; i < l; ++i) {
if (u.hasOwnProperty(arr[i])) {
continue
}
return a;
a.push(arr[i])
u[arr[i]] = 1
}
return a
}
/*
* Checks if value is the language type of Object.
* Checks if value is the language type of Object.
* (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is an object, else false.
*/
function isObject(value) {
return value !== null && typeof value === 'object';
export function isObject (value) {
const type = typeof value
return value !== null && (type === 'object' || type === 'function')
}
/*
* A function to create flexibly-numbered lists of integers,
* handy for each and map loops. start, if omitted, defaults to 0; step defaults to 1.
* Returns a list of integers from start (inclusive) to stop (exclusive),
* incremented (or decremented) by step, exclusive.
* Note that ranges that stop before they start are considered to be zero-length instead of
* A function to create flexibly-numbered lists of integers,
* handy for each and map loops. start, if omitted, defaults to 0; step defaults to 1.
* Returns a list of integers from start (inclusive) to stop (exclusive),
* incremented (or decremented) by step, exclusive.
* Note that ranges that stop before they start are considered to be zero-length instead of
* negative if you'd like a negative range, use a negative step.
*/
function range(start, stop, step) {
if (arguments.length === 1) {
stop = start;
start = 0;
}
step = step || 1;
export function range (start, stop, step) {
if (arguments.length === 1) {
stop = start
start = 0
}
step = step || 1
var arr = [];
for (var i = start; i < stop; i += step) {
arr.push(i);
}
return arr;
const arr = []
for (let i = start; i < stop; i += step) {
arr.push(i)
}
return arr
}
exports.isString = isString;
exports.isArray = isArray;
exports.isObject = isObject;
exports.isError = isError;
exports.range = range;
exports.forOwn = forOwn;
exports.assign = assign;
exports.uniq = uniq;
exports.echo = echo;
+45
View File
@@ -0,0 +1,45 @@
import { assign } from './util/underscore.js'
export default function whiteSpaceCtrl (tokens, options) {
options = assign({ greedy: true }, options)
let inRaw = false
tokens.forEach((token, i) => {
if (shouldTrimLeft(token, inRaw, options)) {
trimLeft(tokens[i - 1], options.greedy)
}
if (token.type === 'tag' && token.name === 'raw') inRaw = true
if (token.type === 'tag' && token.name === 'endraw') inRaw = false
if (shouldTrimRight(token, inRaw, options)) {
trimRight(tokens[i + 1], options.greedy)
}
})
}
function shouldTrimLeft (token, inRaw, options) {
if (inRaw) return false
if (token.type === 'tag') return token.trim_left || options.trim_tag_left
if (token.type === 'value') return token.trim_left || options.trim_value_left
}
function shouldTrimRight (token, inRaw, options) {
if (inRaw) return false
if (token.type === 'tag') return token.trim_right || options.trim_tag_right
if (token.type === 'value') return token.trim_right || options.trim_value_right
}
function trimLeft (token, greedy) {
if (!token || token.type !== 'html') return
const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
token.value = token.value.replace(rLeft, '')
}
function trimRight (token, greedy) {
if (!token || token.type !== 'html') return
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
token.value = token.value.replace(rRight, '')
}
-22
View File
@@ -1,22 +0,0 @@
const Liquid = require('..');
const lexical = Liquid.lexical;
const Promise = require('any-promise');
const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`);
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('assign', {
parse: function(token){
var match = token.args.match(re);
assert(match, `illegal token ${token.raw}`);
this.key = match[1];
this.value = match[2];
},
render: function(scope) {
scope.set(this.key, liquid.evalOutput(this.value, scope));
return Promise.resolve('');
}
});
};
-32
View File
@@ -1,32 +0,0 @@
const Liquid = require('..');
const lexical = Liquid.lexical;
const re = new RegExp(`(${lexical.identifier.source})`);
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('capture', {
parse: function(tagToken, remainTokens) {
var match = tagToken.args.match(re);
assert(match, `${tagToken.args} not valid identifier`);
this.variable = match[1];
this.templates = [];
var stream = liquid.parser.parseStream(remainTokens);
stream.on('tag:endcapture', token => stream.stop())
.on('template', tpl => this.templates.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
return liquid.renderer.renderTemplates(this.templates, scope)
.then((html) => {
scope.set(this.variable, html);
});
}
});
};
-45
View File
@@ -1,45 +0,0 @@
const Liquid = require('..');
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('case', {
parse: function(tagToken, remainTokens) {
this.cond = tagToken.args;
this.cases = [];
this.elseTemplates = [];
var p = [],
stream = liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
if (!this.cases[token.args]) {
this.cases.push({
val: token.args,
templates: p = []
});
}
})
.on('tag:else', token => p = this.elseTemplates)
.on('tag:endcase', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
for (var i = 0; i < this.cases.length; i++) {
var branch = this.cases[i];
var val = Liquid.evalExp(branch.val, scope);
var cond = Liquid.evalExp(this.cond, scope);
if (val === cond) {
return liquid.renderer.renderTemplates(branch.templates, scope);
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
}
});
};
-17
View File
@@ -1,17 +0,0 @@
module.exports = function(liquid) {
liquid.registerTag('comment', {
parse: function(tagToken, remainTokens) {
var stream = liquid.parser.parseStream(remainTokens);
stream
.on('token', token => {
if(token.name === 'endcomment') stream.stop();
})
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
}
});
};
-44
View File
@@ -1,44 +0,0 @@
const Liquid = require('..');
const Promise = require('any-promise');
const lexical = Liquid.lexical;
const groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`);
const candidatesRE = new RegExp(lexical.value.source, 'g');
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('cycle', {
parse: function(tagToken, remainTokens) {
var match = groupRE.exec(tagToken.args);
assert(match, `illegal tag: ${tagToken.raw}`);
this.group = match[1] || '';
var candidates = match[2];
this.candidates = [];
while(match = candidatesRE.exec(candidates)){
this.candidates.push(match[0]);
}
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`);
},
render: function(scope, hash) {
var group = Liquid.evalValue(this.group, scope);
var fingerprint = `cycle:${group}:` + this.candidates.join(',');
var register = scope.get('liquid');
var idx = register[fingerprint];
if(idx === undefined){
idx = register[fingerprint] = 0;
}
var candidate = this.candidates[idx];
idx = (idx + 1) % this.candidates.length;
register[fingerprint] = idx;
return Promise.resolve(Liquid.evalValue(candidate, scope));
}
});
};
-20
View File
@@ -1,20 +0,0 @@
const Liquid = require('..');
const lexical = Liquid.lexical;
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('decrement', {
parse: function(token) {
var match = token.args.match(lexical.identifier);
assert(match, `illegal identifier ${token.args}`);
this.variable = match[0];
},
render: function(scope, hash) {
var v = scope.get(this.variable);
if (typeof v !== 'number') v = 0;
scope.set(this.variable, v - 1);
}
});
};
-88
View File
@@ -1,88 +0,0 @@
const Liquid = require('..');
const lexical = Liquid.lexical;
const mapSeries = require('../src/util/promise.js').mapSeries;
const RenderBreakError = Liquid.Types.RenderBreakError;
const assert = require('../src/util/assert.js');
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*` +
`(?:\\s+(reversed))?$`);
module.exports = function(liquid) {
liquid.registerTag('for', {
parse: function(tagToken, remainTokens) {
var match = re.exec(tagToken.args);
assert(match, `illegal tag: ${tagToken.raw}`);
this.variable = match[1];
this.collection = match[2];
this.reversed = !!match[3];
this.templates = [];
this.elseTemplates = [];
var p, stream = liquid.parser.parseStream(remainTokens)
.on('start', () => p = this.templates)
.on('tag:else', () => p = this.elseTemplates)
.on('tag:endfor', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
var collection = Liquid.evalExp(this.collection, scope);
if (Liquid.isFalsy(collection)) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
}
var length = collection.length;
var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit;
collection = collection.slice(offset, offset + limit);
if (this.reversed) collection.reverse();
var contexts = collection.map((item, i) => {
var ctx = {};
ctx[this.variable] = item;
ctx.forloop = {
first: i === 0,
index: i + 1,
index0: i,
last: i === length - 1,
length: length,
rindex: length - i,
rindex0: length - i - 1,
stop: false,
skip: false
};
return ctx;
});
var html = '';
return mapSeries(contexts, (context) => {
scope.push(context);
return liquid.renderer
.renderTemplates(this.templates, scope)
.then(partial => html += partial)
.catch(e => {
if (e instanceof RenderBreakError) {
html += e.resolvedHTML;
if (e.message === 'continue') return;
}
throw e;
})
.then(() => scope.pop());
}).catch((e) => {
if (e instanceof RenderBreakError && e.message === 'break') {
return;
}
throw e;
}).then(() => html);
}
});
};
-46
View File
@@ -1,46 +0,0 @@
const Liquid = require('..');
module.exports = function(liquid) {
liquid.registerTag('if', {
parse: function(tagToken, remainTokens) {
this.branches = [];
this.elseTemplates = [];
var p, stream = liquid.parser.parseStream(remainTokens)
.on('start', x => this.branches.push({
cond: tagToken.args,
templates: p = []
}))
.on('tag:elsif', token => {
if (!this.branches[token.args]) {
this.branches.push({
cond: token.args,
templates: p = []
});
}
})
.on('tag:else', token => p = this.elseTemplates)
.on('tag:endif', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i];
var cond = Liquid.evalExp(branch.cond, scope);
if (Liquid.isTruthy(cond)) {
return liquid.renderer.renderTemplates(branch.templates, scope);
}
}
return liquid.renderer.renderTemplates(this.elseTemplates, scope);
}
});
};
-42
View File
@@ -1,42 +0,0 @@
const Liquid = require('..');
const lexical = Liquid.lexical;
const withRE = new RegExp(`with\\s+(${lexical.value.source})`);
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('include', {
parse: function(token){
var match = lexical.value.exec(token.args);
assert(match, `illegal token ${token.raw}`);
this.value = match[0];
match = withRE.exec(token.args);
if(match){
this.with = match[1];
}
},
render: function(scope, hash) {
var filepath = Liquid.evalValue(this.value, scope);
var register = scope.get('liquid');
var originBlocks = register.blocks;
register.blocks = {};
if(this.with){
hash[filepath] = Liquid.evalValue(this.with, scope);
}
return liquid.getTemplate(filepath, register.root)
.then((templates) => {
scope.push(hash);
return liquid.renderer.renderTemplates(templates, scope);
})
.then((html) => {
scope.pop();
register.blocks = originBlocks;
return html;
});
}
});
};
-20
View File
@@ -1,20 +0,0 @@
const Liquid = require('..');
const assert = require('../src/util/assert.js');
const lexical = Liquid.lexical;
module.exports = function(liquid) {
liquid.registerTag('increment', {
parse: function(token) {
var match = token.args.match(lexical.identifier);
assert(match, `illegal identifier ${token.args}`);
this.variable = match[0];
},
render: function(scope, hash) {
var v = scope.get(this.variable);
if (typeof v !== 'number') v = 0;
scope.set(this.variable, v + 1);
}
});
};
-16
View File
@@ -1,16 +0,0 @@
module.exports = function(engine){
require("./assign.js")(engine);
require("./capture.js")(engine);
require("./case.js")(engine);
require("./comment.js")(engine);
require("./cycle.js")(engine);
require("./decrement.js")(engine);
require("./for.js")(engine);
require("./if.js")(engine);
require("./include.js")(engine);
require("./increment.js")(engine);
require("./layout.js")(engine);
require("./raw.js")(engine);
require("./tablerow.js")(engine);
require("./unless.js")(engine);
};
-66
View File
@@ -1,66 +0,0 @@
const Liquid = require('..');
const Promise = require('any-promise');
const lexical = Liquid.lexical;
const assert = require('../src/util/assert.js');
module.exports = function(liquid) {
liquid.registerTag('layout', {
parse: function(token, remainTokens){
var match = lexical.value.exec(token.args);
assert(match, `illegal token ${token.raw}`);
this.layout = match[0];
this.tpls = liquid.parser.parse(remainTokens);
},
render: function(scope, hash) {
var layout = Liquid.evalValue(this.layout, scope);
var register = scope.get('liquid');
// render the remaining tokens immediately
return liquid.renderer.renderTemplates(this.tpls, scope)
// now register.blocks contains rendered blocks
.then(() => liquid.getTemplate(layout, register.root))
// push the hash
.then(templates => (scope.push(hash), templates))
// render the parent
.then(templates => liquid.renderer.renderTemplates(templates, scope))
// pop the hash
.then(partial => (scope.pop(), partial));
}
});
liquid.registerTag('block', {
parse: function(token, remainTokens){
var match = /\w+/.exec(token.args);
this.block = match ? match[0] : 'anonymous';
this.tpls = [];
var stream = liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`);
});
stream.start();
},
render: function(scope){
var register = scope.get('liquid');
var html = register.blocks[this.block];
// if not defined yet
if (html === undefined) {
return liquid.renderer.renderTemplates(this.tpls, scope)
.then((partial) => {
register.blocks[this.block] = partial;
return partial;
});
}
// if already defined by desendents
else {
register.blocks[this.block] = html;
return Promise.resolve(html);
}
}
});
};
-26
View File
@@ -1,26 +0,0 @@
const Promise = require('any-promise');
module.exports = function(liquid) {
liquid.registerTag('raw', {
parse: function(tagToken, remainTokens) {
this.tokens = [];
var stream = liquid.parser.parseStream(remainTokens);
stream
.on('token', token => {
if(token.name === 'endraw') stream.stop();
else this.tokens.push(token);
})
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
var tokens = this.tokens.map(token => token.raw).join('');
return Promise.resolve(tokens);
}
});
};
-95
View File
@@ -1,95 +0,0 @@
const Liquid = require('..');
const Promise = require('any-promise');
const lexical = Liquid.lexical;
const assert = require('../src/util/assert.js');
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*$`);
module.exports = function(liquid) {
liquid.registerTag('tablerow', {
parse: function(tagToken, remainTokens) {
var match = re.exec(tagToken.args);
assert(match, `illegal tag: ${tagToken.raw}`);
this.variable = match[1];
this.collection = match[2];
this.templates = [];
var p, stream = liquid.parser.parseStream(remainTokens)
.on('start', x => p = this.templates)
.on('tag:endtablerow', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
var collection = Liquid.evalExp(this.collection, scope) || [];
var html = '<table>';
var offset = hash.offset || 0;
var limit = (hash.limit === undefined) ? collection.length : hash.limit;
var cols = hash.cols, row, col;
if (!cols) throw new Error(`illegal cols: ${cols}`);
// build array of arguments to pass to sequential promises...
collection = collection.slice(offset, offset + limit);
var contexts = [];
collection.some((item, i) => {
var ctx = {};
ctx[this.variable] = item;
// We are just putting together an array of the arguments we will be passing to our sequential promises
contexts.push(ctx);
});
// This executes an array of promises sequentially for every argument in the contexts array - http://webcache.googleusercontent.com/search?q=cache:rNbMUn9TPtkJ:joost.vunderink.net/blog/2014/12/15/processing-an-array-of-promises-sequentially-in-node-js/+&cd=5&hl=en&ct=clnk&gl=us
// It's fundamentally equivalent to the following...
// emptyPromise.then(renderTemplates(args0).then(renderTemplates(args1).then(renderTemplates(args2)...
var lastPromise = contexts.reduce((promise, context, currentIndex) => {
return promise.then((partial) => {
row = Math.floor(currentIndex / cols) + 1;
col = (currentIndex % cols) + 1;
if(col === 1) {
if(row !== 1){
html += '</tr>';
}
html += `<tr class="row${row}">`;
}
//ctx[this.variable] = context;
return html += `<td class="col${col}">`;
})
.then((partial) => {
scope.push(context);
return liquid.renderer.renderTemplates(this.templates, scope)
})
.then((partial) => {
scope.pop(context);
html += partial;
return html += '</td>';
});
}, Promise.resolve('')); // start the reduce chain with a resolved Promise. After first run, the "promise" argument
// in our reduce callback will be the returned promise from our "then" above. In this
// case, the promise returned from liquid.renderer.renderTemplates.
return lastPromise
.then(() => {
if(row > 0) {
html += '</tr>';
}
html += '</table>';
return html;
})
.catch((error) => {
throw error;
});
}
});
};
-30
View File
@@ -1,30 +0,0 @@
const Liquid = require('..');
module.exports = function(liquid) {
liquid.registerTag('unless', {
parse: function(tagToken, remainTokens) {
this.templates = [];
this.elseTemplates = [];
var p, stream = liquid.parser.parseStream(remainTokens)
.on('start', x => {
p = this.templates;
this.cond = tagToken.args;
})
.on('tag:else', token => p = this.elseTemplates)
.on('tag:endunless', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`);
});
stream.start();
},
render: function(scope, hash) {
var cond = Liquid.evalExp(this.cond, scope);
return Liquid.isFalsy(cond) ?
liquid.renderer.renderTemplates(this.templates, scope) :
liquid.renderer.renderTemplates(this.elseTemplates, scope);
}
});
};
+11
View File
@@ -0,0 +1,11 @@
{
"rules": {
"no-unused-expressions": "off"
},
"env": {
"mocha": true
},
"plugins": [
"mocha"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"rules": {
"no-var": 0,
"prefer-const": 0
}
}

Some files were not shown because too many files have changed in this diff Show More