Compare commits

...
56 Commits
Author SHA1 Message Date
harttle af4fe1b330 6.2.0 2019-01-10 13:30:05 +08:00
harttle 5f5c62a51e fix banner link 2019-01-10 13:29:39 +08:00
oott123andJun Yang 21f4d6bf08 export types variable 2019-01-10 13:28:13 +08:00
oott123andJun Yang ba54bec82f feat: add typescript definitions 2019-01-10 13:28:13 +08:00
Jun Yang c2e1bc7055 6.1.1 2018-11-22 09:36:18 +08:00
Andrew BarclayandJun Yang 2055258193 fix assign tag regex to include linebreaks in value 2018-11-22 09:31:22 +08:00
harttle d65ea14b9f 6.1.0 2018-10-14 00:53:43 +08:00
steven anthonyandJun Yang f1b4c4fe14 Added react-liquidjs-demo 2018-10-10 19:59:43 +08:00
harttle 6af631b77b feature: url_decode, working on #94 2018-10-08 10:37:24 +08:00
Jun Yang d504c8107e 6.0.1 2018-10-06 12:20:13 +08:00
Jun Yang c0181aec31 remove dependency from 'util' 2018-10-01 23:08:39 +08:00
Jun Yang 22a7a85d50 test case for Regexp rendering 2018-10-01 23:00:49 +08:00
Jun Yang 044be8253d fixes #91 2018-10-01 22:51:40 +08:00
Jun Yang 8a629980d9 refactor: move fs util into template.js 2018-10-01 22:06:03 +08:00
harttle 5412c5a39f fixes #88 2018-09-29 16:45:11 +08:00
harttle b7445f26d6 6.0.0 2018-08-28 00:37:00 +08:00
harttle 3213c6d23a fix: ci 2018-08-28 00:27:44 +08:00
harttle 829afc24a7 fix: ci 2018-08-28 00:17:26 +08:00
harttle 0049a56864 fix: ci 2018-08-28 00:05:50 +08:00
harttle f8e0587542 fix: ci 2018-08-27 23:54:43 +08:00
harttle c3c57a560b fix: travis 2018-08-27 23:49:11 +08:00
harttle 5378f2709b test: cover all 2018-08-27 23:27:42 +08:00
harttle d6876bd9ec refactor: bundle template.js respectively 2018-08-27 21:37:39 +08:00
harttle 31c39561c9 feature: get nyc, babel and coveralls working 2018-08-26 21:54:35 +08:00
harttle d828c4021e fix: unit test on node6.x 2018-08-21 01:21:30 +08:00
harttle 4d4a2d04ec refactor 2018-08-21 01:18:13 +08:00
harttle bc9d8f92af refactor: import rollup 2018-08-20 23:58:41 +08:00
harttle 4ece4e208f 5.3.0-0 2018-08-17 01:34:57 +08:00
harttle 4ad3d2a52b modified: .eslintignore 2018-08-17 01:34:07 +08:00
harttle fa7cd8f4ff refactor: import babelify 2018-08-17 01:32:16 +08:00
harttle 87360d5207 refactor: mv es5 files to build/ 2018-08-16 23:20:13 +08:00
harttle 19891b2df0 refactor: es6 building and linting 2018-08-16 23:14:12 +08:00
harttle 5f634b0b5e refactor: moving all ES6 files to src/, working on #83 2018-08-16 00:22:46 +08:00
harttle 9f597843c9 5.2.0 2018-08-14 00:12:42 +08:00
harttle 887b736f39 test cases for stringify 2018-08-14 00:11:50 +08:00
harttle 7a3689067b feature: support toLiquid, working on #81 2018-08-14 00:11:50 +08:00
harttle 5b43520d78 feature: variable lookup respects to to_liquid, #81 2018-08-14 00:11:50 +08:00
harttle ea38a9e1f5 refactor: test/render.js use local scope 2018-08-14 00:11:50 +08:00
harttle fa5b731daf fix string type checking and circular reference, working on #81 2018-08-14 00:11:50 +08:00
harttle 96829b5293 fix: express demo, fixes #79 2018-07-25 21:51:42 +08:00
harttle 4cf7215a67 5.1.1 2018-07-24 19:39:12 +08:00
harttle 50006db26d fix: test/util/strftime.js 2018-07-24 19:38:47 +08:00
harttle 2d81223d99 update outdated packages 2018-07-24 19:37:21 +08:00
harttle 3038115c21 5.1.0 2018-07-24 14:53:47 +08:00
harttle fdbf4b838a add package.lock 2018-07-24 14:53:28 +08:00
harttle 7ab0e96419 feature: template string for {%include%} argument, #72 2018-07-24 14:49:23 +08:00
harttle 48484fe52f 5.0.2 2018-07-21 23:42:18 +08:00
harttle 14d1956232 test case for _.forOwn 2018-07-21 23:23:25 +08:00
harttle c3e7859cdd fix: increment/decrement independent from capture, #76 2018-07-21 23:19:28 +08:00
harttle c3de3fbb19 fix: post-increment, pre-decrement working on #76 2018-07-21 22:59:52 +08:00
harttle b5cfd2d2c5 fix: {%assign%} now adds a shading scope 2018-07-21 21:30:40 +08:00
harttle 36f84e4b3c refactor: pop the same context as the pushed one 2018-07-21 17:12:46 +08:00
harttle 63b5658604 refactor: src/scope.js 2018-07-21 17:02:18 +08:00
Ryan KennedyandJun Yang c79700ef1a fix: correctly understand variables with question marks
Fixes #77
2018-07-18 10:15:43 +08:00
harttle 6755462056 5.0.1 2018-07-16 15:04:40 +08:00
harttle 02806b2257 fix #75 2018-07-16 15:02:31 +08:00
136 changed files with 44808 additions and 4983 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules
dist
build
coverage
+6 -1
View File
@@ -1,11 +1,16 @@
{
"extends": "standard",
"env": {
"es6": true,
"browser": true,
"node": true
},
"plugins": [
"standard",
"promise"
]
],
"rules": {
"no-var": 2,
"prefer-const": 2
}
}
+8 -35
View File
@@ -1,41 +1,14 @@
# Logs
logs
# logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# tests
coverage/
.nyc_output/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# modules
node_modules/
build/
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# vim
# editors
.*.swp
package-lock.json
+3 -1
View File
@@ -1 +1,3 @@
test/
/coverage
/test
.*
+5 -2
View File
@@ -5,5 +5,8 @@ node_js:
- "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
script:
- if (( $TRAVIS_NODE_VERSION >= 6 )); then npm run test; fi
- npm run e2e
after_success:
- if (( $TRAVIS_NODE_VERSION >= 6 )); then npm run coveralls; fi
+1
View File
@@ -25,6 +25,7 @@ Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is
* Dynamic file locating (enabled by default), which means layout/partial name can be an variable in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
* Along with [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops), we provide an alias `.toLiquid()` to align with your code styles.
## TOC
+3 -6
View File
@@ -1,28 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<title></title>
<title>liquidjs for the browser</title>
<script src="../../dist/liquid.js"></script>
</head>
<body>
<script>
var engine = window.Liquid({
// root: './',
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) {
+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({
let app = express()
let engine = 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']
let 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>
+12
View File
@@ -0,0 +1,12 @@
const Liquid = require('../..')
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
engine.renderFile('todolist', ctx).then(console.log)
+5
View File
@@ -0,0 +1,5 @@
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
+21
View File
@@ -0,0 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
File diff suppressed because it is too large Load Diff
+16737
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "react-liquidjs",
"version": "0.1.0",
"private": true,
"dependencies": {
"dns": "^0.2.2",
"html-react-parser": "^0.4.7",
"liquidjs": "^6.0.1",
"path": "^0.12.7",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-promise": "^2.0.3",
"react-scripts": "2.0.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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

After

Width:  |  Height:  |  Size: 2.6 KiB

+127
View File
@@ -0,0 +1,127 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
// Execute callback
if (config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
+18
View File
@@ -0,0 +1,18 @@
<header className="App-header">
{{ logo | image }}
<h2>Welcome {{name | capitalize}}</h2>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<p>
You are using liquidjs & Reactjs
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
File diff suppressed because it is too large Load Diff
+3628
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
+3408 -2447
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
-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
+6671
View File
File diff suppressed because it is too large Load Diff
+69 -31
View File
@@ -1,19 +1,24 @@
{
"name": "liquidjs",
"version": "5.0.0",
"version": "6.2.0",
"description": "Liquid template engine by pure JavaScript: compatible to shopify, easy to extend.",
"main": "index.js",
"main": "dist/liquid.common.js",
"module": "src/index.js",
"browser": "dist/liquid.js",
"types": "src/index.d.ts",
"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",
"lint": "eslint src/ test/ *.js",
"dev": "mocha test/unit",
"test": "cross-env NODE_ENV=test nyc --reporter=html mocha test/unit",
"e2e": "mocha test/e2e",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"dist": "rollup -c && ls -lh dist",
"demo:browser": "echo open http://localhost:8080/demo/browser && http-server -c-1",
"demo:nodejs": "node ./demo/nodejs/index.js",
"demo:express": "cd ./demo/express/ && npm start",
"preversion": "npm run lint && npm test",
"version": "npm run dist && git add -A dist",
"postversion": "git push && git push --tags"
"postversion": "git push --tags"
},
"repository": {
"type": "git",
@@ -35,36 +40,69 @@
"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",
"babel-core": "^6.26.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-runtime": "^6.26.0",
"chai": "^4.1.2",
"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",
"cross-env": "^5.2.0",
"eslint": "^5.2.0",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-mocha": "^5.1.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"express": "^4.16.1",
"istanbul": "^0.4.5",
"http-server": "^0.11.1",
"jsdom": "^11.5.1",
"mocha": "^4.0.0",
"mocha": "^5.2.0",
"mock-fs": "^4.4.1",
"sinon": "^4.0.1",
"sinon-chai": "^2.14.0",
"supertest": "^3.0.0",
"uglify-js": "^3.1.3"
"nyc": "^13.0.1",
"regenerator-runtime": "^0.12.1",
"rollup": "^0.64.1",
"rollup-plugin-alias": "^1.4.0",
"rollup-plugin-babel": "^3.0.7",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-shim": "^1.0.0",
"rollup-plugin-uglify": "^4.0.0",
"sinon": "^6.1.4",
"sinon-chai": "^3.2.0",
"supertest": "^3.0.0"
},
"nyc": {
"require": [
"babel-core/register"
],
"sourceMap": false,
"instrument": false
},
"babel": {
"presets": [
"env"
],
"plugins": [
[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}
]
],
"env": {
"test": {
"plugins": [
"istanbul"
]
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import shim from 'rollup-plugin-shim'
import alias from 'rollup-plugin-alias'
import babel from 'rollup-plugin-babel'
import {uglify} from 'rollup-plugin-uglify'
import pkg from './package.json'
import nodeResolve from 'rollup-plugin-node-resolve'
const fake = {fs: `export default {}`, path: `export default {}`}
const version = process.env.VERSION || pkg.version
const sourcemap = true
const banner = `/*
* liquidjs@${version}, https://github.com/harttle/liquidjs
* (c) 2016-${new Date().getFullYear()} harttle
* Released under the MIT License.
*/`
const treeshake = {
propertyReadSideEffects: false
}
const input = 'src/index.js'
const babelConf = {
babelrc: false,
'presets': [['env', {'modules': false}]],
'plugins': ['external-helpers']
}
export default [{
output: [{
file: 'dist/liquid.common.js',
name: 'Liquid',
format: 'cjs',
sourcemap,
banner
}],
external: ['path', 'fs'],
plugins: [
nodeResolve(),
babel(babelConf)
],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.js',
name: 'Liquid',
format: 'umd',
sourcemap,
banner
}],
plugins: [
shim(fake),
alias({
'./template': './template-browser'
}),
nodeResolve(),
babel(babelConf)
],
treeshake,
input
}, {
output: [{
file: 'dist/liquid.min.js',
name: 'Liquid',
format: 'umd',
sourcemap
}],
plugins: [
shim(fake),
nodeResolve(),
babel(babelConf),
uglify()
],
treeshake,
input
}]
+20 -20
View File
@@ -1,27 +1,27 @@
const lexical = require('./lexical.js')
const Syntax = require('./syntax.js')
const assert = require('./util/assert.js')
const _ = require('./util/underscore.js')
import * as lexical from './lexical.js'
import {evalValue} from './syntax.js'
import assert from './util/assert.js'
import {assign, create} from './util/underscore.js'
var valueRE = new RegExp(`${lexical.value.source}`, 'g')
const valueRE = new RegExp(`${lexical.value.source}`, 'g')
module.exports = function (options) {
options = _.assign({}, options)
var filters = {}
export default function (options) {
options = assign({}, options)
let filters = {}
var _filterInstance = {
const _filterInstance = {
render: function (output, scope) {
var args = this.args.map(arg => Syntax.evalValue(arg, scope))
const args = this.args.map(arg => evalValue(arg, scope))
args.unshift(output)
return this.filter.apply(null, args)
},
parse: function (str) {
var match = lexical.filterLine.exec(str)
let match = lexical.filterLine.exec(str)
assert(match, 'illegal filter: ' + str)
var name = match[1]
var argList = match[2] || ''
var filter = filters[name]
const name = match[1]
const argList = match[2] || ''
const filter = filters[name]
if (typeof filter !== 'function') {
if (options.strict_filters) {
throw new TypeError(`undefined filter: ${name}`)
@@ -32,12 +32,12 @@ module.exports = function (options) {
return this
}
var args = []
const 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
const v = match[0]
const re = new RegExp(`${v}\\s*:`, 'g')
const keyMatch = re.exec(match.input)
const currentMatchIsKey = keyMatch && keyMatch.index === match.index
currentMatchIsKey ? args.push(`'${v}'`) : args.push(v)
}
@@ -50,7 +50,7 @@ module.exports = function (options) {
}
function construct (str) {
var instance = Object.create(_filterInstance)
const instance = create(_filterInstance)
return instance.parse(str)
}
+20 -20
View File
@@ -1,15 +1,15 @@
const strftime = require('./src/util/strftime.js')
const _ = require('./src/util/underscore.js')
const isTruthy = require('./src/syntax.js').isTruthy
import strftime from './util/strftime.js'
import * as _ from './util/underscore.js'
import {isTruthy} from './syntax.js'
var escapeMap = {
const escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
}
var unescapeMap = {
const unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
@@ -17,14 +17,14 @@ var unescapeMap = {
'&#39;': "'"
}
var filters = {
const filters = {
'abs': v => Math.abs(v),
'append': (v, arg) => v + arg,
'capitalize': str => stringify(str).charAt(0).toUpperCase() + str.slice(1),
'ceil': v => Math.ceil(v),
'concat': (v, arg) => Array.prototype.concat.call(v, arg),
'date': (v, arg) => {
var date = v
let date = v
if (v === 'now') {
date = new Date()
} else if (_.isString(v)) {
@@ -41,7 +41,7 @@ var filters = {
'first': v => v[0],
'floor': v => Math.floor(v),
'join': (v, arg) => v.join(arg),
'last': v => v[v.length - 1],
'last': v => _.last(v),
'lstrip': v => stringify(v).replace(/^\s+/, ''),
'map': (arr, arg) => arr.map(v => v[arg]),
'minus': bindFixed((v, arg) => v - arg),
@@ -56,7 +56,7 @@ var filters = {
'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
'reverse': v => v.reverse(),
'round': (v, arg) => {
var amp = Math.pow(10, arg || 0)
const amp = Math.pow(10, arg || 0)
return Math.round(v * amp, arg) / amp
},
'rstrip': str => stringify(str).replace(/\s+$/, ''),
@@ -78,13 +78,13 @@ var filters = {
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...'
var arr = v.split(' ')
var ret = arr.slice(0, l).join(' ')
const arr = v.split(' ')
let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o
return ret
},
'uniq': function (arr) {
var u = {}
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false
@@ -94,7 +94,8 @@ var filters = {
})
},
'upcase': str => stringify(str).toUpperCase(),
'url_encode': encodeURIComponent
'url_decode': x => x.split('+').map(decodeURIComponent).join(' '),
'url_encode': x => x.split(' ').map(encodeURIComponent).join('+')
}
function escape (str) {
@@ -106,7 +107,7 @@ function unescape (str) {
}
function getFixed (v) {
var p = (v + '').split('.')
const p = (v + '').split('.')
return (p.length > 1) ? p[1].length : 0
}
@@ -120,18 +121,17 @@ function stringify (obj) {
function bindFixed (cb) {
return (l, r) => {
var f = getMaxFixed(l, r)
const 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())
}
export default function registerAll (liquid) {
return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func))
}
registerAll.filters = filters
module.exports = registerAll
+81
View File
@@ -0,0 +1,81 @@
declare function Liquid(): Liquid.LiquidEngine;
declare namespace Liquid {
export interface LiquidEngine {
private init(tag, filter, options): LiquidEngine
private respectCache(key, getter): Promise<any>
private evalValue: typeof Liquid.evalValue
public parse(html: string, filepath: string): Liquid.Template
public render(tpl: Template, ctx: any, opts: Options): Promise<string>
public parseAndRender(html: string, ctx: any, opts: Options): Promise<string>
public getTemplate(file: string, root: string): Promise<Liquid.Template>
public renderFile(file: string, ctx: any, opts: Options): Promise<string>
public registerFilter(name: string, filter: Filter): void
public registerTag(name: string, tag: Tag): void
public express(opts: Options): any
}
export interface Template { }
export interface Options {
/** `root` is a directory or an array of directories to resolve layouts and includes, as well as the filename passed in when calling `.renderFile()`. If an array, the files are looked up in the order they occur in the array. Defaults to `["."]`*/
root: string | string[]
/** `extname` is used to lookup the template file when filepath doesn't include an extension name. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */
extname: string
/** `cache` indicates whether or not to cache resolved templates. Defaults to `false`. */
cache: boolean
/** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
dynamicPartials: boolean
/** `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
strict_filters: boolean
/** `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
trim_tag_right: boolean
/** `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_tag_left: boolean
/** ``trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
trim_value_right: boolean
/** `trim_value_left` is similiar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_value_left: boolean
/** `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
greedy: boolean
}
export interface Tag {
// TODO: tokens type; this type
parse(this: any, tagToken: any, remainTokens: any): void
render(this: any, scope: any, hash: any): void
}
export type Filter = (...args?: any) => string
export function isTruthy(val: any): boolean;
export function isFalsy(val: any): boolean;
export function evalExp(exp: string, scope: any): any;
export function evalValue(str: string, scope: any): any;
export const Types: Types;
declare class LiquidError extends Error {
public input: string
public line: number
public file: string
}
export class ParseError extends LiquidError {
public originalError: Error
}
export class TokenizationError extends LiquidError { }
export class RenderBreakError extends LiquidError { }
export class AssertionError extends LiquidError { }
export interface Types {
ParseError: typeof ParseError,
TokenizationError: typeof TokenizationError,
RenderBreakError: typeof RenderBreakError,
AssertionError: typeof AssertionError,
AssignScope: any,
CaptureScope: any,
IncrementScope: any,
DecrementScope: any
}
}
export = Liquid;
export default Liquid;
+130
View File
@@ -0,0 +1,130 @@
import 'regenerator-runtime/runtime'
import * as Scope from './scope'
import * as template from './template'
import * as _ from './util/underscore.js'
import assert from './util/assert.js'
import * as tokenizer from './tokenizer.js'
import Render from './render.js'
import Tag from './tag.js'
import Filter from './filter.js'
import Parser from './parser'
import {isTruthy, isFalsy, evalExp, evalValue} from './syntax.js'
import {ParseError, TokenizationError, RenderBreakError, AssertionError} from './util/error.js'
import tags from './tags/index.js'
import filters from './filters.js'
const _engine = {
init: function (tag, filter, options) {
if (options.cache) {
this.cache = {}
}
this.options = options
this.tag = tag
this.filter = filter
this.parser = Parser(tag, filter)
this.renderer = Render()
tags(this, Liquid)
filters(this, Liquid)
return this
},
parse: function (html, filepath) {
const tokens = tokenizer.parse(html, filepath, this.options)
return this.parser.parse(tokens)
},
render: function (tpl, ctx, opts) {
opts = _.assign({}, this.options, opts)
const scope = Scope.factory(ctx, opts)
return this.renderer.renderTemplates(tpl, scope)
},
parseAndRender: async function (html, ctx, opts) {
const tpl = await this.parse(html)
return this.render(tpl, ctx, opts)
},
getTemplate: async function (file, root) {
const filepath = await template.resolve(file, root, this.options)
return this.respectCache(filepath, async () => {
const str = await template.read(filepath)
return this.parse(str, filepath)
})
},
renderFile: async function (file, ctx, opts) {
opts = _.assign({}, opts)
const templates = await this.getTemplate(file, opts.root)
return this.render(templates, ctx, opts)
},
respectCache: async function (key, getter) {
const cacheEnabled = this.options.cache
if (cacheEnabled && this.cache[key]) {
return this.cache[key]
}
const value = await getter()
if (cacheEnabled) {
this.cache[key] = value
}
return value
},
evalValue: function (str, scope) {
const tpl = this.parser.parseValue(str.trim())
return this.renderer.evalValue(tpl, scope)
},
registerFilter: function (name, filter) {
return this.filter.register(name, filter)
},
registerTag: function (name, tag) {
return this.tag.register(name, tag)
},
express: function (opts) {
opts = opts || {}
const self = this
return function (filePath, ctx, cb) {
assert(_.isArray(this.root) || _.isString(this.root),
'illegal views root, are you using express.js?')
opts.root = this.root
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
}
}
function normalizeStringArray (value) {
if (_.isArray(value)) return value
if (_.isString(value)) return [value]
throw new TypeError('illegal root: ' + value)
}
export default function Liquid (options) {
options = _.assign({
root: ['.'],
cache: false,
extname: '',
dynamicPartials: true,
trim_tag_right: false,
trim_tag_left: false,
trim_value_right: false,
trim_value_left: false,
greedy: true,
strict_filters: false,
strict_variables: false
}, options)
options.root = normalizeStringArray(options.root)
const engine = _.create(_engine)
engine.init(Tag(), Filter(options), options)
return engine
}
Liquid.isTruthy = isTruthy
Liquid.isFalsy = isFalsy
Liquid.evalExp = evalExp
Liquid.evalValue = evalValue
Liquid.Types = {
ParseError,
TokenizationError,
RenderBreakError,
AssertionError,
AssignScope: {},
CaptureScope: {},
IncrementScope: {},
DecrementScope: {}
}
+39 -69
View File
@@ -1,76 +1,76 @@
// quote related
var singleQuoted = /'[^']*'/
var doubleQuoted = /"[^"]*"/
var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
var quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
const singleQuoted = /'[^']*'/
const doubleQuoted = /"[^"]*"/
export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
// basic types
var integer = /-?\d+/
var number = /-?\d+\.?\d*|\.?\d+/
var bool = /true|false/
export const integer = /-?\d+/
export const number = /-?\d+\.?\d*|\.?\d+/
export const bool = /true|false/
// peoperty access
var identifier = /[\w-]+/
var subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`)
var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`)
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`)
// property access
export const identifier = /[\w-]+[?]?/
export const subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`)
export const literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`)
export const variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`)
// range related
var rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`)
var range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`)
var rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`)
export const rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`)
export const range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`)
export const rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`)
var value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
export const value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
// hash related
var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`)
var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g')
export const hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`)
export const hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g')
// full match
var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*)\\s*$`)
var literalLine = new RegExp(`^${literal.source}$`, 'i')
var variableLine = new RegExp(`^${variable.source}$`)
var numberLine = new RegExp(`^${number.source}$`)
var boolLine = new RegExp(`^${bool.source}$`, 'i')
var quotedLine = new RegExp(`^${quoted.source}$`)
var rangeLine = new RegExp(`^${rangeCapture.source}$`)
var integerLine = new RegExp(`^${integer.source}$`)
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*)\\s*$`)
export const literalLine = new RegExp(`^${literal.source}$`, 'i')
export const variableLine = new RegExp(`^${variable.source}$`)
export const numberLine = new RegExp(`^${number.source}$`)
export const boolLine = new RegExp(`^${bool.source}$`, 'i')
export const quotedLine = new RegExp(`^${quoted.source}$`)
export const rangeLine = new RegExp(`^${rangeCapture.source}$`)
export const integerLine = new RegExp(`^${integer.source}$`)
// filter related
var 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}$`)
export const valueDeclaration = new RegExp(`(?:${identifier.source}\\s*:\\s*)?${value.source}`)
export const valueList = new RegExp(`${valueDeclaration.source}(\\s*,\\s*${valueDeclaration.source})*`)
export const filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g')
export const filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`)
export const filterLine = new RegExp(`^${filterCapture.source}$`)
var operators = [
export const operators = [
/\s+or\s+/,
/\s+and\s+/,
/==|!=|<=|>=|<|>|\s+contains\s+/
]
function isInteger (str) {
export function isInteger (str) {
return integerLine.test(str)
}
function isLiteral (str) {
export function isLiteral (str) {
return literalLine.test(str)
}
function isRange (str) {
export function isRange (str) {
return rangeLine.test(str)
}
function isVariable (str) {
export function isVariable (str) {
return variableLine.test(str)
}
function matchValue (str) {
export function matchValue (str) {
return value.exec(str)
}
function parseLiteral (str) {
var res = str.match(numberLine)
export function parseLiteral (str) {
let res = str.match(numberLine)
if (res) {
return Number(str)
}
@@ -84,33 +84,3 @@ function parseLiteral (str) {
}
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
}
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = function (isTruthy) {
export default function (isTruthy) {
return {
'==': (l, r) => l === r,
'!=': (l, r) => l !== r,
+16 -15
View File
@@ -1,9 +1,10 @@
const lexical = require('./lexical.js')
const ParseError = require('./util/error.js').ParseError
const assert = require('./util/assert.js')
import * as lexical from './lexical.js'
import {create} from './util/underscore.js'
import {ParseError} from './util/error.js'
import assert from './util/assert.js'
module.exports = function (Tag, Filter) {
var stream = {
export default function (Tag, Filter) {
const stream = {
init: function (tokens) {
this.tokens = tokens
this.handlers = {}
@@ -14,7 +15,7 @@ module.exports = function (Tag, Filter) {
return this
},
trigger: function (event, arg) {
var h = this.handlers[event]
const h = this.handlers[event]
if (typeof h === 'function') {
h(arg)
return true
@@ -22,14 +23,14 @@ module.exports = function (Tag, Filter) {
},
start: function () {
this.trigger('start')
var token
let token
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue
if (token.type === 'tag' &&
this.trigger(`tag:${token.name}`, token)) {
continue
}
var template = parseToken(token, this.tokens)
const template = parseToken(token, this.tokens)
this.trigger('template', template)
}
if (!this.stopRequested) this.trigger('end')
@@ -42,8 +43,8 @@ module.exports = function (Tag, Filter) {
}
function parse (tokens) {
var token
var templates = []
let token
const templates = []
while ((token = tokens.shift())) {
templates.push(parseToken(token, tokens))
}
@@ -52,7 +53,7 @@ module.exports = function (Tag, Filter) {
function parseToken (token, tokens) {
try {
var tpl = null
let tpl = null
if (token.type === 'tag') {
tpl = parseTag(token, tokens)
} else if (token.type === 'value') {
@@ -73,13 +74,13 @@ module.exports = function (Tag, Filter) {
}
function parseValue (str) {
var match = lexical.matchValue(str)
let match = lexical.matchValue(str)
assert(match, `illegal value string: ${str}`)
var initial = match[0]
const initial = match[0]
str = str.substr(match.index + match[0].length)
var filters = []
const filters = []
while ((match = lexical.filter.exec(str))) {
filters.push([match[0].trim()])
}
@@ -92,7 +93,7 @@ module.exports = function (Tag, Filter) {
}
function parseStream (tokens) {
var s = Object.create(stream)
const s = create(stream)
return s.init(tokens)
}
+39 -44
View File
@@ -1,67 +1,62 @@
const Syntax = require('./syntax.js')
const mapSeries = require('./util/promise.js').mapSeries
const RenderBreakError = require('./util/error.js').RenderBreakError
const RenderError = require('./util/error.js').RenderError
const assert = require('./util/assert.js')
import {evalExp} from './syntax.js'
import {RenderBreakError, RenderError} from './util/error.js'
import {stringify, create} from './util/underscore.js'
import assert from './util/assert.js'
var render = {
renderTemplates: function (templates, scope) {
const render = {
renderTemplates: async function (templates, scope) {
assert(scope, 'unable to evalTemplates: scope undefined')
var html = ''
return mapSeries(templates, (tpl) => {
return renderTemplate.call(this, tpl)
.then(partial => (html += partial))
.catch(e => {
if (e instanceof RenderBreakError) {
e.resolvedHTML = html
throw e
}
throw new RenderError(e, tpl)
})
}).then(() => html)
function renderTemplate (template) {
if (template.type === 'tag') {
return this.renderTag(template, scope)
.then(partial => partial === undefined ? '' : partial)
} else if (template.type === 'value') {
return Promise.resolve()
.then(() => this.evalValue(template, scope))
.then(partial => partial === undefined ? '' : stringify(partial))
} else { // template.type === 'html'
return Promise.resolve(template.value)
let html = ''
for (const tpl of templates) {
try {
html += await renderTemplate.call(this, tpl)
} catch (e) {
if (e instanceof RenderBreakError) {
e.resolvedHTML = html
throw e
}
throw new RenderError(e, tpl)
}
}
return html
async function renderTemplate (template) {
if (template.type === 'tag') {
const partial = await this.renderTag(template, scope)
return partial === undefined ? '' : partial
}
if (template.type === 'value') {
return this.renderValue(template, scope)
}
return template.value
}
},
renderTag: function (template, scope) {
renderTag: async function (template, scope) {
if (template.name === 'continue') {
return Promise.reject(new RenderBreakError('continue'))
throw new RenderBreakError('continue')
}
if (template.name === 'break') {
return Promise.reject(new RenderBreakError('break'))
throw new RenderBreakError('break')
}
return template.render(scope)
},
renderValue: async function (template, scope) {
const partial = this.evalValue(template, scope)
return partial === undefined ? '' : stringify(partial)
},
evalValue: function (template, scope) {
assert(scope, 'unable to evalValue: scope undefined')
return template.filters.reduce(
(prev, filter) => filter.render(prev, scope),
Syntax.evalExp(template.initial, scope))
evalExp(template.initial, scope))
}
}
function factory () {
var instance = Object.create(render)
export default function () {
const instance = create(render)
return instance
}
function stringify (val) {
if (typeof val === 'string') return val
return JSON.stringify(val)
}
module.exports = factory
+86 -106
View File
@@ -1,70 +1,81 @@
const _ = require('./util/underscore.js')
const lexical = require('./lexical.js')
const assert = require('./util/assert.js')
import * as _ from './util/underscore.js'
import * as lexical from './lexical.js'
import assert from './util/assert.js'
var Scope = {
const Scope = {
getAll: function () {
var ctx = {}
for (var i = this.scopes.length - 1; i >= 0; i--) {
_.assign(ctx, this.scopes[i])
}
return ctx
return this.contexts.reduce((ctx, val) => _.assign(ctx, val), _.create(null))
},
get: function (str) {
try {
return this.getPropertyByPath(this.scopes, str)
} catch (e) {
if (!/undefined variable/.test(e.message) || this.opts.strict_variables) {
throw e
get: function (path) {
const paths = this.propertyAccessSeq(path)
const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
return paths.reduce((value, key) => this.readProperty(value, key), scope)
},
set: function (path, v) {
const paths = this.propertyAccessSeq(path)
let scope = this.findContextFor(paths[0]) || _.last(this.contexts)
paths.some((key, i) => {
if (!_.isObject(scope)) {
return true
}
}
},
set: function (k, v) {
var scope = this.findScopeFor(k)
setPropertyByPath(scope, k, v)
return this
},
push: function (ctx) {
assert(ctx, `trying to push ${ctx} into scopes`)
return this.scopes.push(ctx)
},
pop: function () {
return this.scopes.pop()
},
findScopeFor: function (key) {
var i = this.scopes.length - 1
while (i >= 0 && !(key in this.scopes[i])) {
i--
}
if (i < 0) {
i = this.scopes.length - 1
}
return this.scopes[i]
if (i === paths.length - 1) {
scope[key] = v
return true
}
if (undefined === scope[key]) {
scope[key] = {}
}
scope = scope[key]
})
},
unshift: function (ctx) {
assert(ctx, `trying to push ${ctx} into scopes`)
return this.scopes.unshift(ctx)
return this.contexts.unshift(ctx)
},
shift: function () {
return this.scopes.shift()
push: function (ctx) {
return this.contexts.push(ctx)
},
getPropertyByPath: function (scopes, path) {
var paths = this.propertyAccessSeq(path + '')
if (!paths.length) {
throw new TypeError('undefined variable: ' + path)
pop: function (ctx) {
if (!arguments.length) {
return this.contexts.pop()
}
var key = paths.shift()
var value = getValueFromScopes(key, scopes)
return paths.reduce(
(value, key) => {
if (_.isNil(value)) {
throw new TypeError('undefined variable: ' + key)
}
return getValueFromParent(key, value)
},
value
)
const i = this.contexts.findIndex(scope => scope === ctx)
if (i === -1) {
throw new TypeError('scope not found, cannot pop')
}
return this.contexts.splice(i, 1)[0]
},
findContextFor: function (key, filter) {
filter = filter || (() => true)
for (let i = this.contexts.length - 1; i >= 0; i--) {
const candidate = this.contexts[i]
if (!filter(candidate)) continue
if (key in candidate) {
return candidate
}
}
return null
},
readProperty: function (obj, key) {
let val
if (_.isNil(obj)) {
val = undefined
} else {
if (typeof obj.to_liquid === 'function') {
obj = obj.to_liquid()
} else if (typeof obj.toLiquid === 'function') {
obj = obj.toLiquid()
}
if (key === 'size' && (_.isArray(obj) || _.isString(obj))) {
val = obj.length
} else {
val = obj[key]
}
}
if (_.isNil(val) && this.opts.strict_variables) {
throw new TypeError(`undefined variable: ${key}`)
}
return val
},
/*
@@ -76,16 +87,17 @@ var Scope = {
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
propertyAccessSeq: function (str) {
var seq = []
var name = ''
var j
var i = 0
str = String(str)
const seq = []
let name = ''
let j
let i = 0
while (i < str.length) {
switch (str[i]) {
case '[':
push()
var delemiter = str[i + 1]
const delemiter = str[i + 1]
if (/['"]/.test(delemiter)) { // foo["bar"]
j = str.indexOf(delemiter, i + 2)
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
@@ -97,7 +109,7 @@ var Scope = {
assert(j !== -1, `unbalanced []: ${str}`)
name = str.slice(i + 1, j)
if (!lexical.isInteger(name)) { // foo[bar] vs. foo[1]
name = this.get(name)
name = String(this.get(name))
}
push()
i = j + 1
@@ -113,6 +125,10 @@ var Scope = {
}
}
push()
if (!seq.length) {
throw new TypeError(`invalid path:"${str}"`)
}
return seq
function push () {
@@ -122,45 +138,9 @@ var Scope = {
}
}
function setPropertyByPath (obj, path, val) {
var paths = (path + '').replace(/\[/g, '.').replace(/\]/g, '').split('.')
for (var i = 0; i < paths.length; i++) {
var key = paths[i]
if (!_.isObject(obj)) {
// cannot set property of non-object
return
}
// for end point
if (i === paths.length - 1) {
return (obj[key] = val)
}
// if path not exist
if (undefined === obj[key]) {
obj[key] = {}
}
obj = obj[key]
}
}
function getValueFromParent (key, value) {
return (key === 'size' && (_.isArray(value) || _.isString(value)))
? value.length
: value[key]
}
function getValueFromScopes (key, scopes) {
for (var i = scopes.length - 1; i > -1; i--) {
var scope = scopes[i]
if (scope.hasOwnProperty(key)) {
return scope[key]
}
}
throw new TypeError('undefined variable: ' + key)
}
function matchRightBracket (str, begin) {
var stack = 1 // count of '[' - count of ']'
for (var i = begin; i < str.length; i++) {
let stack = 1 // count of '[' - count of ']'
for (let i = begin; i < str.length; i++) {
if (str[i] === '[') {
stack++
}
@@ -174,16 +154,16 @@ function matchRightBracket (str, begin) {
return -1
}
exports.factory = function (ctx, opts) {
var defaultOptions = {
export function factory (ctx, opts) {
const defaultOptions = {
dynamicPartials: true,
strict_variables: false,
strict_filters: false,
blocks: {},
root: []
}
var scope = Object.create(Scope)
const scope = _.create(Scope)
scope.opts = _.assign(defaultOptions, opts)
scope.scopes = [ctx || {}]
scope.contexts = [ctx || {}]
return scope
}
+21 -23
View File
@@ -1,27 +1,29 @@
const operators = require('./operators.js')(isTruthy)
const lexical = require('./lexical.js')
const assert = require('../src/util/assert.js')
import Operators from './operators.js'
import * as lexical from './lexical.js'
import assert from './util/assert.js'
function evalExp (exp, scope) {
const operators = Operators(isTruthy)
export function evalExp (exp, scope) {
assert(scope, 'unable to evalExp: scope undefined')
var operatorREs = lexical.operators
var match
for (var i = 0; i < operatorREs.length; i++) {
var operatorRE = operatorREs[i]
var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
const operatorREs = lexical.operators
let match
for (let i = 0; i < operatorREs.length; i++) {
const operatorRE = operatorREs[i]
const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`)
if ((match = exp.match(expRE))) {
var l = evalExp(match[1], scope)
var op = operators[match[2].trim()]
var r = evalExp(match[3], scope)
const l = evalExp(match[1], scope)
const op = operators[match[2].trim()]
const r = evalExp(match[3], scope)
return op(l, r)
}
}
if ((match = exp.match(lexical.rangeLine))) {
var low = evalValue(match[1], scope)
var high = evalValue(match[2], scope)
var range = []
for (var j = low; j <= high; j++) {
const low = evalValue(match[1], scope)
const high = evalValue(match[2], scope)
const range = []
for (let j = low; j <= high; j++) {
range.push(j)
}
return range
@@ -30,7 +32,7 @@ function evalExp (exp, scope) {
return evalValue(exp, scope)
}
function evalValue (str, scope) {
export function evalValue (str, scope) {
str = str && str.trim()
if (!str) return undefined
@@ -43,14 +45,10 @@ function evalValue (str, scope) {
throw new TypeError(`cannot eval '${str}' as value`)
}
function isTruthy (val) {
export function isTruthy (val) {
return !isFalsy(val)
}
function isFalsy (val) {
export function isFalsy (val) {
return val === false || undefined === val || val === null
}
module.exports = {
evalExp, evalValue, isTruthy, isFalsy
}
+22 -21
View File
@@ -1,39 +1,40 @@
const lexical = require('./lexical.js')
const Syntax = require('./syntax.js')
const assert = require('./util/assert.js')
import {hashCapture} from './lexical.js'
import {create} from './util/underscore.js'
import {evalValue} from './syntax.js'
import assert from './util/assert.js'
function hash (markup, scope) {
var obj = {}
var match
lexical.hashCapture.lastIndex = 0
while ((match = lexical.hashCapture.exec(markup))) {
var k = match[1]
var v = match[2]
obj[k] = Syntax.evalValue(v, scope)
const obj = {}
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
const k = match[1]
const v = match[2]
obj[k] = evalValue(v, scope)
}
return obj
}
module.exports = function () {
var tagImpls = {}
export default function () {
let tagImpls = {}
var _tagInstance = {
render: function (scope) {
var obj = hash(this.token.args, scope)
var impl = this.tagImpl
const _tagInstance = {
render: async function (scope) {
const obj = hash(this.token.args, scope)
const impl = this.tagImpl
if (typeof impl.render !== 'function') {
return Promise.resolve('')
return ''
}
return Promise.resolve().then(() => impl.render(scope, obj))
return impl.render(scope, obj)
},
parse: function (token, tokens) {
this.type = 'tag'
this.token = token
this.name = token.name
var tagImpl = tagImpls[this.name]
const tagImpl = tagImpls[this.name]
assert(tagImpl, `tag ${this.name} not found`)
this.tagImpl = Object.create(tagImpl)
this.tagImpl = create(tagImpl)
if (this.tagImpl.parse) {
this.tagImpl.parse(token, tokens)
}
@@ -45,7 +46,7 @@ module.exports = function () {
}
function construct (token, tokens) {
var instance = Object.create(_tagInstance)
const instance = create(_tagInstance)
instance.parse(token, tokens)
return instance
}
+23
View File
@@ -0,0 +1,23 @@
import assert from '../util/assert.js'
import {identifier} from '../lexical.js'
import {create} from '../util/underscore.js'
export default function (liquid, Liquid) {
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
const {AssignScope} = Liquid.Types
liquid.registerTag('assign', {
parse: function (token) {
const match = token.args.match(re)
assert(match, `illegal token ${token.raw}`)
this.key = match[1]
this.value = match[2]
},
render: function (scope) {
const ctx = create(AssignScope)
ctx[this.key] = liquid.evalValue(this.value, scope)
scope.push(ctx)
return Promise.resolve('')
}
})
}
+32
View File
@@ -0,0 +1,32 @@
import assert from '../util/assert.js'
import {create} from '../util/underscore.js'
import {identifier} from '../lexical.js'
export default function (liquid, Liquid) {
const re = new RegExp(`(${identifier.source})`)
const {CaptureScope} = Liquid.Types
liquid.registerTag('capture', {
parse: function (tagToken, remainTokens) {
const match = tagToken.args.match(re)
assert(match, `${tagToken.args} not valid identifier`)
this.variable = match[1]
this.templates = []
const stream = liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', token => stream.stop())
.on('template', tpl => this.templates.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope, hash) {
const html = await liquid.renderer.renderTemplates(this.templates, scope)
const ctx = create(CaptureScope)
ctx[this.variable] = html
scope.push(ctx)
}
})
}
+7 -9
View File
@@ -1,6 +1,4 @@
const Liquid = require('..')
module.exports = function (liquid) {
export default function (liquid, Liquid) {
liquid.registerTag('case', {
parse: function (tagToken, remainTokens) {
@@ -8,8 +6,8 @@ module.exports = function (liquid) {
this.cases = []
this.elseTemplates = []
var p = []
var stream = liquid.parser.parseStream(remainTokens)
let p = []
const stream = liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
this.cases.push({
val: token.args,
@@ -27,10 +25,10 @@ module.exports = function (liquid) {
},
render: function (scope, hash) {
for (var i = 0; i < this.cases.length; i++) {
var branch = this.cases[i]
var val = Liquid.evalExp(branch.val, scope)
var cond = Liquid.evalExp(this.cond, scope)
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = Liquid.evalExp(branch.val, scope)
const cond = Liquid.evalExp(this.cond, scope)
if (val === cond) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
+2 -2
View File
@@ -1,7 +1,7 @@
module.exports = function (liquid) {
export default function (liquid) {
liquid.registerTag('comment', {
parse: function (tagToken, remainTokens) {
var stream = liquid.parser.parseStream(remainTokens)
const stream = liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
if (token.name === 'endcomment') stream.stop()
+43
View File
@@ -0,0 +1,43 @@
import assert from '../util/assert.js'
import {value as rValue} from '../lexical.js'
export default function (liquid, Liquid) {
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(rValue.source, 'g')
liquid.registerTag('cycle', {
parse: function (tagToken, remainTokens) {
let match = groupRE.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.group = match[1] || ''
const candidates = match[2]
this.candidates = []
while ((match = candidatesRE.exec(candidates))) {
this.candidates.push(match[0])
}
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: function (scope, hash) {
const group = Liquid.evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.opts.groups = scope.opts.groups || {}
let idx = groups[fingerprint]
if (idx === undefined) {
idx = groups[fingerprint] = 0
}
const candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
return Liquid.evalValue(candidate, scope)
}
})
}
+32
View File
@@ -0,0 +1,32 @@
import {create} from '../util/underscore.js'
import assert from '../util/assert.js'
import {identifier} from '../lexical.js'
export default function (liquid, Liquid) {
const {CaptureScope, AssignScope, DecrementScope} = Liquid.Types
liquid.registerTag('decrement', {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
let context = scope.findContextFor(
this.variable,
ctx => {
const proto = Object.getPrototypeOf(ctx)
return proto !== CaptureScope && proto !== AssignScope
}
)
if (!context) {
context = create(DecrementScope)
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
return --context[this.variable]
}
})
}
+93
View File
@@ -0,0 +1,93 @@
import {mapSeries} from '../util/promise.js'
import {isString, isObject, isArray} from '../util/underscore.js'
import assert from '../util/assert.js'
import {identifier, value, hash} from '../lexical.js'
export default function (liquid, Liquid) {
const RenderBreakError = Liquid.Types.RenderBreakError
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*` +
`(?:\\s+(reversed))?` +
`(?:\\s+${hash.source})*$`)
liquid.registerTag('for', {parse, render})
function parse (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.reversed = !!match[3]
this.templates = []
this.elseTemplates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
}
async function render (scope, hash) {
let collection = Liquid.evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
collection = [collection]
} else if (isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]])
}
}
if (!isArray(collection) || !collection.length) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
const contexts = collection.map((item, i) => {
const ctx = {}
ctx[this.variable] = item
ctx.forloop = {
first: i === 0,
index: i + 1,
index0: i,
last: i === collection.length - 1,
length: collection.length,
rindex: collection.length - i,
rindex0: collection.length - i - 1
}
return ctx
})
let html = ''
let finished = false
await mapSeries(contexts, async context => {
if (finished) return
scope.push(context)
try {
html += await liquid.renderer.renderTemplates(this.templates, scope)
} catch (e) {
if (e instanceof RenderBreakError) {
html += e.resolvedHTML
if (e.message === 'break') {
finished = true
}
} else throw e
}
scope.pop(context)
})
return html
}
}
+5 -8
View File
@@ -1,14 +1,12 @@
const Liquid = require('..')
module.exports = function (liquid) {
export default function (liquid, Liquid) {
liquid.registerTag('if', {
parse: function (tagToken, remainTokens) {
this.branches = []
this.elseTemplates = []
var p
var stream = liquid.parser.parseStream(remainTokens)
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
cond: tagToken.args,
templates: (p = [])
@@ -30,9 +28,8 @@ module.exports = function (liquid) {
},
render: function (scope, hash) {
for (var i = 0; i < this.branches.length; i++) {
var branch = this.branches[i]
var cond = Liquid.evalExp(branch.cond, scope)
for (const branch of this.branches) {
const cond = Liquid.evalExp(branch.cond, scope)
if (Liquid.isTruthy(cond)) {
return liquid.renderer.renderTemplates(branch.templates, scope)
}
+57
View File
@@ -0,0 +1,57 @@
import assert from '../util/assert.js'
import {value, quotedLine} from '../lexical.js'
const staticFileRE = /[^\s,]+/
export default function (liquid, Liquid) {
const withRE = new RegExp(`with\\s+(${value.source})`)
liquid.registerTag('include', {
parse: function (token) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticValue = match[0]
}
match = value.exec(token.args)
if (match) {
this.value = match[0]
}
match = withRE.exec(token.args)
if (match) {
this.with = match[1]
}
},
render: async function (scope, hash) {
let filepath
if (scope.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
const template = this.value.slice(1, -1)
filepath = await liquid.parseAndRender(template, scope.getAll(), scope.opts)
} else {
filepath = Liquid.evalValue(this.value, scope)
}
} else {
filepath = this.staticValue
}
assert(filepath, `cannot include with empty filename`)
const originBlocks = scope.opts.blocks
const originBlockMode = scope.opts.blockMode
scope.opts.blocks = {}
scope.opts.blockMode = 'output'
if (this.with) {
hash[filepath] = Liquid.evalValue(this.with, scope)
}
const templates = await liquid.getTemplate(filepath, scope.opts.root)
scope.push(hash)
const html = await liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
scope.opts.blocks = originBlocks
scope.opts.blockMode = originBlockMode
return html
}
})
}
+34
View File
@@ -0,0 +1,34 @@
import assert from '../util/assert.js'
import {create} from '../util/underscore.js'
import {identifier} from '../lexical.js'
export default function (liquid, Liquid) {
const {CaptureScope, AssignScope, IncrementScope} = Liquid.Types
liquid.registerTag('increment', {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
let context = scope.findContextFor(
this.variable,
ctx => {
const proto = Object.getPrototypeOf(ctx)
return proto !== CaptureScope && proto !== AssignScope
}
)
if (!context) {
context = create(IncrementScope)
scope.unshift(context)
}
if (typeof context[this.variable] !== 'number') {
context[this.variable] = 0
}
const val = context[this.variable]
context[this.variable]++
return val
}
})
}
+31
View File
@@ -0,0 +1,31 @@
import For from './for.js'
import Assign from './assign.js'
import Capture from './capture.js'
import Case from './case.js'
import Comment from './comment.js'
import Include from './include.js'
import Decrement from './decrement.js'
import Cycle from './cycle.js'
import If from './if.js'
import Increment from './increment.js'
import Layout from './layout.js'
import Raw from './raw.js'
import Tablerow from './tablerow.js'
import Unless from './unless.js'
export default function (engine, Liquid) {
Assign(engine, Liquid)
Capture(engine, Liquid)
Case(engine, Liquid)
Comment(engine, Liquid)
Cycle(engine, Liquid)
Decrement(engine, Liquid)
For(engine, Liquid)
If(engine, Liquid)
Include(engine, Liquid)
Increment(engine, Liquid)
Layout(engine, Liquid)
Raw(engine, Liquid)
Tablerow(engine, Liquid)
Unless(engine, Liquid)
}
+75
View File
@@ -0,0 +1,75 @@
import assert from '../util/assert.js'
import {value as rValue} from '../lexical.js'
/*
* blockMode:
* * "store": store rendered html into blocks
* * "output": output rendered html
*/
export default function (liquid, Liquid) {
const staticFileRE = /\S+/
liquid.registerTag('layout', {
parse: function (token, remainTokens) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticLayout = match[0]
}
match = rValue.exec(token.args)
if (match) {
this.layout = match[0]
}
this.tpls = liquid.parser.parse(remainTokens)
},
render: async function (scope, hash) {
const layout = scope.opts.dynamicPartials
? Liquid.evalValue(this.layout, scope)
: this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
scope.opts.blockMode = 'store'
const html = await liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.opts.blocks[''] === undefined) {
scope.opts.blocks[''] = html
}
const templates = await liquid.getTemplate(layout, scope.opts.root)
scope.push(hash)
scope.opts.blockMode = 'output'
const partial = await liquid.renderer.renderTemplates(templates, scope)
scope.pop(hash)
return partial
}
})
liquid.registerTag('block', {
parse: function (token, remainTokens) {
const match = /\w+/.exec(token.args)
this.block = match ? match[0] : ''
this.tpls = []
const stream = liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`)
})
stream.start()
},
render: async function (scope) {
const childDefined = scope.opts.blocks[this.block]
const html = childDefined !== undefined
? childDefined
: await liquid.renderer.renderTemplates(this.tpls, scope)
if (scope.opts.blockMode === 'store') {
scope.opts.blocks[this.block] = html
return ''
}
return html
}
})
}
+4 -5
View File
@@ -1,22 +1,21 @@
module.exports = function (liquid) {
export default function (liquid) {
liquid.registerTag('raw', {
parse: function (tagToken, remainTokens) {
this.tokens = []
var stream = liquid.parser.parseStream(remainTokens)
const stream = liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
if (token.name === 'endraw') stream.stop()
else this.tokens.push(token)
})
.on('end', x => {
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
var tokens = this.tokens.map(token => token.raw).join('')
return Promise.resolve(tokens)
return this.tokens.map(token => token.raw).join('')
}
})
}
+70
View File
@@ -0,0 +1,70 @@
import {mapSeries} from '../util/promise.js'
import assert from '../util/assert.js'
import {identifier, value, hash} from '../lexical.js'
export default function (liquid, Liquid) {
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` +
`(?:\\s+${hash.source})*$`)
liquid.registerTag('tablerow', {
parse: function (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.templates = []
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope, hash) {
let collection = Liquid.evalExp(this.collection, scope) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
const contexts = collection.map((item, i) => {
const ctx = {}
ctx[this.variable] = item
return ctx
})
let row
let html = ''
await mapSeries(contexts, async (context, idx) => {
row = Math.floor(idx / cols) + 1
const col = (idx % cols) + 1
if (col === 1) {
if (row !== 1) {
html += '</tr>'
}
html += `<tr class="row${row}">`
}
html += `<td class="col${col}">`
scope.push(context)
html += await liquid.renderer.renderTemplates(this.templates, scope)
html += '</td>'
scope.pop(context)
return html
})
if (row > 0) {
html += '</tr>'
}
return html
}
})
}
+4 -6
View File
@@ -1,12 +1,10 @@
const Liquid = require('..')
module.exports = function (liquid) {
export default function (liquid, Liquid) {
liquid.registerTag('unless', {
parse: function (tagToken, remainTokens) {
this.templates = []
this.elseTemplates = []
var p
var stream = liquid.parser.parseStream(remainTokens)
let p
const stream = liquid.parser.parseStream(remainTokens)
.on('start', x => {
p = this.templates
this.cond = tagToken.args
@@ -22,7 +20,7 @@ module.exports = function (liquid) {
},
render: function (scope, hash) {
var cond = Liquid.evalExp(this.cond, scope)
const cond = Liquid.evalExp(this.cond, scope)
return Liquid.isFalsy(cond)
? liquid.renderer.renderTemplates(this.templates, scope)
: liquid.renderer.renderTemplates(this.elseTemplates, scope)
+52
View File
@@ -0,0 +1,52 @@
import {last, isArray} from './util/underscore'
function domResolve (root, path) {
const base = document.createElement('base')
base.href = root
const head = document.getElementsByTagName('head')[0]
head.insertBefore(base, head.firstChild)
const a = document.createElement('a')
a.href = path
const resolved = a.href
head.removeChild(base)
return resolved
}
export function resolve (filepath, root, options) {
root = root || options.root
if (isArray(root)) {
root = root[0]
}
if (root.length && last(root) !== '/') {
root += '/'
}
const url = domResolve(root, filepath)
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
const last = path.split('/').pop()
if (/\.\w+$/.test(last)) {
return str
}
return origin + path + options.extname
})
}
export async function read (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.onerror = () => {
reject(new Error('An error occurred whilst receiving the response.'))
}
xhr.open('GET', url)
xhr.send()
})
}
+29
View File
@@ -0,0 +1,29 @@
import * as _ from './util/underscore.js'
import path from 'path'
import {anySeries} from './util/promise.js'
import fs from 'fs'
const statFileAsync = _.promisify(fs.stat)
const readFileAsync = _.promisify(fs.readFile)
export async function resolve (filepath, root, options) {
if (!path.extname(filepath)) {
filepath += options.extname
}
root = options.root.concat(root || [])
root = _.uniq(root)
const paths = root.map(root => path.resolve(root, filepath))
return anySeries(paths, async path => {
try {
await statFileAsync(path)
return path
} catch (e) {
e.message = `${e.code}: Failed to lookup ${filepath} in: ${root}`
throw e
}
})
}
export async function read (filepath) {
return readFileAsync(filepath, 'utf8')
}
+20 -21
View File
@@ -1,19 +1,21 @@
const lexical = require('./lexical.js')
const TokenizationError = require('./util/error.js').TokenizationError
const _ = require('./util/underscore.js')
const whiteSpaceCtrl = require('./whitespace-ctrl.js')
const assert = require('./util/assert.js')
import * as lexical from './lexical.js'
import {TokenizationError} from './util/error.js'
import * as _ from './util/underscore.js'
import assert from './util/assert.js'
import whiteSpaceCtrl from './whitespace-ctrl.js'
function parse (input, file, options) {
export {default as whiteSpaceCtrl} from './whitespace-ctrl.js'
export function parse (input, file, options) {
assert(_.isString(input), 'illegal input')
var rLiquid = /({%-?([\s\S]*?)-?%})|({{-?([\s\S]*?)-?}})/g
var currIndent = 0
var lineNumber = LineNumber(input)
var lastMatchEnd = 0
var tokens = []
const rLiquid = /({%-?([\s\S]*?)-?%})|({{-?([\s\S]*?)-?}})/g
let currIndent = 0
const lineNumber = LineNumber(input)
let lastMatchEnd = 0
const tokens = []
for (var match; (match = rLiquid.exec(input)); lastMatchEnd = rLiquid.lastIndex) {
for (let match; (match = rLiquid.exec(input)); lastMatchEnd = rLiquid.lastIndex) {
if (match.index > lastMatchEnd) {
tokens.push(parseHTMLToken(lastMatchEnd, match.index))
}
@@ -28,8 +30,8 @@ function parse (input, file, options) {
return tokens
function parseTagToken (raw, value, pos) {
var match = value.match(lexical.tagLine)
var token = {
const match = value.match(lexical.tagLine)
const token = {
type: 'tag',
indent: currIndent,
line: lineNumber.get(pos),
@@ -62,7 +64,7 @@ function parse (input, file, options) {
}
function parseHTMLToken (begin, end) {
var htmlFragment = input.slice(begin, end)
const htmlFragment = input.slice(begin, end)
currIndent = _.last((htmlFragment).split('\n')).length
return {
@@ -74,18 +76,15 @@ function parse (input, file, options) {
}
function LineNumber (html) {
var parsedLinesCount = 0
var lastMatchBegin = -1
let parsedLinesCount = 0
let lastMatchBegin = -1
return {
get: function (pos) {
var lines = html.slice(lastMatchBegin + 1, pos).split('\n')
const lines = html.slice(lastMatchBegin + 1, pos).split('\n')
parsedLinesCount += lines.length - 1
lastMatchBegin = pos
return parsedLinesCount + 1
}
}
}
exports.parse = parse
exports.whiteSpaceCtrl = whiteSpaceCtrl
+2 -4
View File
@@ -1,10 +1,8 @@
const AssertionError = require('./error.js').AssertionError
import {AssertionError} from './error.js'
function assert (predicate, message) {
export default function (predicate, message) {
if (!predicate) {
message = message || `expect ${predicate} to be true`
throw new AssertionError(message)
}
}
module.exports = assert
+19 -27
View File
@@ -1,4 +1,4 @@
const _ = require('./underscore.js')
import * as _ from './underscore.js'
function initError () {
this.name = this.constructor.name
@@ -14,29 +14,29 @@ function initLiquidError (err, token) {
this.line = token.line
this.file = token.file
var context = mkContext(token.input, token.line)
const context = mkContext(token.input, token.line)
this.message = mkMessage(err.message, token)
this.stack = context +
'\n' + (this.stack || this.message) +
(err.stack ? '\nFrom ' + err.stack : '')
}
function TokenizationError (message, token) {
export function TokenizationError (message, token) {
initLiquidError.call(this, {message: message}, token)
}
TokenizationError.prototype = Object.create(Error.prototype)
TokenizationError.prototype = _.create(Error.prototype)
TokenizationError.prototype.constructor = TokenizationError
function ParseError (e, token) {
export function ParseError (e, token) {
_.assign(this, e)
this.originalError = e
initLiquidError.call(this, e, token)
}
ParseError.prototype = Object.create(Error.prototype)
ParseError.prototype = _.create(Error.prototype)
ParseError.prototype.constructor = ParseError
function RenderError (e, tpl) {
export function RenderError (e, tpl) {
// return the original render error
if (e instanceof RenderError) {
return e
@@ -46,29 +46,29 @@ function RenderError (e, tpl) {
initLiquidError.call(this, e, tpl.token)
}
RenderError.prototype = Object.create(Error.prototype)
RenderError.prototype = _.create(Error.prototype)
RenderError.prototype.constructor = RenderError
function RenderBreakError (message) {
export function RenderBreakError (message) {
initError.call(this)
this.message = message + ''
}
RenderBreakError.prototype = Object.create(Error.prototype)
RenderBreakError.prototype = _.create(Error.prototype)
RenderBreakError.prototype.constructor = RenderBreakError
function AssertionError (message) {
export function AssertionError (message) {
initError.call(this)
this.message = message + ''
}
AssertionError.prototype = Object.create(Error.prototype)
AssertionError.prototype = _.create(Error.prototype)
AssertionError.prototype.constructor = AssertionError
function mkContext (input, line) {
var lines = input.split('\n')
var begin = Math.max(line - 2, 1)
var end = Math.min(line + 3, lines.length)
const lines = input.split('\n')
const begin = Math.max(line - 2, 1)
const end = Math.min(line + 3, lines.length)
var context = _
const context = _
.range(begin, end + 1)
.map(l => [
(l === line) ? '>> ' : ' ',
@@ -82,9 +82,9 @@ function mkContext (input, line) {
}
function align (n, max) {
var length = (max + '').length
var str = n + ''
var blank = Array(length - str.length).join(' ')
const length = (max + '').length
const str = n + ''
const blank = Array(length - str.length).join(' ')
return blank + str
}
@@ -98,11 +98,3 @@ function mkMessage (msg, token) {
}
return msg
}
module.exports = {
TokenizationError,
ParseError,
RenderBreakError,
AssertionError,
RenderError
}
-20
View File
@@ -1,20 +0,0 @@
const fs = require('fs')
function readFileAsync (filepath) {
return new Promise(function (resolve, reject) {
fs.readFile(filepath, 'utf8', function (err, content) {
err ? reject(err) : resolve(content)
})
})
};
function statFileAsync (path) {
return new Promise(function (resolve, reject) {
fs.stat(path, (err, stat) => err ? reject(err) : resolve(stat))
})
};
module.exports = {
readFileAsync,
statFileAsync
}
+5 -8
View File
@@ -4,8 +4,8 @@
* @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
function anySeries (iterable, iteratee) {
var ret = Promise.reject(new Error('init'))
export function anySeries (iterable, iteratee) {
let ret = Promise.reject(new Error('init'))
iterable.forEach(function (item, idx) {
ret = ret.catch(e => iteratee(item, idx, iterable))
})
@@ -18,9 +18,9 @@ function anySeries (iterable, iteratee) {
* @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
function mapSeries (iterable, iteratee) {
var ret = Promise.resolve('init')
var result = []
export function mapSeries (iterable, iteratee) {
let ret = Promise.resolve('init')
const result = []
iterable.forEach(function (item, idx) {
ret = ret
.then(() => iteratee(item, idx, iterable))
@@ -28,6 +28,3 @@ function mapSeries (iterable, iteratee) {
})
return ret.then(() => result)
}
exports.anySeries = anySeries
exports.mapSeries = mapSeries
+31 -32
View File
@@ -1,32 +1,33 @@
var monthNames = [
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
]
var monthNamesShort = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
'Nov', 'Dec'
]
var dayNames = [
const dayNames = [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
]
var dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
var suffixes = {
const monthNamesShort = monthNames.map(abbr)
const dayNamesShort = dayNames.map(abbr)
const suffixes = {
1: 'st',
2: 'nd',
3: 'rd',
'default': 'th'
}
function abbr (str) {
return str.slice(0, 3)
}
// prototype extensions
var _date = {
const _date = {
daysInMonth: function (d) {
var feb = _date.isLeapYear(d) ? 29 : 28
const feb = _date.isLeapYear(d) ? 29 : 28
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
},
getDayOfYear: function (d) {
var num = 0
for (var i = 0; i < d.getMonth(); ++i) {
let num = 0
for (let i = 0; i < d.getMonth(); ++i) {
num += _date.daysInMonth(d)[i]
}
return num + d.getDate()
@@ -36,21 +37,21 @@ var _date = {
// TODO: that comment was retarted. fix it.
getWeekOfYear: function (d, startDay) {
// Skip to startDay of this week
var now = this.getDayOfYear(d) + (startDay - d.getDay())
const now = this.getDayOfYear(d) + (startDay - d.getDay())
// Find the first startDay of the year
var jan1 = new Date(d.getFullYear(), 0, 1)
var then = (7 - jan1.getDay() + startDay)
const jan1 = new Date(d.getFullYear(), 0, 1)
const then = (7 - jan1.getDay() + startDay)
return _number.pad(Math.floor((now - then) / 7) + 1, 2)
},
isLeapYear: function (d) {
var year = d.getFullYear()
const year = d.getFullYear()
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
},
getSuffix: function (d) {
var str = d.getDate().toString()
var index = parseInt(str.slice(-1))
const str = d.getDate().toString()
const index = parseInt(str.slice(-1))
return suffixes[index] || suffixes['default']
},
@@ -59,11 +60,11 @@ var _date = {
}
}
var _number = {
const _number = {
pad: function (value, size, ch) {
if (!ch) ch = '0'
var result = value.toString()
var pad = size - result.length
let result = value.toString()
let pad = size - result.length
while (pad-- > 0) {
result = ch + result
@@ -73,7 +74,7 @@ var _number = {
}
}
var formatCodes = {
const formatCodes = {
a: function (d) {
return dayNamesShort[d.getDay()]
},
@@ -162,7 +163,7 @@ var formatCodes = {
return d.getFullYear()
},
z: function (d) {
var tz = d.getTimezoneOffset() / 60 * 100
const tz = d.getTimezoneOffset() / 60 * 100
return (tz > 0 ? '-' : '+') + _number.pad(Math.abs(tz), 4)
},
'%': function () {
@@ -172,13 +173,13 @@ var formatCodes = {
formatCodes.h = formatCodes.b
formatCodes.N = formatCodes.L
var strftime = function (d, format) {
var output = ''
var remaining = format
export default function (d, format) {
let output = ''
let remaining = format
while (true) {
var r = /%./g
var results = r.exec(remaining)
const r = /%./g
const results = r.exec(remaining)
// No more format codes. Add the remaining text and return
if (!results) {
@@ -190,10 +191,8 @@ var strftime = function (d, format) {
remaining = remaining.slice(r.lastIndex)
// Add the format code
var ch = results[0].charAt(1)
var func = formatCodes[ch]
const ch = results[0].charAt(1)
const func = formatCodes[ch]
output += func ? func.call(this, d) : '%' + ch
}
}
module.exports = strftime
+62 -47
View File
@@ -5,21 +5,61 @@ const toStr = Object.prototype.toString
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is a string, else false.
*/
function isString (value) {
return value instanceof String || typeof value === 'string'
export function isString (value) {
return toStr.call(value) === '[object String]'
}
function isNil (value) {
export function promisify (fn) {
return function () {
return new Promise((resolve, reject) => {
fn(...arguments, (err, result) => {
err ? reject(err) : resolve(result)
})
})
}
}
export function stringify (value) {
if (isNil(value)) {
return String(value)
}
if (typeof value.to_liquid === 'function') {
return stringify(value.to_liquid())
}
if (typeof value.toLiquid === 'function') {
return stringify(value.toLiquid())
}
if (isString(value) || value instanceof RegExp || value instanceof Date) {
return value.toString()
}
const cache = []
return JSON.stringify(value, (key, value) => {
if (isObject(value)) {
if (cache.indexOf(value) !== -1) {
return
}
cache.push(value)
}
return value
})
}
export function create (proto) {
return Object.create(proto)
}
export function isNil (value) {
return value === null || value === undefined
}
function isArray (value) {
export function isArray (value) {
// be compatible with IE 8
return toStr.call(value) === '[object Array]'
}
function isError (value) {
var signature = Object.prototype.toString.call(value)
export function isError (value) {
const signature = toStr.call(value)
// [object XXXError]
return signature.substr(-6, 5) === 'Error' ||
(typeof value.message === 'string' && typeof value.name === 'string')
@@ -33,9 +73,9 @@ function isError (value) {
* @param {Function} iteratee The function invoked per iteration.
* @return {Object} Returns object.
*/
function forOwn (object, iteratee) {
export function forOwn (object, iteratee) {
object = object || {}
for (var k in object) {
for (const k in object) {
if (object.hasOwnProperty(k)) {
if (iteratee(object[k], k, object) === false) break
}
@@ -54,30 +94,21 @@ function forOwn (object, iteratee) {
* @param {...Object} sources The source objects.
* @return {Object} Returns object.
*/
function assign (object) {
export function assign (object) {
object = isObject(object) ? object : {}
var srcs = Array.prototype.slice.call(arguments, 1)
srcs.forEach(function (src) {
_assignBinary(object, src)
})
const srcs = Array.prototype.slice.call(arguments, 1)
srcs.forEach((src) => Object.assign(object, src))
return object
}
function _assignBinary (dst, src) {
forOwn(src, function (v, k) {
dst[k] = v
})
return dst
}
function last (arr) {
export function last (arr) {
return arr[arr.length - 1]
}
function uniq (arr) {
var u = {}
var a = []
for (var i = 0, l = arr.length; i < l; ++i) {
export function uniq (arr) {
const u = {}
const a = []
for (let i = 0, l = arr.length; i < l; ++i) {
if (u.hasOwnProperty(arr[i])) {
continue
}
@@ -93,9 +124,9 @@ function uniq (arr) {
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is an object, else false.
*/
function isObject (value) {
var type = typeof value
return value != null && (type === 'object' || type === 'function')
export function isObject (value) {
const type = typeof value
return value !== null && (type === 'object' || type === 'function')
}
/*
@@ -106,32 +137,16 @@ function isObject (value) {
* Note that ranges that stop before they start are considered to be zero-length instead of
* negative if you'd like a negative range, use a negative step.
*/
function range (start, stop, step) {
export function range (start, stop, step) {
if (arguments.length === 1) {
stop = start
start = 0
}
step = step || 1
var arr = []
for (var i = start; i < stop; i += step) {
const arr = []
for (let i = start; i < stop; i += step) {
arr.push(i)
}
return arr
}
// lang
exports.isString = isString
exports.isObject = isObject
exports.isArray = isArray
exports.isNil = isNil
exports.isError = isError
// array
exports.range = range
exports.last = last
// object
exports.forOwn = forOwn
exports.assign = assign
exports.uniq = uniq
-23
View File
@@ -1,23 +0,0 @@
const resolve = require('resolve-url')
const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/
const urlRe = /^(?:\w+:)?\/\/([^\s.]+\.\S{2}|localhost[:?\d]*)\S*$/
// https://github.com/jinder/path/blob/master/path.js#L567
exports.extname = function (path) {
return splitPathRe.exec(path).slice(1)[3]
}
// https://www.npmjs.com/package/is-url
exports.valid = function (path) {
return urlRe.test(path)
}
exports.resolve = function (root, path) {
if (Object.prototype.toString.call(root) === '[object Array]') {
root = root[0]
}
if (root && root.charAt(root.length - 1) !== '/') {
root += '/'
}
return resolve(root, path)
}
+6 -8
View File
@@ -1,8 +1,8 @@
const _ = require('./util/underscore.js')
import {assign} from './util/underscore.js'
function whiteSpaceCtrl (tokens, options) {
options = _.assign({ greedy: true }, options)
var inRaw = false
export default function whiteSpaceCtrl (tokens, options) {
options = assign({ greedy: true }, options)
let inRaw = false
tokens.forEach((token, i) => {
if (shouldTrimLeft(token, inRaw, options)) {
@@ -33,15 +33,13 @@ function shouldTrimRight (token, inRaw, options) {
function trimLeft (token, greedy) {
if (!token || token.type !== 'html') return
var rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
token.value = token.value.replace(rLeft, '')
}
function trimRight (token, greedy) {
if (!token || token.type !== 'html') return
var rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
token.value = token.value.replace(rRight, '')
}
module.exports = whiteSpaceCtrl
-19
View File
@@ -1,19 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const re = new RegExp(`(${lexical.identifier.source})\\s*=(.*)`)
const assert = require('../src/util/assert.js')
module.exports = function (liquid) {
liquid.registerTag('assign', {
parse: function (token) {
var match = token.args.match(re)
assert(match, `illegal token ${token.raw}`)
this.key = match[1]
this.value = match[2]
},
render: function (scope) {
scope.set(this.key, liquid.evalValue(this.value, scope))
return Promise.resolve('')
}
})
}
-30
View File
@@ -1,30 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const re = new RegExp(`(${lexical.identifier.source})`)
const assert = require('../src/util/assert.js')
module.exports = function (liquid) {
liquid.registerTag('capture', {
parse: function (tagToken, remainTokens) {
var match = tagToken.args.match(re)
assert(match, `${tagToken.args} not valid identifier`)
this.variable = match[1]
this.templates = []
var stream = liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', token => stream.stop())
.on('template', tpl => this.templates.push(tpl))
.on('end', x => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
return liquid.renderer.renderTemplates(this.templates, scope)
.then((html) => {
scope.set(this.variable, html)
})
}
})
}
-43
View File
@@ -1,43 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const groupRE = new RegExp(`^(?:(${lexical.value.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(lexical.value.source, 'g')
const assert = require('../src/util/assert.js')
module.exports = function (liquid) {
liquid.registerTag('cycle', {
parse: function (tagToken, remainTokens) {
var match = groupRE.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.group = match[1] || ''
var candidates = match[2]
this.candidates = []
while ((match = candidatesRE.exec(candidates))) {
this.candidates.push(match[0])
}
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: function (scope, hash) {
var group = Liquid.evalValue(this.group, scope)
var fingerprint = `cycle:${group}:` + this.candidates.join(',')
var groups = scope.opts.groups = scope.opts.groups || {}
var idx = groups[fingerprint]
if (idx === undefined) {
idx = groups[fingerprint] = 0
}
var candidate = this.candidates[idx]
idx = (idx + 1) % this.candidates.length
groups[fingerprint] = idx
return Promise.resolve(Liquid.evalValue(candidate, scope))
}
})
}
-18
View File
@@ -1,18 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const assert = require('../src/util/assert.js')
module.exports = function (liquid) {
liquid.registerTag('decrement', {
parse: function (token) {
var match = token.args.match(lexical.identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
var v = scope.get(this.variable)
if (typeof v !== 'number') v = 0
scope.set(this.variable, v - 1)
}
})
}
-96
View File
@@ -1,96 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const mapSeries = require('../src/util/promise.js').mapSeries
const _ = require('../src/util/underscore.js')
const RenderBreakError = Liquid.Types.RenderBreakError
const assert = require('../src/util/assert.js')
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*` +
`(?:\\s+(reversed))?` +
`(?:\\s+${lexical.hash.source})*$`)
module.exports = function (liquid) {
liquid.registerTag('for', {
parse: function (tagToken, remainTokens) {
var match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.reversed = !!match[3]
this.templates = []
this.elseTemplates = []
var p
var stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
var collection = Liquid.evalExp(this.collection, scope)
if (!Array.isArray(collection)) {
if (_.isString(collection) && collection.length > 0) {
collection = [collection]
} else if (_.isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]])
}
}
if (!Array.isArray(collection) || !collection.length) {
return liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
var offset = hash.offset || 0
var limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
var contexts = collection.map((item, i) => {
var ctx = {}
ctx[this.variable] = item
ctx.forloop = {
first: i === 0,
index: i + 1,
index0: i,
last: i === collection.length - 1,
length: collection.length,
rindex: collection.length - i,
rindex0: collection.length - i - 1
}
return ctx
})
var html = ''
return mapSeries(contexts, (context) => {
return Promise.resolve()
.then(() => scope.push(context))
.then(() => liquid.renderer.renderTemplates(this.templates, scope))
.then(partial => (html += partial))
.catch(e => {
if (e instanceof RenderBreakError) {
html += e.resolvedHTML
if (e.message === 'continue') return
}
throw e
})
.then(() => scope.pop())
}).catch((e) => {
if (e instanceof RenderBreakError && e.message === 'break') {
return
}
throw e
}).then(() => html)
}
})
}
-52
View File
@@ -1,52 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const withRE = new RegExp(`with\\s+(${lexical.value.source})`)
const staticFileRE = /[^\s,]+/
const assert = require('../src/util/assert.js')
module.exports = function (liquid) {
liquid.registerTag('include', {
parse: function (token) {
var match = staticFileRE.exec(token.args)
if (match) {
this.staticValue = match[0]
}
match = lexical.value.exec(token.args)
if (match) {
this.value = match[0]
}
match = withRE.exec(token.args)
if (match) {
this.with = match[1]
}
},
render: function (scope, hash) {
var filepath = scope.opts.dynamicPartials
? Liquid.evalValue(this.value, scope)
: this.staticValue
assert(filepath, `cannot include with empty filename`)
var originBlocks = scope.opts.blocks
var originBlockMode = scope.opts.blockMode
scope.opts.blocks = {}
scope.opts.blockMode = 'output'
if (this.with) {
hash[filepath] = Liquid.evalValue(this.with, scope)
}
return liquid.getTemplate(filepath, scope.opts.root)
.then((templates) => {
scope.push(hash)
return liquid.renderer.renderTemplates(templates, scope)
})
.then((html) => {
scope.pop()
scope.opts.blocks = originBlocks
scope.opts.blockMode = originBlockMode
return html
})
}
})
}
-18
View File
@@ -1,18 +0,0 @@
const Liquid = require('..')
const assert = require('../src/util/assert.js')
const lexical = Liquid.lexical
module.exports = function (liquid) {
liquid.registerTag('increment', {
parse: function (token) {
var match = token.args.match(lexical.identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
render: function (scope, hash) {
var v = scope.get(this.variable)
if (typeof v !== 'number') v = 0
scope.set(this.variable, v + 1)
}
})
}
-16
View File
@@ -1,16 +0,0 @@
module.exports = function (engine) {
require('./assign.js')(engine)
require('./capture.js')(engine)
require('./case.js')(engine)
require('./comment.js')(engine)
require('./cycle.js')(engine)
require('./decrement.js')(engine)
require('./for.js')(engine)
require('./if.js')(engine)
require('./include.js')(engine)
require('./increment.js')(engine)
require('./layout.js')(engine)
require('./raw.js')(engine)
require('./tablerow.js')(engine)
require('./unless.js')(engine)
}
-84
View File
@@ -1,84 +0,0 @@
const Liquid = require('..')
const lexical = Liquid.lexical
const assert = require('../src/util/assert.js')
const staticFileRE = /\S+/
/*
* blockMode:
* * "store": store rendered html into blocks
* * "output": output rendered html
*/
module.exports = function (liquid) {
liquid.registerTag('layout', {
parse: function (token, remainTokens) {
var match = staticFileRE.exec(token.args)
if (match) {
this.staticLayout = match[0]
}
match = lexical.value.exec(token.args)
if (match) {
this.layout = match[0]
}
this.tpls = liquid.parser.parse(remainTokens)
},
render: function (scope, hash) {
var layout = scope.opts.dynamicPartials ? Liquid.evalValue(this.layout, scope) : this.staticLayout
assert(layout, `cannot apply layout with empty filename`)
// render the remaining tokens immediately
scope.opts.blockMode = 'store'
return liquid.renderer.renderTemplates(this.tpls, scope)
.then(html => {
if (scope.opts.blocks[''] === undefined) {
scope.opts.blocks[''] = html
}
return liquid.getTemplate(layout, scope.opts.root)
})
.then(templates => {
// push the hash
scope.push(hash)
scope.opts.blockMode = 'output'
return liquid.renderer.renderTemplates(templates, scope)
})
// pop the hash
.then(partial => {
scope.pop()
return partial
})
}
})
liquid.registerTag('block', {
parse: function (token, remainTokens) {
var match = /\w+/.exec(token.args)
this.block = match ? match[0] : ''
this.tpls = []
var stream = liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`)
})
stream.start()
},
render: function (scope) {
return Promise.resolve(scope.opts.blocks[this.block])
.then(html => html === undefined
// render default block
? liquid.renderer.renderTemplates(this.tpls, scope)
// use child-defined block
: html)
.then(html => {
if (scope.opts.blockMode === 'store') {
scope.opts.blocks[this.block] = html
return ''
}
return html
})
}
})
}
-83
View File
@@ -1,83 +0,0 @@
const Liquid = require('..')
const mapSeries = require('../src/util/promise.js').mapSeries
const lexical = Liquid.lexical
const assert = require('../src/util/assert.js')
const re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` +
`(${lexical.value.source})` +
`(?:\\s+${lexical.hash.source})*$`)
module.exports = function (liquid) {
liquid.registerTag('tablerow', {
parse: function (tagToken, remainTokens) {
var match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.templates = []
var p
var stream = liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', token => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: function (scope, hash) {
var collection = Liquid.evalExp(this.collection, scope) || []
var html = ''
var offset = hash.offset || 0
var limit = (hash.limit === undefined) ? collection.length : hash.limit
var cols = hash.cols
var row
var col
// build array of arguments to pass to sequential promises...
collection = collection.slice(offset, offset + limit)
if (!cols) cols = collection.length
var contexts = []
collection.some((item, i) => {
var ctx = {}
ctx[this.variable] = item
contexts.push(ctx)
})
return mapSeries(contexts,
(context, idx) => {
row = Math.floor(idx / cols) + 1
col = (idx % cols) + 1
if (col === 1) {
if (row !== 1) {
html += '</tr>'
}
html += `<tr class="row${row}">`
}
html += `<td class="col${col}">`
scope.push(context)
return liquid.renderer
.renderTemplates(this.templates, scope)
.then((partial) => {
scope.pop(context)
html += partial
html += '</td>'
return html
})
})
.then(() => {
if (row > 0) {
html += '</tr>'
}
return html
})
}
})
}
+6
View File
@@ -0,0 +1,6 @@
{
"rules": {
"no-var": 0,
"prefer-const": 0
}
}
+10 -7
View File
@@ -1,9 +1,12 @@
const chai = require('chai')
const expect = chai.expect
const mock = require('mock-fs')
const request = require('supertest')
const express = require('express')
const Liquid = require('..')
const mock = require('mock-fs')
const Liquid = require('../../dist/liquid.common.js')
const chaiAsPromised = require('chai-as-promised')
const expect = chai.expect
chai.use(chaiAsPromised)
describe('engine#express()', function () {
var app, engine
@@ -36,11 +39,11 @@ describe('engine#express()', function () {
.expect(200, done)
})
it('should pass error when file not found', function (done) {
var view = {
const view = {
root: []
}
var file = '/not-exist.html'
var ctx = {}
const file = '/not-exist.html'
const ctx = {}
engine.express().call(view, file, ctx, function (err) {
try {
expect(err.code).to.equal('ENOENT')
@@ -82,7 +85,7 @@ describe('engine#express()', function () {
.expect(200, done)
})
it('should respect express views (Undefined) when lookup', function (done) {
var files = {}
const files = {}
files[process.cwd() + '/views/include.html'] = '{% include file %}'
files[process.cwd() + '/views/bar.html'] = 'bar'
mock(files)
+59
View File
@@ -0,0 +1,59 @@
var chai = require('chai')
var Liquid = require('../..')
var expect = chai.expect
chai.use(require('chai-as-promised'))
describe('.parseAndRender()', function () {
var engine, strictEngine
beforeEach(function () {
engine = Liquid()
strictEngine = Liquid({
strict_filters: true
})
})
it('should value object', function () {
var ctx = { obj: {foo: 'bar'} }
return expect(engine.parseAndRender('{{obj}}', ctx)).to.eventually.equal('{"foo":"bar"}')
})
it('should value array', function () {
var ctx = { arr: [-2, 'a'] }
return expect(engine.parseAndRender('{{arr}}', ctx)).to.eventually.equal('[-2,"a"]')
})
it('should value undefined to empty', function () {
return expect(engine.parseAndRender('foo{{zzz}}bar', {})).to.eventually.equal('foobar')
})
it('should render as null when filter undefined', function () {
return expect(engine.parseAndRender('{{"foo" | filter1}}', {})).to.eventually.equal('foo')
})
it('should throw upon undefined filter when strict_filters set', function () {
return expect(strictEngine.parseAndRender('{{"foo" | filter1}}', {})).to
.be.rejectedWith(/undefined filter: filter1/)
})
it('should parse html', function () {
expect(function () {
engine.parse('{{obj}}')
}).to.not.throw()
expect(function () {
engine.parse('<html><head>{{obj}}</head></html>')
}).to.not.throw()
})
it('should render template multiple times', function () {
var ctx = {obj: {foo: 'bar'}}
var template = engine.parse('{{obj}}')
return engine.render(template, ctx)
.then(result => expect(result).to.equal('{"foo":"bar"}'))
.then(() => engine.render(template, ctx))
.then((result) => expect(result).to.equal('{"foo":"bar"}'))
})
it('should render filters', function () {
var ctx = {names: ['alice', 'bob']}
var template = engine.parse('<p>{{names | join: ","}}</p>')
return expect(engine.render(template, ctx)).to.eventually.equal('<p>alice,bob</p>')
})
it('should render accessive filters', function () {
var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' +
'{{ my_array | first }}'
return expect(engine.parseAndRender(src)).to.eventually.equal('apples')
})
})
+78
View File
@@ -0,0 +1,78 @@
var chai = require('chai')
var mock = require('mock-fs')
var Liquid = require('../..')
var expect = chai.expect
chai.use(require('chai-as-promised'))
describe('#renderFile()', function () {
var engine
beforeEach(function () {
engine = Liquid({
root: '/root/',
extname: '.html'
})
mock({
'/root/files/bar': 'bar',
'/root/files/foo.html': 'foo',
'/root/files/name.html': 'My name is {{name}}.',
'/un-readable.html': mock.file({
mode: '0000'
})
})
})
afterEach(function () {
mock.restore()
})
it('should render file', function () {
return expect(engine.renderFile('/root/files/foo.html', {}))
.to.eventually.equal('foo')
})
it('should find files without extname', function () {
var engine = Liquid({root: '/root'})
return expect(engine.renderFile('/root/files/bar', {}))
.to.eventually.equal('bar')
})
it('should accept relative path', function () {
return expect(engine.renderFile('files/foo.html'))
.to.eventually.equal('foo')
})
it('should resolve array as root', function () {
engine = Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.renderFile('files/foo.html'))
.to.eventually.equal('foo')
})
it('should default root to cwd', function () {
var files = {}
files[process.cwd() + '/foo.html'] = 'FOO'
mock(files)
engine = Liquid({
extname: '.html'
})
return expect(engine.renderFile('foo.html'))
.to.eventually.equal('FOO')
})
it('should render file with context', function () {
return expect(engine.renderFile('/root/files/name.html', {name: 'harttle'}))
.to.eventually.equal('My name is harttle.')
})
it('should use default extname', function () {
return expect(engine.renderFile('files/name', {name: 'harttle'})).to.eventually.equal('My name is harttle.')
})
it('should throw with lookup list when file not exist', function () {
engine = Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.renderFile('/not/exist.html')).to
.be.rejectedWith(/failed to lookup \/not\/exist.html in: \/boo,\/root\//i)
})
it('should throw when file not readable', function () {
return expect(engine.renderFile('/un-readable.html')).to
.be.rejectedWith(/EACCES/)
})
})
@@ -1,7 +1,7 @@
const chai = require('chai')
const expect = chai.expect
const Liquid = require('..')
const Liquid = require('../..')
const liquid = new Liquid()
const expect = chai.expect
chai.use(require('chai-as-promised'))
const cases = [
+18 -13
View File
@@ -1,20 +1,22 @@
const Liquid = require('..')
const sinon = require('sinon')
const chai = require('chai')
const expect = chai.expect
var Liquid = require('../../dist/liquid.js')
var sinon = require('sinon')
var chai = require('chai')
var expect = chai.expect
chai.use(require('chai-as-promised'))
describe('xhr', () => {
if (process.version.match(/^v(\d+)/)[1] < 8) {
console.info('jsdom not supported, skipping xhr...')
return
}
const JSDOM = require('jsdom').JSDOM
var server, engine, dom
var JSDOM = require('jsdom').JSDOM
var server, engine
beforeEach(() => {
server = sinon.createFakeServer()
server.autoRespond = true
server.respondWith('GET', 'https://example.com/views/hello.html',
[200, {'Content-Type': 'text/plain'}, 'hello {{name}}'])
dom = new JSDOM('', {
var dom = new JSDOM('', {
url: 'https://example.com/foo/bar.html',
contentType: 'text/html',
includeNodeLocations: true
@@ -64,15 +66,18 @@ describe('xhr', () => {
})
it('should throw error', function (done) {
engine.renderFile('hello.html')
.then(() => done('should not be resolved'))
.catch(function (e) {
expect(e.message).to.equal('An error occurred whilst sending the response.')
expect(e.message).to.equal('An error occurred whilst receiving the response.')
done()
})
server.requests[0].error()
global.XMLHttpRequest.onCreate = function (request) {
setTimeout(() => request.error())
}
})
})
describe('root', () => {
it('should support with null', () => {
describe('#renderFile() with root specified', () => {
it('should support undefined root', () => {
engine = Liquid({
extname: '.html'
})
@@ -81,12 +86,12 @@ describe('xhr', () => {
return expect(engine.renderFile('hello.html', {name: 'alice5'}))
.to.eventually.equal('hello alice5')
})
it('should support with empty', () => {
it('should support empty root', () => {
engine = Liquid({
root: '',
extname: '.html'
})
server.respondWith('GET', 'https://example.com/foo/hello.html',
server.respondWith('https://example.com/foo/hello.html',
[200, {'Content-Type': 'text/plain'}, 'hello {{name}}'])
return expect(engine.renderFile('hello.html', {name: 'alice5'}))
.to.eventually.equal('hello alice5')
-138
View File
@@ -1,138 +0,0 @@
const chai = require('chai')
const expect = chai.expect
const Liquid = require('..')
const mock = require('mock-fs')
chai.use(require('chai-as-promised'))
describe('liquid', function () {
var engine, strictEngine, ctx
beforeEach(function () {
ctx = {
name: 'harttle',
arr: [-2, 'a'],
obj: {
foo: 'bar'
}
}
engine = Liquid({
root: '/root/',
extname: '.html'
})
strictEngine = Liquid({
root: '/root',
extname: '.html',
strict_filters: true
})
mock({
'/root/files/bar': 'bar',
'/root/files/foo.html': 'foo',
'/root/files/name.html': 'My name is {{name}}.',
'/un-readable.html': mock.file({
mode: '0000'
})
})
})
afterEach(function () {
mock.restore()
})
describe('Liquid', function () {
it('should ignore invalid root option', function () {
var liquid = Liquid({ root: /regex/ })
expect(liquid.options.root).to.deep.equal([])
})
})
describe('{{value}}', function () {
it('should value object', function () {
return expect(engine.parseAndRender('{{obj}}', ctx)).to.eventually.equal('{"foo":"bar"}')
})
it('should value array', function () {
return expect(engine.parseAndRender('{{arr}}', ctx)).to.eventually.equal('[-2,"a"]')
})
it('should value undefined to empty', function () {
return expect(engine.parseAndRender('foo{{zzz}}bar', ctx)).to.eventually.equal('foobar')
})
it('should render as null when filter undefined', function () {
return expect(engine.parseAndRender('{{"foo" | filter1}}', ctx)).to.eventually.equal('foo')
})
it('should throw upon undefined filter when strict_filters set', function () {
return expect(strictEngine.parseAndRender('{{arr | filter1}}', ctx)).to
.be.rejectedWith(/undefined filter: filter1/)
})
})
it('should parse html', function () {
expect(function () {
engine.parse('{{obj}}')
}).to.not.throw()
expect(function () {
engine.parse('<html><head>{{obj}}</head></html>')
}).to.not.throw()
})
it('should render template multiple times', function () {
var template = engine.parse('{{obj}}')
return engine.render(template, ctx)
.then(result => expect(result).to.equal('{"foo":"bar"}'))
.then(() => engine.render(template, ctx))
.then((result) => expect(result).to.equal('{"foo":"bar"}'))
})
it('should render filters', function () {
var template = engine.parse('<p>{{arr | join: "_"}}</p>')
return expect(engine.render(template, ctx)).to.eventually.equal('<p>-2_a</p>')
})
it('should render accessive filters', function () {
var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' +
'{{ my_array | first }}'
return expect(engine.parseAndRender(src)).to.eventually.equal('apples')
})
describe('#renderFile()', function () {
it('should render file', function () {
return expect(engine.renderFile('/root/files/foo.html', ctx))
.to.eventually.equal('foo')
})
it('should find files without extname', function () {
var engine = Liquid({root: '/root'})
return expect(engine.renderFile('/root/files/bar', ctx))
.to.eventually.equal('bar')
})
it('should accept relative path', function () {
return expect(engine.renderFile('files/foo.html'))
.to.eventually.equal('foo')
})
it('should resolve array as root', function () {
engine = Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.renderFile('files/foo.html'))
.to.eventually.equal('foo')
})
it('should default root to cwd', function () {
var files = {}
files[process.cwd() + '/foo.html'] = 'FOO'
mock(files)
engine = Liquid({
extname: '.html'
})
return expect(engine.renderFile('foo.html'))
.to.eventually.equal('FOO')
})
it('should render file with context', function () {
return expect(engine.renderFile('/root/files/name.html', ctx)).to.eventually.equal('My name is harttle.')
})
it('should use default extname', function () {
return expect(engine.renderFile('files/name', ctx)).to.eventually.equal('My name is harttle.')
})
it('should throw with lookup list when file not exist', function () {
engine = Liquid({
root: ['/boo', '/root/'],
extname: '.html'
})
return expect(engine.renderFile('/not/exist.html')).to
.be.rejectedWith(/failed to lookup \/not\/exist.html in: \/boo,\/root\//i)
})
it('should throw when file not readable', function () {
return expect(engine.renderFile('/un-readable.html')).to
.be.rejectedWith(/EACCES/)
})
})
})
+2
View File
@@ -0,0 +1,2 @@
--require babel-core/register
--recursive
-66
View File
@@ -1,66 +0,0 @@
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const expect = chai.expect
const sinonChai = require('sinon-chai')
const sinon = require('sinon')
chai.use(sinonChai)
chai.use(chaiAsPromised)
var tag = require('../src/tag.js')()
var Scope = require('../src/scope.js')
var filter = require('../src/filter')()
var Render = require('../src/render.js')
var Template = require('../src/parser.js')(tag, filter)
describe('render', function () {
var scope, render
beforeEach(function () {
scope = Scope.factory({
foo: {
bar: ['a', 2]
}
})
filter.clear()
tag.clear()
render = Render()
})
describe('.renderTemplates()', function () {
it('should throw when scope undefined', function () {
expect(function () {
render.renderTemplates([])
}).to.throw(/scope undefined/)
})
it('should render html', function () {
return expect(render.renderTemplates([{type: 'html', value: '<p>'}], scope)).to.eventually.equal('<p>')
})
})
it('should eval filter with correct arguments', function () {
var date = sinon.stub().returns('y')
var time = sinon.spy()
filter.register('date', date)
filter.register('time', time)
var tpl = Template.parseValue('foo.bar[0] | date: "b" | time:2')
render.evalValue(tpl, scope)
expect(date).to.have.been.calledWith('a', 'b')
expect(time).to.have.been.calledWith('y', 2)
})
describe('.evalValue()', function () {
it('should throw when scope undefined', function () {
expect(function () {
render.evalValue()
}).to.throw(/scope undefined/)
})
it('should eval value', function () {
filter.register('date', (l, r) => l + r)
filter.register('time', (l, r) => l + 3 * r)
var tpl = Template.parseValue('foo.bar[0] | date: "b" | time:2')
expect(render.evalValue(tpl, scope)).to.equal('ab6')
})
})
})
-98
View File
@@ -1,98 +0,0 @@
const chai = require('chai')
const sinon = require('sinon')
const expect = chai.expect
chai.use(require('sinon-chai'))
var tag = require('../src/tag.js')()
var Scope = require('../src/scope.js')
describe('tag', function () {
var scope
before(function () {
scope = Scope.factory({
foo: 'bar',
arr: [2, 1],
bar: {
coo: 'uoo'
}
})
tag.clear()
})
it('should throw when not registered', function () {
expect(function () {
tag.construct({
type: 'tag',
value: 'foo',
name: 'foo'
}, [])
}).to.throw(/tag foo not found/)
})
it('should register simple tag', function () {
expect(function () {
tag.register('foo', {
render: x => 'bar'
})
}).not.throw()
})
it('should call tag.render', function () {
var spy = sinon.spy()
tag.register('foo', {
render: spy
})
return tag
.construct({
type: 'tag',
value: 'foo',
name: 'foo'
}, [])
.render(scope, {})
.then(() => expect(spy).to.have.been.called)
})
describe('hash', function () {
var spy, token
beforeEach(function () {
spy = sinon.spy()
tag.register('foo', {
render: spy
})
token = {
type: 'tag',
value: 'foo aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo',
name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo'
}
})
it('should call tag.render with scope', function () {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope))
})
it('should resolve identifier hash', function () {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
aa: 'bar'
}))
})
it('should accept space between key/value', function () {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch({}, {
bb: 2
}))
})
it('should resolve number value hash', function () {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
cc: 2.3
}))
})
it('should resolve property access hash', function () {
return tag.construct(token, []).render(scope, {})
.then(() => expect(spy).to.have.been.calledWithMatch(scope, {
dd: 'uoo'
}))
})
})
})
-53
View File
@@ -1,53 +0,0 @@
const Liquid = require('../..')
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-as-promised'))
describe('tags/assign', function () {
var liquid = Liquid()
it('should throw when variable expression illegal', function () {
var src = '{% assign / %}'
var ctx = {}
return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/)
})
it('should assign as string', function () {
var src = '{% assign foo="bar" %}{{foo}}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('bar')
})
it('should assign as array', function () {
var src = '{% assign foo=(1..3) %}{{foo}}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('[1,2,3]')
})
it('should assign as filter result', function () {
var src = '{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('A')
})
it('should assign var-1', function () {
var src = '{% assign var-1 = 5 %}{{ var-1 }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('5')
})
it('should assign var-', function () {
var src = '{% assign var- = 5 %}{{ var- }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('5')
})
it('should assign -var', function () {
var src = '{% assign -var = 5 %}{{ -var }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('5')
})
it('should assign -5-5', function () {
var src = '{% assign -5-5 = 5 %}{{ -5-5 }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('5')
})
it('should assign 4-3', function () {
var src = '{% assign 4-3 = 5 %}{{ 4-3 }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('5')
})
it('should not assign -6', function () {
var src = '{% assign -6 = 5 %}{{ -6 }}'
return expect(liquid.parseAndRender(src)).to.eventually.equal('-6')
})
})
-38
View File
@@ -1,38 +0,0 @@
const Liquid = require('../..')
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-as-promised'))
describe('tags/decrement', function () {
var liquid = Liquid()
it('should throw when variable expression illegal', function () {
var src = '{% decrement / %}{{one}}'
var ctx = {}
return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/)
})
it('should support decrement', function () {
var src = '{% decrement one %}{{one}}'
var ctx = {
one: 1
}
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('0')
})
it('should decrement undefined', function () {
var src = '{% decrement empty %}{{empty}}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('-1')
})
it('should support decrement multiple times', function () {
var src = '{% decrement foo %}{%decrement foo%}{{foo}}'
var ctx = {
foo: 1
}
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('-1')
})
})
-32
View File
@@ -1,32 +0,0 @@
const Liquid = require('../..')
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-as-promised'))
describe('tags/increment', function () {
var liquid = Liquid()
it('should support increment', function () {
var src = '{% increment one %}{{one}}'
var ctx = {
one: 1
}
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('2')
})
it('should increment undefined', function () {
var src = '{% increment empty %}{{empty}}'
return expect(liquid.parseAndRender(src))
.to.eventually.equal('1')
})
it('should support increment multiple times', function () {
var src = '{% increment foo %}{%increment foo%}{{foo}}'
var ctx = {
foo: 1
}
return expect(liquid.parseAndRender(src, ctx))
.to.eventually.equal('3')
})
})
-24
View File
@@ -1,24 +0,0 @@
const Liquid = require('../..')
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-as-promised'))
describe('tags/raw', function () {
var liquid = Liquid()
it('should support raw 1', function () {
return expect(liquid.parseAndRender('{% raw%}'))
.to.be.rejectedWith(/{% raw%} not closed/)
})
it('should support raw 2', function () {
var src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
var dst = '{{ 5 | plus: 6 }} is equal to 11.'
return expect(liquid.parseAndRender(src))
.to.eventually.equal(dst)
})
it('should support raw 3', function () {
var src = '{% raw %}\n{{ foo}} \n{% endraw %}'
var dst = '\n{{ foo}} \n'
return expect(liquid.parseAndRender(src))
.to.eventually.equal(dst)
})
})
+21 -17
View File
@@ -1,21 +1,21 @@
const chai = require('chai')
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const expect = chai.expect
import chai from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import Filter from '../../src/filter.js'
import {factory as scopeFactory} from '../../src/scope.js'
chai.use(sinonChai)
var filter = require('../src/filter.js')()
var Scope = require('../src/scope.js')
const expect = chai.expect
const filter = Filter()
describe('filter', function () {
var scope
let scope
beforeEach(function () {
filter.clear()
scope = Scope.factory()
scope = scopeFactory()
})
it('should return default filter when not registered', function () {
var result = filter.construct('foo')
const result = filter.construct('foo')
expect(result.name).to.equal('foo')
})
@@ -27,7 +27,7 @@ describe('filter', function () {
it('should parse argument syntax', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: a, "b"')
const f = filter.construct('foo: a, "b"')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal(['a', '"b"'])
@@ -49,7 +49,7 @@ describe('filter', function () {
})
it('should call filter with corrct arguments', function () {
var spy = sinon.spy()
const spy = sinon.spy()
filter.register('foo', spy)
filter.construct('foo: 33').render('foo', scope)
expect(spy).to.have.been.calledWith('foo', 33)
@@ -57,36 +57,40 @@ describe('filter', function () {
it('should support arguments as named key/values', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: key1: "literal1", key2: value2')
const f = filter.construct('foo: key1: "literal1", key2: value2')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal([ '\'key1\'', '"literal1"', '\'key2\'', 'value2' ])
})
it('should support arguments as named key/values with inline literals', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: "test0", key1: "literal1", key2: value2')
const f = filter.construct('foo: "test0", key1: "literal1", key2: value2')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal([ '"test0"', '\'key1\'', '"literal1"', '\'key2\'', 'value2' ])
})
it('should support arguments as named key/values with inline values', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: test0, key1: "literal1", key2: value2')
const f = filter.construct('foo: test0, key1: "literal1", key2: value2')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal([ 'test0', '\'key1\'', '"literal1"', '\'key2\'', 'value2' ])
})
it('should support argument values named same as keys', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: a: a')
const f = filter.construct('foo: a: a')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal(['\'a\'', 'a'])
})
it('should support argument literals named same as keys', function () {
filter.register('foo', x => x)
var f = filter.construct('foo: a: "a"')
const f = filter.construct('foo: a: "a"')
expect(f.name).to.equal('foo')
expect(f.args).to.deep.equal(['\'a\'', '"a"'])
})
it('should not throw undefined filter by default', function () {
expect(filter.construct('undefined').render('foo', scope)).to.equal('foo')
})
})

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