Compare commits

...
132 Commits
Author SHA1 Message Date
semantic-release-bot 84ea2c7488 chore(release): 8.0.0 [skip ci]
# [8.0.0](https://github.com/harttle/liquidjs/compare/v7.5.1...v8.0.0) (2019-03-10)

### Code Refactoring

* use camelCase for JavaScript APIs ([64e0c87](https://github.com/harttle/liquidjs/commit/64e0c87))

### Features

* promise support for drops, working on [#65](https://github.com/harttle/liquidjs/issues/65) ([4a8088d](https://github.com/harttle/liquidjs/commit/4a8088d))

### BREAKING CHANGES

* Options and method names in JavaScript API are now renamed to cammelCase, for a complete list see #109
2019-03-10 11:28:39 +00:00
Jun Yang 64e0c876e9 refactor: use camelCase for JavaScript APIs
BREAKING CHANGE: Options and method names in JavaScript API are now renamed to cammelCase, for a complete list see #109
2019-03-10 19:21:09 +08:00
Jun Yang 4a8088d4e4 feat: promise support for drops, working on #65 2019-03-10 18:05:52 +08:00
harttle ad0930f152 test: cover parseFilters in template/value.ts 2019-03-06 01:46:35 +08:00
harttle 998832c772 refactor: mv custom filter test 2019-03-06 01:40:31 +08:00
semantic-release-bot 4ba224043b chore(release): 7.5.1 [skip ci]
## [7.5.1](https://github.com/harttle/liquidjs/compare/v7.5.0...v7.5.1) (2019-03-05)

### Bug Fixes

* named params for filters, working on [#113](https://github.com/harttle/liquidjs/issues/113) ([5ffc904](https://github.com/harttle/liquidjs/commit/5ffc904))
2019-03-05 17:39:56 +00:00
harttle 5ffc904f80 fix: named params for filters, working on #113 2019-03-06 01:30:47 +08:00
semantic-release-bot 08646f75b0 chore(release): 7.5.0 [skip ci]
# [7.5.0](https://github.com/harttle/liquidjs/compare/v7.4.0...v7.5.0) (2019-03-01)

### Features

* tablerowloop object ([3647305](https://github.com/harttle/liquidjs/commit/3647305))
2019-03-01 22:06:31 +00:00
Jun Yang 3647305dcf feat: tablerowloop object
See: https://www.rubydoc.info/gems/liquid/Liquid/TablerowloopDrop
2019-03-02 06:00:30 +08:00
Jun Yang cca0306f5b chore: fix mocha pattern for windows
See: https://github.com/mochajs/mocha/issues/2895
2019-03-02 03:57:38 +08:00
Jun Yang eeeacc4492 chore: fix rollup.config.ts 2019-03-02 03:42:57 +08:00
Jun Yang d44e1d727a chore: fix test on windows 10 2019-03-02 03:36:33 +08:00
semantic-release-bot f960904df4 chore(release): 7.4.0 [skip ci]
# [7.4.0](https://github.com/harttle/liquidjs/compare/v7.3.1...v7.4.0) (2019-02-28)

### Bug Fixes

* math filters now return number, resolves [#110](https://github.com/harttle/liquidjs/issues/110) ([b4acdb4](https://github.com/harttle/liquidjs/commit/b4acdb4))

### Features

* exported Drop interface for [#107](https://github.com/harttle/liquidjs/issues/107) ([7bee9fc](https://github.com/harttle/liquidjs/commit/7bee9fc)), closes [#109](https://github.com/harttle/liquidjs/issues/109)
2019-02-28 05:13:55 +00:00
harttle b4acdb463a fix: math filters now return number, resolves #110
* fix linting
* numbers are no longer fixed:
    i.e. {{0.3 | minus 0.1}} now renders as 0.19999
    Stick to JavaScript number behavior, see: 8.0.0
2019-02-28 13:02:51 +08:00
harttle 21d674e22b chore: fix benchmark npm script 2019-02-28 00:05:37 +08:00
harttle 7bee9fc92d feat: exported Drop interface for #107
Deprecate snake_cased options and APIs, sed #109
2019-02-28 00:05:08 +08:00
harttle b69c3a3203 refactor: IDrop interface => abstract method 2019-02-28 00:05:08 +08:00
harttle d5b9916b2f refactor: Drop.prototype.value => valueOf 2019-02-28 00:05:08 +08:00
semantic-release-bot d0118402e4 chore(release): 7.3.1 [skip ci]
## [7.3.1](https://github.com/harttle/liquidjs/compare/v7.3.0...v7.3.1) (2019-02-25)

### Bug Fixes

* **#108:** remove absolute path in emitted d.ts ([53a835a](https://github.com/harttle/liquidjs/commit/53a835a)), closes [#108](https://github.com/harttle/liquidjs/issues/108)
2019-02-25 17:03:07 +00:00
harttle 31bc71d472 chore: fix linting 2019-02-26 00:56:12 +08:00
harttle 53a835ab54 fix(#108): remove absolute path in emitted d.ts 2019-02-26 00:43:30 +08:00
harttle 75b3e15ca9 chore: fix rollup.config.ts types 2019-02-25 00:12:31 +08:00
harttle f34573ee21 chore: stick with es6 for testing 2019-02-24 23:42:44 +08:00
harttle 875f71d5f2 chore: a simple liquid_method_missing case for #97 2019-02-24 23:08:53 +08:00
harttle 407ac6d0a4 refactor: remove /dist from repo 2019-02-24 23:03:12 +08:00
semantic-release-bot f9f35ebd6c chore(release): 7.3.0 [skip ci]
# [7.3.0](https://github.com/harttle/liquidjs/compare/v7.2.2...v7.3.0) (2019-02-24)

### Features

* nil/null/empty/blank literals, resolves [#102](https://github.com/harttle/liquidjs/issues/102) ([88c9e96](https://github.com/harttle/liquidjs/commit/88c9e96))
2019-02-24 13:55:31 +00:00
harttle 88c9e96392 feat: nil/null/empty/blank literals, resolves #102 2019-02-24 21:50:01 +08:00
semantic-release-bot 54b2adce12 chore(release): 7.2.2 [skip ci]
## [7.2.2](https://github.com/harttle/liquidjs/compare/v7.2.1...v7.2.2) (2019-02-23)

### Bug Fixes

* filters break when argument contains [()|, fixes [#89](https://github.com/harttle/liquidjs/issues/89) ([e977669](https://github.com/harttle/liquidjs/commit/e977669))
2019-02-23 22:32:04 +00:00
harttle 542968e133 refactor: remove unused lexical regexps 2019-02-24 06:26:33 +08:00
harttle 3b7351d9a0 chore: fix linting 2019-02-24 05:03:59 +08:00
harttle e977669118 fix: filters break when argument contains [()|, fixes #89 2019-02-24 04:48:42 +08:00
harttle 279458c670 test: add benchmark 2019-02-23 21:57:02 +08:00
semantic-release-bot 37d12a14eb chore(release): 7.2.1 [skip ci]
## [7.2.1](https://github.com/harttle/liquidjs/compare/v7.2.0...v7.2.1) (2019-02-22)

### Bug Fixes

* default length for truncate and truncatewords ([56c7992](https://github.com/harttle/liquidjs/commit/56c7992))
2019-02-22 19:56:16 +00:00
harttle 3cf01603e8 chore: fix linting 2019-02-23 03:50:45 +08:00
harttle 56c7992b76 fix: default length for truncate and truncatewords 2019-02-23 03:11:07 +08:00
harttle 5b6100d12b refactor: strictly typed 2019-02-23 00:49:54 +08:00
Jun YangandGitHub 51f7e66f60 Update CHANGELOG.md 2019-02-21 01:19:25 +08:00
Jun YangandGitHub 952ab6e73b Update CHANGELOG.md 2019-02-21 01:16:32 +08:00
semantic-release-bot 4171b2e023 chore(release): 7.2.0 [skip ci]
# [7.2.0](https://github.com/harttle/liquidjs/compare/v7.1.0...v7.2.0) (2019-02-20)

### Features

* override output/tag delimiter, fixes [#54](https://github.com/harttle/liquidjs/issues/54) ([d20a043](https://github.com/harttle/liquidjs/commit/d20a043))
2019-02-20 17:12:01 +00:00
harttle d20a043fbb feat: override output/tag delimiter, fixes #54 2019-02-21 01:07:32 +08:00
harttle 3dbab238ae BREAKING CHANGE: trim_value_* -> trim_output_* 2019-02-21 01:07:32 +08:00
semantic-release-bot d7d00421e2 chore(release): 7.1.0 [skip ci]
# [7.1.0](https://github.com/harttle/liquidjs/compare/v7.0.2...v7.1.0) (2019-02-20)

### Features

* throw an Error if delimiter not matched ([c33d8f6](https://github.com/harttle/liquidjs/commit/c33d8f6))
2019-02-20 14:45:44 +00:00
harttle c33d8f6828 feat: throw an Error if delimiter not matched 2019-02-20 22:40:09 +08:00
harttle c13a16fdd6 7.0.2 2019-02-20 10:02:10 +08:00
harttle 4bd35de867 chore: use white list to publish npm 2019-02-20 10:01:09 +08:00
harttle 29f53304bc 7.0.1 2019-02-19 23:55:38 +08:00
harttle cbebb00ef1 fix: unit test on Node.js 6 2019-02-19 23:25:08 +08:00
harttle 64a9eb79c5 style: fix unused vars in unit test 2019-02-19 23:01:59 +08:00
harttle 852c6ad453 refactor: remove mock-fs from dev dependency 2019-02-19 22:28:27 +08:00
harttle f432aade6a chore(TypeScript): remove mock-fs 2019-02-17 21:37:13 +08:00
harttle aa49b4f117 chore(TypeScript): fix unit tests and coverage 2019-02-17 20:46:33 +08:00
harttle fc9ebdb90f chore(TypeScript): fix linting and generate .d.ts 2019-02-17 18:20:04 +08:00
harttle 7ee87008c7 chore(TypeScript): demo for typescript 2019-02-17 14:22:13 +08:00
harttle 677e8511e6 chore(TypeScript): refactor objects into classes
fix: `Nil`(null, undefined) now renders as empty string
change: `parser.parseValue()` renamed to `parser.parseOutput`
change: registered tags/filters become static and shared across different liquid instances
2019-02-17 04:55:30 +08:00
semantic-release-bot b51b0dabca chore(release): 7.0.0 [skip ci]
# [7.0.0](https://github.com/harttle/liquidjs/compare/v6.4.3...v7.0.0) (2019-02-14)

### chore

* **TypeScript:** ship Liquid to class ([1cc7249](https://github.com/harttle/liquidjs/commit/1cc7249))

### BREAKING CHANGES

* **TypeScript:** calling `Liquid()` without `new` now becomes invalid
2019-02-14 16:02:23 +00:00
harttle 1cc7249a71 chore(TypeScript): ship Liquid to class
BREAKING CHANGE: calling `Liquid()` without `new` now becomes invalid
2019-02-14 23:53:40 +08:00
harttle a626de632c chore(CI): remove node_js 4 2019-02-14 23:15:29 +08:00
harttle 83a8e2d310 chore(CI): skip unit test on node_js 4 2019-02-14 23:11:13 +08:00
harttle c37b330751 chore(typescript): ship to TypeScript 2019-02-14 22:33:45 +08:00
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
251 changed files with 54656 additions and 8483 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules
dist
demo
coverage
+13 -1
View File
@@ -1,11 +1,23 @@
{
"extends": "standard",
"env": {
"mocha": true,
"es6": true,
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"mocha",
"standard",
"@typescript-eslint",
"promise"
]
],
"rules": {
"no-var": 2,
"prefer-const": 2,
"no-unused-vars": "off",
"import/export": "off",
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }]
}
}
+10 -35
View File
@@ -1,41 +1,16 @@
# Logs
logs
# logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# cache
.rpt2_cache
coverage/
.nyc_output/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# modules
node_modules/
dist/
# 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
package-lock.json
.vscode
-1
View File
@@ -1 +0,0 @@
test/
+17 -7
View File
@@ -1,9 +1,19 @@
language: node_js
node_js:
- "8"
- "6"
- "4"
before_script:
- npm install -g mocha
after_script:
- npm run lcov && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
- "10"
- "8"
- "6"
jobs:
include:
- stage: test
name: 'Linting'
script: npm run lint
- name: 'Coverage'
script: npm run coverage-coveralls
- stage: release
if: branch = master
script: skip
deploy:
provider: script
skip_cleanup: true
script: npx semantic-release
+121
View File
@@ -0,0 +1,121 @@
# [8.0.0](https://github.com/harttle/liquidjs/compare/v7.5.1...v8.0.0) (2019-03-10)
### Code Refactoring
* use camelCase for JavaScript APIs ([64e0c87](https://github.com/harttle/liquidjs/commit/64e0c87))
### Features
* promise support for drops, working on [#65](https://github.com/harttle/liquidjs/issues/65) ([4a8088d](https://github.com/harttle/liquidjs/commit/4a8088d))
### BREAKING CHANGES
* Options and method names in JavaScript API are now renamed to cammelCase, for a complete list see #109
## [7.5.1](https://github.com/harttle/liquidjs/compare/v7.5.0...v7.5.1) (2019-03-05)
### Bug Fixes
* named params for filters, working on [#113](https://github.com/harttle/liquidjs/issues/113) ([5ffc904](https://github.com/harttle/liquidjs/commit/5ffc904))
# [7.5.0](https://github.com/harttle/liquidjs/compare/v7.4.0...v7.5.0) (2019-03-01)
### Features
* tablerowloop object ([3647305](https://github.com/harttle/liquidjs/commit/3647305))
# [7.4.0](https://github.com/harttle/liquidjs/compare/v7.3.1...v7.4.0) (2019-02-28)
### Bug Fixes
* math filters now return number, resolves [#110](https://github.com/harttle/liquidjs/issues/110) ([b4acdb4](https://github.com/harttle/liquidjs/commit/b4acdb4))
### Features
* exported Drop interface for [#107](https://github.com/harttle/liquidjs/issues/107) ([7bee9fc](https://github.com/harttle/liquidjs/commit/7bee9fc)), closes [#109](https://github.com/harttle/liquidjs/issues/109)
## [7.3.1](https://github.com/harttle/liquidjs/compare/v7.3.0...v7.3.1) (2019-02-25)
### Bug Fixes
* **#108:** remove absolute path in emitted d.ts ([53a835a](https://github.com/harttle/liquidjs/commit/53a835a)), closes [#108](https://github.com/harttle/liquidjs/issues/108)
# [7.3.0](https://github.com/harttle/liquidjs/compare/v7.2.2...v7.3.0) (2019-02-24)
### Features
* nil/null/empty/blank literals, resolves [#102](https://github.com/harttle/liquidjs/issues/102) ([88c9e96](https://github.com/harttle/liquidjs/commit/88c9e96))
## [7.2.2](https://github.com/harttle/liquidjs/compare/v7.2.1...v7.2.2) (2019-02-23)
### Bug Fixes
* filters break when argument contains [()|, fixes [#89](https://github.com/harttle/liquidjs/issues/89) ([e977669](https://github.com/harttle/liquidjs/commit/e977669))
## [7.2.1](https://github.com/harttle/liquidjs/compare/v7.2.0...v7.2.1) (2019-02-22)
### Bug Fixes
* default length for truncate and truncatewords ([56c7992](https://github.com/harttle/liquidjs/commit/56c7992))
# [7.2.0](https://github.com/harttle/liquidjs/compare/v7.1.0...v7.2.0) (2019-02-20)
### Features
* override output/tag delimiter, fixes [#54](https://github.com/harttle/liquidjs/issues/54) ([d20a043](https://github.com/harttle/liquidjs/commit/d20a043))
### BREAKING CHANGES
* `trim_value_left` option renamed to `trim_output_left`, `trim_value_right` option renamed to `trim_output_right`
# [7.1.0](https://github.com/harttle/liquidjs/compare/v7.0.2...v7.1.0) (2019-02-20)
### Features
* throw an Error if delimiter not matched ([c33d8f6](https://github.com/harttle/liquidjs/commit/c33d8f6))
# [7.0.0](https://github.com/harttle/liquidjs/compare/v6.4.3...v7.0.0) (2019-02-14)
### chore
* **TypeScript:** ship Liquid to class ([1cc7249](https://github.com/harttle/liquidjs/commit/1cc7249))
### BREAKING CHANGES
* **TypeScript:** calling `Liquid()` without `new` now becomes invalid
## [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))
+47 -12
View File
@@ -9,6 +9,8 @@
[![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)
This is a liquid implementation for both Node.js and browsers. Website: <http://harttle.github.io/liquidjs/>, Live Demo: <https://jsfiddle.net/6u40xbzs/>
@@ -22,9 +24,11 @@ This is a liquid implementation for both Node.js and browsers. Website: <http://
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:
* 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).
* Dynamic file locating (enabled by default), that means layout/partial names are treated as variables 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) is replaced by `.toLiquid()`
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) is replaced by JavaScript `.toString()`
## TOC
@@ -41,6 +45,7 @@ Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is
* [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
@@ -55,7 +60,7 @@ Parse and Render:
```javascript
var Liquid = require('liquidjs');
var engine = Liquid();
var engine = new Liquid();
engine
.parseAndRender('{{name | capitalize}}', {name: 'alice'})
@@ -78,7 +83,7 @@ engine
## Render from File
```javascript
var engine = Liquid({
var engine = new Liquid({
root: path.resolve(__dirname, 'views/'), // dirs to lookup layouts/includes
extname: '.liquid' // the extname used for layouts/includes, defaults ""
});
@@ -177,21 +182,25 @@ Defaults to `["."]`
* `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`.
* `strictFilters` 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.
* `strictVariables` 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`.
* `trimTagRight` 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.
* `trimTagLeft` is similiar to `trimTagRight`, 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`.
* `trimOutputRight` 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.
* `trimOutputLeft` is similiar to `trimOutputRight`, 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`.
* `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags.
* `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs.
* `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
@@ -207,7 +216,7 @@ Filter arguments will be passed to the registered filter function, for example:
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
```
See existing filter implementations here: <https://github.com/harttle/liquidjs/blob/master/filters.js>
See existing filter implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/filters>
## Register Tags
@@ -227,7 +236,33 @@ engine.registerTag('upper', {
* `parse`: Read tokens from `remainTokens` until your end token.
* `render`: Combine scope data with your parsed tokens into HTML string.
See existing tag implementations here: <https://github.com/harttle/liquidjs/blob/master/tags/>
See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/tags>
## 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
+5
View File
@@ -0,0 +1,5 @@
{
"rules": {
"no-unused-expressions": "off"
}
}
+49
View File
@@ -0,0 +1,49 @@
import * as Benchmark from 'benchmark'
import Liquid from '../src/liquid'
import TagToken from '../src/parser/tag-token'
import Scope from '../src/scope/scope'
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
engine.registerTag('header', {
parse: function (token: TagToken) {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (scope: Scope) {
const title = this.liquid.evalValue(this.content, scope)
return `<h1>${title}</h1>`
}
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
const template = `
{%header content: "welcome to liquid" | capitalize%}
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
`
export default function () {
console.log('--- demo ---')
return new Promise(resolve => {
new Benchmark.Suite('demo')
.add('demo', {
defer: true,
fn: (d: any) => engine.parseAndRender(template, ctx).then(x => d.resolve(x))
})
.on('cycle', (event: any) => console.log(String(event.target)))
.on('complete', resolve)
.run({ 'async': true })
})
}
+11
View File
@@ -0,0 +1,11 @@
import output from './output'
import tag from './tag'
import demo from './demo'
async function main () {
await output()
await tag()
await demo()
}
main()
+26
View File
@@ -0,0 +1,26 @@
import * as Benchmark from 'benchmark'
import Liquid from '../src/liquid'
const liquid = new Liquid()
export default function () {
console.log('--- output ---')
return new Promise(resolve => {
new Benchmark.Suite('output')
.add('literal', test('{{false}}{{"foo"}}{{32.322}}'))
.add('truncate', test('{{"foobar" | truncate: 3}}'))
.add('date', test('{{"now" | date: "%d%Y%m"}}'))
.add('escape', test('{{"1<2" | escape}}'))
.add('default', test('{{"" | default: 3}}'))
.on('cycle', (event: any) => console.log(String(event.target)))
.on('complete', resolve)
.run({ 'async': true })
})
}
function test (str: string) {
return {
defer: true,
fn: (d: any) => liquid.parseAndRender(str).then(x => d.resolve(x))
}
}
+30
View File
@@ -0,0 +1,30 @@
import * as Benchmark from 'benchmark'
import Liquid from '../src/liquid'
const liquid = new Liquid()
export default function () {
console.log('--- tag ---')
return new Promise(resolve => {
new Benchmark.Suite('tag')
.add('if', test('{% if "foobar" %}foo{% endif %}'))
.add('unless', test('{%unless "foo"%}true{%else%}false{%endunless%}'))
.add('for', test('{% for i in (1..3) %}{{fooloop.index}}{% endfor %}'))
.add('switch', test('{%case 3%}{% when 1 %}1{% when 2 %}2{% when 3 %}3{%endcase%}'))
.add('assign', test('{%assign a="foo bar"%}'))
.add('capture', test('{%capture foo%}what is this{%endcapture%}'))
.add('increment', test('{%increment a%}'))
.add('decrement', test('{%decrement a%}'))
.add('tablerow', test('{%tablerow i in (1..10) cols:3%}{%endtablerow%}'))
.on('cycle', (event: any) => console.log(String(event.target)))
.on('complete', resolve)
.run({ 'async': true })
})
}
function test (str: string) {
return {
defer: true,
fn: (d: any) => liquid.parseAndRender(str).then(x => d.resolve(x))
}
}
+5 -8
View File
@@ -1,28 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="../../dist/liquid.js"></script>
<title>liquidjs for the browser</title>
<script src="node_modules/liquidjs/dist/liquid.min.js"></script>
</head>
<body>
<script>
var engine = window.Liquid({
// root: './',
var engine = new window.Liquid({
extname: '.html',
cache: true
});
var src = '<h2>Welcome to {{ name | capitalize}}, ' +
'access time: {{date|date: "%Y-%m-%d %H:%M:%S"}}</h2>';
'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.body.innerHTML += html
return engine.renderFile('hello', ctx);
})
.then(function(html) {
+220
View File
@@ -0,0 +1,220 @@
{
"name": "liquidjs-demo-browser",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
},
"colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
"dev": true
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "2.1.1"
}
},
"ecstatic": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.1.tgz",
"integrity": "sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ==",
"dev": true,
"requires": {
"he": "1.2.0",
"mime": "1.6.0",
"minimist": "1.2.0",
"url-join": "2.0.5"
}
},
"eventemitter3": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
"dev": true
},
"follow-redirects": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
"dev": true,
"requires": {
"debug": "3.2.6"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"http-proxy": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
"integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
"dev": true,
"requires": {
"eventemitter3": "3.1.0",
"follow-redirects": "1.7.0",
"requires-port": "1.0.0"
}
},
"http-server": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
"dev": true,
"requires": {
"colors": "1.0.3",
"corser": "2.0.1",
"ecstatic": "3.3.1",
"http-proxy": "1.17.0",
"opener": "1.4.3",
"optimist": "0.6.1",
"portfinder": "1.0.20",
"union": "0.4.6"
}
},
"liquidjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-7.0.0.tgz",
"integrity": "sha512-/JL+VQUI++6iCS7JQ4fPPeji6vyvHCU/iuz2bLSiE0tn0eO3kf8GEyfaXnQUHSroy8SmJu7HQIvjvOErtYEvUg=="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
},
"dependencies": {
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
}
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"opener": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
"dev": true
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"requires": {
"minimist": "0.0.10",
"wordwrap": "0.0.3"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
"dev": true
}
}
},
"portfinder": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
"integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
"dev": true,
"requires": {
"async": "1.5.2",
"debug": "2.6.9",
"mkdirp": "0.5.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
}
},
"qs": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"union": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
"dev": true,
"requires": {
"qs": "2.3.3"
}
},
"url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "liquidjs-demo-browser",
"version": "1.0.0",
"description": "Liquidjs Demo for Browser Usage",
"main": "index.html",
"scripts": {
"start": "http-server -c-1 "
},
"author": "harttle <[email protected]>",
"license": "ISC",
"devDependencies": {
"http-server": "^0.11.1"
},
"dependencies": {
"liquidjs": "^7.0.0"
}
}
+5 -5
View File
@@ -1,8 +1,8 @@
var express = require('express')
var app = express()
var Liquid = require('../..')
const express = require('express')
const Liquid = require('../..')
var engine = Liquid({
const app = express()
const engine = new Liquid({
root: __dirname, // for layouts and partials
extname: '.liquid'
})
@@ -12,7 +12,7 @@ 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']
const todos = ['fork and clone', 'make it better', 'make a pull request']
res.render('todolist', {
todos: todos,
title: 'Welcome to liquidjs!'
+1 -1
View File
@@ -1,5 +1,5 @@
const app = require('./app.js')
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
console.log('Express running: http://localhost:3000')
})
+1 -1
View File
@@ -2,7 +2,7 @@
<ul>
{% for todo in todos %}
<li>{% include 'todo' id=forloop.index %}</li>
<li>{% include 'todo', id:forloop.index %}</li>
{% endfor %}
</ul>
+26
View File
@@ -0,0 +1,26 @@
const Liquid = require('liquidjs')
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
engine.registerTag('header', {
parse: function (token) {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (scope, hash) {
const title = this.liquid.evalValue(this.content, scope)
return `<h1>${title}</h1>`
}
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
engine.renderFile('todolist', ctx)
.then(console.log)
.catch(err => console.error(err.stack))
+12
View File
@@ -0,0 +1,12 @@
{
"name": "Node.js demo for liquidjs",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"liquidjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-7.0.0.tgz",
"integrity": "sha512-/JL+VQUI++6iCS7JQ4fPPeji6vyvHCU/iuz2bLSiE0tn0eO3kf8GEyfaXnQUHSroy8SmJu7HQIvjvOErtYEvUg=="
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "liquidjs-demo-nodejs",
"description": "Node.js demo for liquidjs",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"liquidjs": "^7.0.0"
}
}
+7
View File
@@ -0,0 +1,7 @@
{%header content: "welcome to liquid" | capitalize%}
<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 = new 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
+25
View File
@@ -0,0 +1,25 @@
import Liquid from 'liquidjs'
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
engine.registerTag('header', {
parse: function (token) {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (scope, hash) {
const title = this.liquid.evalValue(this['content'], scope)
return `<h1>${title}</h1>`
}
})
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)
+8701
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
{
"name": "liquidjs-demo-typescript",
"description": "TypeScript demo for liquidjs",
"main": "index.ts",
"scripts": {
"start": "ts-node index.ts"
},
"dependencies": {
"liquidjs": "^7.0.0"
},
"devDependencies": {
"ts-node": "^8.0.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
{%header content: "welcome to liquid" | capitalize%}
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
View File
-2624
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
-138
View File
@@ -1,138 +0,0 @@
'use strict'
const strftime = require('./src/util/strftime.js')
const _ = require('./src/util/underscore.js')
const isTruthy = require('./src/syntax.js').isTruthy
let escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
}
let unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'"
}
let 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),
'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) => {
let 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(/<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 = '...'
let arr = v.split(' ')
let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o
return ret
},
'uniq': function (arr) {
let 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) {
let 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) => {
let f = getMaxFixed(l, r)
return cb(l, r).toFixed(f)
}
}
function registerAll (liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func))
}
function isValidDate (date) {
return date instanceof Date && !isNaN(date.getTime())
}
registerAll.filters = filters
module.exports = registerAll
-189
View File
@@ -1,189 +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 url = require('./src/util/url.js')
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 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, this.options)
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))
},
renderFile: function (filepath, ctx, opts) {
opts = _.assign({}, opts)
return this.getTemplate(filepath, opts.root)
.then(templates => this.render(templates, ctx, opts))
},
evalValue: function (str, scope) {
var 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)
},
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) => {
e.message = `${e.code}: Failed to lookup ${filepath} in: ${root}`
throw e
})
},
getTemplate: function (filepath, root) {
return typeof XMLHttpRequest === 'undefined'
? this.getTemplateFromFile(filepath, root)
: this.getTemplateFromUrl(filepath, root)
},
getTemplateFromFile: 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))
}
})
},
getTemplateFromUrl: function (filepath, root) {
var fullUrl
if (url.valid(filepath)) {
fullUrl = filepath
} else {
if (!url.extname(filepath)) {
filepath += this.options.extname
}
fullUrl = url.resolve(root || this.options.root, filepath)
}
if (this.options.cache) {
var tpl = this.cache[filepath]
if (tpl) {
return Promise.resolve(tpl)
}
}
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
var tpl = this.parse(xhr.responseText)
if (this.options.cache) {
this.cache[filepath] = tpl
}
resolve(tpl)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.onerror = () => {
reject(new Error('An error occurred whilst sending the response.'))
}
xhr.open('GET', fullUrl)
xhr.send()
})
},
express: function (opts) {
opts = opts || {}
var self = this
return function (filePath, ctx, callback) {
assert(Array.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({
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)
var engine = Object.create(_engine)
engine.init(Tag(), Filter(options), options)
return engine
}
function normalizeStringArray (value) {
if (Array.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
Vendored
+6
View File
@@ -0,0 +1,6 @@
declare module 'rollup-plugin-uglify' {
export function uglify (): any
}
declare module 'rollup-plugin-replace' {
export default function replace(options: any): any
}
+10775
View File
File diff suppressed because it is too large Load Diff
+78 -42
View File
@@ -1,24 +1,31 @@
{
"name": "liquidjs",
"version": "5.0.2",
"version": "8.0.0",
"description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.",
"main": "index.js",
"main": "dist/liquid.common.js",
"types": "dist/liquid.d.ts",
"browser": "dist/liquid.js",
"scripts": {
"lint": "eslint .",
"test": "mocha --recursive",
"coverage": "cross-env NODE_ENV=test istanbul cover --report html ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"lcov": "cross-env NODE_ENV=test istanbul cover --report lcovonly ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"dist": "npm run browserify && npm run uglify",
"browserify": "browserify index.js -s Liquid --ignore path -t [ babelify --global true --presets [ es2015 ] ] > dist/liquid.js",
"uglify": "uglifyjs dist/liquid.js --compress warnings=false --mangle --output dist/liquid.min.js",
"preversion": "npm run lint && npm test",
"version": "npm run dist && git add -A dist",
"postversion": "git push && git push --tags"
"lint": "eslint . --ext .ts",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
"e2e": "npm run build && mocha \"test/e2e/**/*.ts\"",
"test": "npm run build && mocha \"test/**/*.ts\"",
"benchmark": "ts-node benchmark",
"coverage-html": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"",
"coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls",
"build": "rollup -c rollup.config.ts && ls -lh dist",
"version": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/harttle/liquidjs.git"
},
"files": [
"dist",
"LICENSE",
"README.md"
],
"engines": {
"node": ">=4.8.7"
},
@@ -35,36 +42,65 @@
"url": "https://github.com/harttle/liquidjs/issues"
},
"homepage": "https://github.com/harttle/liquidjs#readme",
"dependencies": {
"resolve-url": "^0.2.1"
},
"browser": {
"fs": false
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babelify": "^8.0.0",
"browserify": "^14.4.0",
"chai": "^4.1.2",
"@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",
"@types/benchmark": "^1.0.31",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0",
"@types/express": "^4.16.1",
"@types/jsdom": "^12.2.2",
"@types/mocha": "^5.2.6",
"@types/sinon": "^7.0.6",
"@types/sinon-chai": "^3.2.2",
"@types/supertest": "^2.0.7",
"@typescript-eslint/eslint-plugin": "^1.3.0",
"benchmark": "^2.1.4",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.3",
"eslint": "^4.8.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"express": "^4.16.1",
"istanbul": "^0.4.5",
"jsdom": "^11.5.1",
"mocha": "^4.0.0",
"mock-fs": "^4.4.1",
"sinon": "^4.0.1",
"sinon-chai": "^2.14.0",
"supertest": "^3.0.0",
"uglify-js": "^3.1.3"
}
"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.3.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"express": "^4.16.4",
"jsdom": "^13.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"regenerator-runtime": "^0.12.1",
"rollup": "^1.1.2",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-typescript2": "^0.19.2",
"rollup-plugin-uglify": "^6.0.2",
"semantic-release": "^15.13.3",
"sinon": "^7.2.3",
"sinon-chai": "^3.3.0",
"supertest": "^3.4.2",
"ts-node": "^8.0.2",
"tslib": "^1.9.3",
"typescript": "^3.3.3"
},
"release": {
"branch": "master",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git"
]
},
"nyc": {
"extension": [
".ts"
]
},
"dependencies": {}
}
+93
View File
@@ -0,0 +1,93 @@
import { uglify } from 'rollup-plugin-uglify'
import pkg from './package.json'
import typescript from 'rollup-plugin-typescript2'
import replace from 'rollup-plugin-replace'
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/liquid.ts'
export default [{
output: [{
file: 'dist/liquid.common.js',
name: 'Liquid',
format: 'cjs',
sourcemap,
banner
}],
external: ['path', 'fs'],
plugins: [typescript({
tsconfigOverride: {
include: [ 'src' ],
exclude: [ 'test', 'benchmark' ],
compilerOptions: {
target: 'es5',
module: 'ES2015'
}
}
})],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.js',
name: 'Liquid',
format: 'umd',
sourcemap,
banner
}],
plugins: [
replace({
include: './src/liquid.ts',
delimiters: ['', ''],
'./fs/node': './fs/browser'
}),
typescript({
tsconfigOverride: {
include: [ 'src' ],
exclude: [ 'test', 'benchmark' ],
compilerOptions: {
target: 'es5',
module: 'ES2015'
}
}
})
],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.min.js',
name: 'Liquid',
format: 'umd',
sourcemap
}],
plugins: [
replace({
include: './src/liquid.ts',
delimiters: ['', ''],
'./fs/node': './fs/browser'
}),
typescript({
tsconfigOverride: {
include: [ 'src' ],
exclude: [ 'test', 'benchmark' ],
compilerOptions: {
target: 'es5',
module: 'ES2015'
}
}
}),
uglify()
],
treeshake,
input
}]
+24
View File
@@ -0,0 +1,24 @@
import { last } from '../../util/underscore'
export default {
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
'last': <T>(v: T[]): T => last(v),
'first': <T>(v: T[]): T => v[0],
'map': <T1, T2>(arr: {[key: string]: T1}[], arg: string): T1[] => arr.map(v => v[arg]),
'reverse': (v: any[]) => v.reverse(),
'sort': <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg),
'size': (v: string | any[]) => v.length,
'concat': <T1, T2>(v: T1[], arg: T2[] | T2): Array<T1 | T2> => Array.prototype.concat.call(v, arg),
'slice': <T>(v: T[], begin: number, length: number): T[] => {
if (length === undefined) length = 1
return v.slice(begin, begin + length)
},
'uniq': function<T> (arr: T[]): T[] {
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(String(val))) return false
u[String(val)] = true
return true
})
}
}
+18
View File
@@ -0,0 +1,18 @@
import strftime from '../../util/strftime'
import { isString } from '../../util/underscore'
export default {
'date': (v: string | Date, arg: string) => {
let date = v
if (v === 'now') {
date = new Date()
} else if (isString(v)) {
date = new Date(v)
}
return isValidDate(date) ? strftime(date, arg) : v
}
}
function isValidDate (date: any): date is Date {
return date instanceof Date && !isNaN(date.getTime())
}
+29
View File
@@ -0,0 +1,29 @@
const escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
}
const unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#34;': '"',
'&#39;': "'"
}
function escape (str: string) {
return String(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
}
function unescape (str: string) {
return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
}
export default {
'escape': escape,
'escape_once': (str: string) => escape(unescape(str)),
'newline_to_br': (v: string) => v.replace(/\n/g, '<br />'),
'strip_html': (v: string) => v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
}
+9
View File
@@ -0,0 +1,9 @@
import html from './html'
import str from './string'
import math from './math'
import url from './url'
import array from './array'
import date from './date'
import obj from './object'
export default { ...html, ...str, ...math, ...url, ...date, ...obj, ...array }
+14
View File
@@ -0,0 +1,14 @@
export default {
'abs': (v: number) => Math.abs(v),
'ceil': (v: number) => Math.ceil(v),
'divided_by': (v: number, arg: number) => v / arg,
'floor': (v: number) => Math.floor(v),
'minus': (v: number, arg: number) => v - arg,
'modulo': (v: number, arg: number) => v % arg,
'round': (v: number, arg: number = 0) => {
const amp = Math.pow(10, arg)
return Math.round(v * amp) / amp
},
'plus': (v: number, arg: number) => Number(v) + Number(arg),
'times': (v: number, arg: number) => v * arg
}
+5
View File
@@ -0,0 +1,5 @@
import { isTruthy } from '../../render/syntax'
export default {
'default': <T1, T2>(v: T1, arg: T2): T1 | T2 => isTruthy(v) ? v : arg
}
+30
View File
@@ -0,0 +1,30 @@
import { FilterImpl } from '../../template/filter/filter-impl'
export default {
'append': (v: string, arg: string) => v + arg,
'prepend': (v: string, arg: string) => arg + v,
'capitalize': (str: string) => String(str).charAt(0).toUpperCase() + str.slice(1),
'lstrip': (v: string) => String(v).replace(/^\s+/, ''),
'downcase': (v: string) => v.toLowerCase(),
'upcase': (str: string) => String(str).toUpperCase(),
'remove': (v: string, arg: string) => v.split(arg).join(''),
'remove_first': (v: string, l: string) => v.replace(l, ''),
'replace': (v: string, pattern: string, replacement: string) =>
String(v).split(pattern).join(replacement),
'replace_first': (v: string, arg1: string, arg2: string) => String(v).replace(arg1, arg2),
'rstrip': (str: string) => String(str).replace(/\s+$/, ''),
'split': (v: string, arg: string) => String(v).split(arg),
'strip': (v: string) => String(v).trim(),
'strip_newlines': (v: string) => String(v).replace(/\n/g, ''),
'truncate': (v: string, l: number = 50, o: string = '...') => {
v = String(v)
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
},
'truncatewords': (v: string, l: number = 15, o: string = '...') => {
const arr = v.split(/\s+/)
let ret = arr.slice(0, l).join(' ')
if (arr.length >= l) ret += o
return ret
}
} as {[key: string]: FilterImpl}
+4
View File
@@ -0,0 +1,4 @@
export default {
'url_decode': (x: string) => x.split('+').map(decodeURIComponent).join(' '),
'url_encode': (x: string) => x.split(' ').map(encodeURIComponent).join('+')
}
+22
View File
@@ -0,0 +1,22 @@
import assert from '../../util/assert'
import { identifier } from '../../parser/lexical'
import { AssignScope } from '../../scope/scopes'
import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
export default {
parse: function (token: TagToken) {
const match = token.args.match(re) as RegExpMatchArray
assert(match, `illegal token ${token.raw}`)
this.key = match[1]
this.value = match[2]
},
render: async function (scope: Scope) {
const ctx = new AssignScope()
ctx[this.key] = await this.liquid.evalValue(this.value, scope)
scope.push(ctx)
}
} as ITagImplOptions
+34
View File
@@ -0,0 +1,34 @@
import BlockMode from '../../scope/block-mode'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import ITemplate from '../../template/itemplate'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
export default {
parse: function (token: TagToken, remainTokens: Token[]) {
const match = /\w+/.exec(token.args)
this.block = match ? match[0] : ''
this.tpls = [] as ITemplate[]
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', (tpl: ITemplate) => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope) {
const childDefined = scope.blocks[this.block]
const html = childDefined !== undefined
? childDefined
: await this.liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.blockMode === BlockMode.STORE) {
scope.blocks[this.block] = html
return ''
}
return html
}
} as ITagImplOptions
+7
View File
@@ -0,0 +1,7 @@
import { RenderBreakError } from '../../util/error'
export default {
render: async function () {
throw new RenderBreakError('break')
}
}
+33
View File
@@ -0,0 +1,33 @@
import assert from '../../util/assert'
import { identifier } from '../../parser/lexical'
import { CaptureScope } from '../../scope/scopes'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const re = new RegExp(`(${identifier.source})`)
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = tagToken.args.match(re) as RegExpMatchArray
assert(match, `${tagToken.args} not valid identifier`)
this.variable = match[1]
this.templates = []
const stream = this.liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', () => stream.stop())
.on('template', (tpl) => this.templates.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope) {
const html = await this.liquid.renderer.renderTemplates(this.templates, scope)
const ctx = new CaptureScope()
ctx[this.variable] = html
scope.push(ctx)
}
} as ITagImplOptions
+44
View File
@@ -0,0 +1,44 @@
import { evalExp } from '../../render/syntax'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.cond = tagToken.args
this.cases = []
this.elseTemplates = []
let p: ITemplate[] = []
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:when', (token: TagToken) => {
this.cases.push({
val: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope) {
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = await evalExp(branch.val, scope)
const cond = await evalExp(this.cond, scope)
if (val === cond) {
return this.liquid.renderer.renderTemplates(branch.templates, scope)
}
}
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
} as ITagImplOptions
+17
View File
@@ -0,0 +1,17 @@
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import ITagImplOptions from '../../template/tag/itag-impl-options'
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const stream = this.liquid.parser.parseStream(remainTokens)
stream
.on('token', (token: TagToken) => {
if (token.name === 'endcomment') stream.stop()
})
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
}
} as ITagImplOptions
+7
View File
@@ -0,0 +1,7 @@
import { RenderBreakError } from '../../util/error'
export default {
render: async function () {
throw new RenderBreakError('continue')
}
}
+43
View File
@@ -0,0 +1,43 @@
import assert from '../../util/assert'
import { value as rValue } from '../../parser/lexical'
import { evalValue } from '../../render/syntax'
import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(rValue.source, 'g')
export default <ITagImplOptions>{
parse: function (tagToken: TagToken) {
let match: RegExpExecArray | null = groupRE.exec(tagToken.args) as RegExpExecArray
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: async function (scope: Scope) {
const group = await evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.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 evalValue(candidate, scope)
}
}
+30
View File
@@ -0,0 +1,30 @@
import assert from '../../util/assert'
import { identifier } from '../../parser/lexical'
import { CaptureScope, AssignScope, DecrementScope } from '../../scope/scopes'
import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
export default {
parse: function (token: TagToken) {
const match = token.args.match(identifier) as RegExpMatchArray
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope: Scope) {
let context = scope.findContextFor(
this.variable,
ctx => {
return !(ctx instanceof CaptureScope) && !(ctx instanceof AssignScope)
}
)
if (!context) {
context = new DecrementScope()
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
return --context[this.variable]
}
} as ITagImplOptions
+82
View File
@@ -0,0 +1,82 @@
import { isString, isObject, isArray } from '../../util/underscore'
import { evalExp } from '../../render/syntax'
import assert from '../../util/assert'
import { identifier, value, hash } from '../../parser/lexical'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import Hash from '../../template/tag/hash'
import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
import { ForloopDrop } from '../../drop/forloop-drop'
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*` +
`(?:\\s+(reversed))?` +
`(?:\\s+${hash.source})*$`)
export default <ITagImplOptions>{
type: 'block',
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = re.exec(tagToken.args) as RegExpExecArray
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: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope, hash: Hash) {
let collection = await evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
collection = [collection] as string[]
} else if (isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]]) as Array<[string, any]>
}
}
if (!isArray(collection) || !collection.length) {
return this.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 context = { forloop: new ForloopDrop(collection.length) }
scope.push(context)
let html = ''
for (const item of collection) {
context[this.variable] = item
try {
html += await this.liquid.renderer.renderTemplates(this.templates, scope)
} catch (e) {
if (e.name === 'RenderBreakError') {
html += e.resolvedHTML
if (e.message === 'break') break
} else throw e
}
context.forloop.next()
}
scope.pop()
return html
}
}
+45
View File
@@ -0,0 +1,45 @@
import { evalExp, isTruthy } from '../../render/syntax'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import ITemplate from '../../template/itemplate'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.branches = []
this.elseTemplates = []
let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
cond: tagToken.args,
templates: (p = [])
}))
.on('tag:elsif', (token: TagToken) => {
this.branches.push({
cond: token.args,
templates: p = []
})
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope) {
for (const branch of this.branches) {
const cond = await evalExp(branch.cond, scope)
if (isTruthy(cond)) {
return this.liquid.renderer.renderTemplates(branch.templates, scope)
}
}
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
} as ITagImplOptions
+60
View File
@@ -0,0 +1,60 @@
import assert from '../../util/assert'
import { value, quotedLine } from '../../parser/lexical'
import { evalValue } from '../../render/syntax'
import BlockMode from '../../scope/block-mode'
import TagToken from '../../parser/tag-token'
import Scope from '../../scope/scope'
import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const staticFileRE = /[^\s,]+/
const withRE = new RegExp(`with\\s+(${value.source})`)
export default <ITagImplOptions>{
parse: function (token: TagToken) {
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: Scope, hash: Hash) {
let filepath
if (scope.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await this.liquid.parseAndRender(template, scope.getAll(), scope.opts)
} else {
filepath = await evalValue(this.value, scope)
}
} else {
filepath = this.staticValue
}
assert(filepath, `cannot include with empty filename`)
const originBlocks = scope.blocks
const originBlockMode = scope.blockMode
scope.blocks = {}
scope.blockMode = BlockMode.OUTPUT
if (this.with) {
hash[filepath] = await evalValue(this.with, scope)
}
const templates = await this.liquid.getTemplate(filepath, scope.opts)
scope.push(hash)
const html = await this.liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
scope.blocks = originBlocks
scope.blockMode = originBlockMode
return html
}
}
+30
View File
@@ -0,0 +1,30 @@
import assert from '../../util/assert'
import { identifier } from '../../parser/lexical'
import { CaptureScope, AssignScope, IncrementScope } from '../../scope/scopes'
import ITagImplOptions from '../../template/tag/itag-impl-options'
export default {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match![0]
},
render: function (scope) {
let context = scope.findContextFor(
this.variable,
ctx => {
return !(ctx instanceof CaptureScope) && !(ctx instanceof AssignScope)
}
)
if (!context) {
context = new IncrementScope()
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
const val = context[this.variable]
context[this.variable]++
return val
}
} as ITagImplOptions
+24
View File
@@ -0,0 +1,24 @@
import assign from './assign'
import For from './for'
import capture from './capture'
import Case from './case'
import comment from './comment'
import include from './include'
import decrement from './decrement'
import cycle from './cycle'
import If from './if'
import increment from './increment'
import layout from './layout'
import block from './block'
import raw from './raw'
import tablerow from './tablerow'
import unless from './unless'
import Break from './break'
import Continue from './continue'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const tags: { [key: string]: ITagImplOptions } = {
assign, 'for': For, capture, 'case': Case, comment, include, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
}
export default tags
+46
View File
@@ -0,0 +1,46 @@
import assert from '../../util/assert'
import { value as rValue } from '../../parser/lexical'
import { evalValue } from '../../render/syntax'
import BlockMode from '../../scope/block-mode'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options'
const staticFileRE = /\S+/
export default {
parse: function (token: TagToken, remainTokens: Token[]) {
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 = this.liquid.parser.parse(remainTokens)
},
render: async function (scope: Scope, hash: Hash) {
const layout = scope.opts.dynamicPartials
? await evalValue(this.layout, scope)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
scope.blockMode = BlockMode.STORE
const html = await this.liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.blocks[''] === undefined) {
scope.blocks[''] = html
}
const templates = await this.liquid.getTemplate(layout, scope.opts)
scope.push(hash)
scope.blockMode = BlockMode.OUTPUT
const partial = await this.liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
return partial
}
} as ITagImplOptions
+23
View File
@@ -0,0 +1,23 @@
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import ITagImplOptions from '../../template/tag/itag-impl-options'
export default <ITagImplOptions>{
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.tokens = []
const stream = this.liquid.parser.parseStream(remainTokens)
stream
.on('token', (token: TagToken) => {
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 () {
return this.tokens.map((token: Token) => token.raw).join('')
}
}
+65
View File
@@ -0,0 +1,65 @@
import assert from '../../util/assert'
import { evalExp } from '../../render/syntax'
import { identifier, value, hash } from '../../parser/lexical'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import ITemplate from '../../template/itemplate'
import Scope from '../../scope/scope'
import Hash from '../../template/tag/hash'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*$`)
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = re.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.templates = []
let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', () => stream.stop())
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope, hash: Hash) {
let collection = await 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 tablerowloop = new TablerowloopDrop(collection.length, cols)
const ctx = { tablerowloop }
scope.push(ctx)
let html = ''
for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
ctx[this.variable] = collection[idx]
if (tablerowloop.col0() === 0) {
if (tablerowloop.row() !== 1) html += '</tr>'
html += `<tr class="row${tablerowloop.row()}">`
}
html += `<td class="col${tablerowloop.col()}">`
html += await this.liquid.renderer.renderTemplates(this.templates, scope)
html += '</td>'
}
if (collection.length) html += '</tr>'
scope.pop(ctx)
return html
}
} as ITagImplOptions
+34
View File
@@ -0,0 +1,34 @@
import { evalExp, isFalsy } from '../../render/syntax'
import TagToken from '../../parser/tag-token'
import Token from '../../parser/token'
import Scope from '../../scope/scope'
import ITagImplOptions from '../../template/tag/itag-impl-options'
import ParseStream from '../../parser/parse-stream'
export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.templates = []
this.elseTemplates = []
let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => {
p = this.templates
this.cond = tagToken.args
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endunless', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope) {
const cond = await evalExp(this.cond, scope)
return isFalsy(cond)
? this.liquid.renderer.renderTemplates(this.templates, scope)
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
} as ITagImplOptions
+12
View File
@@ -0,0 +1,12 @@
import { isNil, isString } from '../util/underscore'
import { Drop } from '../drop/drop'
import { EmptyDrop } from '../drop/empty-drop'
export class BlankDrop extends EmptyDrop {
equals (value: any) {
if (value === false) return true
if (isNil(value instanceof Drop ? value.valueOf() : value)) return true
if (isString(value)) return /^\s*$/.test(value)
return super.equals(value)
}
}
+9
View File
@@ -0,0 +1,9 @@
export abstract class Drop {
valueOf (): any {
return undefined
}
liquidMethodMissing (key: string): Promise<string | undefined> | string | undefined {
return undefined
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Drop } from './drop'
import { IComparable } from './icomparable'
import { isObject, isString, isArray } from '../util/underscore'
export class EmptyDrop extends Drop implements IComparable {
equals (value: any) {
if (isString(value) || isArray(value)) return value.length === 0
if (isObject(value)) return Object.keys(value).length === 0
return false
}
gt () {
return false
}
geq () {
return false
}
lt () {
return false
}
leq () {
return false
}
valueOf () {
return ''
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Drop } from './drop'
export class ForloopDrop extends Drop {
protected i: number = 0
length: number
constructor (length: number) {
super()
this.length = length
}
next () {
this.i++
}
index0 () {
return this.i
}
index () {
return this.i + 1
}
first () {
return this.i === 0
}
last () {
return this.i === this.length - 1
}
rindex () {
return this.length - this.i
}
rindex0 () {
return this.length - this.i - 1
}
valueOf () {
return JSON.stringify(this)
}
}
+13
View File
@@ -0,0 +1,13 @@
import { isFunction } from '../util/underscore'
export interface IComparable {
equals: (rhs: any) => boolean
gt: (rhs: any) => boolean
geq: (rhs: any) => boolean
lt: (rhs: any) => boolean
leq: (rhs: any) => boolean
}
export function isComparable (arg: any): arg is IComparable {
return arg && isFunction(arg.equals)
}
+25
View File
@@ -0,0 +1,25 @@
import { Drop } from './drop'
import { IComparable } from './icomparable'
import { isNil } from '../util/underscore'
import { BlankDrop } from '../drop/blank-drop'
export class NullDrop extends Drop implements IComparable {
equals (value: any) {
return isNil(value instanceof Drop ? value.valueOf() : value) || value instanceof BlankDrop
}
gt () {
return false
}
geq () {
return false
}
lt () {
return false
}
leq () {
return false
}
valueOf () {
return null
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ForloopDrop } from './forloop-drop'
export class TablerowloopDrop extends ForloopDrop {
private cols: number
constructor (length: number, cols: number) {
super(length)
this.length = length
this.cols = cols
}
row () {
return Math.floor(this.i / this.cols) + 1
}
col0 () {
return (this.i % this.cols)
}
col () {
return this.col0() + 1
}
col_first () { // eslint-disable-line
return this.col0() === 0
}
col_last () { // eslint-disable-line
return this.col() === this.cols
}
}
-68
View File
@@ -1,68 +0,0 @@
const lexical = require('./lexical.js')
const Syntax = require('./syntax.js')
const assert = require('./util/assert.js')
const _ = require('./util/underscore.js')
var valueRE = new RegExp(`${lexical.value.source}`, 'g')
module.exports = function (options) {
options = _.assign({}, options)
var 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)
var name = match[1]
var argList = match[2] || ''
var 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
}
var args = []
while ((match = valueRE.exec(argList.trim()))) {
var v = match[0]
var re = new RegExp(`${v}\\s*:`, 'g')
var keyMatch = re.exec(match.input)
var 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 construct (str) {
var instance = Object.create(_filterInstance)
return instance.parse(str)
}
function register (name, filter) {
filters[name] = filter
}
function clear () {
filters = {}
}
return {
construct, register, clear
}
}
+51
View File
@@ -0,0 +1,51 @@
import { last } from '../util/underscore'
import IFS from './ifs'
function domResolve (root: string, path: string) {
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
}
function resolve (root: string, filepath: string, ext: string) {
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 + ext
})
}
async function readFile (url: string): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText as string)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.onerror = () => {
reject(new Error('An error occurred whilst receiving the response.'))
}
xhr.open('GET', url)
xhr.send()
})
}
async function exists () {
return true
}
export default { readFile, resolve, exists } as IFS
+5
View File
@@ -0,0 +1,5 @@
export default interface IFS {
exists: (filepath: string) => Promise<boolean>
readFile: (filepath:string) => Promise<string>
resolve: (root: string, file: string, ext: string) => string
}
+22
View File
@@ -0,0 +1,22 @@
import * as _ from '../util/underscore'
import { resolve, extname } from 'path'
import { stat, readFile } from 'fs'
import IFS from './ifs'
const statAsync = _.promisify(stat)
const readFileAsync = _.promisify<string, string, string>(readFile)
const fs: IFS = {
exists: (filepath: string) => {
return statAsync(filepath).then(() => true).catch(() => false)
},
readFile: filepath => {
return readFileAsync(filepath, 'utf8')
},
resolve: (root: string, file: string, ext: string) => {
if (!extname(file)) file += ext
return resolve(root, file)
}
}
export default fs
-116
View File
@@ -1,116 +0,0 @@
// quote related
var singleQuoted = /'[^']*'/
var doubleQuoted = /"[^"]*"/
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
var quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
// basic types
var integer = /-?\d+/
var number = /-?\d+\.?\d*|\.?\d+/
var 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})*`)
// 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})\\)`)
var 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')
// full match
var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\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}$`)
// filter related
var valueDeclaration = new RegExp(`(?:${identifier.source}\\s*:\\s*)?${value.source}`)
var valueList = new RegExp(`${valueDeclaration.source}(\\s*,\\s*${valueDeclaration.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}$`)
var operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
]
function isInteger (str) {
return integerLine.test(str)
}
function isLiteral (str) {
return literalLine.test(str)
}
function isRange (str) {
return rangeLine.test(str)
}
function isVariable (str) {
return variableLine.test(str)
}
function matchValue (str) {
return value.exec(str)
}
function parseLiteral (str) {
var 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
}
+90
View File
@@ -0,0 +1,90 @@
import * as _ from './util/underscore'
export interface LiquidOptions {
/** `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
/** `strictFilters` 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`. */
strictFilters?: boolean
/** `strictVariables` 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`. */
strictVariables?: boolean
/** `trimTagRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
trimTagRight?: boolean
/** `trimTagLeft` is similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trimTagLeft?: boolean
/** ``trimOutputRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
trimOutputRight?: boolean
/** `trimOutputLeft` is similar to `trimOutputRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trimOutputLeft?: boolean
/** `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags **/
tagDelimiterLeft?: string,
tagDelimiterRight?: string,
/** `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs **/
outputDelimiterLeft?: string,
outputDelimiterRight?: string,
/** `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
}
interface NormalizedOptions extends LiquidOptions {
root?: string[]
}
export interface NormalizedFullOptions extends NormalizedOptions {
root: string[]
extname: string
cache: boolean
dynamicPartials: boolean
strictFilters: boolean
strictVariables: boolean
trimTagRight: boolean
trimTagLeft: boolean
trimOutputRight: boolean
trimOutputLeft: boolean
tagDelimiterLeft: string,
tagDelimiterRight: string,
outputDelimiterLeft: string,
outputDelimiterRight: string,
greedy: boolean
}
const defaultOptions: NormalizedFullOptions = {
root: ['.'],
cache: false,
extname: '',
dynamicPartials: true,
trimTagRight: false,
trimTagLeft: false,
trimOutputRight: false,
trimOutputLeft: false,
greedy: true,
tagDelimiterLeft: '{%',
tagDelimiterRight: '%}',
outputDelimiterLeft: '{{',
outputDelimiterRight: '}}',
strictFilters: false,
strictVariables: false
}
export function normalize (options?: LiquidOptions): NormalizedOptions {
options = options || {}
if (options.hasOwnProperty('root')) {
options.root = normalizeStringArray(options.root)
}
return options as NormalizedOptions
}
export function applyDefault (options?: NormalizedOptions): NormalizedFullOptions {
return { ...defaultOptions, ...options }
}
function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string]
return []
}
+97
View File
@@ -0,0 +1,97 @@
import Scope from './scope/scope'
import * as Types from './types'
import fs from './fs/node'
import * as _ from './util/underscore'
import ITemplate from './template/itemplate'
import Tokenizer from './parser/tokenizer'
import Render from './render/render'
import Tag from './template/tag/tag'
import { Filter } from './template/filter/filter'
import Parser from './parser/parser'
import ITagImplOptions from './template/tag/itag-impl-options'
import Value from './template/value'
import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax'
import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters'
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImpl } from './template/filter/filter-impl'
export default class Liquid {
public options: NormalizedFullOptions
public renderer: Render
public parser: Parser
private cache: object = {}
private tokenizer: Tokenizer
constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
this.parser = new Parser(this)
this.renderer = new Render()
this.tokenizer = new Tokenizer(this.options)
_.forOwn(builtinTags, (conf, name) => this.registerTag(name, conf))
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
}
parse (html: string, filepath?: string) {
const tokens = this.tokenizer.tokenize(html, filepath)
return this.parser.parse(tokens)
}
render (tpl: Array<ITemplate>, ctx?: object, opts?: LiquidOptions) {
const options = { ...this.options, ...normalize(opts) }
const scope = new Scope(ctx, options)
return this.renderer.renderTemplates(tpl, scope)
}
async parseAndRender (html: string, ctx?: object, opts?: LiquidOptions) {
const tpl = await this.parse(html)
return this.render(tpl, ctx, opts)
}
async getTemplate (file: string, opts?: LiquidOptions) {
const options = normalize(opts)
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
const paths = roots.map(root => fs.resolve(root, file, this.options.extname))
for (const filepath of paths) {
if (!(await fs.exists(filepath))) continue
if (this.options.cache && this.cache[filepath]) return this.cache[filepath]
const value = this.parse(await fs.readFile(filepath), filepath)
if (this.options.cache) this.cache[filepath] = value
return value
}
const err = new Error('ENOENT') as any
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
err.code = 'ENOENT'
throw err
}
async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts)
const templates = await this.getTemplate(file, options)
return this.render(templates, ctx, opts)
}
evalValue (str: string, scope: Scope) {
return new Value(str, this.options.strictFilters).value(scope)
}
registerFilter (name: string, filter: FilterImpl) {
return Filter.register(name, filter)
}
registerTag (name: string, tag: ITagImplOptions) {
return Tag.register(name, tag)
}
plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid)
}
express () {
const self = this
return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) {
const opts = { root: this.root }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
}
static default = Liquid
static isTruthy = isTruthy
static isFalsy = isFalsy
static evalExp = evalExp
static evalValue = evalValue
static Types = Types
}
-17
View File
@@ -1,17 +0,0 @@
module.exports = 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)
}
}
-105
View File
@@ -1,105 +0,0 @@
const lexical = require('./lexical.js')
const ParseError = require('./util/error.js').ParseError
const assert = require('./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
}
}
function parse (tokens) {
var token
var templates = []
while ((token = tokens.shift())) {
templates.push(parseToken(token, tokens))
}
return templates
}
function parseToken (token, tokens) {
try {
var 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 parseTag (token, tokens) {
if (token.name === 'continue' || token.name === 'break') return token
return Tag.construct(token, tokens)
}
function parseValue (str) {
var match = lexical.matchValue(str)
assert(match, `illegal value string: ${str}`)
var initial = match[0]
str = str.substr(match.index + match[0].length)
var filters = []
while ((match = lexical.filter.exec(str))) {
filters.push([match[0].trim()])
}
return {
type: 'value',
initial: initial,
filters: filters.map(str => Filter.construct(str))
}
}
function parseStream (tokens) {
var s = Object.create(stream)
return s.init(tokens)
}
return {
parse,
parseTag,
parseStream,
parseValue
}
}
+18
View File
@@ -0,0 +1,18 @@
import Token from './token'
import { last } from '../util/underscore'
export default class DelimitedToken extends Token {
trimLeft: boolean
trimRight: boolean
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, input, line, pos, file)
this.trimLeft = value[0] === '-'
this.trimRight = last(value) === '-'
this.value = value
.slice(
this.trimLeft ? 1 : 0,
this.trimRight ? -1 : value.length
)
.trim()
}
}
+9
View File
@@ -0,0 +1,9 @@
import Token from './token'
export default class HTMLToken extends Token {
constructor (str: string, input: string, line: number, col: number, file?: string) {
super(str, input, line, col, file)
this.type = 'html'
this.value = str
}
}
+43
View File
@@ -0,0 +1,43 @@
// quote related
const singleQuoted = /'[^']*'/
const doubleQuoted = /"[^"]*"/
export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
// basic types
export const number = /[+-]?(?:\d+\.?\d*|\.?\d+)/
export const bool = /true|false/
// 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
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})\\)`)
export const value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
// hash related
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
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*?)\\s*$`)
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 operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
]
export function isRange (str: string) {
return rangeLine.test(str)
}
+8
View File
@@ -0,0 +1,8 @@
import DelimitedToken from './delimited-token'
export default class OutputToken extends DelimitedToken {
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, input, line, pos, file)
this.type = 'output'
}
}
+47
View File
@@ -0,0 +1,47 @@
import Token from '../parser/token'
import ITemplate from '../template/itemplate'
import TagToken from './tag-token'
type ParseToken = ((token: Token, remainTokens: Array<Token>) => ITemplate)
export default class ParseStream {
private tokens: Array<Token>
private handlers: {[key: string]: (arg: any) => void} = {}
private stopRequested: boolean = false
private parseToken: ParseToken
constructor (tokens: Array<Token>, parseToken: ParseToken) {
this.tokens = tokens
this.parseToken = parseToken
}
on<T extends ITemplate | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream {
this.handlers[name] = cb
return this
}
trigger <T extends Token | ITemplate> (event: string, arg?: T) {
const h = this.handlers[event]
if (typeof h === 'function') {
h(arg)
return true
}
return false
}
start () {
this.trigger('start')
let token: Token | undefined
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue
if (token.type === 'tag' && this.trigger(`tag:${(<TagToken>token).name}`, token)) {
continue
}
const template = this.parseToken(token, this.tokens)
this.trigger('template', template)
}
if (!this.stopRequested) this.trigger('end')
return this
}
stop () {
this.stopRequested = true
return this
}
}
+42
View File
@@ -0,0 +1,42 @@
import { ParseError } from '../util/error'
import Liquid from '../liquid'
import ParseStream from './parse-stream'
import Token from './token'
import TagToken from './tag-token'
import OutputToken from './output-token'
import Tag from '../template/tag/tag'
import Output from '../template/output'
import HTML from '../template/html'
import ITemplate from '../template/itemplate'
export default class Parser {
liquid: Liquid
constructor (liquid: Liquid) {
this.liquid = liquid
}
parse (tokens: Array<Token>) {
let token
const templates: ITemplate[] = []
while ((token = tokens.shift())) {
templates.push(this.parseToken(token, tokens))
}
return templates
}
parseToken (token: Token, remainTokens: Array<Token>) {
try {
if (token.type === 'tag') {
return new Tag(token as TagToken, remainTokens, this.liquid)
}
if (token.type === 'output') {
return new Output(token as OutputToken, this.liquid.options.strictFilters)
}
return new HTML(token)
} catch (e) {
throw new ParseError(e, token)
}
}
parseStream (tokens: Array<Token>) {
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
}
}
+18
View File
@@ -0,0 +1,18 @@
import DelimitedToken from './delimited-token'
import { TokenizationError } from '../util/error'
import * as lexical from './lexical'
export default class TagToken extends DelimitedToken {
name: string
args: string
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, input, line, pos, file)
this.type = 'tag'
const match = this.value.match(lexical.tagLine)
if (!match) {
throw new TokenizationError(`illegal tag syntax`, this)
}
this.name = match[1]
this.args = match[2]
}
}

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