mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-13 03:10:40 -07:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aed43ab2b | ||
|
|
662533ae2c | ||
|
|
a8a09d5a6d | ||
|
|
7ab49f999a | ||
|
|
552819a84b | ||
|
|
8bfb6428ae | ||
|
|
568bd5f9cb | ||
|
|
ed489865b6 | ||
|
|
afec88b04c | ||
|
|
3a0d80d1f4 | ||
|
|
956b51ea95 | ||
|
|
5c3522f339 | ||
|
|
6d00257e15 | ||
|
|
03a30e6dc4 | ||
|
|
4775227358 |
@@ -838,6 +838,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "spokodev",
|
||||
"name": "spokodev",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/239690017?v=4",
|
||||
"profile": "https://github.com/spokodev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
description: Architecture overview for liquidjs internals
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## Async/sync duality via generators
|
||||
|
||||
All core logic is written once as a `Generator` function (`function *`). Use `yield` where you'd normally `await` a potentially async value.
|
||||
|
||||
- `toPromise(generator)` drives it **asynchronously** — awaits yielded promises.
|
||||
- `toValueSync(generator)` drives it **synchronously** — passes yielded values through as-is.
|
||||
|
||||
Never duplicate logic into separate async and sync methods. A single generator serves both paths.
|
||||
|
||||
When wrapping an async+sync function pair (e.g. `contains`/`containsSync`, `exists`/`existsSync`, `readFile`/`readFileSync`), use `toLiquidAsync(asyncFn, syncFn?)` which returns a `LiquidAsync<F>` — one function that picks the sync or async implementation based on a leading `sync: boolean` arg. Then `yield` the result inside a generator to let the driver handle it in both modes.
|
||||
|
||||
See `src/util/async.ts`.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description: Project conventions for liquidjs
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
- Keep edits minimal: change only what the task requires, match existing style.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
description: Testing conventions — e2e uses built dist, integration uses src
|
||||
globs: test/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
## End-to-end tests (`test/e2e`)
|
||||
|
||||
- **Use the built package**, not TypeScript sources under `src/`.
|
||||
- Import the public API from the package root (for example `import { Liquid } from '../..'`), which resolves through `package.json` to **`dist/`** (`main`, `module`, etc.).
|
||||
- **Avoid** `import … from '../../src/liquid'` (or other `src/` paths) in `test/e2e/**` so e2e matches what consumers get from npm and you do not depend on an unbuilt tree.
|
||||
|
||||
## Integration and unit tests
|
||||
|
||||
- Tests under `test/integration/`, `src/**/*.spec.ts`, and similar may import from **`src/`** when the suite is meant to run against the current TypeScript sources (typical for this repo’s Jest setup).
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 'latest'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '22'
|
||||
node-version: 'latest'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 'latest'
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 'latest'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 'latest'
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -24,12 +24,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ github.ref == 'refs/heads/master' }} ]; then
|
||||
npx semantic-release
|
||||
else
|
||||
npx semantic-release --dry-run
|
||||
fi
|
||||
run: npx semantic-release
|
||||
- name: Archive npm failure logs
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
|
||||
@@ -7,20 +7,10 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
timezone: [Etc/GMT, Asia/Shanghai, America/New_York]
|
||||
node-version: [22]
|
||||
node-version: [latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
timezone: America/New_York
|
||||
node-versoin: 22
|
||||
- os: ubuntu-latest
|
||||
timezone: Etc/GMT
|
||||
node-version: 20
|
||||
- os: ubuntu-latest
|
||||
timezone: Asia/Shanghai
|
||||
node-version: 18
|
||||
- os: ubuntu-latest
|
||||
timezone: Asia/Shanghai
|
||||
node-version: 16
|
||||
node-version: lts/*
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -39,7 +29,7 @@ jobs:
|
||||
name: dist-${{ matrix.os }}
|
||||
path: dist
|
||||
- name: Run Test
|
||||
run: TZ=${{ matrix.timezone }} npm test
|
||||
run: TZ=${{ matrix.timezone || 'Etc/GMT' }} npm test
|
||||
- name: Archive npm failure logs
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
@@ -57,7 +47,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: latest
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -11,9 +11,11 @@ coverage/
|
||||
node_modules/
|
||||
|
||||
# tmp
|
||||
.local/
|
||||
docs/themes/navy/source/js/liquid.browser.min.js
|
||||
docs/themes/navy/layout/partial/all-contributors.swig
|
||||
docs/themes/navy/layout/partial/financial-contributors.swig
|
||||
docs/themes/navy/layout/partial/used-by.swig
|
||||
dist/
|
||||
demo/*/yarn.json
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const gitPlugin = [
|
||||
'@semantic-release/git',
|
||||
{
|
||||
assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'],
|
||||
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
|
||||
}
|
||||
]
|
||||
|
||||
const githubPlugin = [
|
||||
'@semantic-release/github',
|
||||
{
|
||||
assets: [
|
||||
{ path: 'dist/*.umd.js', label: 'liquid.js' },
|
||||
{ path: 'dist/*.min.js', label: 'liquid.min.js' },
|
||||
{ path: 'dist/*.min.js.map', label: 'liquid.min.js.map' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const basePlugins = [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/changelog',
|
||||
'@semantic-release/npm'
|
||||
]
|
||||
|
||||
// next is branch-protected (PR-only); skip @semantic-release/git there and publish to npm only.
|
||||
const onMaster = process.env.GITHUB_REF === 'refs/heads/master'
|
||||
|
||||
module.exports = {
|
||||
branches: [
|
||||
'master',
|
||||
{ name: 'next', prerelease: 'alpha' }
|
||||
],
|
||||
plugins: [
|
||||
...basePlugins,
|
||||
...(onMaster ? [gitPlugin] : []),
|
||||
githubPlugin
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# LiquidJS
|
||||
|
||||
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support. TypeScript in `src/`, bundles in `dist/`. Docs site in `docs/` (Hexo, `navy` theme).
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `src/parser`, `src/render`, `src/tags`, `src/filters` | Template parse and render |
|
||||
| `src/context`, `src/template`, `src/tokens` | Scope, templates, token stream |
|
||||
| `src/util/async.ts` | `toPromise`, `toValueSync`, `toLiquidAsync` |
|
||||
| `test/` | Jest |
|
||||
| `docs/source/` | Doc markdown; sidebar in `docs/source/_data/sidebar.yml` |
|
||||
| `docs/themes/navy/` | Layout, CSS, JS |
|
||||
| `.local/` | Scratch, repro, PoC (gitignored) |
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
npm run build # after src/ changes, before npm test
|
||||
npm test
|
||||
npm run lint
|
||||
npm run check # build + build:docs + test + lint + perf:diff (manual)
|
||||
npm run build:docs
|
||||
cd docs && npm start # http://localhost:4000
|
||||
npm run perf:diff
|
||||
```
|
||||
|
||||
PR CI (`pull_request`): build, lint, test, coverage, performance. Docs build runs on push to `master` only.
|
||||
|
||||
PR titles: conventional format (`feat:`, `fix:`, `docs:`, …) — checked by CI. Releases on `master` use semantic-release from merged commits.
|
||||
|
||||
Backward-compatible API changes expected unless doing an intentional major break.
|
||||
|
||||
## Architecture
|
||||
|
||||
All core logic is one `function *` per feature. Use `yield` where you'd normally `await` a potentially async value.
|
||||
|
||||
- `toPromise(generator)` — async driver; awaits yielded promises
|
||||
- `toValueSync(generator)` — sync driver; passes yielded values through as-is
|
||||
|
||||
Never duplicate logic into separate async and sync methods. One generator serves both paths.
|
||||
|
||||
When wrapping an async+sync pair (e.g. `contains`/`containsSync`, `readFile`/`readFileSync`), use `toLiquidAsync(asyncFn, syncFn?)` — returns a `LiquidAsync<F>` that picks sync or async via a leading `sync: boolean` arg. `yield` the result inside a generator. See `src/util/async.ts`.
|
||||
|
||||
## Style
|
||||
|
||||
Make minimal changes only. Avoid sweeping edits. Always check after you made changes.
|
||||
|
||||
- Change only what the task requires. No drive-by refactors, test harnesses, or extra files unless asked.
|
||||
- Match existing patterns in the file you edit.
|
||||
- Repro, PoC, and scratch files go in `.local/` — not tracked `poc/` folders or one-off scripts under `docs/`.
|
||||
|
||||
### Comments
|
||||
|
||||
- Do not add narrative comments. Code should be clear from structure and naming; if it needs explanation, refactor instead.
|
||||
- Comments follow existing repo usage only: non-obvious invariants, `@deprecated`, JSDoc on public API where TypeDoc needs it. Not for explaining changes to the author, migration history, or restating what the code already says.
|
||||
- Comments document the code; they do not fix unclear code.
|
||||
|
||||
### Tests
|
||||
|
||||
- Assert observable behavior, not internal implementation details.
|
||||
- Avoid duplicate coverage; keep test diffs minimal.
|
||||
- **E2E** (`test/e2e/`): import from the package root (resolves to `dist/` via `package.json`). Do not import from `src/` — e2e must match what npm consumers get.
|
||||
- **Integration/unit** (`test/integration/`, etc.): may import from `src/` against current TypeScript sources.
|
||||
|
||||
### Docs site
|
||||
|
||||
- Reuse existing asset paths under `docs/source/` and `docs/themes/navy/` — no new asset directories unless asked.
|
||||
- Front matter `title:` is plain text (no backticks).
|
||||
- `docs/source/llms.txt` — deployed to https://liquidjs.com/llms.txt for web agents (llms.txt spec).
|
||||
- After theme/markdown changes: build or serve locally, check in a browser (light and dark), not only curl or editor preview.
|
||||
|
||||
### README
|
||||
|
||||
- Research original sources before reordering contributors, logos, or lists.
|
||||
|
||||
## Verify
|
||||
|
||||
- Do not commit, push, amend, or open a PR unless asked.
|
||||
- After changes: verify yourself via CLI or UI (tests, `cd docs && npm start`, browser) before reporting done. Do not tell the user to check instead.
|
||||
- Before push on sweeping changes: run `npm run check`.
|
||||
- Confirm facts from `.github/workflows`, `package.json`, and library docs — not stale human docs or assumptions.
|
||||
- When replacing or integrating a library: read its docs and understand what the previous setup did before changing behavior.
|
||||
|
||||
### Security fixes
|
||||
|
||||
- Reproduce on current `master` first. Smallest fix that addresses the reported issue.
|
||||
- If Shopify/Ruby Liquid behaves the same, document unsafe usage in filter/docs instead of changing behavior.
|
||||
|
||||
## Docs
|
||||
|
||||
- Published: https://liquidjs.com
|
||||
- Repo agent instructions: this file (`AGENTS.md`)
|
||||
@@ -1,3 +1,17 @@
|
||||
## [10.27.1](https://github.com/harttle/liquidjs/compare/v10.27.0...v10.27.1) (2026-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve round function; improvement to [#873](https://github.com/harttle/liquidjs/issues/873) ([#901](https://github.com/harttle/liquidjs/issues/901)) ([956b51e](https://github.com/harttle/liquidjs/commit/956b51ea953eb52d9eba7409b7f51e379023fec4))
|
||||
* **security:** charge pop filter allocation to memoryLimit ([#907](https://github.com/harttle/liquidjs/issues/907)) ([8a0c74a](https://github.com/harttle/liquidjs/commit/8a0c74a7fcb1671aa1dcb71ec82ba0602dc90d04))
|
||||
* **strip_html:** infinite loop for strip_html ([5c3522f](https://github.com/harttle/liquidjs/commit/5c3522f33928aae66f0fe85c36e1d9015c768fe2))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **parser:** memoize createTrie to avoid rebuilding tries per Tokenizer ([#911](https://github.com/harttle/liquidjs/issues/911)) ([3a0d80d](https://github.com/harttle/liquidjs/commit/3a0d80d1f4526af0fbca2bb2e0a9c51669d2fd3e))
|
||||
|
||||
# [10.27.0](https://github.com/harttle/liquidjs/compare/v10.26.0...v10.27.0) (2026-05-15)
|
||||
|
||||
|
||||
|
||||
@@ -1,72 +1,86 @@
|
||||
# liquidjs
|
||||
# LiquidJS
|
||||
|
||||
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.
|
||||
|
||||
[](https://www.npmjs.org/package/liquidjs)
|
||||
[](https://www.npmjs.org/package/liquidjs)
|
||||
[](https://coveralls.io/github/harttle/liquidjs?branch=master)
|
||||
[](https://github.com/harttle/liquidjs/actions/workflows/ci-build.yml?query=branch%3Amaster)
|
||||
[](https://github.com/harttle/liquidjs/blob/master/LICENSE)
|
||||
[](https://github.com/harttle/liquidjs)
|
||||
[](https://coveralls.io/github/harttle/liquidjs?branch=master)
|
||||
[](https://github.com/harttle/liquidjs/blob/master/LICENSE)
|
||||
|
||||
A simple, expressive and safe [Shopify][shopify/liquid] / GitHub Pages compatible template engine in pure JavaScript.
|
||||
**The purpose of this repo** is to provide a standard Liquid implementation for the JavaScript community so that [Jekyll sites](https://jekyllrb.com), [GitHub Pages](https://pages.github.com/) and [Shopify templates](https://themes.shopify.com/) can be ported to Node.js without pain.
|
||||
[Documentation][doc] · [Playground](https://liquidjs.com/playground.html) · [Setup guide][setup] · [Contributing][contribution]
|
||||
|
||||
* [Documentation][doc]
|
||||
* Please star [LiquidJS on GitHub][github]!
|
||||
* Financial support via [GitHub Sponsors](https://github.com/sponsors/harttle).
|
||||
<a href="https://liquidjs.com/playground.html">
|
||||
<img src="docs/source/playground-demo.gif" alt="LiquidJS playground: edit a template and context, see live HTML output" width="980" style="display: block; margin: 0 auto;" />
|
||||
</a>
|
||||
|
||||
<p align="center"><a href="https://liquidjs.com"><img height="155px" width="155px" src="https://liquidjs.com/icon/mstile-310x310.png" alt="logo"></a></p>
|
||||
<p align="center"><sub>Try the <a href="https://liquidjs.com/playground.html">online playground</a>.</sub></p>
|
||||
|
||||
## What's it like?
|
||||
## Quick start
|
||||
|
||||
Basically there're two types of Liquid syntax: tags enclosed by `{% %}` and outputs enclosed by `{{ }}`. A Liquid template looks like:
|
||||
```js
|
||||
import { Liquid } from 'liquidjs'
|
||||
|
||||
```liquid
|
||||
{% if username %}
|
||||
{{ username | append: ", welcome to LiquidJS!" | capitalize }}
|
||||
{% endif %}
|
||||
const engine = new Liquid()
|
||||
const html = await engine.parseAndRender(
|
||||
'Hello, {{ name | capitalize }}!',
|
||||
{ name: 'liquid' }
|
||||
)
|
||||
//=> 'Hello, Liquid!'
|
||||
```
|
||||
|
||||
[A live demo](https://liquidjs.com/playground.html) is also available and here's a [quick tutorial](https://liquidjs.com/tutorials/intro-to-liquid.html) for Liquid syntax.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Install from npm in Node.js:
|
||||
**Node.js**
|
||||
|
||||
```bash
|
||||
npm install liquidjs
|
||||
```
|
||||
|
||||
Or use the UMD bundle from jsDelivr:
|
||||
**Browser** (jsDelivr UMD bundle)
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
|
||||
```
|
||||
|
||||
Or render directly from CLI using npx:
|
||||
**CLI**
|
||||
|
||||
```bash
|
||||
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
|
||||
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Liquid"}'
|
||||
```
|
||||
|
||||
For more details, refer to the [Setup Guide][setup].
|
||||
See the [setup guide][setup] for partials, layouts, caching, and other options.
|
||||
|
||||
## Who's Using LiquidJS?
|
||||
## Used by
|
||||
|
||||
- [Eleventy](https://www.11ty.dev/): Eleventy, a simpler static site generator.
|
||||
- [Github Docs](https://github.com/github/docs): The open-source repo for docs.github.com.
|
||||
- [Kibana](https://github.com/elastic/kibana): Elastic's analytics and visualization platform for Elasticsearch; workflow features use LiquidJS for Liquid templates.
|
||||
- [Opensense](https://www.opensense.com/): The smarter way to send email.
|
||||
- [Directus](https://docs.directus.io/): an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
|
||||
- [Rock](https://www.rockrms.com/): An open source CMS, Relationship Management System (RMS) and Church Management System (ChMS) all rolled into one.
|
||||
- [Mitosis](https://github.com/BuilderIO/mitosis): Write components once, run everywhere. Compiles to React, Vue, Qwik, Solid, Angular, Svelte, and more.
|
||||
- [Pattern Lab](https://patternlab.io/): a frontend workshop environment that helps you build, view, test, and showcase your design system's UI components.
|
||||
- [Builder.io](https://www.builder.io/m/developers): the first and only headless CMS with a visual editor that lets you drag and drop with your components, directly within your current site or app. Completely API-driven, for cleaner code and simpler workflows.
|
||||
- [Microsoft Power Pages](https://learn.microsoft.com/en-us/power-pages/introduction): a secure, enterprise-grade, low-code software as a service (SaaS) platform for creating, hosting, and administering modern external-facing business websites.
|
||||
- [Azure API Management developer portal](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal): an automatically generated, fully customizable website with the documentation of your APIs.
|
||||
- [WISMOlabs](https://wismolabs.com/): Post Purchase Experience platform for eCommerce retailers enhancing customer satisfaction by using LiquidJS to provide customizable post-purchase experiences through programmable email, SMS, order tracking pages, and webhooks.
|
||||
- [Freshet](https://chromewebstore.google.com/detail/freshet/mpclplhdencffbilobpcapccnihpelcg): *JSON in, page out* — a Chrome extension that uses LiquidJS templates per URL pattern, so the JSON becomes a rendered, useful page.
|
||||
<!-- USED-BY-BEGIN -->
|
||||
<p align="center" style="line-height: 2.5;">
|
||||
<a href="https://www.11ty.dev/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/35147177?v=4&s=100" height="80" style="vertical-align: middle;" alt="Eleventy" title="Eleventy"/></a>
|
||||
<a href="https://www.opensense.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Opensense" title="Opensense"/></a>
|
||||
<a href="https://www.microsoft.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" height="80" style="vertical-align: middle;" alt="Power Pages, Azure API Management developer portal" title="Power Pages, Azure API Management developer portal"/></a>
|
||||
<a href="https://docs.github.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/9919?v=4&s=100" height="80" style="vertical-align: middle;" alt="GitHub Docs" title="GitHub Docs"/></a>
|
||||
<a href="https://github.com/elastic/kibana" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6764390?v=4&s=100" height="80" style="vertical-align: middle;" alt="Kibana" title="Kibana"/></a>
|
||||
<a href="https://www.shopify.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/8085?v=4&s=100" height="80" style="vertical-align: middle;" alt="Shopify CLI, Checkout Blocks" title="Shopify CLI, Checkout Blocks"/></a>
|
||||
<a href="https://customer.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1152079?v=4&s=100" height="80" style="vertical-align: middle;" alt="Customer IO" title="Customer IO"/></a>
|
||||
<br/>
|
||||
<a href="https://syntax.fm/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/130389858?v=4&s=100" height="80" style="vertical-align: middle;" alt="Syntax Podcast" title="Syntax Podcast"/></a>
|
||||
<a href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=liquidjs" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/27130435?s=200&v=4" width="80" style="vertical-align: middle;" alt="TestMu AI" title="TestMu AI"/></a>
|
||||
<a href="https://chudovo.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/Chudovo/avatar/256.png?height=100" width="160" style="vertical-align: middle;background: white;padding: 8px 16px;" alt="Chudovo" title="Chudovo"/></a>
|
||||
<a href="https://www.pakstyle.pk/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/pakstyle/2b81605/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="PakStyle.pk" title="PakStyle.pk"/></a>
|
||||
<a href="https://www.escorta.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/escortacom/avatar/256.png?height=100" height="45" style="vertical-align: middle;" alt="EscortA.com" title="EscortA.com"/></a>
|
||||
<a href="https://opencollective.com/touchless" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/touchless/273bc74/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Touchless" title="Touchless"/></a>
|
||||
<br/>
|
||||
<a href="https://www.dropkiq.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/1bertlol/43a8ea8/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Dropkiq" title="Dropkiq"/></a>
|
||||
<a href="https://directus.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/15967950?v=4&s=100" height="80" style="vertical-align: middle;" alt="Directus" title="Directus"/></a>
|
||||
<a href="https://www.builder.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F51a002f4a17f4fc4a829b8891a1c25ee" height="80" style="vertical-align: middle;" alt="Builder.io, Mitosis" title="Builder.io, Mitosis"/></a>
|
||||
<a href="https://patternlab.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/4733935?v=4&s=100" height="80" style="vertical-align: middle;" alt="Pattern Lab" title="Pattern Lab"/></a>
|
||||
<a href="https://www.rockrms.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://www.rockrms.com/GetImage.ashx?id=72533" height="80" style="vertical-align: middle;" alt="Rock RMS" title="Rock RMS"/></a>
|
||||
<a href="https://wismolabs.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://cdn-ldlap.nitrocdn.com/DsLJMZpUUekfsitqBnNmsRAnzbwPoIzE/assets/images/optimized/rev-5df8687/wismolabs.com/wp-content/uploads/2023/03/favicon-300x300.png" height="80" style="vertical-align: middle;" alt="WISMOlabs" title="WISMOlabs"/></a>
|
||||
<a href="https://chromewebstore.google.com/detail/freshet/mpclplhdencffbilobpcapccnihpelcg" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://raw.githubusercontent.com/MattAltermatt/freshet/main/public/icon-128.png" height="80" style="vertical-align: middle;" alt="Freshet" title="Freshet"/></a>
|
||||
</p>
|
||||
<!-- USED-BY-END -->
|
||||
|
||||
Feel free to create a PR or contact me to add your use case into this list!
|
||||
Products and projects running on LiquidJS. [Open a PR](https://github.com/harttle/liquidjs/edit/master/README.md) to add yours.
|
||||
|
||||
## Financial Support
|
||||
|
||||
@@ -78,7 +92,7 @@ If you personally love LiquidJS or it's benefiting your business, please conside
|
||||
<a href="https://www.opensense.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Opensense Inc." title="Opensense"/></a>
|
||||
<a href="https://github.com/microsoft" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" height="80" style="vertical-align: middle;" alt="Microsoft" title="Microsoft"/></a>
|
||||
<a href="https://sentry.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1396951?v=4&s=100" height="80" style="vertical-align: middle;" alt="Sentry" title="Sentry"/></a>
|
||||
<a href="https://www.checkoutblocks.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/114603307?v=4&s=100" height="80" style="vertical-align: middle;" alt="Checkout Blocks" title="Checkout Blocks"/></a>
|
||||
<a href="https://github.com/Checkout-Blocks" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/114603307?v=4&s=100" height="80" style="vertical-align: middle;" alt="Checkout Blocks" title="Checkout Blocks"/></a>
|
||||
<a href="https://customer.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1152079?v=4&s=100" height="80" style="vertical-align: middle;" alt="Customer IO" title="Customer IO"/></a>
|
||||
<a href="https://syntax.fm/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/130389858?v=4&s=100" height="80" style="vertical-align: middle;" alt="Syntax Podcast" title="Syntax Podcast"/></a>
|
||||
<br/>
|
||||
@@ -226,6 +240,9 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/talboren"><img src="https://avatars.githubusercontent.com/u/68807791?v=4?s=100" width="100px;" alt="Tal"/><br /><sub><b>Tal</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=talboren" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://filonov.dev"><img src="https://avatars.githubusercontent.com/u/813224?v=4?s=100" width="100px;" alt="Vladimir Filonov"/><br /><sub><b>Vladimir Filonov</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=VladimirFilonov" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/spokodev"><img src="https://avatars.githubusercontent.com/u/239690017?v=4?s=100" width="100px;" alt="spokodev"/><br /><sub><b>spokodev</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=spokodev" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -234,8 +251,10 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
[shopify/liquid]: https://shopify.github.io/liquid/
|
||||
[plugins]: https://liquidjs.com/tutorials/plugins.html#Plugin-List
|
||||
## License
|
||||
|
||||
[MIT](LICENSE) © [Jun Yang](https://github.com/harttle)
|
||||
|
||||
[setup]: https://liquidjs.com/tutorials/setup.html
|
||||
[doc]: https://liquidjs.com
|
||||
[github]: https://github.com/harttle/liquidjs
|
||||
|
||||
+5
-3
@@ -6,8 +6,10 @@ Only the latest major version is supported with security updates. It can be chan
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please contact harttleharttle@gmail.com to report a vulnerability or change request.
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within 1 week).
|
||||
Report them via [GitHub Security Advisories — Report a vulnerability](https://github.com/harttle/liquidjs/security/advisories/new).
|
||||
|
||||
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within a month).
|
||||
- Otherwise, it'll be scheduled in the same priority of feature request (which is lower than bugs).
|
||||
- If the request is declined, you'll receive a reply email anyway (most likely there will be a discussion).
|
||||
- If the request is declined, you'll receive a reply anyway (most likely there will be a discussion).
|
||||
|
||||
@@ -33,7 +33,9 @@ function transformFinancial (html) {
|
||||
|
||||
const allContributors = transformContributors(extractSection(readme, 'ALL-CONTRIBUTORS-LIST:START', 'ALL-CONTRIBUTORS-LIST:END'))
|
||||
const financialContributors = transformFinancial(extractSection(readme, 'FINANCIAL-CONTRIBUTORS-BEGIN', 'FINANCIAL-CONTRIBUTORS-END'))
|
||||
const usedBy = transformFinancial(extractSection(readme, 'USED-BY-BEGIN', 'USED-BY-END'))
|
||||
|
||||
const outDir = path.join(root, 'docs/themes/navy/layout/partial')
|
||||
fs.writeFileSync(path.join(outDir, 'all-contributors.swig'), allContributors)
|
||||
fs.writeFileSync(path.join(outDir, 'financial-contributors.swig'), financialContributors)
|
||||
fs.writeFileSync(path.join(outDir, 'used-by.swig'), usedBy)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
title: LiquidJS
|
||||
subtitle: "A simple, expressive, and safe template engine for JavaScript."
|
||||
description: "LiquidJS is a simple, expressive, and safe template engine for JavaScript, compatible with Shopify and GitHub Pages."
|
||||
subtitle: "A simple, expressive, extensible Liquid template engine for JavaScript"
|
||||
description: "A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support."
|
||||
author: Harttle
|
||||
language: en
|
||||
timezone: UTC
|
||||
|
||||
@@ -6,6 +6,10 @@ title: strip_html
|
||||
|
||||
Removes any HTML tags from a string.
|
||||
|
||||
{% note warn Not safe for HTML output %}
|
||||
This filter removes tags by string scanning; it does not parse HTML5 the way a browser does, and it is not a sanitizer. The result may still be unsafe when inserted into HTML. Use [escape][escape], [escape_once][escape_once], or [`outputEscape: "escape"`][outputEscape] for untrusted output.
|
||||
{% endnote %}
|
||||
|
||||
Input
|
||||
```liquid
|
||||
{{ "Have <em>you</em> read <strong>Ulysses</strong>?" | strip_html }}
|
||||
@@ -15,3 +19,7 @@ Output
|
||||
```text
|
||||
Have you read Ulysses?
|
||||
```
|
||||
|
||||
[escape]: ./escape.html
|
||||
[escape_once]: ./escape.html
|
||||
[outputEscape]: ../tutorials/options.html#outputEscape
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
layout: index
|
||||
description: LiquidJS is a simple, expressive and safe Shopify / GitHub Pages compatible template engine in pure JavaScript.
|
||||
subtitle: A simple, expressive and safe template engine.
|
||||
---
|
||||
ul#intro-feature-list
|
||||
li.intro-feature-wrap
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# LiquidJS
|
||||
|
||||
> A simple, expressive, extensible Liquid template engine for JavaScript
|
||||
|
||||
## Tutorials
|
||||
|
||||
- [Introduction to Liquid](https://liquidjs.com/tutorials/intro-to-liquid.html)
|
||||
- [Setup](https://liquidjs.com/tutorials/setup.html)
|
||||
- [Options](https://liquidjs.com/tutorials/options.html)
|
||||
- [Render files](https://liquidjs.com/tutorials/render-file.html)
|
||||
- [Partials and layouts](https://liquidjs.com/tutorials/partials-and-layouts.html)
|
||||
- [Express.js](https://liquidjs.com/tutorials/use-in-expressjs.html)
|
||||
- [Register filters and tags](https://liquidjs.com/tutorials/register-filters-tags.html)
|
||||
- [Plugins](https://liquidjs.com/tutorials/plugins.html)
|
||||
- [Sync and async](https://liquidjs.com/tutorials/sync-and-async.html)
|
||||
- [Operators](https://liquidjs.com/tutorials/operators.html)
|
||||
- [Truthy and falsy](https://liquidjs.com/tutorials/truthy-and-falsy.html)
|
||||
- [Security model](https://liquidjs.com/tutorials/security-model.html)
|
||||
- [Differences from Shopify Liquid](https://liquidjs.com/tutorials/differences.html)
|
||||
- [Migrate to v9](https://liquidjs.com/tutorials/migrate-to-9.html)
|
||||
- [Changelog](https://liquidjs.com/tutorials/changelog.html)
|
||||
|
||||
## Reference
|
||||
|
||||
- [Tags](https://liquidjs.com/tags/overview.html)
|
||||
- [Filters](https://liquidjs.com/filters/overview.html)
|
||||
- [API (TypeDoc)](https://liquidjs.com/api/)
|
||||
- [Playground](https://liquidjs.com/playground.html)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"short_name": "LiquidJS",
|
||||
"name": "LiquidJS",
|
||||
"description": "A simple, expressive and safe Shopify / GitHub Pages compatible template engine in pure JavaScript.",
|
||||
"description": "A simple, expressive, extensible Liquid template engine for JavaScript",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon/apple-touch-icon-57x57.png",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -3,9 +3,9 @@ title: The Liquid Template Language
|
||||
describe: A short introduction to the Liquid template language and some simple demos.
|
||||
---
|
||||
|
||||
LiquidJS is a simple, expressive and safe [Shopify][shopify/liquid] / GitHub Pages compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by GitHub Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff].
|
||||
Liquid is a template language originally implemented in Ruby and used by Shopify, Jekyll, and GitHub Pages. LiquidJS implements it in JavaScript; see [Differences with Shopify/liquid][diff] for compatibility notes.
|
||||
|
||||
LiquidJS syntax is relatively simple. There are 2 types of markups in LiquidJS:
|
||||
There are 2 types of markups in LiquidJS:
|
||||
|
||||
- **Tags**. A tag consists of a tag name and optional arguments wrapped between `{%raw%}{%{%endraw%}` and `%}`.
|
||||
- **Outputs**. An output consists of a value and a list of filters, which is optional, wrapped between `{%raw%}{{{%endraw%}` and `}}`.
|
||||
@@ -56,5 +56,4 @@ Typically tags appear in pairs with a start tag and a corresponding end tag. For
|
||||
|
||||
A complete list of tags supported by LiquidJS can be found [here](../tags/overview.html).
|
||||
|
||||
[shopify/liquid]: https://github.com/Shopify/liquid
|
||||
[diff]: ./differences.html
|
||||
|
||||
Vendored
+5
-1
@@ -13,10 +13,14 @@ index:
|
||||
description: 'Thanks to these wonderful people! See <a href="tutorials/contribution-guidelines.html">contribution guidelines</a> if you'd like to help.'
|
||||
sponsors:
|
||||
title: Sponsors
|
||||
description: 'If you personally love LiquidJS or it's benefiting your business, please <a href="https://github.com/sponsors/harttle">sponsor us</a>!'
|
||||
description: 'Organizations and individuals who <a href="https://github.com/sponsors/harttle">sponsor LiquidJS</a>. Thank you!'
|
||||
used_by:
|
||||
title: Used by
|
||||
description: 'Products and projects running on LiquidJS. <a href="https://github.com/harttle/liquidjs/edit/master/README.md">Open a PR</a> to add yours.'
|
||||
|
||||
playground:
|
||||
title: Playground
|
||||
lead: Edit a template and context JSON — rendered HTML updates as you type.
|
||||
loading: Loading...
|
||||
|
||||
page:
|
||||
|
||||
Vendored
+18
-5
@@ -1,6 +1,6 @@
|
||||
<header id="banner" class="wrapper">
|
||||
<div class="inner inner-content">
|
||||
<h2 id="banner-title">{{ page.subtitle }}</h2>
|
||||
<h2 id="banner-title">{{ page.subtitle | default(config.subtitle) }}</h2>
|
||||
<div id="banner-share">{{ partial('partial/share') }}</div>
|
||||
<div id="banner-start">
|
||||
<code id="banner-start-command">npm install liquidjs</code><a id="banner-start-link" href="./tutorials/setup.html"><i class="icon-arrow-right"></i></a>
|
||||
@@ -14,15 +14,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contributors-wrap">
|
||||
<div id="used-by-wrap">
|
||||
<div class="wrapper">
|
||||
<div class="inner inner-content">
|
||||
<div class="section-header">
|
||||
<h3>{{__('index.contributors.title')}}</h3>
|
||||
<p class="description">{{__('index.contributors.description')}}</p>
|
||||
<h3>{{__('index.used_by.title')}}</h3>
|
||||
<p class="description">{{__('index.used_by.description')}}</p>
|
||||
</div>
|
||||
<div class="contributors">
|
||||
{{ partial('partial/all-contributors') }}
|
||||
{{ partial('partial/used-by') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,3 +40,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contributors-wrap">
|
||||
<div class="wrapper">
|
||||
<div class="inner inner-content">
|
||||
<div class="section-header">
|
||||
<h3>{{__('index.contributors.title')}}</h3>
|
||||
<p class="description">{{__('index.contributors.description')}}</p>
|
||||
</div>
|
||||
<div class="contributors">
|
||||
{{ partial('partial/all-contributors') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-3
@@ -1,6 +1,11 @@
|
||||
{% if page.layout === 'playground' %}
|
||||
{{ js('js/liquid.browser.min.js') }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/ace.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/mode-liquid.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/mode-json.js"></script>
|
||||
<script>ace.config.set('basePath', 'https://cdn.jsdelivr.net/npm/[email protected]/src-min/');</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/components/prism-markup.min.js"></script>
|
||||
{% endif %}
|
||||
|
||||
{{ js('js/main') }}
|
||||
@@ -17,12 +22,10 @@ debug: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GM713991QQ"></script>
|
||||
<script>
|
||||
<script type="text/plain" data-category="analytics">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-GM713991QQ');
|
||||
</script>
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"people": [
|
||||
"alice",
|
||||
"bob",
|
||||
"carol"
|
||||
]
|
||||
"name": "liquid"
|
||||
}
|
||||
|
||||
+1
-9
@@ -1,9 +1 @@
|
||||
<ul>
|
||||
{%- for person in people %}
|
||||
<li>
|
||||
<a href="{{person | prepend: "https://example.com/"}}">
|
||||
{{ person | capitalize }}
|
||||
</a>
|
||||
</li>
|
||||
{%- endfor%}
|
||||
</ul>
|
||||
<p>Hello, {{ name | capitalize }}!</p>
|
||||
|
||||
+1
@@ -5,6 +5,7 @@
|
||||
{{__('footer.license')}}
|
||||
</div>
|
||||
<div id="footer-links">
|
||||
<button type="button" class="footer-link cookie-preferences" data-cc="show-consentModal" title="Manage cookie preferences"><i class="icon-shield"></i></button>
|
||||
<a href="https://twitter.com/{{ config.twitter }}" class="footer-link" target="_blank"><i class="icon-twitter"></i></a>
|
||||
<a href="https://opencollective.com/{{ config.oc }}" class="footer-link" target="_blank"><i class="icon-opencollective"></i></a>
|
||||
<a href="https://github.com/{{ config.github }}" class="footer-link" target="_blank"><i class="icon-github"></i></a>
|
||||
|
||||
+3
-8
@@ -32,12 +32,7 @@
|
||||
<meta name="msapplication-TileImage" content="{{ url_for('icon/mstile-144x144.png') }}">
|
||||
{{ css('css/navy') }}
|
||||
{{ feed_tag('atom.xml') }}
|
||||
<script src="https://cdn.cookiehub.eu/c2/e8e44c93.js"></script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return;
|
||||
var cpm = {};
|
||||
window.cookiehub.load(cpm);
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orestbida/[email protected]/dist/cookieconsent.css">
|
||||
<script src="https://cdn.jsdelivr.net/gh/orestbida/[email protected]/dist/cookieconsent.umd.js"></script>
|
||||
{{ js('js/cookieconsent-config') }}
|
||||
</head>
|
||||
|
||||
+37
-13
@@ -1,21 +1,45 @@
|
||||
<div id="playground" role="main">
|
||||
<div class="wrapper">
|
||||
<h1 class="inner">{{__('playground.title')}}</h1>
|
||||
<header class="playground-hero inner">
|
||||
<div class="playground-hero-text">
|
||||
<h1>{{__('playground.title')}}</h1>
|
||||
<p class="playground-lead">{{__('playground.lead')}}</p>
|
||||
</div>
|
||||
<p class="playground-version version"></p>
|
||||
</header>
|
||||
<div class="loader" role=status aria-busy=true></div>
|
||||
<div id="editors" class="inner hide" aria-hide=true>
|
||||
<div class="area-tpl editor-wrapper">
|
||||
<h2>Template</h2>
|
||||
<div class="editor" id="editorEl">{{ raw('partial/demo.liquid') }}</div>
|
||||
</div>
|
||||
<div class="area-data editor-wrapper">
|
||||
<h2>Context</h2>
|
||||
<div class="editor" id="dataEl">{{ raw('partial/demo.json') }}</div>
|
||||
</div>
|
||||
<div class="area-output editor-wrapper">
|
||||
<h2>Output</h2>
|
||||
<div class="editor" id="previewEl">{{__('playground.loading')}}</div>
|
||||
<div class="playground-workspace">
|
||||
<div class="playground-pane area-tpl">
|
||||
<div class="pane-head">
|
||||
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
|
||||
<h2>Template</h2>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<div class="editor" id="editorEl">{{ raw('partial/demo.liquid') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playground-pane area-data">
|
||||
<div class="pane-head">
|
||||
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
|
||||
<h2>Context</h2>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<div class="editor" id="dataEl">{{ raw('partial/demo.json') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playground-pane area-output">
|
||||
<div class="pane-head">
|
||||
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
|
||||
<h2>Output</h2>
|
||||
</div>
|
||||
<div class="pane-body">
|
||||
<div class="output-preview" id="previewEl">
|
||||
<pre class="highlight"><code class="language-markup" id="previewCode">{{__('playground.loading')}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="inner version"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#cc-main
|
||||
--cc-btn-primary-bg: var(--color-link)
|
||||
--cc-btn-primary-color: #fff
|
||||
--cc-btn-primary-border-color: var(--color-link)
|
||||
--cc-btn-primary-hover-bg: var(--color-link-hover)
|
||||
--cc-btn-primary-hover-border-color: var(--color-link-hover)
|
||||
--cc-btn-secondary-bg: #eaeff2
|
||||
--cc-btn-secondary-color: var(--color-default)
|
||||
--cc-btn-secondary-border-color: #eaeff2
|
||||
--cc-toggle-on-bg: var(--color-link)
|
||||
|
||||
.cc--darkmode #cc-main
|
||||
--cc-btn-secondary-bg: #3a4248
|
||||
--cc-btn-secondary-color: var(--color-default)
|
||||
--cc-btn-secondary-border-color: #3a4248
|
||||
@@ -42,6 +42,13 @@
|
||||
@media mq-normal
|
||||
font-size: 30px
|
||||
|
||||
.cookie-preferences
|
||||
background: none
|
||||
border: 0
|
||||
padding: 0
|
||||
cursor: pointer
|
||||
color: inherit
|
||||
|
||||
.icon-oc
|
||||
height: 36px
|
||||
width: 30px
|
||||
|
||||
+2
-2
@@ -126,7 +126,7 @@
|
||||
background: var(--color-link-hover)
|
||||
color: #fff
|
||||
|
||||
#sponsors-wrap, #contributors-wrap
|
||||
#used-by-wrap, #sponsors-wrap, #contributors-wrap
|
||||
background: var(--color-navy-lighter)
|
||||
border-top: 1px solid #161d24
|
||||
border-bottom: 1px solid #161d24
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
#contributors-wrap
|
||||
border: none
|
||||
overflow: hidden;
|
||||
overflow: hidden
|
||||
|
||||
.contributors
|
||||
tr
|
||||
|
||||
+260
-55
@@ -1,107 +1,312 @@
|
||||
#playground
|
||||
--playground-gap: 12px
|
||||
--playground-radius: 10px
|
||||
--playground-inset: 16px
|
||||
background: var(--color-content-bg)
|
||||
overflow: hidden
|
||||
box-shadow: var(--panel-shadow)
|
||||
|
||||
.wrapper
|
||||
margin-bottom: 40px
|
||||
margin-bottom: 32px
|
||||
@media mq-mobile
|
||||
margin-bottom: 20px
|
||||
|
||||
.playground-hero
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
align-items: flex-end
|
||||
justify-content: space-between
|
||||
gap: 16px 24px
|
||||
padding-top: 32px
|
||||
padding-bottom: 20px
|
||||
@media mq-mobile
|
||||
padding-top: 16px
|
||||
padding-bottom: 12px
|
||||
gap: 10px
|
||||
align-items: flex-start
|
||||
|
||||
.playground-hero-text
|
||||
flex: 1 1 280px
|
||||
min-width: 0
|
||||
|
||||
h1
|
||||
font-size: 36px
|
||||
font-weight: 300
|
||||
margin-top: 40px
|
||||
margin-bottom: 24px
|
||||
color: var(--color-default)
|
||||
|
||||
h2
|
||||
font-size: 0.8125rem
|
||||
font-size: 28px
|
||||
font-weight: 600
|
||||
text-transform: uppercase
|
||||
letter-spacing: 0.04em
|
||||
color: var(--color-gray)
|
||||
letter-spacing: -0.02em
|
||||
margin: 0 0 8px
|
||||
color: var(--color-default)
|
||||
@media mq-mobile
|
||||
font-size: 22px
|
||||
margin-bottom: 4px
|
||||
|
||||
.playground-lead
|
||||
margin: 0
|
||||
font-size: 15px
|
||||
line-height: 1.5
|
||||
color: var(--color-gray)
|
||||
@media mq-mobile
|
||||
font-size: 14px
|
||||
line-height: 1.45
|
||||
|
||||
.playground-version
|
||||
flex: 0 0 auto
|
||||
margin: 0
|
||||
font-size: 12px
|
||||
line-height: 1.4
|
||||
font-family: font-mono
|
||||
padding: 6px 12px
|
||||
border-radius: 999px
|
||||
background: var(--playground-surface)
|
||||
border: 1px solid var(--color-border)
|
||||
color: var(--color-gray)
|
||||
a
|
||||
color: var(--color-default)
|
||||
text-decoration: none
|
||||
font-weight: 500
|
||||
&:hover
|
||||
color: var(--color-link)
|
||||
text-decoration: none
|
||||
|
||||
#editors
|
||||
display: grid
|
||||
overflow: hidden
|
||||
margin-bottom: 0
|
||||
height: 75vh
|
||||
min-height: 480px
|
||||
|
||||
.playground-workspace
|
||||
display: grid
|
||||
gap: var(--playground-gap)
|
||||
grid-template-columns: 1fr 1fr
|
||||
grid-template-rows: 3fr 2fr
|
||||
grid-gap: 16px
|
||||
align-items: stretch
|
||||
@media mq-normal
|
||||
overflow: hidden
|
||||
height: 75vh
|
||||
max-height: unquote('calc(100vh - 200px)')
|
||||
min-height: 520px
|
||||
@media mq-mobile
|
||||
height: auto
|
||||
min-height: 0
|
||||
grid-template-columns: 1fr
|
||||
grid-template-rows: auto
|
||||
grid-gap: 20px
|
||||
gap: 12px
|
||||
|
||||
.area-tpl
|
||||
grid-row: 1
|
||||
grid-column: 1
|
||||
min-height: 0
|
||||
--pane-dot: var(--color-link)
|
||||
.area-data
|
||||
grid-row: 2
|
||||
grid-column: 1
|
||||
min-height: 0
|
||||
--pane-dot: var(--highlight-orange)
|
||||
.area-output
|
||||
grid-column: 2
|
||||
grid-row: 1 / -1
|
||||
min-height: 0
|
||||
min-width: 0
|
||||
--pane-dot: var(--highlight-green)
|
||||
@media mq-mobile
|
||||
grid-row: auto
|
||||
grid-column: 1
|
||||
|
||||
.editor-wrapper
|
||||
.playground-pane
|
||||
display: flex
|
||||
gap: 8px
|
||||
flex-direction: column
|
||||
min-height: 0
|
||||
overflow: hidden
|
||||
.editor
|
||||
flex: 1 1 auto
|
||||
min-height: 0
|
||||
position: relative
|
||||
code-block-chrome()
|
||||
overflow: hidden
|
||||
@media mq-mobile
|
||||
min-height: 240px
|
||||
.ace_editor
|
||||
font-family: font-mono
|
||||
font-size: 14px
|
||||
line-height: 1.5
|
||||
border-radius: 6px
|
||||
.ace_scrollbar
|
||||
z-index: 2
|
||||
background: var(--playground-pane-head)
|
||||
border: 1px solid var(--code-border)
|
||||
border-radius: var(--playground-radius)
|
||||
box-shadow: var(--code-shadow)
|
||||
|
||||
.version
|
||||
font-size: 0.8125rem
|
||||
line-height: 1.5
|
||||
.pane-head
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 10px
|
||||
flex-shrink: 0
|
||||
height: 36px
|
||||
padding: 0 var(--playground-inset)
|
||||
border-bottom: 1px solid var(--code-border)
|
||||
@media mq-mobile
|
||||
height: 32px
|
||||
padding: 0 10px
|
||||
h2
|
||||
font-size: 13px
|
||||
font-weight: 600
|
||||
letter-spacing: 0.01em
|
||||
text-transform: none
|
||||
color: var(--color-default)
|
||||
margin: 0
|
||||
@media mq-mobile
|
||||
font-size: 12px
|
||||
|
||||
.pane-indicator
|
||||
width: 8px
|
||||
height: 8px
|
||||
border-radius: 50%
|
||||
flex-shrink: 0
|
||||
background: unquote('color-mix(in srgb, var(--pane-dot) 38%, var(--color-border))')
|
||||
transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease
|
||||
|
||||
&[data-state="active"]
|
||||
background: var(--pane-dot)
|
||||
animation: playground-dot-typing 0.85s ease-in-out infinite
|
||||
|
||||
&[data-state="pending"]
|
||||
background: var(--highlight-yellow)
|
||||
|
||||
&[data-state="ok"]
|
||||
background: var(--highlight-green)
|
||||
animation: playground-dot-ok 0.45s ease-out
|
||||
|
||||
&[data-state="error"]
|
||||
background: var(--highlight-red)
|
||||
animation: playground-dot-error 0.35s ease-out
|
||||
|
||||
.area-output .pane-indicator
|
||||
&[data-state="pending"]
|
||||
animation: playground-dot-pending 0.55s ease-in-out infinite
|
||||
|
||||
@keyframes playground-dot-typing
|
||||
0%, 100%
|
||||
transform: scale(1)
|
||||
box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--pane-dot) 0%, transparent)')
|
||||
50%
|
||||
transform: scale(1.2)
|
||||
box-shadow: 0 0 0 4px unquote('color-mix(in srgb, var(--pane-dot) 28%, transparent)')
|
||||
|
||||
@keyframes playground-dot-pending
|
||||
0%, 100%
|
||||
transform: scale(1)
|
||||
opacity: 0.75
|
||||
50%
|
||||
transform: scale(1.12)
|
||||
opacity: 1
|
||||
|
||||
@keyframes playground-dot-ok
|
||||
0%
|
||||
transform: scale(0.85)
|
||||
box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--highlight-green) 50%, transparent)')
|
||||
70%
|
||||
transform: scale(1.15)
|
||||
box-shadow: 0 0 0 5px unquote('color-mix(in srgb, var(--highlight-green) 0%, transparent)')
|
||||
100%
|
||||
transform: scale(1)
|
||||
box-shadow: none
|
||||
|
||||
@keyframes playground-dot-error
|
||||
0%, 100%
|
||||
transform: translateX(0)
|
||||
20%
|
||||
transform: translateX(-2px)
|
||||
40%
|
||||
transform: translateX(2px)
|
||||
60%
|
||||
transform: translateX(-1px)
|
||||
80%
|
||||
transform: translateX(1px)
|
||||
|
||||
.pane-body
|
||||
flex: 1 1 auto
|
||||
min-height: 0
|
||||
min-width: 0
|
||||
display: flex
|
||||
flex-direction: column
|
||||
overflow: hidden
|
||||
background: var(--highlight-background)
|
||||
|
||||
.area-tpl .pane-body,
|
||||
.area-data .pane-body
|
||||
padding: var(--playground-inset)
|
||||
box-sizing: border-box
|
||||
@media mq-mobile
|
||||
padding: 12px
|
||||
|
||||
.area-tpl .ace_gutter,
|
||||
.area-data .ace_gutter
|
||||
display: none
|
||||
width: 0
|
||||
min-width: 0
|
||||
|
||||
.area-tpl .ace_editor,
|
||||
.area-data .ace_editor,
|
||||
.area-tpl .ace_scroller,
|
||||
.area-data .ace_scroller,
|
||||
.area-tpl .ace_content,
|
||||
.area-data .ace_content,
|
||||
.area-tpl .ace_text-layer,
|
||||
.area-data .ace_text-layer
|
||||
background: transparent
|
||||
|
||||
.editor
|
||||
flex: 1 1 auto
|
||||
min-height: 0
|
||||
position: relative
|
||||
overflow: hidden
|
||||
@media mq-mobile
|
||||
min-height: 180px
|
||||
|
||||
.output-preview
|
||||
flex: 1 1 auto
|
||||
min-height: 0
|
||||
min-width: 0
|
||||
width: 100%
|
||||
overflow: auto
|
||||
@media mq-mobile
|
||||
min-height: 120px
|
||||
pre.highlight
|
||||
margin: 0
|
||||
min-height: 100%
|
||||
width: 100%
|
||||
box-sizing: border-box
|
||||
padding: var(--playground-inset)
|
||||
border: none
|
||||
box-shadow: none
|
||||
border-radius: 0
|
||||
background: transparent
|
||||
color: var(--highlight-foreground)
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
white-space: pre-wrap
|
||||
overflow-wrap: break-word
|
||||
@media mq-mobile
|
||||
padding: 12px
|
||||
code
|
||||
display: block
|
||||
width: 100%
|
||||
box-sizing: border-box
|
||||
font-family: font-mono
|
||||
font-size: 14px
|
||||
line-height: 1.55
|
||||
color: var(--highlight-foreground)
|
||||
background: transparent
|
||||
padding: 0
|
||||
white-space: inherit
|
||||
overflow-wrap: inherit
|
||||
@media mq-mobile
|
||||
font-size: 13px
|
||||
|
||||
.ace_editor
|
||||
font-family: font-mono
|
||||
color: var(--color-gray)
|
||||
margin-top: 20px
|
||||
margin-bottom: 32px
|
||||
a
|
||||
color: inherit
|
||||
text-decoration: none
|
||||
&:hover
|
||||
color: var(--color-link)
|
||||
text-decoration: underline
|
||||
font-size: 14px
|
||||
line-height: 1.55
|
||||
border-radius: 0
|
||||
@media mq-mobile
|
||||
font-size: 13px
|
||||
.ace_scrollbar
|
||||
z-index: 2
|
||||
|
||||
.hide
|
||||
display: none
|
||||
|
||||
.loader
|
||||
width: 48px
|
||||
height: 48px
|
||||
margin: 150px auto 200px
|
||||
border: 3px solid var(--color-border)
|
||||
width: 40px
|
||||
height: 40px
|
||||
margin: 120px auto 160px
|
||||
border: 2px solid var(--color-border)
|
||||
border-top-color: var(--color-link)
|
||||
border-radius: 50%
|
||||
animation: spin 0.8s infinite linear
|
||||
animation: playground-spin 0.7s infinite linear
|
||||
@media mq-mobile
|
||||
margin: 60px auto 80px
|
||||
|
||||
@keyframes spin
|
||||
@keyframes playground-spin
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
|
||||
+4
@@ -37,6 +37,8 @@ vendor-prefixes = webkit moz ms official
|
||||
--highlight-aqua: #0550ae
|
||||
--highlight-blue: #0550ae
|
||||
--highlight-purple: #8250df
|
||||
--playground-surface: #f3f4f6
|
||||
--playground-pane-head: #fff
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -75,6 +77,8 @@ vendor-prefixes = webkit moz ms official
|
||||
--highlight-aqua: #79c0ff
|
||||
--highlight-blue: #79c0ff
|
||||
--highlight-purple: #d2a8ff
|
||||
--playground-surface: hsl(218, 26%, 10%)
|
||||
--playground-pane-head: hsl(218, 22%, 16%)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -9,6 +9,7 @@
|
||||
@import "_partial/page"
|
||||
@import "_partial/mobile_nav"
|
||||
@import "_partial/footer"
|
||||
@import "_partial/cookieconsent"
|
||||
@import "_partial/highlight"
|
||||
@import "_partial/icomoon.css"
|
||||
@import "_partial/docsearch.min.css"
|
||||
@@ -0,0 +1,49 @@
|
||||
(function () {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('cc--darkmode');
|
||||
}
|
||||
|
||||
var config = {
|
||||
guiOptions: {
|
||||
consentModal: {
|
||||
equalWeightButtons: false
|
||||
}
|
||||
},
|
||||
categories: {
|
||||
necessary: {
|
||||
enabled: true,
|
||||
readOnly: true
|
||||
},
|
||||
analytics: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
language: {
|
||||
default: 'en',
|
||||
translations: {
|
||||
en: {
|
||||
consentModal: {
|
||||
title: 'We use cookies',
|
||||
description: 'This site uses cookies for analytics and to improve your experience.',
|
||||
acceptAllBtn: 'Accept',
|
||||
acceptNecessaryBtn: 'Reject'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (/^localhost$|^127\.0\.0\.1$/i.test(location.hostname)) {
|
||||
config.cookie = { secure: false };
|
||||
}
|
||||
|
||||
function run() {
|
||||
CookieConsent.run(config);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}());
|
||||
Vendored
+119
-16
@@ -38,8 +38,8 @@
|
||||
|
||||
(function() {
|
||||
// playground
|
||||
/* global liquidjs, ace */
|
||||
if (!location.pathname.match(/playground.html$/)) return;
|
||||
/* global liquidjs, ace, Prism */
|
||||
if (!/\/playground(?:\.html)?$/.test(location.pathname)) return;
|
||||
updateVersion(liquidjs.version);
|
||||
const engine = new liquidjs.Liquid({
|
||||
memoryLimit: 1e5,
|
||||
@@ -48,14 +48,19 @@
|
||||
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const editor = createEditor('editorEl', 'liquid');
|
||||
const dataEditor = createEditor('dataEl', 'json');
|
||||
const preview = createEditor('previewEl', 'html');
|
||||
preview.setReadOnly(true);
|
||||
preview.renderer.setShowGutter(false);
|
||||
preview.renderer.setPadding(16);
|
||||
const previewCode = document.getElementById('previewCode');
|
||||
const indicatorTpl = document.querySelector('.area-tpl .pane-indicator');
|
||||
const indicatorData = document.querySelector('.area-data .pane-indicator');
|
||||
const indicatorOutput = document.querySelector('.area-output .pane-indicator');
|
||||
|
||||
const editors = [editor, dataEditor, preview];
|
||||
const editors = [editor, dataEditor];
|
||||
let previewValue = '';
|
||||
let hadPreview = false;
|
||||
let renderTimer = null;
|
||||
const RENDER_DELAY = 180;
|
||||
colorScheme.addEventListener('change', function() {
|
||||
editors.forEach(applyEditorTheme);
|
||||
if (previewValue) setPreview(previewValue);
|
||||
});
|
||||
|
||||
const init = parseArgs(location.hash.slice(1));
|
||||
@@ -63,9 +68,11 @@
|
||||
editor.setValue(init.tpl, 1);
|
||||
dataEditor.setValue(init.data, 1);
|
||||
}
|
||||
editor.on('change', update);
|
||||
dataEditor.on('change', update);
|
||||
update();
|
||||
editor.on('change', onTemplateChange);
|
||||
dataEditor.on('change', onContextChange);
|
||||
editor.on('focus', function () { setIndicator(indicatorTpl, 'active'); });
|
||||
dataEditor.on('focus', function () { setIndicator(indicatorData, 'active'); });
|
||||
scheduleUpdate();
|
||||
ready();
|
||||
|
||||
function ready() {
|
||||
@@ -87,6 +94,8 @@
|
||||
|
||||
function applyEditorTheme(editor) {
|
||||
editor.setTheme(getEditorTheme());
|
||||
editor.renderer.setPadding(0);
|
||||
editor.container.style.background = 'transparent';
|
||||
}
|
||||
|
||||
function createEditor(id, lang) {
|
||||
@@ -96,15 +105,61 @@
|
||||
fontFamily: '"Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace',
|
||||
fontSize: '14px',
|
||||
showPrintMargin: false,
|
||||
showGutter: false,
|
||||
highlightActiveLine: false,
|
||||
tabSize: 2,
|
||||
useSoftTabs: true,
|
||||
scrollPastEnd: 0.25
|
||||
scrollPastEnd: 0
|
||||
});
|
||||
editor.getSession().setMode('ace/mode/' + lang);
|
||||
editor.renderer.setScrollMargin(8, 8, 0, 0);
|
||||
editor.renderer.setShowGutter(false);
|
||||
if (editor.renderer.$gutter) {
|
||||
editor.renderer.$gutter.style.display = 'none';
|
||||
}
|
||||
editor.renderer.setScrollMargin(0, 0, 0, 0);
|
||||
bindClipboard(editor);
|
||||
return editor;
|
||||
}
|
||||
|
||||
function bindClipboard(editor) {
|
||||
editor.commands.addCommand({
|
||||
name: 'copy',
|
||||
bindKey: {win: 'Ctrl-C', mac: 'Command-C'},
|
||||
exec: function (ed) {
|
||||
const text = ed.getCopyText();
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
},
|
||||
readOnly: true
|
||||
});
|
||||
editor.commands.addCommand({
|
||||
name: 'cut',
|
||||
bindKey: {win: 'Ctrl-X', mac: 'Command-X'},
|
||||
exec: function (ed) {
|
||||
const text = ed.getCopyText();
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
ed.insert('');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
editor.commands.addCommand({
|
||||
name: 'paste',
|
||||
bindKey: {win: 'Ctrl-V', mac: 'Command-V'},
|
||||
exec: function (ed) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.readText().then(function (text) {
|
||||
ed.insert(text);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgs(hash) {
|
||||
if (!hash) return;
|
||||
try {
|
||||
@@ -118,16 +173,64 @@
|
||||
return utoa(obj.tpl) + ',' + utoa(obj.data);
|
||||
}
|
||||
|
||||
function setPreview(value) {
|
||||
previewValue = value;
|
||||
previewCode.textContent = value;
|
||||
if (window.Prism) {
|
||||
delete previewCode.dataset.highlighted;
|
||||
window.Prism.highlightElement(previewCode);
|
||||
}
|
||||
}
|
||||
|
||||
function setIndicator(indicator, state) {
|
||||
if (indicator) indicator.dataset.state = state;
|
||||
}
|
||||
|
||||
function onTemplateChange() {
|
||||
setIndicator(indicatorTpl, 'active');
|
||||
if (indicatorData.dataset.state !== 'error') setIndicator(indicatorData, 'idle');
|
||||
setIndicator(indicatorOutput, 'pending');
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
function onContextChange() {
|
||||
setIndicator(indicatorData, 'active');
|
||||
if (indicatorTpl.dataset.state !== 'error') setIndicator(indicatorTpl, 'idle');
|
||||
setIndicator(indicatorOutput, 'pending');
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
function scheduleUpdate() {
|
||||
clearTimeout(renderTimer);
|
||||
renderTimer = setTimeout(update, RENDER_DELAY);
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const tpl = editor.getValue();
|
||||
const data = dataEditor.getValue();
|
||||
history.replaceState({}, '', '#' + serializeArgs({tpl, data}));
|
||||
let parsed;
|
||||
try {
|
||||
const html = await engine.parseAndRender(tpl, JSON.parse(data));
|
||||
preview.setValue(html, 1);
|
||||
parsed = JSON.parse(data);
|
||||
} catch (err) {
|
||||
preview.setValue(err.stack, 1);
|
||||
throw err;
|
||||
setIndicator(indicatorData, 'error');
|
||||
setIndicator(indicatorTpl, 'idle');
|
||||
setIndicator(indicatorOutput, 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const html = await engine.parseAndRender(tpl, parsed);
|
||||
if (html !== '' || !hadPreview) {
|
||||
setPreview(html);
|
||||
if (html !== '') hadPreview = true;
|
||||
}
|
||||
setIndicator(indicatorTpl, 'idle');
|
||||
setIndicator(indicatorData, 'idle');
|
||||
setIndicator(indicatorOutput, 'ok');
|
||||
} catch (err) {
|
||||
setIndicator(indicatorTpl, 'error');
|
||||
setIndicator(indicatorData, 'idle');
|
||||
setIndicator(indicatorOutput, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "10.27.0",
|
||||
"version": "10.27.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "liquidjs",
|
||||
"version": "10.27.0",
|
||||
"version": "10.27.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^10.0.0"
|
||||
@@ -64,7 +64,7 @@
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
||||
+8
-44
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "liquidjs",
|
||||
"version": "10.27.0",
|
||||
"version": "10.27.1",
|
||||
"sideEffects": false,
|
||||
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
|
||||
"description": "A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.",
|
||||
"main": "dist/liquid.node.js",
|
||||
"module": "dist/liquid.node.mjs",
|
||||
"es2015": "dist/liquid.browser.mjs",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"types": "dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint \"**/*.mjs\" \"**/*.ts\" .",
|
||||
@@ -29,12 +29,15 @@
|
||||
"build:min": "BUNDLES=min rollup -c rollup.config.mjs",
|
||||
"build:umd": "BUNDLES=umd rollup -c rollup.config.mjs",
|
||||
"build:charmap": "./bin/character-gen.js > src/util/character.ts",
|
||||
"build:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog build:docs-hexo",
|
||||
"prepare:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog",
|
||||
"build:docs": "run-s prepare:docs build:docs-hexo",
|
||||
"build:docs-liquid": "cross-env BUNDLES=min rollup -c rollup.config.mjs && shx cp dist/liquid.browser.min.js docs/themes/navy/source/js/",
|
||||
"build:contributors": "node bin/build-contributors.js",
|
||||
"build:apidoc": "shx rm -rf docs/source/api && typedoc --plugin typedoc-plugin-missing-exports ./src --gitRevision master --out docs/source/api",
|
||||
"build:changelog": "node bin/build-changelog.js",
|
||||
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/"
|
||||
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/",
|
||||
"serve:docs": "cd docs && npm run start",
|
||||
"dev:docs": "run-s prepare:docs serve:docs"
|
||||
},
|
||||
"bin": {
|
||||
"liquidjs": "./bin/liquid.js",
|
||||
@@ -118,45 +121,6 @@
|
||||
"dependencies": {
|
||||
"commander": "^10.0.0"
|
||||
},
|
||||
"release": {
|
||||
"branch": "master",
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "dist/*.umd.js",
|
||||
"label": "liquid.js"
|
||||
},
|
||||
{
|
||||
"path": "dist/*.min.js",
|
||||
"label": "liquid.min.js"
|
||||
},
|
||||
{
|
||||
"path": "dist/*.min.js.map",
|
||||
"label": "liquid.min.js.map"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
},
|
||||
|
||||
+4
-4
@@ -89,7 +89,7 @@ const nodeEsm = {
|
||||
plugins: [
|
||||
versionInjection,
|
||||
replace(esmRequire),
|
||||
typescript(tsconfig('es6'))
|
||||
typescript(tsconfig('ES2020'))
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
@@ -108,7 +108,7 @@ const browserEsm = {
|
||||
replace(browserBase64),
|
||||
replace(browserCrypto),
|
||||
replace(browserStream),
|
||||
typescript(tsconfig('es6'))
|
||||
typescript(tsconfig('ES2020'))
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
@@ -128,7 +128,7 @@ const browserUmd = {
|
||||
replace(browserBase64),
|
||||
replace(browserCrypto),
|
||||
replace(browserStream),
|
||||
typescript(tsconfig('es5'))
|
||||
typescript(tsconfig('ES2020'))
|
||||
],
|
||||
treeshake,
|
||||
input
|
||||
@@ -148,7 +148,7 @@ const browserMin = {
|
||||
replace(browserBase64),
|
||||
replace(browserCrypto),
|
||||
replace(browserStream),
|
||||
typescript(tsconfig('es5')),
|
||||
typescript(tsconfig('ES2020')),
|
||||
uglify()
|
||||
],
|
||||
treeshake,
|
||||
|
||||
@@ -2,11 +2,6 @@ import * as base64 from './base64-impl-browser'
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
describe('base64-impl/browser', function () {
|
||||
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
|
||||
console.info('jsdom not supported, skipping base64-impl-browser...')
|
||||
return
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
const dom = new JSDOM(``, {
|
||||
url: 'https://example.com/',
|
||||
|
||||
@@ -3,10 +3,6 @@ import * as sinon from 'sinon'
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
describe('fs/browser', function () {
|
||||
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
|
||||
console.info('jsdom not supported, skipping template-browser...')
|
||||
return
|
||||
}
|
||||
beforeEach(function () {
|
||||
const dom = new JSDOM(``, {
|
||||
url: 'https://example.com/foo/bar/',
|
||||
|
||||
@@ -183,6 +183,21 @@ describe('Context', function () {
|
||||
ctx.push({ foo: Object.create({ bar: 'BAR' }) })
|
||||
return expect(() => ctx.getSync(['foo', 'bar'])).toThrow(/undefined variable: foo.bar/)
|
||||
})
|
||||
it('should return undefined for inherited array indices', function () {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype[0] = 'POLLUTED'
|
||||
try {
|
||||
const a: number[] = []
|
||||
a.length = 1
|
||||
ctx.push({ foo: a })
|
||||
expect(ctx.getSync(['foo', 0])).toEqual(undefined)
|
||||
expect(ctx.getSync(['foo', -1])).toEqual(undefined)
|
||||
expect(ctx.getSync(['foo', 'first'])).toEqual(undefined)
|
||||
expect(ctx.getSync(['foo', 'last'])).toEqual(undefined)
|
||||
} finally {
|
||||
delete (Array.prototype as any)[0]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getAll()', function () {
|
||||
|
||||
+11
-13
@@ -3,7 +3,7 @@ import { Drop } from '../drop/drop'
|
||||
import { __assign } from 'tslib'
|
||||
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options'
|
||||
import { createScope, Scope } from './scope'
|
||||
import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue } from '../util'
|
||||
import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, isNumber, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue, readArrayElement } from '../util'
|
||||
|
||||
type PropertyKey = string | number;
|
||||
|
||||
@@ -103,7 +103,7 @@ export class Context {
|
||||
return this.scopes[0]
|
||||
}
|
||||
public spawn (scope = {}) {
|
||||
const ctx = new Context(scope, this.opts, {
|
||||
return new Context(scope, this.opts, {
|
||||
sync: this.sync,
|
||||
globals: this.globals,
|
||||
strictVariables: this.strictVariables,
|
||||
@@ -112,8 +112,6 @@ export class Context {
|
||||
renderLimit: this.renderLimit,
|
||||
memoryLimit: this.memoryLimit
|
||||
})
|
||||
ctx.setRegister('partialStack', this.getRegister('partialStack', [] as string[]))
|
||||
return ctx
|
||||
}
|
||||
private findScope (key: string | number) {
|
||||
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
||||
@@ -127,13 +125,13 @@ export class Context {
|
||||
obj = toLiquid(obj)
|
||||
key = toValue(key) as PropertyKey
|
||||
if (isNil(obj)) return obj
|
||||
if (isArray(obj) && (key as number) < 0) return obj[obj.length + +key]
|
||||
if (isArray(obj) && isNumber(key)) return readArrayElement(obj, key, this.ownPropertyOnly)
|
||||
const value = readJSProperty(obj, key, this.ownPropertyOnly)
|
||||
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key, this)
|
||||
if (isFunction(value)) return value.call(obj)
|
||||
if (key === 'size') return readSize(obj)
|
||||
else if (key === 'first') return readFirst(obj)
|
||||
else if (key === 'last') return readLast(obj)
|
||||
else if (key === 'first') return readFirst(obj, this.ownPropertyOnly)
|
||||
else if (key === 'last') return readLast(obj, this.ownPropertyOnly)
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -143,14 +141,14 @@ export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: b
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
function readFirst (obj: Scope) {
|
||||
if (isArray(obj)) return obj[0]
|
||||
return obj['first']
|
||||
function readFirst (obj: Scope, ownPropertyOnly: boolean) {
|
||||
if (isArray(obj)) return readArrayElement(obj, 0, ownPropertyOnly)
|
||||
return readJSProperty(obj, 'first', ownPropertyOnly)
|
||||
}
|
||||
|
||||
function readLast (obj: Scope) {
|
||||
if (isArray(obj)) return obj[obj.length - 1]
|
||||
return obj['last']
|
||||
function readLast (obj: Scope, ownPropertyOnly: boolean) {
|
||||
if (isArray(obj)) return readArrayElement(obj, -1, ownPropertyOnly)
|
||||
return readJSProperty(obj, 'last', ownPropertyOnly)
|
||||
}
|
||||
|
||||
function readSize (obj: Scope) {
|
||||
|
||||
+18
-11
@@ -1,4 +1,4 @@
|
||||
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, orderedCompare, isArray, isNil, last as arrayLast, isArrayLike, toEnumerable } from '../util'
|
||||
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, orderedCompare, isArray, isNil, isArrayLike, readArrayElement, toEnumerable } from '../util'
|
||||
import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
|
||||
import { Value, FilterImpl } from '../template'
|
||||
import { Tokenizer } from '../parser'
|
||||
@@ -8,12 +8,17 @@ import { EmptyDrop } from '../drop'
|
||||
export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) {
|
||||
const array = toArray(v)
|
||||
const sep = isNil(arg) ? ' ' : stringify(arg)
|
||||
const complexity = array.length * (1 + sep.length)
|
||||
this.context.memoryLimit.use(complexity)
|
||||
return array.join(sep)
|
||||
let outputSize = sep.length * Math.max(array.length - 1, 0)
|
||||
for (let i = 0; i < array.length; i++) outputSize += String(array[i]).length
|
||||
this.context.memoryLimit.use(outputSize)
|
||||
return Array.prototype.join.call(array, sep)
|
||||
})
|
||||
export const last = argumentsToValue(function (this: FilterImpl, v: any) {
|
||||
return isArrayLike(v) ? readArrayElement(v, -1, this.context.ownPropertyOnly) : ''
|
||||
})
|
||||
export const first = argumentsToValue(function (this: FilterImpl, v: any) {
|
||||
return isArrayLike(v) ? readArrayElement(v, 0, this.context.ownPropertyOnly) : ''
|
||||
})
|
||||
export const last = argumentsToValue((v: any) => isArrayLike(v) ? arrayLast(v) : '')
|
||||
export const first = argumentsToValue((v: any) => isArrayLike(v) ? v[0] : '')
|
||||
export const reverse = argumentsToValue(function (this: FilterImpl, v: any[]) {
|
||||
const array = toArray(v)
|
||||
this.context.memoryLimit.use(array.length)
|
||||
@@ -41,7 +46,7 @@ export function * sort_natural<T> (this: FilterImpl, arr: T[], property?: string
|
||||
return yield * sortBy.call(this, arr, property, caseInsensitiveCompare)
|
||||
}
|
||||
|
||||
export const size = (v: string | any[]) => (v && v.length) || 0
|
||||
export const size = (v: string | any[]) => v?.length || 0
|
||||
|
||||
export function * map (this: FilterImpl, arr: Scope[], property: string): IterableIterator<unknown> {
|
||||
const results = []
|
||||
@@ -66,14 +71,14 @@ export function * sum (this: FilterImpl, arr: Scope[], property?: string): Itera
|
||||
export function compact<T> (this: FilterImpl, arr: T[]) {
|
||||
const array = toArray(arr)
|
||||
this.context.memoryLimit.use(array.length)
|
||||
return array.filter(x => !isNil(toValue(x)))
|
||||
return Array.prototype.filter.call(array, x => !isNil(toValue(x)))
|
||||
}
|
||||
|
||||
export function concat<T1, T2> (this: FilterImpl, v: T1[], arg: T2[] = []): (T1 | T2)[] {
|
||||
const lhs = toArray(v)
|
||||
const rhs = toArray(arg)
|
||||
this.context.memoryLimit.use(lhs.length + rhs.length)
|
||||
return lhs.concat(rhs)
|
||||
return Array.prototype.concat.call(lhs, rhs)
|
||||
}
|
||||
|
||||
export function push<T> (this: FilterImpl, v: T[], arg: T): T[] {
|
||||
@@ -110,7 +115,9 @@ export function slice<T> (this: FilterImpl, v: T[] | string, begin: number, leng
|
||||
if (!isArray(v)) v = stringify(v)
|
||||
begin = begin < 0 ? v.length + begin : begin
|
||||
this.context.memoryLimit.use(length)
|
||||
return v.slice(begin, begin + length)
|
||||
return isArray(v)
|
||||
? Array.prototype.slice.call(v, begin, begin + length)
|
||||
: String.prototype.slice.call(v, begin, begin + length)
|
||||
}
|
||||
|
||||
function expectedMatcher (this: FilterImpl, expected: any): (v: any) => boolean {
|
||||
@@ -132,7 +139,7 @@ function * filter<T extends object> (this: FilterImpl, include: boolean, arr: T[
|
||||
values.push(yield evalToken(token, this.context.spawn(item)))
|
||||
}
|
||||
const matcher = expectedMatcher.call(this, expected)
|
||||
return arr.filter((_, i) => matcher(values[i]) === include)
|
||||
return Array.prototype.filter.call(arr, (_, i) => matcher(values[i]) === include)
|
||||
}
|
||||
|
||||
function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export function strip_html (this: FilterImpl, v: string) {
|
||||
if (e >= 0) { i = e + closer.length; break }
|
||||
blocks.delete(opener)
|
||||
}
|
||||
if (i === lt) return out + str.slice(lt)
|
||||
if (i <= lt) return out + str.slice(lt)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
+3
-4
@@ -8,14 +8,13 @@ export const divided_by = argumentsToNumber((dividend: number, divisor: number,
|
||||
export const floor = argumentsToNumber(Math.floor)
|
||||
export const minus = argumentsToNumber((v: number, arg: number) => v - arg)
|
||||
export const plus = argumentsToNumber((lhs: number, rhs: number) => lhs + rhs)
|
||||
export const modulo = argumentsToNumber((v: number, arg: number) => v % arg)
|
||||
export const modulo = argumentsToNumber((v: number, arg: number) => ((v % arg) + arg) % arg)
|
||||
export const times = argumentsToNumber((v: number, arg: number) => v * arg)
|
||||
|
||||
export function round (v: number, arg = 0) {
|
||||
v = toNumber(v)
|
||||
arg = toNumber(arg)
|
||||
const amp = Math.pow(10, arg)
|
||||
const scaled = v * amp
|
||||
// Round half away from zero
|
||||
return Math.sign(v) * Math.round(Math.abs(scaled)) / amp
|
||||
const scaled = (v * amp) * (1 + Number.EPSILON)
|
||||
return Math.round(scaled) / amp
|
||||
}
|
||||
|
||||
+29
-5
@@ -2,6 +2,18 @@ import { isFalsy } from '../render/boolean'
|
||||
import { identify, isArray, isString, toValue } from '../util/underscore'
|
||||
import { FilterImpl } from '../template'
|
||||
|
||||
function chargeJsonReplacerValue (memoryLimit: { use(count: number): void }, val: unknown) {
|
||||
if (typeof val === 'string') {
|
||||
memoryLimit.use(val.length)
|
||||
} else if (val === null || typeof val === 'number' || typeof val === 'boolean') {
|
||||
memoryLimit.use(JSON.stringify(val).length)
|
||||
} else if (Array.isArray(val)) {
|
||||
memoryLimit.use(val.length + 1)
|
||||
} else if (typeof val === 'object') {
|
||||
memoryLimit.use(2)
|
||||
}
|
||||
}
|
||||
|
||||
function defaultFilter<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
|
||||
value = toValue(value)
|
||||
if (isArray(value) || isString(value)) return value.length ? value : defaultValue
|
||||
@@ -9,18 +21,30 @@ function defaultFilter<T1 extends boolean, T2> (this: FilterImpl, value: T1, def
|
||||
return isFalsy(value, this.context) ? defaultValue : value
|
||||
}
|
||||
|
||||
function json (value: any, space = 0) {
|
||||
return JSON.stringify(value, null, space)
|
||||
function json (this: FilterImpl, value: any, space = 0) {
|
||||
const memoryLimit = this.context.memoryLimit
|
||||
return JSON.stringify(value, (_key, val) => {
|
||||
chargeJsonReplacerValue(memoryLimit, val)
|
||||
return val
|
||||
}, space)
|
||||
}
|
||||
|
||||
function inspect (value: any, space = 0) {
|
||||
function inspect (this: FilterImpl, value: any, space = 0) {
|
||||
const memoryLimit = this.context.memoryLimit
|
||||
const ancestors: object[] = []
|
||||
return JSON.stringify(value, function (this: unknown, _key: unknown, value: any) {
|
||||
if (typeof value !== 'object' || value === null) return value
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
chargeJsonReplacerValue(memoryLimit, value)
|
||||
return value
|
||||
}
|
||||
// `this` is the object that value is contained in, i.e., its direct parent.
|
||||
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) ancestors.pop()
|
||||
if (ancestors.includes(value)) return '[Circular]'
|
||||
if (ancestors.includes(value)) {
|
||||
memoryLimit.use('[Circular]'.length)
|
||||
return '[Circular]'
|
||||
}
|
||||
ancestors.push(value)
|
||||
chargeJsonReplacerValue(memoryLimit, value)
|
||||
return value
|
||||
}, space)
|
||||
}
|
||||
|
||||
@@ -209,7 +209,9 @@ export function number_of_words (this: FilterImpl, input: string, mode?: 'cjk' |
|
||||
|
||||
export function array_to_sentence_string (this: FilterImpl, array: unknown[], connector = 'and') {
|
||||
connector = stringify(connector)
|
||||
this.context.memoryLimit.use(array.length + connector.length)
|
||||
let outputSize = connector.length + array.length * 2
|
||||
for (let i = 0; i < array.length; i++) outputSize += stringify(array[i]).length
|
||||
this.context.memoryLimit.use(outputSize)
|
||||
switch (array.length) {
|
||||
case 0:
|
||||
return ''
|
||||
|
||||
@@ -38,7 +38,10 @@ export interface LiquidOptions {
|
||||
strictVariables?: boolean;
|
||||
/** Catch all errors instead of exit upon one. Please note that render errors won't be reached when parse fails. */
|
||||
catchAllErrors?: boolean;
|
||||
/** Hide scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates. */
|
||||
/**
|
||||
* Hide scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates.
|
||||
* This only applies to property/index access on scope objects. Filter transforms and iteration operate on the resolved value with standard JavaScript semantics, so prototype-inherited array indices may still be surfaced by them.
|
||||
*/
|
||||
ownPropertyOnly?: boolean;
|
||||
/** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if`/`elsif`/`unless` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/
|
||||
lenientIf?: boolean;
|
||||
@@ -201,11 +204,11 @@ export const defaultOptions: NormalizedFullOptions = {
|
||||
}
|
||||
|
||||
export function normalize (options: LiquidOptions): NormalizedFullOptions {
|
||||
if (options.hasOwnProperty('root')) {
|
||||
if (!options.hasOwnProperty('partials')) options.partials = options.root
|
||||
if (!options.hasOwnProperty('layouts')) options.layouts = options.root
|
||||
if ('root' in options) {
|
||||
if (!('partials' in options)) options.partials = options.root
|
||||
if (!('layouts' in options)) options.layouts = options.root
|
||||
}
|
||||
if (options.hasOwnProperty('cache')) {
|
||||
if ('cache' in options) {
|
||||
let cache: LiquidCache | undefined
|
||||
if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined
|
||||
else if (typeof options.cache === 'object') cache = options.cache
|
||||
|
||||
@@ -522,6 +522,14 @@ describe('Tokenizer', function () {
|
||||
expect(new Tokenizer('contains b').matchTrie(opTrie)).toBe(8)
|
||||
})
|
||||
})
|
||||
describe('#createTrie()', function () {
|
||||
it('should return the same trie for the same input', () => {
|
||||
expect(createTrie(defaultOperators)).toBe(createTrie(defaultOperators))
|
||||
})
|
||||
it('should return distinct tries for distinct inputs', () => {
|
||||
expect(createTrie({ foo: 1 })).not.toBe(createTrie({ foo: 1 }))
|
||||
})
|
||||
})
|
||||
describe('#readLiquidTagTokens', () => {
|
||||
it('should read newline terminated tokens', () => {
|
||||
const tokenizer = new Tokenizer('echo \'hello\'')
|
||||
|
||||
+10
-15
@@ -3,7 +3,7 @@ import { BlockMode, createScope, Scope } from '../context'
|
||||
import { Parser } from '../parser'
|
||||
import { Argument, Arguments, PartialScope } from '../template'
|
||||
import { isString, isValueToken } from '../util'
|
||||
import { parseFilePath, renderFilePath, ParsedFileName, pushPartialStack, popPartialStack } from './render'
|
||||
import { parseFilePath, renderFilePath, ParsedFileName } from './render'
|
||||
|
||||
export default class extends Tag {
|
||||
private file: ParsedFileName
|
||||
@@ -33,21 +33,16 @@ export default class extends Tag {
|
||||
const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string
|
||||
assert(filepath, () => `illegal file path "${filepath}"`)
|
||||
|
||||
pushPartialStack(ctx, filepath, 'include')
|
||||
const saved = ctx.saveRegister('blocks', 'blockMode')
|
||||
try {
|
||||
ctx.setRegister('blocks', {})
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
const scope = createScope((yield hash.render(ctx)) as Scope)
|
||||
if (withVar) scope[filepath] = yield evalToken(withVar, ctx)
|
||||
const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile)) as Template[]
|
||||
ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope)
|
||||
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||
ctx.pop()
|
||||
} finally {
|
||||
ctx.restoreRegister(saved)
|
||||
popPartialStack(ctx)
|
||||
}
|
||||
ctx.setRegister('blocks', {})
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
const scope = createScope((yield hash.render(ctx)) as Scope)
|
||||
if (withVar) scope[filepath] = yield evalToken(withVar, ctx)
|
||||
const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile)) as Template[]
|
||||
ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope)
|
||||
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||
ctx.pop()
|
||||
ctx.restoreRegister(saved)
|
||||
}
|
||||
|
||||
public * children (partials: boolean, sync: boolean): Generator<unknown, Template[]> {
|
||||
|
||||
+17
-35
@@ -59,32 +59,27 @@ export default class extends Tag {
|
||||
const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string
|
||||
assert(filepath, () => `illegal file path "${filepath}"`)
|
||||
|
||||
pushPartialStack(ctx, filepath, 'render')
|
||||
try {
|
||||
const childCtx = ctx.spawn()
|
||||
const scope = childCtx.bottom()
|
||||
__assign(scope, yield hash.render(ctx))
|
||||
if (this.with) {
|
||||
const { value, alias } = this.with
|
||||
scope[alias || filepath] = yield evalToken(value, ctx)
|
||||
}
|
||||
const childCtx = ctx.spawn()
|
||||
const scope = childCtx.bottom()
|
||||
__assign(scope, yield hash.render(ctx))
|
||||
if (this.with) {
|
||||
const { value, alias } = this.with
|
||||
scope[alias || filepath] = yield evalToken(value, ctx)
|
||||
}
|
||||
|
||||
if (this.forBinding) {
|
||||
const { value, alias } = this.forBinding
|
||||
const collection = toEnumerable(yield evalToken(value, ctx))
|
||||
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias as string)
|
||||
for (const item of collection) {
|
||||
scope[alias as string] = item
|
||||
const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile)) as Template[]
|
||||
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||
scope['forloop'].next()
|
||||
}
|
||||
} else {
|
||||
if (this.forBinding) {
|
||||
const { value, alias } = this.forBinding
|
||||
const collection = toEnumerable(yield evalToken(value, ctx))
|
||||
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias as string)
|
||||
for (const item of collection) {
|
||||
scope[alias as string] = item
|
||||
const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile)) as Template[]
|
||||
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||
scope['forloop'].next()
|
||||
}
|
||||
} finally {
|
||||
popPartialStack(ctx)
|
||||
} else {
|
||||
const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile)) as Template[]
|
||||
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,16 +173,3 @@ export function * renderFilePath (file: ParsedFileName, ctx: Context, liquid: Li
|
||||
if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx)
|
||||
return yield evalToken(file, ctx)
|
||||
}
|
||||
|
||||
export function pushPartialStack (ctx: Context, filepath: string, tag: 'render' | 'include') {
|
||||
const stack: string[] = ctx.getRegister('partialStack', [])
|
||||
if (ctx.renderLimit.isUnlimited() && stack.includes(filepath)) {
|
||||
throw new Error(`${tag} tag cannot be nested`)
|
||||
}
|
||||
stack.push(filepath)
|
||||
}
|
||||
|
||||
export function popPartialStack (ctx: Context) {
|
||||
const stack: string[] = ctx.getRegister('partialStack', [])
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,4 @@ export class Limiter {
|
||||
assert(+count <= this.limit, this.message)
|
||||
}
|
||||
}
|
||||
isUnlimited () {
|
||||
return !Number.isFinite(this.limit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,16 @@ export type Trie<T> = {
|
||||
needBoundary?: true
|
||||
} & Record<string, any>
|
||||
|
||||
// Tries are built once per input object and reused: the Tokenizer rebuilds them
|
||||
// on every instantiation, but `input` (operators/literalValues) is a stable
|
||||
// reference. WeakMap-keying by `input` lets short-lived operator objects (and
|
||||
// their tries) be garbage collected. The returned trie is treated as read-only
|
||||
// by callers (matchTrie only reads it); do not mutate it.
|
||||
const trieCache = new WeakMap<TrieInput<any>, Trie<any>>()
|
||||
|
||||
export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
|
||||
const cached = trieCache.get(input)
|
||||
if (cached) return cached
|
||||
const trie: Trie<T> = {}
|
||||
for (const [name, data] of Object.entries(input)) {
|
||||
let node = trie
|
||||
@@ -29,5 +38,6 @@ export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
|
||||
node.data = data
|
||||
node.end = true
|
||||
}
|
||||
trieCache.set(input, trie)
|
||||
return trie
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Drop } from '../drop/drop'
|
||||
|
||||
export const toString = Object.prototype.toString
|
||||
export const hasOwnProperty = Object.prototype.hasOwnProperty
|
||||
const toLowerCase = String.prototype.toLowerCase
|
||||
|
||||
export const hasOwnProperty = Object.hasOwnProperty
|
||||
|
||||
export function isString (value: any): value is string {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
@@ -42,6 +41,12 @@ export function stringify (value: any): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function readArrayElement (arr: any[], index: number, ownPropertyOnly: boolean) {
|
||||
if (index < 0) index = arr.length + index
|
||||
if (ownPropertyOnly && !hasOwnProperty.call(arr, index)) return undefined
|
||||
return arr[index]
|
||||
}
|
||||
|
||||
export function toEnumerable<T = unknown> (val: any): T[] {
|
||||
val = toValue(val)
|
||||
if (isArray(val)) return val
|
||||
@@ -84,8 +89,7 @@ export function isUndefined (value: any): boolean {
|
||||
}
|
||||
|
||||
export function isArray (value: any): value is any[] {
|
||||
// be compatible with IE 8
|
||||
return toString.call(value) === '[object Array]'
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
export function isArrayLike (value: any): value is any[] {
|
||||
|
||||
@@ -117,25 +117,4 @@ describe('.parseAndRender()', function () {
|
||||
await expect(liquid.renderFile('template')).rejects.toThrow(/block tag cannot be nested/)
|
||||
})
|
||||
})
|
||||
describe('render/include: self-referential partial regression', function () {
|
||||
it('should reject self-referential {% render %} via in-memory templates (no hang / OOM)', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' } })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should reject self-referential {% include %} (no hang / OOM)', async function () {
|
||||
let root: string
|
||||
root = mkdtempSync(join(tmpdir(), 'liquid-e2e-include-nested-'))
|
||||
try {
|
||||
writeFileSync(join(root, 'self.html'), 'A{% include "self.html" %}B')
|
||||
const liquid = new Liquid({ root, extname: '.html' })
|
||||
await expect(liquid.renderFile('self')).rejects.toThrow(/include tag cannot be nested/)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
it('should allow self-referential {% render %} when renderLimit is finite', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' }, renderLimit: 0.01 })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/template render limit exceeded/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Liquid } from '../../../src/liquid'
|
||||
|
||||
describe('ownPropertyOnly / inherited array indices', function () {
|
||||
const engine = new Liquid({ ownPropertyOnly: true })
|
||||
|
||||
function pollutedArrays () {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Array.prototype[0] = 'ARRAY_PROTO_POLLUTED'
|
||||
;(Object.prototype as any).secret = 'OBJECT_PROTO_POLLUTED'
|
||||
const a: any[] = []
|
||||
a.length = 1
|
||||
const o = {}
|
||||
return {
|
||||
a,
|
||||
o,
|
||||
cleanup () {
|
||||
delete (Array.prototype as any)[0]
|
||||
delete (Object.prototype as any).secret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cases: [string, (ctx: ReturnType<typeof pollutedArrays>) => object, string][] = [
|
||||
['{{ a[0] }}', ({ a }) => ({ a }), ''],
|
||||
['{{ a[-1] }}', ({ a }) => ({ a }), ''],
|
||||
['{{ o.secret }}', ({ o }) => ({ o }), ''],
|
||||
['{{ a.first }}', ({ a }) => ({ a }), ''],
|
||||
['{{ a.last }}', ({ a }) => ({ a }), ''],
|
||||
['{{ a | first }}', ({ a }) => ({ a }), ''],
|
||||
['{{ a | last }}', ({ a }) => ({ a }), ''],
|
||||
['{% assign x = a | first %}{{ x }}', ({ a }) => ({ a }), '']
|
||||
]
|
||||
|
||||
it.each(cases)('%s', function (src, scopeFn, expected) {
|
||||
const ctx = pollutedArrays()
|
||||
try {
|
||||
expect(engine.parseAndRenderSync(src, scopeFn(ctx))).toBe(expected)
|
||||
} finally {
|
||||
ctx.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('still allows array length and size', function () {
|
||||
const { a, cleanup } = pollutedArrays()
|
||||
try {
|
||||
expect(engine.parseAndRenderSync('{{ a.size }}', { a })).toBe('1')
|
||||
const arr = [1, 2]
|
||||
expect(engine.parseAndRenderSync('{{ arr | first }}', { arr })).toBe('1')
|
||||
expect(engine.parseAndRenderSync('{{ arr[-1] }}', { arr })).toBe('2')
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -85,5 +85,9 @@ describe('filters/html', function () {
|
||||
expect(liquid.parseAndRenderSync('{{"<img\rsrc=x\ronerror=alert(1)>" | strip_html}}')).toBe('')
|
||||
expect(liquid.parseAndRenderSync('{{"<svg\nonload=alert(1)>" | strip_html}}')).toBe('')
|
||||
})
|
||||
it('should not loop on unclosed openers (GHSA-m7fp-h3p4-hr49)', function () {
|
||||
expect(liquid.parseAndRenderSync('{{ "a<" | strip_html }}')).toBe('a<')
|
||||
expect(liquid.parseAndRenderSync('{{ "hello<world<again" | strip_html }}')).toBe('hello<world<again')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,6 +50,9 @@ describe('filters/math', function () {
|
||||
expect(Number(html)).toBeCloseTo(3.357, 3)
|
||||
})
|
||||
it('should convert string', () => test('{{ "24" | modulo: "7" }}', '3'))
|
||||
it('should follow divisor sign for negative dividend', () => test('{{ -7 | modulo: 3 }}', '2'))
|
||||
it('should follow divisor sign for negative divisor', () => test('{{ 7 | modulo: -3 }}', '-2'))
|
||||
it('should follow divisor sign for negative float', () => test('{{ -4.5 | modulo: 3 }}', '1.5'))
|
||||
})
|
||||
describe('plus', function () {
|
||||
it('should return "6" for 4,2', () => test('{{ 4 | plus: 2 }}', '6'))
|
||||
@@ -70,6 +73,10 @@ describe('filters/math', function () {
|
||||
it('should return "183.36" for 183.357,2',
|
||||
() => test('{{183.357|round: 2}}', '183.36'))
|
||||
it('should convert string to number', () => test('{{"2.7"|round}}', '3'))
|
||||
it('odd number 1.005 should round correctly', () => test('{{1.005|round:2}}', '1.01'))
|
||||
it('odd number -1.005 should round correctly', () => test('{{num|round:2}}', { num: -1.005 }, '-1.01'))
|
||||
it('odd number 9.075 should round correctly', () => test('{{9.075|round:2}}', '9.08'))
|
||||
it('odd number -9.075 should round correctly', () => test('{{num|round:2}}', { num: -9.075 }, '-9.08'))
|
||||
})
|
||||
describe('times', function () {
|
||||
it('should return "6" for 3,2', () => test('{{ 3 | times: 2 }}', '6'))
|
||||
|
||||
@@ -89,6 +89,47 @@ describe('DoS related', function () {
|
||||
const liquid = new Liquid({ memoryLimit: 100 })
|
||||
await expect(liquid.parseAndRender('{{ array | sample: 1 | size }}', { array })).rejects.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should charge join by produced output size, not element count', () => {
|
||||
const array = ['a'.repeat(100), 'b'.repeat(100)]
|
||||
const liquid = new Liquid({ memoryLimit: 100 })
|
||||
expect(() => liquid.parseAndRenderSync('{{ array | join: "" }}', { array }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should allow join within memoryLimit', () => {
|
||||
const array = ['a'.repeat(20), 'b'.repeat(20)]
|
||||
const liquid = new Liquid({ memoryLimit: 100 })
|
||||
expect(liquid.parseAndRenderSync('{{ array | join: "" }}', { array })).toBe('a'.repeat(20) + 'b'.repeat(20))
|
||||
})
|
||||
it('should prevent concat doubling from bypassing join memoryLimit', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 1e4 })
|
||||
const src = '{%- assign a = s | split: "NOSEP" -%}' +
|
||||
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
|
||||
'{{ a | join: "" | size }}'
|
||||
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should charge array_to_sentence_string by produced output size', () => {
|
||||
const array = ['a'.repeat(100), 'b'.repeat(100), 'c'.repeat(100)]
|
||||
const liquid = new Liquid({ memoryLimit: 100 })
|
||||
expect(() => liquid.parseAndRenderSync('{{ array | array_to_sentence_string }}', { array }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should charge json serialization of concat-doubled arrays', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 1e4 })
|
||||
const src = '{%- assign a = s | split: "NOSEP" -%}' +
|
||||
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
|
||||
'{{ a | json | size }}'
|
||||
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should charge inspect serialization of concat-doubled arrays', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 1e4 })
|
||||
const src = '{%- assign a = s | split: "NOSEP" -%}' +
|
||||
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
|
||||
'{{ a | inspect | size }}'
|
||||
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should charge strip_html input length to memoryLimit', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 100 })
|
||||
expect(() => liquid.parseAndRenderSync('{{ s | strip_html }}', { s: 'a'.repeat(200) }))
|
||||
|
||||
@@ -296,36 +296,4 @@ describe('tags/include', function () {
|
||||
return expect(html).toBe('FOO-')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recursion', function () {
|
||||
it('should reject self-referential {% include %} (no OOM / hang)', function () {
|
||||
mock({
|
||||
'/self.html': 'A{% include "self.html" %}B'
|
||||
})
|
||||
return expect(liquid.renderFile('/self.html')).rejects.toThrow(/include tag cannot be nested/)
|
||||
})
|
||||
it('should reject indirect {% include %} cycle (no OOM / hang)', function () {
|
||||
mock({
|
||||
'/a.html': '{% include "b.html" %}',
|
||||
'/b.html': '{% include "a.html" %}'
|
||||
})
|
||||
return expect(liquid.renderFile('/a.html')).rejects.toThrow(/include tag cannot be nested/)
|
||||
})
|
||||
it('should allow self-referential {% include %} when renderLimit is finite', function () {
|
||||
mock({
|
||||
'/self.html': 'A{% include "self.html" %}B'
|
||||
})
|
||||
const limited = new Liquid({ root: '/', renderLimit: 0.01 })
|
||||
return expect(limited.renderFile('/self.html')).rejects.toThrow(/template render limit exceeded/)
|
||||
})
|
||||
it('should allow legitimate nested {% include %} chain', async function () {
|
||||
mock({
|
||||
'/a.html': 'A{% include "b.html" %}',
|
||||
'/b.html': 'B{% include "c.html" %}',
|
||||
'/c.html': 'C'
|
||||
})
|
||||
const html = await liquid.renderFile('/a.html')
|
||||
expect(html).toBe('ABC')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -394,31 +394,4 @@ describe('tags/render', function () {
|
||||
expect(html).toBe('Xchild with redY')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recursion', function () {
|
||||
it('should reject self-referential {% render %} via in-memory templates (no OOM / hang)', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' } })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should reject indirect {% render %} cycle (no OOM / hang)', async function () {
|
||||
mock({
|
||||
'/a.html': '{% render "b.html" %}',
|
||||
'/b.html': '{% render "a.html" %}'
|
||||
})
|
||||
await expect(liquid.renderFile('/a.html')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should allow self-referential {% render %} when renderLimit is finite', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' }, renderLimit: 0.01 })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/template render limit exceeded/)
|
||||
})
|
||||
it('should allow legitimate nested {% render %} chain', async function () {
|
||||
mock({
|
||||
'/a.html': 'A{% render "b.html" %}',
|
||||
'/b.html': 'B{% render "c.html" %}',
|
||||
'/c.html': 'C'
|
||||
})
|
||||
const html = await liquid.renderFile('/a.html')
|
||||
expect(html).toBe('ABC')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module":"CommonJS",
|
||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||
"target": "ES2020",
|
||||
"module":"ES2020",
|
||||
"lib": ["ES2020", "dom"],
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
|
||||
Reference in New Issue
Block a user