Compare commits

..
Author SHA1 Message Date
Yang JunandCursor dd6bb0fb60 fix: resolve TS errors without deprecated tsconfig options
Co-authored-by: Cursor <[email protected]>
2026-06-07 01:35:03 +08:00
Yang JunandCursor 9b0b9be849 fix: restore tsconfig settings and changelog build
Re-add suppressImplicitAnyIndexErrors and downlevelIteration removed in
a75033e2c, which broke the rollup TypeScript build on CI. Drop zh-cn
changelog output now that translations were removed.

Co-authored-by: Cursor <[email protected]>
2026-06-06 23:55:02 +08:00
Yang JunandCursor be18f33e20 docs: revamp homepage and switch to custom GitHub buttons
- Make the docs English-only by removing all zh-cn content, the language switcher UI, and related JS/config

- Rework homepage feature cards (Safe & Typed, Pure JavaScript, Shopify & Jekyll, Streaming) and refresh section colors/layout

- Replace buttons.github.io with custom Star/Sponsor buttons featuring a live star count and dark-mode support

- Drop the buttons.js script and tidy banner, header, footer, and share partials

Co-authored-by: Cursor <[email protected]>
2026-06-06 23:39:06 +08:00
Yang Jun 09bfe13f65 chore: replace husky with prepush check 2026-06-06 14:21:02 +08:00
Yang Jun a75033e2ca docs: add GitHub buttons and improve option docs 2026-06-06 13:28:29 +08:00
134 changed files with 447 additions and 1940 deletions
-27
View File
@@ -838,33 +838,6 @@
"contributions": [
"code"
]
},
{
"login": "spokodev",
"name": "spokodev",
"avatar_url": "https://avatars.githubusercontent.com/u/239690017?v=4",
"profile": "https://github.com/spokodev",
"contributions": [
"code"
]
},
{
"login": "YacovGold",
"name": "YacovGold",
"avatar_url": "https://avatars.githubusercontent.com/u/8984042?v=4",
"profile": "https://github.com/YacovGold",
"contributions": [
"code"
]
},
{
"login": "sarathfrancis90",
"name": "Sarath Francis",
"avatar_url": "https://avatars.githubusercontent.com/u/9289498?v=4",
"profile": "https://github.com/sarathfrancis90",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+6
View File
@@ -0,0 +1,6 @@
---
description: Project conventions for liquidjs
alwaysApply: true
---
- Keep edits minimal: change only what the task requires, match existing style.
+17
View File
@@ -0,0 +1,17 @@
---
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 repos Jest setup).
+1 -3
View File
@@ -11,11 +11,9 @@ coverage/
node_modules/
# tmp
.local/
docs/themes/navy/source/js/liquid.browser.min.js
docs/public/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
-94
View File
@@ -1,94 +0,0 @@
# 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`)
-45
View File
@@ -1,48 +1,3 @@
# [10.29.0](https://github.com/harttle/liquidjs/compare/v10.28.0...v10.29.0) (2026-08-11)
### Features
* add unregisterFilter method ([#946](https://github.com/harttle/liquidjs/issues/946)) ([69b2c58](https://github.com/harttle/liquidjs/commit/69b2c589f9b69a34427cb8533ddb938bd997914f))
* **filters:** add squish filter ([#943](https://github.com/harttle/liquidjs/issues/943)) ([875513f](https://github.com/harttle/liquidjs/commit/875513f4c5136bed0c64562cccabb21a7db8d36c))
# [10.28.0](https://github.com/harttle/liquidjs/compare/v10.27.2...v10.28.0) (2026-08-01)
### Bug Fixes
* **date:** %s returns Unix epoch unaffected by display timezone ([#932](https://github.com/harttle/liquidjs/issues/932)) ([39c8743](https://github.com/harttle/liquidjs/commit/39c87437c5ef38ede9a208c9d55cd13231c6c023)), closes [#931](https://github.com/harttle/liquidjs/issues/931)
### Features
* Add support of inner expressions enclosed by parentheses ([#863](https://github.com/harttle/liquidjs/issues/863)) ([afa5f54](https://github.com/harttle/liquidjs/commit/afa5f5400428fc1ec935aca0282e579224660c95))
## [10.27.2](https://github.com/harttle/liquidjs/compare/v10.27.1...v10.27.2) (2026-07-09)
### Bug Fixes
* charge join/json/inspect filters by produced output size ([#925](https://github.com/harttle/liquidjs/issues/925)) ([7ab49f9](https://github.com/harttle/liquidjs/commit/7ab49f999ac045ec1e87f3a7a9fd68dd9e8602b3))
* **date:** zero-pad milliseconds when formatting %N fractional seconds ([#929](https://github.com/harttle/liquidjs/issues/929)) ([2634f9d](https://github.com/harttle/liquidjs/commit/2634f9de7b1228cd887b7cab880af8a795c77053))
* enforce ownPropertyOnly for inherited array indices ([#924](https://github.com/harttle/liquidjs/issues/924)) ([552819a](https://github.com/harttle/liquidjs/commit/552819a84b80c62306fe61072628a756272dc749))
* **filters:** modulo should follow divisor sign for negative operands ([#922](https://github.com/harttle/liquidjs/issues/922)) ([568bd5f](https://github.com/harttle/liquidjs/commit/568bd5f9cb99f596292c09fd70b00284b8216f0c))
* **filters:** return empty for out-of-range slice begin or negative length ([#928](https://github.com/harttle/liquidjs/issues/928)) ([f9a1316](https://github.com/harttle/liquidjs/commit/f9a1316d161f4f20018c833160f42dfcf0cde507))
## [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)
+42 -63
View File
@@ -1,86 +1,72 @@
# 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.
# liquidjs
[![npm version](https://img.shields.io/npm/v/liquidjs.svg?logo=npm&style=flat-square)](https://www.npmjs.org/package/liquidjs)
[![npm downloads](https://img.shields.io/npm/dm/liquidjs.svg?style=flat-square)](https://www.npmjs.org/package/liquidjs)
[![Build Status](https://img.shields.io/github/actions/workflow/status/harttle/liquidjs/ci-build.yml?branch=master&style=flat-square)](https://github.com/harttle/liquidjs/actions/workflows/ci-build.yml?query=branch%3Amaster)
[![Coverage](https://img.shields.io/coveralls/harttle/liquidjs.svg?style=flat-square)](https://coveralls.io/github/harttle/liquidjs?branch=master)
[![License: MIT](https://img.shields.io/github/license/harttle/liquidjs?style=flat-square)](https://github.com/harttle/liquidjs/blob/master/LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/harttle/liquidjs/ci-build.yml?branch=master&style=flat-square)](https://github.com/harttle/liquidjs/actions/workflows/ci-build.yml?query=branch%3Amaster)
[![DUB license](https://img.shields.io/dub/l/vibe-d.svg?style=flat-square)](https://github.com/harttle/liquidjs/blob/master/LICENSE)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/harttle/liquidjs)
[Documentation][doc] · [Playground](https://liquidjs.com/playground.html) · [Setup guide][setup] · [Contributing][contribution]
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.
<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>
* [Documentation][doc]
* Please star [LiquidJS on GitHub][github]!
* Financial support via [GitHub Sponsors](https://github.com/sponsors/harttle).
<p align="center"><sub>Try the <a href="https://liquidjs.com/playground.html">online playground</a>.</sub></p>
<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>
## Quick start
## What's it like?
```js
import { Liquid } from 'liquidjs'
Basically there're two types of Liquid syntax: tags enclosed by `{% %}` and outputs enclosed by `{{ }}`. A Liquid template looks like:
const engine = new Liquid()
const html = await engine.parseAndRender(
'Hello, {{ name | capitalize }}!',
{ name: 'liquid' }
)
//=> 'Hello, Liquid!'
```liquid
{% if username %}
{{ username | append: ", welcome to LiquidJS!" | capitalize }}
{% endif %}
```
[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
**Node.js**
Install from npm in Node.js:
```bash
npm install liquidjs
```
**Browser** (jsDelivr UMD bundle)
Or use the UMD bundle from jsDelivr:
```html
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
```
**CLI**
Or render directly from CLI using npx:
```bash
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Liquid"}'
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
```
See the [setup guide][setup] for partials, layouts, caching, and other options.
For more details, refer to the [Setup Guide][setup].
## Used by
## Who's Using LiquidJS?
<!-- 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&amp;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&amp;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&amp;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&amp;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 -->
- [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.
Products and projects running on LiquidJS. [Open a PR](https://github.com/harttle/liquidjs/edit/master/README.md) to add yours.
Feel free to create a PR or contact me to add your use case into this list!
## Financial Support
@@ -92,7 +78,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://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://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://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/>
@@ -240,11 +226,6 @@ 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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YacovGold"><img src="https://avatars.githubusercontent.com/u/8984042?v=4?s=100" width="100px;" alt="YacovGold"/><br /><sub><b>YacovGold</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=YacovGold" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarathfrancis90"><img src="https://avatars.githubusercontent.com/u/9289498?v=4?s=100" width="100px;" alt="Sarath Francis"/><br /><sub><b>Sarath Francis</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=sarathfrancis90" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -253,10 +234,8 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
<!-- ALL-CONTRIBUTORS-LIST:END -->
## License
[MIT](LICENSE) © [Jun Yang](https://github.com/harttle)
[shopify/liquid]: https://shopify.github.io/liquid/
[plugins]: https://liquidjs.com/tutorials/plugins.html#Plugin-List
[setup]: https://liquidjs.com/tutorials/setup.html
[doc]: https://liquidjs.com
[github]: https://github.com/harttle/liquidjs
+3 -5
View File
@@ -6,10 +6,8 @@ Only the latest major version is supported with security updates. It can be chan
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Please contact harttleharttle@gmail.com to report a vulnerability or change request.
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).
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within 1 week).
- 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 anyway (most likely there will be a discussion).
- If the request is declined, you'll receive a reply email anyway (most likely there will be a discussion).
-2
View File
@@ -33,9 +33,7 @@ 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
View File
@@ -1,6 +1,6 @@
title: LiquidJS
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."
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."
author: Harttle
language: en
timezone: UTC
-2
View File
@@ -2,8 +2,6 @@
'use strict'
require('./prism-bash-extend')
const { resolve, basename } = require('path')
const { readFileSync } = require('fs')
const cheerio = require('cheerio')
-34
View File
@@ -1,34 +0,0 @@
'use strict'
/**
* Extend Prism's bash grammar with extra CLI commands for docs code blocks.
* Hexo loads scripts from docs/scripts/ during init, before `hexo generate`
* highlights fenced code via syntax_highlighter: prismjs.
*
* Bash highlights known commands via a large hard-coded regex (see prism-bash).
* insertBefore is the supported extension point when a command is not in that list.
* Add names to EXTRA_BASH_COMMANDS as needed.
*
* After editing this file, run `npx hexo clean` before generate/serve so
* Hexo re-highlights cached pages (db.json does not invalidate on script changes).
*/
const EXTRA_BASH_COMMANDS = [
'npx'
]
const Prism = require('prismjs')
require('prismjs/components/prism-bash')
const escaped = EXTRA_BASH_COMMANDS.map((cmd) =>
cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
)
Prism.languages.insertBefore('bash', 'function', {
'cli-command': {
pattern: new RegExp(
`(^|[\\s;|&]|[<>]\\()(?:${escaped.join('|')})(?=$|[)\\s;|&])`
),
lookbehind: true,
alias: ['builtin', 'class-name']
}
})
+1 -2
View File
@@ -94,7 +94,6 @@ filters:
sort: sort.html
sort_natural: sort_natural.html
split: split.html
squish: squish.html
strip: strip.html
strip_html: strip_html.html
strip_newlines: strip_newlines.html
@@ -115,7 +114,7 @@ filters:
tags:
overview: overview.html
"# (inline comment)": inline_comment.html
"#": inline_comment.html
assign: assign.html
capture: capture.html
case: case.html
+6 -7
View File
@@ -3,18 +3,18 @@ title: date
---
{% since %}v1.9.1{% endsince %}
The `date` filter is used to convert a timestamp into the specified format.
Date filter is used to convert a timestamp into the specified format.
* LiquidJS tries to conform to Shopify/Liquid, which uses Ruby's core [Time#strftime(string)](https://www.ruby-doc.org/core/Time.html#method-i-strftime). There are differences with [Ruby's format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html):
* LiquidJS tries to conform to Shopify/Liquid, which uses Ruby's core [Time#strftime(string)](https://www.ruby-doc.org/core/Time.html#method-i-strftime). There're differences with [Ruby's format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html):
* `%Z` (since v10.11.1) is replaced by the passed-in timezone name from `LiquidOption` or in-place value (see TimeZone below). If passed-in timezone is an offset number instead of string, it'll behave like `%z`. If there's none passed-in timezone, it returns [the runtime's default time zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone).
* LiquidJS provides an additional `%q` flag for date ordinals. e.g. `{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
* Date literals are first converted to a `Date` object via [new Date()][jsDate], which means literal values are considered in the runtime's time zone by default.
* Date literals are firstly converted to `Date` object via [new Date()][jsDate], that means literal values are considered in runtime's time zone by default.
* The format filter argument is optional:
* If not provided, it defaults to `%A, %B %-e, %Y at %-l:%M %P %z`.
* The above default can be overridden by [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) LiquidJS option.
* LiquidJS `date` supports locale specific weekdays and month names, which will fallback to English where `Intl` is not supported.
* Ordinals (`%q`) and Jekyll specific date filters are English-only.
* [`locale`](/api/interfaces/LiquidOptions.html#locale) can be set when creating a Liquid instance. Defaults to `Intl.DateTimeFormat().resolvedOptions().locale`.
* [`locale`](/api/interfaces/LiquidOptions.html#locale) can be set when creating Liquid instance. Defaults to `Intl.DateTimeFormat().resolvedOptions.locale`).
### Examples
```liquid
@@ -26,15 +26,14 @@ The `date` filter is used to convert a timestamp into the specified format.
```
# TimeZone
* During output, LiquidJS uses the local timezone, which can be overridden by:
* During output, LiquidJS uses local timezone which can override by:
* setting a timezone in-place when calling `date` filter, or
* setting the [`timezoneOffset`](/api/interfaces/LiquidOptions.html#timezoneOffset) LiquidJS option
* It defaults to the runtime's timezone.
* It defaults to runtime's time one.
* Offset can be set as,
* minutes: `-360` means `'+06:00'` and `360` means `'-06:00'`
* timeZone ID: `Asia/Colombo` or `America/New_York`
* See [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for TZ database values
* `%s` (seconds since the Unix epoch) identifies an instant rather than a wall-clock time, so it's not affected by the display timezone.
### Examples
```liquid
+1 -1
View File
@@ -4,7 +4,7 @@ title: json
{% since %}v9.10.0{% endsince %}
Convert values to string via `JSON.stringify()`, for debugging purposes.
Convert values to string via `JSON.stringify()`, for debug purpose.
Input
```liquid
+9 -9
View File
@@ -5,17 +5,17 @@ description: Description and demo for each Liquid filter
LiquidJS implements business-logic independent filters that are typically implemented in [shopify/liquid][shopify/liquid]. This section contains the specification and demos for all the filters implemented by LiquidJS.
There are 40+ filters supported by LiquidJS. These filters can be categorized into these groups:
There's 40+ filters supported by LiquidJS. These filters can be categorized into these groups:
Categories | Filters
--- | ---
Math | `plus`, `minus`, `modulo`, `times`, `floor`, `ceil`, `round`, `divided_by`, `abs`, `at_least`, `at_most`
String | `append`, `prepend`, `capitalize`, `upcase`, `downcase`, `strip`, `lstrip`, `rstrip`, `strip_newlines`, `split`, `replace`, `replace_first`, `replace_last`,`remove`, `remove_first`, `remove_last`, `truncate`, `truncatewords`, `normalize_whitespace`, `number_of_words`, `array_to_sentence_string`
HTML/URI | `escape`, `escape_once`, `url_encode`, `url_decode`, `strip_html`, `newline_to_br`, `xml_escape`, `cgi_escape`, `uri_escape`, `slugify`
Array | `slice`, `map`, `sort`, `sort_natural`, `uniq`, `where`, `where_exp`, `group_by`, `group_by_exp`, `find`, `find_exp`, `first`, `last`, `join`, `reverse`, `concat`, `compact`, `size`, `push`, `pop`, `shift`, `unshift`
Date | `date`, `date_to_xmlschema`, `date_to_rfc822`, `date_to_string`, `date_to_long_string`
Misc | `default`, `json`, `jsonify`, `inspect`, `raw`, `to_integer`
Base64 | `base64_encode`, `base64_decode`
Crypto | `sha256`, `hmac_sha256`
Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most
String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last,remove, remove_first, remove_last, truncate, truncatewords, normalize_whitespace, number_of_words, array_to_sentence_string
HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape, slugify
Array | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift
Date | date, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
Misc | default, json, jsonify, inspect, raw, to_integer
Base64 | base64_encode, base64_decode
Crypto | sha256, hmac_sha256
[shopify/liquid]: https://github.com/Shopify/liquid
-18
View File
@@ -1,18 +0,0 @@
---
title: squish
---
{% since %}v10.28.0{% endsince %}
Removes leading and trailing whitespace from a string, and replaces every run of whitespace inside it with a single space.
Input
```liquid
{{ " Hello there,
Major Tom. " | squish }}
```
Output
```text
Hello there, Major Tom.
```
-8
View File
@@ -6,10 +6,6 @@ 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 }}
@@ -19,7 +15,3 @@ Output
```text
Have you read Ulysses?
```
[escape]: ./escape.html
[escape_once]: ./escape.html
[outputEscape]: ../tutorials/options.html#outputEscape
+1 -1
View File
@@ -36,7 +36,7 @@ Ground control, and so on
## No ellipsis
You can `truncate` to the exact number of characters specified by the first argument and avoid showing trailing characters by passing a blank string as the second argument:
You can truncate to the exact number of characters specified by the first argument and avoid showing trailing characters by passing a blank string as the second argument:
Input
```liquid
+2
View File
@@ -1,4 +1,6 @@
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
-28
View File
@@ -1,28 +0,0 @@
# 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 -1
View File
@@ -1,7 +1,7 @@
{
"short_name": "LiquidJS",
"name": "LiquidJS",
"description": "A simple, expressive, extensible Liquid template engine for JavaScript",
"description": "A simple, expressive and safe Shopify / GitHub Pages compatible template engine in pure JavaScript.",
"icons": [
{
"src": "/icon/apple-touch-icon-57x57.png",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
---
title: assign
title: Assign
---
{% since %}v1.9.1{% endsince %}
+1 -1
View File
@@ -4,7 +4,7 @@ title: case
{% since %}v1.9.1{% endsince %}
Creates a switch statement to compare a variable with different values. `case` initializes the switch statement, and `when` tags compare values.
Creates a switch statement to compare a variable with different values. `case` initializes the switch statement, and `when` compares its values.
Input
```liquid
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: comment
title: Comment
---
{% since %}v1.9.1{% endsince %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: decrement
title: Decrement
---
{% since %}v1.9.1{% endsince %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: echo
title: Echo
---
{% since %}v9.31.0{% endsince %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: for
title: For
---
{% since %}v1.9.1{% endsince %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: if
title: If
---
{% since %}v1.9.1{% endsince %}
+7 -7
View File
@@ -1,5 +1,5 @@
---
title: include
title: Include
---
{% since %}v1.9.1{% endsince %}
@@ -22,11 +22,11 @@ If [extname][extname] option is set, the above `.liquid` extension becomes optio
{% include 'footer' %}
```
When a partial template is rendered by `include`, the code inside it can access its parent's variables but its parent cannot access variables defined inside an included template.
When a partial template is rendered by `include`, the code inside it can access its parent's variables but its parent cannot access variables defined inside a included template.
## Passing Variables
Variables defined in the parent's scope can be passed to the partial template by listing them as parameters on the `include` tag:
Variables defined in parent's scope can be passed to a the partial template by listing them as parameters on the `include` tag:
```liquid
{% assign my_variable = 'apples' %}
@@ -70,11 +70,11 @@ This way, you don't need to escape `"` in the filename expression.
{% include prefix/{{name | append: ".html"}} %}
```
## Jekyll `include`
## Jekyll include
{% since %}v9.33.0{% endsince %}
[jekyllInclude][jekyllInclude] is used to enable Jekyll-like `include` syntax. Defaults to `false`, when set to `true`:
[jekyllInclude][jekyllInclude] is used to enable Jekyll-like include syntax. Defaults to `false`, when set to `true`:
- Filename will be static: `dynamicPartials` now defaults to `false` (instead of `true`). And you can set `dynamicPartials` back to `true`.
- Use `=` instead of `:` to separate parameter key-values.
@@ -86,7 +86,7 @@ For example, the following template:
{% include article.html header="HEADER" content="CONTENT" %}
```
`article.html` with the following content:
`article.html` with following content:
```liquid
<article>
@@ -95,7 +95,7 @@ For example, the following template:
</article>
```
Note that we're referencing the first parameter by `include.header` instead of `header`. It will output the following:
Note that we're referencing the first parameter by `include.header` instead of `header`. Will output following:
```html
<article>
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: increment
title: Increment
---
{% since %}v1.9.1{% endsince %}
+5 -5
View File
@@ -1,5 +1,5 @@
---
title: layout
title: Layout
---
{% since %}v1.9.1{% endsince %}
@@ -31,12 +31,12 @@ If [extname][extname] option is set, the `.liquid` extension becomes optional:
```
{% note info Scoping %}
When a partial template is rendered by the `layout` tag, its template has access to its caller's variables but not vice versa. Variables defined in `layout` will be popped out before control returns to its caller.
When a partial template is rendered by <code>layout</code>, its template have access for its caller's variables but not vice versa. Variables defined in layout will be popped out before control returning to its caller.
{% endnote %}
## Multiple Blocks
The `layout` file can contain multiple blocks, each with a specified name. The following snippets yield same result as in the above example.
The layout file can contain multiple blocks, each with a specified name. The following snippets yield same result as in the above example.
```liquid
// default-layout.liquid
@@ -53,7 +53,7 @@ The `layout` file can contain multiple blocks, each with a specified name. The f
## Default Block Contents
In the above `layout` files, blocks have empty contents. They do not necessarily need to be empty; in that case, the block contents in `layout` files will be used as default templates. The following snippets are also equivalent to the above examples:
In the above layout files, blocks has empty contents. But it's not necessarily be empty, in which case, the block contents in layout files will be used as default templates. The following snippets are also equivalent to the above examples:
```liquid
// default-layout.liquid
@@ -68,7 +68,7 @@ In the above `layout` files, blocks have empty contents. They do not necessarily
## Passing Variables
Variables defined in the current template can be passed to the `layout` template by listing them as parameters on the `layout` tag:
Variables defined in current template can be passed to a the layout template by listing them as parameters on the `layout` tag:
```liquid
{% assign my_variable = 'apples' %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: liquid
title: Liquid
---
{% since %}v9.31.0{% endsince %}
+6 -6
View File
@@ -5,14 +5,14 @@ description: Description and demo for each Liquid tag
LiquidJS implements business-logic independent tags that are typically implemented in [shopify/liquid][shopify/liquid]. This section contains the specification and demos for all the tags implemented by LiquidJS.
There are a dozen tags supported by LiquidJS, including all tags in [shopify/liquid][shopify/liquid]. These tags can be categorized into these groups:
There're a dozen of tags supported by LiquidJS, with all tags in [shopify/liquid][shopify/liquid]. These tags can be categorized into these groups:
Category | Purpose | Tags
--- | --- | ---
Iteration | iterate over a collection | `for`, `cycle`, `tablerow`
Control Flow | control the execution branch of template rendering | `if`, `unless`, `elsif`, `else`, `case`, `when`
Variable | define and alter variables | `assign`, `increment`, `decrement`, `capture`, `echo`
File | include another template or extend a layout template | `render`, `include`, `layout`
Language | temporarily disable LiquidJS syntax | `#`, `raw`, `comment`, `liquid`
Iteration | iterate over a collection | for, cycle, tablerow
Control Flow | control the execution branch of template rendering | if, unless, elsif, else, case, when
Variable | define and alter variables | assign, increment, decrement, capture, echo
File | include another template or extend a layout template | render, include, layout
Language | temporarily disable LiquidJS syntax | # (inline comment), raw, comment, liquid
[shopify/liquid]: https://github.com/Shopify/liquid
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: raw
title: Raw
---
{% since %}v1.9.1{% endsince %}
+2 -2
View File
@@ -1,5 +1,5 @@
---
title: render
title: Render
---
{% since %}v9.2.0{% endsince %}
@@ -32,7 +32,7 @@ When a partial template is rendered, the code inside it can't access its parent'
## Passing Variables
Variables defined in the parent's scope can be passed to the partial template by listing them as parameters on the `render` tag:
Variables defined in parent's scope can be passed to a the partial template by listing them as parameters on the render tag:
```liquid
{% assign my_variable = 'apples' %}
+3 -3
View File
@@ -1,5 +1,5 @@
---
title: tablerow
title: Table Row
---
{% since %}v1.9.1{% endsince %}
@@ -88,7 +88,7 @@ Output
### limit
Exits the `tablerow` after a specific index.
Exits the tablerow after a specific index.
```liquid
{% tablerow product in collection.products cols:2 limit:3 %}
@@ -98,7 +98,7 @@ Exits the `tablerow` after a specific index.
### offset
Starts the `tablerow` after a specific index.
Starts the tablerow after a specific index.
```liquid
{% tablerow product in collection.products cols:2 offset:3 %}
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: unless
title: Unless
---
{% since %}v1.9.1{% endsince %}
@@ -2,7 +2,7 @@
title: Access Scope in Filters
---
As covered in [Register Filters/Tags][register-filters], we can access filter arguments directly in a filter function like:
As covered in [Register Filters/Tags][register-filters], we can access filter arguments directly in filter function like:
```javascript
// Usage: {{ 1 | add: 2, 3 }}
@@ -10,7 +10,7 @@ As covered in [Register Filters/Tags][register-filters], we can access filter ar
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
```
When it comes to stateful filters, for example transforming a URL path to a full URL, we'll need to access an `origin` in the current scope:
When it comes to stateful filters, for example transform a URL path to full URL, we'll need to access a `origin` in current scope:
```javascript
// Usage: {{ '/index.html' | fullURL }}
+2 -2
View File
@@ -2,13 +2,13 @@
title: Caching
---
In a typical website project, we'll have a directory of view templates and they'll be rendered multiple times. In a production environment the template files are not likely to change over time (other than re-deployments). Thus it makes sense to cache the file contents and the parsed templates (in a kind of AST) to improve performance.
In a typical website project, we'll have a directory of view templates and they'll be rendered multiple times. In production environment the template files are not likely to be changed over time (other than re-deployments). Thus it makes sense to cache the file contents and the parsed templates (in a kind of AST) to improve performance.
LiquidJS provides multiple ways to cache the parsed templates to improve performance.
## Programmatically
The [.parse()][parse], [.parseFile()][parseFile], [.parseFileSync()][parseFileSync] APIs are used to parse templates from strings or files. The resulting template can then be rendered multiple times with different context.
The [.parse()][parse], [.parseFile()][parseFile], [.parseFileSync()][parseFileSync] APIs are used to parse templates from string or files. The result template can be then rendered multiple times with different context.
Parse from string:
@@ -16,11 +16,11 @@ Getting started and building is described in [CONTRIBUTING.md](https://github.co
**Commit Message**: Please align to [the Angular Commit Message Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits), especially note the [type identifier](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type), on which semantic-release bot depends.
**Backward-Compatibility**: please be backward-compatible. LiquidJS is used by multiple layers of software, including underlying libraries, compilers, site generators and Web servers. It's not easy to do a major upgrade for most of them.
**Backward-Compatibility**: please be backward-compatible. LiquidJS is used by multiple layers of softwares, including underlying libraries, compilers, site generators and Web servers. It's not easy to do a major upgrade for most of them.
## Financial Support
LiquidJS is Open Source and Free. To help it live and thrive, especially when LiquidJS is benefiting your business, consider contributing on [GitHub Sponsors](https://github.com/sponsors/harttle) or [Open Collective][oc].
LiquidJS is Open Source and Free. To help it live and thrive, especially when LiquidJS is benefiting your business, please consider contribute on [GitHub Sponsors](https://github.com/sponsors/harttle) or [Open Collective][oc].
I'll add all financial contributors into [README.md](https://github.com/harttle/liquidjs#financial-support) and it'll be also shown on https://liquidjs.com after next GitHub Actions build.
+3 -3
View File
@@ -4,7 +4,7 @@ title: Differences with Shopify/liquid
## Compatibility
Being compatible with the Ruby version is one of our priorities. Liquid language is originally [implemented in Ruby][ruby-liquid] and used by Shopify and Jekyll (and thus GitHub Pages). As you can see it's one of the most popular template engines in Ruby. There are lots of people using LiquidJS to serve their templates originally written for Shopify themes and Jekyll sites.
Being compatible with the Ruby version is one of our priorities. Liquid language is originally [implemented in Ruby][ruby-liquid] and used by Shopify and Jekyll (and thus GitHub Pages). As you can see it's one of the most popular template engines in Ruby. There're lots of people using LiquidJS to serve their templates originally written for Shopify themes and Jekyll sites.
So "being compatible" means serving developers from Shopify and Jekyll well:
@@ -13,8 +13,8 @@ So "being compatible" means serving developers from Shopify and Jekyll well:
In the meantime, it's now implemented in JavaScript, that means it has to be more powerful:
* **Async as a first-class citizen**. Filters and tags can be implemented asynchronously by returning a `Promise`.
* **Can also be synchronous**. For scenarios that are not I/O intensive, rendering synchronously can be much faster. You can call synchronous APIs like `.renderSync()` as long as all the filters and tags in the template can be rendered synchronously. All built-in filters/tags support both sync and async render.
* **Async as first-class citizen**. Filters and tags can be implemented asynchronously by return a `Promise`.
* **Also can be sync**. For scenarios that are not I/O intensive, render synchronously can be much faster. You can call synchronous APIs like `.renderSync()` as long as all the filters and tags in template support to be rendered synchronously. All builtin filters/tags support both sync and async render.
* **[Abstract file system][afs]**. Along with async feature, LiquidJS can be used to serve templates stored in Databases [#414][#414], on remote HTTP server [#485][#485], and so on.
* **Additional tags and filters** like `layout` and `json`, `inspect`, `where_exp`, `group_by`, etc., see below for details.
+1 -1
View File
@@ -95,7 +95,7 @@ engine.parseAndRender("{{color}}", context).then(html => console.log(html))
## toLiquid
`toLiquid()` is not a method of `Drop`, but it can be used to return a `Drop`. In cases where you have a fixed structure in the `context` that cannot change its values, you can implement `toLiquid()` to let LiquidJS use the returned value instead of the object itself when rendering templates.
`toLiquid()` is not a method of `Drop`, but it can be used to return a `Drop`. In cases where you have a fixed structure in the `context` that cannot change its values, you can implement `toLiquid()` to let LiquidJS use the returned value instead of itself to render the templates.
```javascript
import { Liquid, Drop } from 'liquidjs'
+3 -3
View File
@@ -2,10 +2,10 @@
title: Escaping
---
Escaping is important in all languages, including LiquidJS. Escaping has two different meanings for a template engine:
Escaping is important in all languages, including LiquidJS. While escaping has 2 different meanings for a template engine:
1. Escaping for the output, i.e. HTML escape. Used to escape HTML special characters so the output will not break HTML structures, aka HTML safe.
2. Escaping for the language itself, i.e. Liquid escape. Used to output strings that are considered special in the Liquid language. This is useful when you're writing an article in a Liquid template to introduce the Liquid language.
2. Escaping for the language itself, i.e. Liquid escape. Used to output strings that's considered special in Liquid language. This will be useful when you're writing an article in Liquid template to introduce Liquid language.
## HTML Escape
@@ -55,7 +55,7 @@ In LiquidJS, {{ this | escape }} will be HTML-escaped, but
{{{ that }}} will not.
```
Within string literals in a LiquidJS template, `\` can be used to escape special characters in string syntax. For example:
Within strings literals in LiquidJS template, `\` can be used to escape special characters in string syntax. For example:
Input
```liquid
+5 -4
View File
@@ -3,9 +3,9 @@ title: The Liquid Template Language
describe: A short introduction to the Liquid template language and some simple demos.
---
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 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].
There are 2 types of markups in LiquidJS:
LiquidJS syntax is relatively simple. There're 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 `}}`.
@@ -38,7 +38,7 @@ A complete list of filters supported by LiquidJS can be found [here](../filters/
## Tags
**Tags** are used to control the template rendering process, manipulating template variables, interacting with other templates, etc. For example `assign` can be used to define a variable that can be later used in the template:
**Tags** are used to control the template rendering process, manipulating template variables, inter-op with other templates, etc. For example `assign` can be used to define a variable which can be later used in the template:
```liquid
{% assign foo = "FOO" %}
@@ -50,10 +50,11 @@ Typically tags appear in pairs with a start tag and a corresponding end tag. For
{% if foo == "FOO" %}
Variable `foo` equals "FOO"
{% else %}
Variable `foo` does not equal "FOO"
Variable `foo` not equals "FOO"
{% endif %}
```
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
+3 -3
View File
@@ -2,7 +2,7 @@
title: Migrate to LiquidJS 9
---
LiquidJS 9 has some fundamental improvements, including bugfixes, new features and performance improvements due to a higher target (see #137). There are also some breaking changes.
LiquidJS 9 has some fundamental improvements, including bugfixes, new features and performance improvement due to higher target(see #137). There're also some breaking changes.
## Features
@@ -14,11 +14,11 @@ LiquidJS 9 has some fundamental improvements, including bugfixes, new features a
* Rewrite boolean expression evaluation order, [#130](https://github.com/harttle/liquidjs/issues/130);
* `break` and `continue` tags omitting output before them, [#123](https://github.com/harttle/liquidjs/issues/123);
* Fixes errors in React.js demo during yarn install, [#145](https://github.com/harttle/liquidjs/issues/145);
* Promise typed Drops are not always awaited.
* Promise typed Drops are not await-ed some times.
## Performance
* Performance Improvements due to targeting Node.js 8, see [#137](https://github.com/harttle/liquidjs/issues/137);
* Performance Improvements due to targeting to Node.js 8, see [#137](https://github.com/harttle/liquidjs/issues/137);
* Memory footprint is reduced by 57.5%, see [#202](https://github.com/harttle/liquidjs/pull/202);
* Render performance is improved by 100.3%, see [#205](https://github.com/harttle/liquidjs/pull/205).
+6 -6
View File
@@ -2,14 +2,14 @@
title: Operators
---
LiquidJS operators are very simple and different. There are 2 types of operators supported:
LiquidJS operators are very simple and different. There're 2 types of operators supported:
* Comparison operators: `==`, `!=`, `>`, `<`, `>=`, `<=`
* Logical operators: `not`, `or`, `and`, `contains`
* Logic operators: `not`, `or`, `and`, `contains`
Thus arithmetic operators are not supported and you cannot add two numbers like this `{% raw %}{{a + b}}{% endraw %}`. Instead, use a filter: `{% raw %}{{ a | plus: b}}{% endraw %}`. Actually `+` is a valid variable name in LiquidJS.
Thus numerical operators are not supported and you cannot even plus two numbers like this `{% raw %}{{a + b}}{% endraw %}`, instead we need a filter `{% raw %}{{ a | plus: b}}{% endraw %}`. Actually `+` is a valid variable name in LiquidJS.
## Logical Operators
## Logic Operators
### not
@@ -59,10 +59,10 @@ Input
1. Comparison operators, and `contains`. All comparison operators alongside `contains` have the same (highest) precedence.
2. `not` operator. It has slightly more precedence than `or` and `and`.
3. `or` and `and` operators. These logical operators have the same (lowest) precedence.
3. `or` and `and` operators. These logic operators have the same (lowest) precedence.
## Associativity
Logical operators are evaluated from right to left, see [shopify docs][operator-order].
Logic operators are evaluated from right to left, see [shopify docs][operator-order].
[operator-order]: https://shopify.dev/docs/api/liquid/basics#order-of-operations
+14 -14
View File
@@ -11,27 +11,27 @@ const engine = new Liquid({
})
```
{% note info API documentation %}
Following is an overview for all the options. For exact types and signatures, see <a href="https://liquidjs.com/api/interfaces/LiquidOptions.html" target="_self">LiquidOptions | API</a>.
{% note info API Document %}
Following is an overview for all the options, for exact types and signatures please refer to <a href="https://liquidjs.com/api/interfaces/LiquidOptions.html" target="_self">LiquidOptions | API</a>.
{% endnote %}
## cache
**cache** is used to improve performance by caching previously parsed template structures, especially in cases when we repeatedly parse or render files.
**cache** is used to improve performance by caching previously parsed template structures, specially in cases when we're repeatedly parse or render files.
It defaults to `false`. When set to `true`, a default LRU cache of size 1024 will be enabled. It can also be a number indicating the cache size you want.
It's default to `false`. When setting to `true` a default LRU cache of size 1024 will be enabled. And certainly it can be a number which indicates the size of cache you want.
Additionally, it can also be a custom cache implementation. See [Caching][caching] for details.
## Partials/Layouts
**root** is used to specify template directories for LiquidJS to look up and read template files. Can be a single string or an array of strings. See [Render Files][render-file] for details.
**root** is used to specify template directories for LiquidJS to lookup and read template files. Can be a single string and an array of strings. See [Render Files][render-file] for details.
**layouts** is used to specify template directories for LiquidJS to look up files for `{% layout %}`. Same format as `root` and will default to `root` if not specified.
**layouts** is used to specify template directories for LiquidJS to lookup files for `{% layout %}`. Same format as `root` and will default to `root` if not specified.
**partials** is used to specify template directories for LiquidJS to look up files for `{% render %}` and `{% include %}`. Same format as `root` and will default to `root` if not specified.
**partials** is used to specify template directories for LiquidJS to lookup files for `{% render %}` and `{% include %}`. Same format as `root` and will default to `root` if not specified.
**relativeReference** is set to `true` by default to allow relative filenames. Note that relatively referenced files also need to be within the corresponding root. For example you can reference another file like `{% render ../foo/bar %}` as long as `../foo/bar` is also within `partials` directory.
**relativeReference** is set to `true` by default to allow relative filenames. Note that relatively referenced files are also need to be within corresponding root. For example you can reference another file like `{% render ../foo/bar %}` as long as `../foo/bar` is also within `partials` directory.
## dynamicPartials
@@ -62,7 +62,7 @@ LiquidJS defaults this option to <code>true</code> to be compatible with shopify
- Use `=` instead of `:` to separate parameter key-values.
- Parameters are under `include` variable instead of current scope.
For example in the following template, `name.html` is not quoted, `header` and `"HEADER"` are separated by `=`, and the `header` parameter is referenced by `include.header`. For more details, see [include][include].
For example in the following template, `name.html` is not quoted, `header` and `"HEADER"` are separated by `=`, and the `header` parameter is referenced by `include.header`. More details please check out [include][include].
```liquid
// entry template
@@ -90,7 +90,7 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
## fs
**fs** is used to define a custom file system implementation which will be used by LiquidJS to look up and read template files. See [Abstract File System][abstract-fs] for details.
**fs** is used to define a custom file system implementation which will be used by LiquidJS to lookup and read template files. See [Abstract File System][abstract-fs] for details.
## globals
@@ -98,9 +98,9 @@ Before 2.0.1, <code>extname</code> is set to `.liquid` by default. To change tha
## jsTruthy
**jsTruthy** is used to use standard JavaScript truthiness rather than Shopify's.
**jsTruthy** is used to use standard JavaScript truthiness rather than the Shopify.
It defaults to `false`. For example, when set to `true`, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.
it defaults to false. For example, when set to true, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.
## outputEscape
@@ -108,13 +108,13 @@ It defaults to `false`. For example, when set to `true`, a blank string would ev
- For untrusted output variables, set `outputEscape: "escape"` makes them be HTML escaped by default. You'll need [raw][raw] filter for direct output.
- `"json"` is useful when you're using LiquidJS to create valid JSON files.
- It can even be a function that allows you to control what variables are output throughout LiquidJS. Please note the input can be any type other than string, e.g. a filter may return a non-string value.
- It can even be a function which allows you to control what variables are output throughout LiquidJS. Please note the input can be any type other than string, e.g. an filter returned an non-string value.
## Date
**timezoneOffset** is used to specify a different timezone to output dates, your local timezone will be used if not specified. For example, set `timezoneOffset: 0` to output all dates in UTC/GMT 00:00.
**preserveTimezones** is a boolean that affects only literal timestamps. When set to `true`, all literal timestamps will remain the same when output. This is a parser option, so Date objects passed to LiquidJS as data will not be affected. Note that `preserveTimezones` has a higher priority than `timezoneOffset`.
**preserveTimezones** is a boolean effects only literal timestamps. When set to `true`, all literal timestamps will remain the same when output. This is a parser option, so Date objects passed to LiquidJS as data will not be affected. Note that `preserveTimezones` has a higher priority than `timezoneOffset`.
**dateFormat** is used to specify a default format to output dates. `%A, %B %-e, %Y at %-l:%M %P %z` will be used if not specified. For example, set `dateFormat: %Y-%m-%dT%H:%M:%S:%LZ` to output all dates in [JavaScript Date.toJson()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON) format.
+2 -2
View File
@@ -4,7 +4,7 @@ title: Parse Parameters
## Access Raw Parameters
As covered in [Register Filters/Tags][register-tags], tag parameters are available on `tagToken.args` as a raw string. For example:
As covered in [Register Filters/Tags][register-tags], tag parameters is available on `tagToken.args` as a raw string. For example:
```javascript
// Usage: {% random foo bar coo %}
@@ -66,7 +66,7 @@ Async calls in LiquidJS are implemented by generators directly, for we can call
## Parse Key-Value Pairs as Named Parameters
Named parameters become very handy when there are optional parameters or lots of parameters, in which case the order of parameters is not important. This is exactly what the [Hash][Hash] class was invented for.
Named parameters become very handy when there're optional parameters or lots of parameters, in which case the order of parameters is not important. This is exactly what [Hash][Hash] class is invented for.
```liquid
{% random from:2, to:max %}
@@ -25,7 +25,7 @@ color: 'red' shape: 'circle'
color: 'yellow' shape: 'square'
```
For more details, see the [render](../tags/render.html) tag.
More details please refer to the [render](../tags/render.html) tag.
{% note tip The &quot;.liquid&quot; Extension %}
The ".liquid" extension in <code>layout</code>, <code>render</code> and <code>include</code> can be omitted if Liquid instance is created using `extname: ".liquid"` option. See <a href="./options.html#extname">the extname option</a> for details.
@@ -54,4 +54,4 @@ My page content
Footer
```
For more details, see the [layout](../tags/layout.html) tag.
More details please refer to the [layout](../tags/layout.html) tag.
+2 -2
View File
@@ -6,9 +6,9 @@ A number of tags and filters can be encapsulated into a **plugin**, which will b
## Write a Plugin
A LiquidJS plugin is a simple function that takes the [Liquid class][liquid] as the first parameter and uses the Liquid instance for `this`. We can call LiquidJS APIs on `this` to make certain changes, especially [register filters and tags][register].
A liquidjs plugin is simple function which takes the [Liquid class][liquid] as the first parameter and the Liquid instance for `this`. We can call liquidjs APIs on `this` to make certain changes, especially [register filters and tags][register].
Now we'll make a plugin to uppercase every letter of the input. Save the following snippet to `upup.js`:
Now we'll make a plugin to upper case every letter of the input, save the following snippet to `upup.js`:
```javascript
/**
+1 -19
View File
@@ -62,23 +62,7 @@ See existing filter implementations here: <https://github.com/harttle/liquidjs/t
## Unregister Tags/Filters
Filters can be unregistered by name:
```javascript
engine.unregisterFilter('plus')
```
With [`strictFilters`][strict-filters] enabled, using an unregistered filter will throw an error. Otherwise, the filter will be skipped.
Built-in filters can be registered again using the exported `filters` object:
```javascript
import { filters } from 'liquidjs'
engine.registerFilter('plus', filters.plus)
```
To disable a tag, or to make a disabled filter throw regardless of `strictFilters`, register a dummy implementation that throws a corresponding error (see [#324](https://github.com/harttle/liquidjs/issues/324)):
In some cases it's desirable to disable some tags/filters (see [#324](https://github.com/harttle/liquidjs/issues/324)), you'll need to register a dummy tag/filter in which an corresponding Error throws.
```javascript
// disable a tag
@@ -97,5 +81,3 @@ function disabledFilter(name) {
}
engine.registerFilter('plus', disabledFilter('plus'));
```
[strict-filters]: /tutorials/options.html#strict
+2 -2
View File
@@ -38,7 +38,7 @@ name: alice
## Template Lookup
Template file names passed to [renderFile][renderFile], [parseFile][parseFile], [renderFileSync][renderFileSync], [parseFileSync][parseFileSync] APIs,
Template files names passed to [renderFile][renderFile], [parseFile][parseFile], [renderFileSync][renderFileSync], [parseFileSync][parseFileSync] APIs,
and [include][include], [layout][layout] tags are resolved against [the root option][root].
It can be a string-typed path (see above example), or a list of root directories, in which case templates will be looked up in that order. e.g.
@@ -96,7 +96,7 @@ var engine = new Liquid({
## In-memory Template
To facilitate rendering without files, there's a `templates` option to specify a mapping of filenames and their content. LiquidJS will read templates from the mapping.
To facilitate rendering w/o files, there's a `templates` option to specify a mapping of filenames and their content. LiquidJS will read templates from the mapping.
```typescript
const engine = new Liquid({
+6 -6
View File
@@ -2,7 +2,7 @@
title: Render Tag Content
---
Custom tags can have content templates and can be nested. This article describes how to implement custom tags that consist of a *begin tag*, an *end tag*, and template content between them.
Custom tags can have content template and can be nested. This article describes how to implement custom tags that consists of a *begin tag*, an *end tag*, and template content between them.
## Render Tag Content
@@ -22,12 +22,12 @@ Expected output:
</div>
```
Firstly, [register][register-tags] a tag named `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`:
Firstly, [register][register-tags] a tag with name `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`,
- `tagToken` is current token `{%raw%}{% wrap %}{%endraw%}`, and
- `remainTokens` is an array of all tokens following `{%raw%}{% wrap %}{%endraw%}` until the end of this template file.
Basically, what we need to do is take/`.shift()` enough tags from `remainTokens` until we get an `endwrap` token (the name can be arbitrary, but by convention it should be `endwrap`). And if there's no `endwrap` until the end of the template file, we need to throw a tag-not-closed `Error`.
Basically, what we need to do is take/`.shift()` enough tags from `remainTokens` until we got a `endwrap` token (the name can be arbitrary, but in convention, we need it to be `endwrap`). And if there's no `endwrap` until the end of template file, we need to throw an tag-not-closed `Error`.
```javascript
engine.registerTag('wrap', {
@@ -57,11 +57,11 @@ engine.registerTag('wrap', {
})
```
`.renderTemplates()` can be async; we need `yield` to wait for it to complete. For more details on async in LiquidJS, see [Sync and Async][async]. Other parts of the `render()` method are quite straightforward. Here's a JSFiddle version: <https://jsfiddle.net/por0zcn1/3/>
`.renderTemplates()` can be async, we need `yield` to wait it complete. More details on async in LiquidJS, please refer to [Sync and Async][async]. Other parts of `render()` method is quite straightforward. Here's a JSFiddle version: <https://jsfiddle.net/por0zcn1/3/>
## Using ParseStream
When it comes to complex tags like [for][for] and [if][if], the `parse()` can be very complicated. There's a [ParseStream][ParseStream] utility to organize the `parse()` in event-based style. Following is a re-written `parse()` using `ParseStream` that does exactly the same as the example above.
When it comes to complex tags like [for][for] and [if][if], the `parse()` can be very complicated. There's a [ParseStream][ParseStream] utility to organize the `parse()` in event-based style. Following is a re-written `parse()` using `ParseStream` and does exactly the same as above example.
```javascript
parse(tagToken, remainTokens) {
@@ -79,7 +79,7 @@ Here's a JSFiddle version: <https://jsfiddle.net/por0zcn1/4/>. For simplicity, t
## Manipulate the Context
The `wrap` tag above doesn't seem very useful; even without using that tag, we can render the content anyway. Now we're going to implement a `repeat` tag to render the content 2 times (we can also add a [parameter][parameter] to render an arbitrary number of times).
The `wrap` tag above doesn't seem to be very useful, without using that tag we can render the content anyway. Now we're going to implement a `repeat` tag to render the content 2 times (we can also add a [parameter][parameter] to render arbitrary times).
```liquid
{% repeat %}
+2 -2
View File
@@ -28,7 +28,7 @@ A typical PC handles `1e8` (100M) characters without issues.
### renderLimit
Restricting template size alone is insufficient because dynamic loops with large counts can occur during rendering. [renderLimit][renderLimit] mitigates this by limiting the time consumed by each `render()` call.
Restricting template size alone is insufficient because dynamic loops with large counts can occur in render time. [renderLimit][renderLimit] mitigates this by limiting the time consumed by each `render()` call.
```liquid
{%- for i in (1..10000000) -%}
@@ -49,7 +49,7 @@ Render time is checked on a per-template basis (before rendering each template).
In other words, `memoryLimit` limits what LiquidJS counts, not every byte your process may allocate.
Even with a small number of templates and iterations, memory usage can grow exponentially. In the following example, memory doubles with each iteration:
Even with small number of templates and iterations, memory usage can grow exponentially. In the following example, memory doubles with each iteration:
```liquid
{% assign array = "1,2,3" | split: "," %}
+1 -1
View File
@@ -47,7 +47,7 @@ Pre-built UMD bundles are also available:
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.umd.js"></script>
```
{% note info Working Demo %} Here's a live demo on jsFiddle: <a href="https://jsfiddle.net/pd4jhzLs/1/" target="_blank">jsfiddle.net/pd4jhzLs/1/</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
{% note info Working Demo %} Here's a living demo on jsFiddle: <a href="https://jsfiddle.net/pd4jhzLs/1/" target="_blank">jsfiddle.net/pd4jhzLs/1/</a>, and the source code is also available in <a href="https://github.com/harttle/liquidjs/blob/master/demo/browser/" target="_blank">liquidjs/demo/browser/</a>.{% endnote %}
{% note warn Compatibility %} You may need a <a href="https://github.com/taylorhakes/promise-polyfill" target="_blank">Promise polyfill</a> for legacy browsers like IE and Android UC, see <a href="https://caniuse.com/#feat=promises" target="_blank">caniuse statistics</a>. {% endnote %}
+3 -3
View File
@@ -5,7 +5,7 @@ title: Static Template Analysis
{% since %}v10.20.0{% endsince %}
{% note warn Experimental %}
Note that this is an experimental feature and future APIs are subject to change. Internal structures returned can be changed without a major version bump.
Note that this is an experimental feature and future APIs are subject to change. And internal structures returned can be changed w/o a major version bump.
{% endnote %}
{% note info Sync and Async %}
@@ -234,9 +234,9 @@ This is an example of an object returned from `Liquid.analyze()`, passing it the
### Analyzing Custom Tags
For static analysis to include results from custom tags, those tags must implement some additional methods defined on the [Template interface](/api/interfaces/Template.html). LiquidJS will use the information returned from these methods to traverse the template and report variable usage.
For static analysis to include results from custom tags, those tags must implement some additional methods defined on the [Template interface]( /api/interfaces/Template.html). LiquidJS will use the information returned from these methods to traverse the template and report variable usage.
Not all methods are required, depending on the kind of tag. If it's a block with a start tag, end tag and any amount of Liquid markup in between, it will need to implement the [`children()`](/api/interfaces/Template.html#children) method. `children()` is defined as a generator, so that we can use it in synchronous and asynchronous contexts, just like `render()`. It should return HTML content, output statements and tags that are child nodes of the current tag.
Not all methods are required, depending in the kind of tag. If it's a block with a start tag, end tag and any amount of Liquid markup in between, it will need to implement the [`children()`](/api/interfaces/Template.html#children) method. `children()` is defined as a generator, so that we can use it in synchronous and asynchronous contexts, just like `render()`. It should return HTML content, output statements and tags that are child nodes of the current tag.
The [`blockScope()`](/api/interfaces/Template.html#blockScope) method is responsible for telling LiquidJS which names will be in scope for the duration of the tag's block. Some of these names could depend on the tag's arguments, and some will be fixed, like `forloop` from the `{% for %}` tag.
+5 -5
View File
@@ -2,11 +2,11 @@
title: Sync and Async
---
LiquidJS supports both synchronous and asynchronous evaluation, and can be used with Promises. To reuse the same set of tag/filter implementations in both sync and async modes, LiquidJS tags are implemented as generators.
LiquidJS supports both sync and async evaluate, and can be used with Promises. To reuse the same set of tag/filter implementations in both sync and async, LiquidJS tags are implemented as generators.
## Sync and Async API
All major methods on [Liquid][Liquid] support both sync and async. These methods return Promises:
All major methods on [Liquid][Liquid] supports both sync and async. These methods return Promises:
- `render()`
- `renderFile()`
@@ -44,11 +44,11 @@ engine.registerTag('upper', class UpperTag extends Tag {
})
```
All built-in tags are implemented this way and are safe to use in both sync and async modes (I'll call it *sync-compatible*). To make your custom tag *sync-compatible*, you'll need to:
All builtin tags are implemented this way and safe to use in both sync and async (I'll call it *sync-compatible*). To make your custom tag *sync-compatible*, you'll need to:
- declare render function as `* render()`, in which
- do not directly `return <Promise>`, and
- do not call any APIs that return a Promise.
- do not call any APIs that returns a Promise.
## Call APIs that return a Promise
@@ -92,7 +92,7 @@ engine.registerTag('upper', class UpperTag extends Tag {
## Async only Tags
If your tag is intended to be used only asynchronously, it can be declared as `async render()` so you can use `await` in its implementation directly:
If your tag is intend to be used only asynchronously, it can be declared as `async render()` so you can use `await` in its implementation directly:
```typescript
import { toPromise, TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
+2 -2
View File
@@ -2,7 +2,7 @@
title: Truthy and Falsy
---
Though [Liquid][sl] is platform-independent, there are [certain differences][diff] with [the Ruby version][ruby], one of which is the `truthy` value.
Though [Liquid][sl] is platform-independent, there're [certain differences][diff] with [the Ruby version][ruby], one of which is the `truthy` value.
## The Truth Table
@@ -24,7 +24,7 @@ value | truthy | falsy
## Use JavaScript Truthy
Note that LiquidJS uses Shopify's truthiness by default. It can be toggled to use standard JavaScript truthiness by setting the **jsTruthy** option to `true`.
Note that liquidjs use Shopify's truthiness by default. But it can be toggled to used standard JavaScript truthiness by setting the **jsTruthy** option to `true`.
value | truthy | falsy
--- | --- | ---
+2 -2
View File
@@ -2,7 +2,7 @@
title: Use in Express.js
---
LiquidJS is compatible with [Express template engines](https://expressjs.com/en/resources/template-engines.html). You can set the LiquidJS instance as the [view engine][express-views] option:
LiquidJS is compatible to the [express template engines](https://expressjs.com/en/resources/template-engines.html). You can set liquidjs instance to the [view engine][express-views] option:
```javascript
var { Liquid } = require('liquidjs');
@@ -50,7 +50,7 @@ res.render('world')
## Caching
Simply setting the [cache option][cache] to true will enable template caching, as explained in [Caching][Caching]. It's recommended to enable cache in a production environment, which can be done by:
Simply setting the [cache option][cache] to true will enable template caching, as explained in [Caching][Caching]. It's recommended to enable cache in production environment, which can be done by:
```javascript
var { Liquid } = require('liquidjs');
+2 -2
View File
@@ -13,14 +13,14 @@ By default, all tags and output markups lines will generate a NL (`\n`), and whi
{{ author }}
```
Outputs (note the blank line):
Outputs (note the blank link):
```
harttle
```
You can include hyphens in tag syntax (`{% raw %}{{-{% endraw %}`, `-}}`, `{% raw %}{%-{% endraw %}`, `-%}`) to strip whitespace from the left or right. For example:
We can include hyphens in your tag syntax (`{% raw %}{{-{% endraw %}`, `-}}`, `{% raw %}{%-{% endraw %}`, `-%}`) to strip whitespace from left or right. For example:
```liquid
{% assign author = "harttle" -%}
+1 -5
View File
@@ -13,14 +13,10 @@ index:
description: 'Thanks to these wonderful people! See <a href="tutorials/contribution-guidelines.html">contribution guidelines</a> if you&#39;d like to help.'
sponsors:
title: Sponsors
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.'
description: 'If you personally love LiquidJS or it&#39;s benefiting your business, please <a href="https://github.com/sponsors/harttle">sponsor us</a>!'
playground:
title: Playground
lead: Edit a template and context JSON — rendered HTML updates as you type.
loading: Loading...
page:
+5 -18
View File
@@ -1,6 +1,6 @@
<header id="banner" class="wrapper">
<div class="inner inner-content">
<h2 id="banner-title">{{ page.subtitle | default(config.subtitle) }}</h2>
<h2 id="banner-title">{{ page.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="used-by-wrap">
<div id="contributors-wrap">
<div class="wrapper">
<div class="inner inner-content">
<div class="section-header">
<h3>{{__('index.used_by.title')}}</h3>
<p class="description">{{__('index.used_by.description')}}</p>
<h3>{{__('index.contributors.title')}}</h3>
<p class="description">{{__('index.contributors.description')}}</p>
</div>
<div class="contributors">
{{ partial('partial/used-by') }}
{{ partial('partial/all-contributors') }}
</div>
</div>
</div>
@@ -40,16 +40,3 @@
</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>
-13
View File
@@ -1,11 +1,6 @@
{% 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') }}
@@ -20,12 +15,4 @@ indexName: 'liquidjs',
inputSelector: '#search-input',
debug: false
});
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GM713991QQ"></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>
+5 -1
View File
@@ -1,3 +1,7 @@
{
"name": "liquid"
"people": [
"alice",
"bob",
"carol"
]
}
+9 -1
View File
@@ -1 +1,9 @@
<p>Hello, {{ name | capitalize }}!</p>
<ul>
{%- for person in people %}
<li>
<a href="{{person | prepend: "https://example.com/"}}">
{{ person | capitalize }}
</a>
</li>
{%- endfor%}
</ul>
-1
View File
@@ -5,7 +5,6 @@
{{__('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
View File
@@ -32,7 +32,4 @@
<meta name="msapplication-TileImage" content="{{ url_for('icon/mstile-144x144.png') }}">
{{ css('css/navy') }}
{{ feed_tag('atom.xml') }}
<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>
+13 -37
View File
@@ -1,45 +1,21 @@
<div id="playground" role="main">
<div class="wrapper">
<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>
<h1 class="inner">{{__('playground.title')}}</h1>
<div class="loader" role=status aria-busy=true></div>
<div id="editors" class="inner hide" aria-hide=true>
<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 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>
</div>
<p class="inner version"></p>
</div>
</div>
-1
View File
@@ -63,7 +63,6 @@ a
#content-wrap
background: var(--color-content-bg)
overflow: hidden
box-shadow: var(--panel-shadow)
.video-container
text-align: center
-15
View File
@@ -1,15 +0,0 @@
#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
-7
View File
@@ -42,13 +42,6 @@
@media mq-normal
font-size: 30px
.cookie-preferences
background: none
border: 0
padding: 0
cursor: pointer
color: inherit
.icon-oc
height: 36px
width: 30px
+1 -1
View File
@@ -58,7 +58,7 @@ logo-size = 36px
text-decoration: none
line-height: logo-size
white-space: nowrap
opacity: 0.85
opacity: 0.7
transition: opacity 0.2s, color 0.2s
display: inline-block
padding: 0 12px
+16 -29
View File
@@ -1,6 +1,19 @@
// https://github.com/chriskempson/tomorrow-theme
// Light: GitHub-style; dark: Tomorrow Night Eighties (tuned for navy palette)
// Palette tokens live in _variables.styl
// Tomorrow Night Eighties
:root {
--highlight-background: #2d2d2d
--highlight-current-line: #393939
--highlight-selection: #515151
--highlight-foreground: #cccccc
--highlight-comment: #999999
--highlight-red: #f2777a
--highlight-orange: #f99157
--highlight-yellow: #ffcc66
--highlight-green: #99cc99
--highlight-aqua: #66cccc
--highlight-blue: #6699cc
--highlight-purple: #cc99cc
}
pre, code
font-family: font-mono
@@ -10,19 +23,14 @@ pre, code
code
padding: 0 5px
border-radius: 4px
pre
padding: 10px 15px
line-height: 22px
code-block-chrome()
code
border: none
display: block
padding: 0
&.line-numbers
white-space: pre
overflow-x: auto
.highlight
padding: 10px 15px
@@ -30,7 +38,6 @@ pre
background: var(--highlight-background)
overflow: auto
margin: 0
code-block-chrome()
table
margin: 0 !important
border: 0
@@ -49,9 +56,6 @@ pre
padding: 0
background: none
white-space: pre
border: none
box-shadow: none
border-radius: 0
.line
height: 22px
@@ -103,21 +107,4 @@ pre
color: var(--highlight-blue)
.keyword
.javascript .function
color: var(--highlight-purple)
// Prism bash: builtins (echo), external commands (npm), and cli-command (npx)
code.language-bash
.builtin,
.class-name,
.function,
.cli-command
color: var(--highlight-bash-command)
.operator
color: var(--highlight-purple)
code.language-shell
.builtin,
.class-name,
.function,
.cli-command
color: var(--highlight-bash-command)
.operator
color: var(--highlight-purple)
color: var(--highlight-purple)
+2 -2
View File
@@ -126,7 +126,7 @@
background: var(--color-link-hover)
color: #fff
#used-by-wrap, #sponsors-wrap, #contributors-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
+16 -31
View File
@@ -1,6 +1,6 @@
note-tip = #3fb950
note-info = #58a6ff
note-warn = #f85149
note-tip = #0fff00
note-info = hsl(200, 100%, 50%)
note-warn = hsl(0, 100%, 50%)
#content
position: relative
@@ -26,7 +26,7 @@ note-warn = #f85149
#article-toc
display: none
width: sidebar-width
opacity: 0.9
opacity: 0.8
@media mq-normal
display: block
@@ -86,7 +86,7 @@ note-warn = #f85149
.article-edit-link
float: right
text-decoration: none;
color: var(--color-gray)
color: #bbb
font-size: 24px
line-height: 36px
transition: 0.2s
@@ -111,31 +111,21 @@ note-warn = #f85149
color: var(--color-default)
@media print
font-size: 12pt
p, ol, ul, dl, table, blockquote, iframe, pre, .highlight
p, ol, ul, dl, table, blockquote, iframe, .highlight
margin: 1em 0
pre + pre,
.highlight + .highlight,
pre + .highlight,
.highlight + pre
margin-top: 1em
h1
font-size: 2em
h2
font-size: 1.5em
border-bottom: 1px solid var(--heading-border)
border-bottom: 1px solid var(--color-border)
padding-bottom: 10px
margin-bottom: 0.75em
margin-bottom: 15px
h3
font-size: 1.3em
h1, h2, h3, h4, h5, h6
line-height: 1em
font-weight: bold
margin-top: 1.75em
margin-bottom: 0.625em
blockquote + h1, blockquote + h2, blockquote + h3, blockquote + h4,
.highlight + h1, .highlight + h2, .highlight + h3, .highlight + h4,
pre + h1, pre + h2, pre + h3, pre + h4
margin-top: 2.25em
margin: 1em 0
hr
border-bottom: none;
border-top: 1px dashed var(--color-border);
@@ -148,9 +138,6 @@ note-warn = #f85149
padding-left: 3px;
vertical-align: super;
zoom: .7
:not(pre) > code
background: var(--inline-code-bg)
color: var(--inline-code-color)
strong
font-weight: bold
em
@@ -169,7 +156,7 @@ note-warn = #f85149
li
p
margin: 0
table, blockquote, iframe, pre, .highlight
table, blockquote, iframe, .highlight
margin: 1em 0
img, video
max-width: 100%
@@ -178,11 +165,8 @@ note-warn = #f85149
blockquote
padding: 0 20px
position: relative
background: var(--blockquote-bg)
border: 1px solid var(--color-border)
border-left: 4px solid #ddd
border-radius: 6px
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06)
border-left: 5px solid #ddd
footer
margin: 1em 0
font-style: italic
@@ -192,16 +176,15 @@ note-warn = #f85149
padding: 0 0.3em
a
color: color-grey
@media (prefers-color-scheme: dark)
border-color: var(--color-gray)
.note
&.tip
border-left-color: note-tip
background: var(--note-tip-bg)
&.info
border-left-color: note-info
background: var(--note-info-bg)
&.warn
border-left-color: note-warn
background: var(--note-warn-bg)
.since
margin-top: 0;
font-style: italic;
@@ -223,7 +206,9 @@ note-warn = #f85149
padding: 5px 15px
tr
&:nth-child(2n)
background: var(--table-stripe-bg)
background: #eee
@media (prefers-color-scheme: dark)
background: var(--color-gray)
.article-footer
margin: 60px 0 0
+24 -288
View File
@@ -1,312 +1,48 @@
#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: 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
margin-bottom: 40px
h1
font-size: 28px
font-weight: 600
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
font-size: 36px
font-weight: 300
margin: 40px 0 24px
#editors
margin-bottom: 0
.playground-workspace
display: grid
gap: var(--playground-gap)
grid-template-columns: 1fr 1fr
grid-template-rows: 3fr 2fr
align-items: stretch
@media mq-normal
overflow: hidden
height: 75vh
max-height: unquote('calc(100vh - 200px)')
min-height: 520px
@media mq-mobile
grid-template-columns: 1fr
grid-template-rows: auto
gap: 12px
overflow: hidden
margin-bottom: 16px
height: 75vh
grid-template-columns: auto auto
grid-template-rows: 60% 40%
grid-gap: 16px
.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
.playground-pane
.editor-wrapper
display: flex
gap: 8px
flex-direction: column
min-height: 0
overflow: hidden
background: var(--playground-pane-head)
border: 1px solid var(--code-border)
border-radius: var(--playground-radius)
box-shadow: var(--code-shadow)
.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
font-size: 14px
line-height: 1.55
border-radius: 0
@media mq-mobile
font-size: 13px
.ace_scrollbar
z-index: 2
.editor
flex-grow: 1
.hide
display: none
.loader
width: 40px
height: 40px
margin: 120px auto 160px
border: 2px solid var(--color-border)
border-top-color: var(--color-link)
border-radius: 50%
animation: playground-spin 0.7s infinite linear
@media mq-mobile
margin: 60px auto 80px
@keyframes playground-spin
width: 75px
height: 75px
margin: 150px auto 200px
border-top: 5px solid #292929
border-right: 5px solid #efefef
border-bottom: 5px solid #efefef
border-left: 5px solid #efefef
border-radius: 100px
animation: spin 1s infinite linear
@keyframes spin
100%
transform: rotate(360deg)
+1 -1
View File
@@ -1,6 +1,7 @@
#sidebar
width: sidebar-width
padding-bottom: 40px
opacity: 0.8
display: none
@media mq-normal
display: block
@@ -25,7 +26,6 @@
line-height: 1
position: relative
width: 100%
transition: color 0.15s
&.current
color: var(--color-link)
&:hover
+20 -78
View File
@@ -4,90 +4,38 @@ vendor-prefixes = webkit moz ms official
// Colors
:root {
--color-default: #374151
--color-default-invert: #e8eaed
--color-gray: #6b7280
--color-border: #e5e7eb
--color-navy: hsl(218, 32%, 11%)
--color-default: #444
--color-default-invert: #d1d2d4
--color-gray: #999
--color-border: #e3e3e3
--color-navy: hsl(210, 25%, 12%)
--color-content-bg: #fff
--color-navy-lighter: hsl(218, 28%, 16%)
--color-link: #2563eb
--color-link-hover: #1d4ed8
--panel-shadow: 0 0 48px rgba(0, 0, 0, 0.18)
--heading-border: var(--color-border)
--code-border: #d0d7de
--code-shadow: 0 1px 2px rgba(0, 0, 0, 0.04)
--inline-code-bg: rgba(175, 184, 193, 0.2)
--inline-code-color: var(--color-default)
--blockquote-bg: transparent
--note-tip-bg: rgba(63, 185, 80, 0.05)
--note-info-bg: rgba(88, 166, 255, 0.05)
--note-warn-bg: rgba(248, 81, 73, 0.05)
--table-stripe-bg: #f3f4f6
--highlight-background: #f6f8fa
--highlight-current-line: #f0f3f6
--highlight-selection: #b6d4fe
--highlight-foreground: #24292f
--highlight-comment: #6e7781
--highlight-red: #cf222e
--highlight-orange: #953800
--highlight-bash-command: #c95100
--highlight-yellow: #7d4e00
--highlight-green: #116329
--highlight-aqua: #0550ae
--highlight-blue: #0550ae
--highlight-purple: #8250df
--playground-surface: #f3f4f6
--playground-pane-head: #fff
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
--color-link: #0e83cd
--color-link-hover: lighten(#0e83cd, 10%)
}
@media (prefers-color-scheme: dark) {
:root {
--color-default: #e6edf3
--color-default-invert: #374151
--color-gray: #9aa4b2
--color-border: #3d444d
--color-default: #d1d2d4
--color-default-invert: #444
--color-gray: invert(#999)
--color-border: #6a6a6a
--color-background: #fff
--color-navy: hsl(218, 32%, 9%)
--color-navy-lighter: hsl(218, 28%, 15%)
--color-content-bg: hsl(218, 28%, 13%)
--color-link: #58a6ff
--color-link-hover: #79b8ff
--panel-shadow: 0 0 64px rgba(0, 0, 0, 0.35)
--heading-border: rgba(255, 255, 255, 0.08)
--code-border: rgba(255, 255, 255, 0.06)
--code-shadow: 0 1px 3px rgba(0, 0, 0, 0.12)
--inline-code-bg: var(--highlight-background)
--inline-code-color: var(--highlight-foreground)
--blockquote-bg: rgba(255, 255, 255, 0.03)
--note-tip-bg: rgba(63, 185, 80, 0.07)
--note-info-bg: rgba(88, 166, 255, 0.07)
--note-warn-bg: rgba(248, 81, 73, 0.07)
--table-stripe-bg: rgba(255, 255, 255, 0.04)
--highlight-background: hsl(218, 24%, 15%)
--highlight-current-line: hsl(218, 18%, 18%)
--highlight-selection: hsl(218, 15%, 24%)
--highlight-foreground: #e6edf3
--highlight-comment: #8b949e
--highlight-red: #ff7b72
--highlight-orange: #ffa657
--highlight-bash-command: var(--highlight-orange)
--highlight-yellow: #e3b341
--highlight-green: #7ee787
--highlight-aqua: #79c0ff
--highlight-blue: #79c0ff
--highlight-purple: #d2a8ff
--playground-surface: hsl(218, 26%, 10%)
--playground-pane-head: hsl(218, 22%, 16%)
--color-navy: hsl(210, 25%, 12%)
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 7%)
--color-content-bg: #1c1c1c
--color-link: #0e83cd
--color-link-hover: lighten(#0e83cd, 10%)
}
}
// Typography
font-sans = -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif
font-sans = "Helvetica Neue", Helvetica, Arial, sans-serif
font-serif = Garamond, Georgia, "Times New Roman", serif
font-mono = "Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace
font-mono = "Source Code Pro", Monaco, Menlo, Consolas, monospace
font-size = 16px
line-height = 1.75
line-height = 1.8em
// Layout
max-width = 1800px
@@ -100,9 +48,3 @@ mq-mobile = "screen and (max-width: 768px)"
mq-normal = "screen and (min-width: 769px)"
mq-small = "screen and (min-width: 992px)"
mq-tablet = "screen and (min-width: 480px)"
// Shared code-block chrome (pre, .highlight, playground editors)
code-block-chrome()
border-radius: 6px
border: 1px solid var(--code-border)
box-shadow: var(--code-shadow)
-1
View File
@@ -9,7 +9,6 @@
@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"
-49
View File
@@ -1,49 +0,0 @@
(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();
}
}());
+25 -148
View File
@@ -38,41 +38,28 @@
(function() {
// playground
/* global liquidjs, ace, Prism */
if (!/\/playground(?:\.html)?$/.test(location.pathname)) return;
/* global liquidjs, ace */
if (!location.pathname.match(/playground.html$/)) return;
updateVersion(liquidjs.version);
const engine = new liquidjs.Liquid({
memoryLimit: 1e5,
renderLimit: 1e5
});
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
const editor = createEditor('editorEl', 'liquid');
const dataEditor = createEditor('dataEl', 'json');
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];
let previewValue = '';
let hadPreview = false;
let renderTimer = null;
const RENDER_DELAY = 180;
colorScheme.addEventListener('change', function() {
editors.forEach(applyEditorTheme);
if (previewValue) setPreview(previewValue);
});
const preview = createEditor('previewEl', 'html');
preview.setReadOnly(true);
preview.renderer.setShowGutter(false);
preview.renderer.setPadding(20);
const init = parseArgs(location.hash.slice(1));
if (init) {
editor.setValue(init.tpl, 1);
dataEditor.setValue(init.data, 1);
}
editor.on('change', onTemplateChange);
dataEditor.on('change', onContextChange);
editor.on('focus', function () { setIndicator(indicatorTpl, 'active'); });
dataEditor.on('focus', function () { setIndicator(indicatorData, 'active'); });
scheduleUpdate();
editor.on('change', update);
dataEditor.on('change', update);
update();
ready();
function ready() {
@@ -80,86 +67,23 @@
loader.classList.add('hide');
loader.setAttribute('aria-busy', false);
const editorsEl = document.querySelector('#editors');
editorsEl.classList.remove('hide');
editorsEl.setAttribute('aria-hide', false);
editors.forEach(function(ed) { ed.resize(); });
}
function getEditorTheme() {
return colorScheme.matches
? 'ace/theme/tomorrow_night_eighties'
: 'ace/theme/github';
}
function applyEditorTheme(editor) {
editor.setTheme(getEditorTheme());
editor.renderer.setPadding(0);
editor.container.style.background = 'transparent';
const editors = document.querySelector('#editors');
editors.classList.remove('hide');
editors.setAttribute('aria-hide', false);
}
function createEditor(id, lang) {
const editor = ace.edit(id);
applyEditorTheme(editor);
editor.setOptions({
fontFamily: '"Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace',
fontSize: '14px',
showPrintMargin: false,
showGutter: false,
highlightActiveLine: false,
tabSize: 2,
useSoftTabs: true,
scrollPastEnd: 0
});
editor.setTheme('ace/theme/tomorrow_night');
editor.getSession().setMode('ace/mode/' + lang);
editor.renderer.setShowGutter(false);
if (editor.renderer.$gutter) {
editor.renderer.$gutter.style.display = 'none';
}
editor.renderer.setScrollMargin(0, 0, 0, 0);
bindClipboard(editor);
editor.getSession().setOptions({
tabSize: 2,
useSoftTabs: true
});
editor.renderer.setScrollMargin(15);
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 {
@@ -173,64 +97,16 @@
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 {
parsed = JSON.parse(data);
const html = await engine.parseAndRender(tpl, JSON.parse(data));
preview.setValue(html, 1);
} catch (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');
preview.setValue(err.stack, 1);
throw err;
}
}
@@ -243,8 +119,9 @@
}
function updateVersion(version) {
document.querySelector('.version').innerHTML =
'liquidjs@<a target="_blank" rel="noopener noreferrer" href="https://www.npmjs.com/package/liquidjs/v/' + version + '">' + version + '</a>';
document.querySelector('.version').innerHTML = `
liquidjs@<a target="_blank" href="https://www.npmjs.com/package/liquidjs/v/${version}">${version}</a>
`
}
}());
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "liquidjs",
"version": "10.29.0",
"version": "10.27.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "liquidjs",
"version": "10.29.0",
"version": "10.27.0",
"license": "MIT",
"dependencies": {
"commander": "^10.0.0"
+5 -8
View File
@@ -1,8 +1,8 @@
{
"name": "liquidjs",
"version": "10.29.0",
"version": "10.27.0",
"sideEffects": false,
"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.",
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"main": "dist/liquid.node.js",
"module": "dist/liquid.node.mjs",
"es2015": "dist/liquid.browser.mjs",
@@ -29,15 +29,12 @@
"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",
"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:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog build:docs-hexo",
"build:docs-liquid": "cross-env BUNDLES=min rollup -c rollup.config.mjs && shx mkdir -p docs/public/js && shx cp dist/liquid.browser.min.js docs/public/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 .nojekyll public/",
"serve:docs": "cd docs && npm run start",
"dev:docs": "run-s prepare:docs serve:docs"
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/"
},
"bin": {
"liquidjs": "./bin/liquid.js",
-15
View File
@@ -183,21 +183,6 @@ 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 () {
+12 -18
View File
@@ -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, isNumber, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue, readArrayElement } from '../util'
import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue } from '../util'
type PropertyKey = string | number;
@@ -31,10 +31,6 @@ export class Context {
* The normalized liquid options object
*/
public opts: NormalizedFullOptions
/**
* Reference to the Liquid instance for filter resolution
*/
public liquid?: any
/**
* Throw when accessing undefined variable?
*/
@@ -42,7 +38,7 @@ export class Context {
public ownPropertyOnly: boolean;
public memoryLimit: Limiter;
public renderLimit: Limiter;
public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, renderOptions: RenderOptions = {}, { memoryLimit, renderLimit, liquid }: { memoryLimit?: Limiter, renderLimit?: Limiter, liquid?: any } = {}) {
public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, renderOptions: RenderOptions = {}, { memoryLimit, renderLimit }: { [key: string]: Limiter } = {}) {
this.sync = !!renderOptions.sync
this.opts = opts
this.globals = renderOptions.globals ?? opts.globals
@@ -51,7 +47,6 @@ export class Context {
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly
this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit)
this.renderLimit = renderLimit ?? new Limiter('template render', getPerformance().now() + (renderOptions.renderLimit ?? opts.renderLimit))
this.liquid = liquid
}
public getRegister<T> (key: string, defaultValue: T = undefined as T): T {
return (this.registers[key] = this.registers[key] || defaultValue)
@@ -115,8 +110,7 @@ export class Context {
ownPropertyOnly: this.ownPropertyOnly
}, {
renderLimit: this.renderLimit,
memoryLimit: this.memoryLimit,
liquid: this.liquid
memoryLimit: this.memoryLimit
})
}
private findScope (key: string | number) {
@@ -131,13 +125,13 @@ export class Context {
obj = toLiquid(obj)
key = toValue(key) as PropertyKey
if (isNil(obj)) return obj
if (isArray(obj) && isNumber(key)) return readArrayElement(obj, key, this.ownPropertyOnly)
if (isArray(obj) && (key as number) < 0) return obj[obj.length + +key]
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, this.ownPropertyOnly)
else if (key === 'last') return readLast(obj, this.ownPropertyOnly)
else if (key === 'first') return readFirst(obj)
else if (key === 'last') return readLast(obj)
return value
}
}
@@ -147,14 +141,14 @@ export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: b
return obj[key]
}
function readFirst (obj: Scope, ownPropertyOnly: boolean) {
if (isArray(obj)) return readArrayElement(obj, 0, ownPropertyOnly)
return readJSProperty(obj, 'first', ownPropertyOnly)
function readFirst (obj: Scope) {
if (isArray(obj)) return obj[0]
return obj['first']
}
function readLast (obj: Scope, ownPropertyOnly: boolean) {
if (isArray(obj)) return readArrayElement(obj, -1, ownPropertyOnly)
return readJSProperty(obj, 'last', ownPropertyOnly)
function readLast (obj: Scope) {
if (isArray(obj)) return obj[obj.length - 1]
return obj['last']
}
function readSize (obj: Scope) {
+13 -23
View File
@@ -1,4 +1,4 @@
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, orderedCompare, isArray, isNil, isArrayLike, readArrayElement, toEnumerable } from '../util'
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, orderedCompare, isArray, isNil, last as arrayLast, isArrayLike, toEnumerable } from '../util'
import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
import { Value, FilterImpl } from '../template'
import { Tokenizer } from '../parser'
@@ -8,17 +8,12 @@ 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)
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) : ''
const complexity = array.length * (1 + sep.length)
this.context.memoryLimit.use(complexity)
return array.join(sep)
})
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)
@@ -71,14 +66,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.prototype.filter.call(array, x => !isNil(toValue(x)))
return array.filter(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 Array.prototype.concat.call(lhs, rhs)
return lhs.concat(rhs)
}
export function push<T> (this: FilterImpl, v: T[], arg: T): T[] {
@@ -93,10 +88,8 @@ export function unshift<T> (this: FilterImpl, v: T[], arg: T): T[] {
return clone
}
export function pop<T> (this: FilterImpl, v: T[]): T[] {
const array = toArray(v)
this.context.memoryLimit.use(array.length)
const clone = [...array]
export function pop<T> (v: T[]): T[] {
const clone = [...toArray(v)]
clone.pop()
return clone
}
@@ -114,11 +107,8 @@ export function slice<T> (this: FilterImpl, v: T[] | string, begin: number, leng
if (isNil(v)) return []
if (!isArray(v)) v = stringify(v)
begin = begin < 0 ? v.length + begin : begin
if (begin < 0 || length < 0) return isArray(v) ? [] : ''
this.context.memoryLimit.use(length)
return isArray(v)
? Array.prototype.slice.call(v, begin, begin + length)
: String.prototype.slice.call(v, begin, begin + length)
return v.slice(begin, begin + length)
}
function expectedMatcher (this: FilterImpl, expected: any): (v: any) => boolean {
@@ -140,7 +130,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 Array.prototype.filter.call(arr, (_, i) => matcher(values[i]) === include)
return arr.filter((_, i) => matcher(values[i]) === include)
}
function * filter_exp<T extends object> (this: FilterImpl, include: boolean, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
@@ -262,7 +252,7 @@ export function sample<T> (this: FilterImpl, v: T[] | string, count = 1): T | st
v = toValue(v)
if (isNil(v)) return []
if (!isArray(v)) v = stringify(v)
this.context.memoryLimit.use(v.length)
this.context.memoryLimit.use(count)
const shuffled = [...v].sort(() => Math.random() - 0.5)
if (count === 1) return shuffled[0]
return shuffled.slice(0, count)
+1 -1
View File
@@ -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
}
+4 -3
View File
@@ -8,13 +8,14 @@ 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) + arg) % arg)
export const modulo = argumentsToNumber((v: number, arg: number) => v % 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) * (1 + Number.EPSILON)
return Math.round(scaled) / amp
const scaled = v * amp
// Round half away from zero
return Math.sign(v) * Math.round(Math.abs(scaled)) / amp
}
+5 -29
View File
@@ -2,18 +2,6 @@ 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
@@ -21,30 +9,18 @@ function defaultFilter<T1 extends boolean, T2> (this: FilterImpl, value: T1, def
return isFalsy(value, this.context) ? defaultValue : value
}
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 json (value: any, space = 0) {
return JSON.stringify(value, null, space)
}
function inspect (this: FilterImpl, value: any, space = 0) {
const memoryLimit = this.context.memoryLimit
function inspect (value: any, space = 0) {
const ancestors: object[] = []
return JSON.stringify(value, function (this: unknown, _key: unknown, value: any) {
if (typeof value !== 'object' || value === null) {
chargeJsonReplacerValue(memoryLimit, value)
return value
}
if (typeof value !== 'object' || value === null) 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)) {
memoryLimit.use('[Circular]'.length)
return '[Circular]'
}
if (ancestors.includes(value)) return '[Circular]'
ancestors.push(value)
chargeJsonReplacerValue(memoryLimit, value)
return value
}, space)
}
+1 -9
View File
@@ -128,12 +128,6 @@ export function strip_newlines (this: FilterImpl, v: string) {
return str.replace(/\r?\n/gm, '')
}
export function squish (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
return str.replace(/\s+/g, ' ').trim()
}
export function capitalize (this: FilterImpl, str: string) {
str = stringify(str)
this.context.memoryLimit.use(str.length)
@@ -215,9 +209,7 @@ 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)
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)
this.context.memoryLimit.use(array.length + connector.length)
switch (array.length) {
case 0:
return ''
+1 -1
View File
@@ -1,6 +1,6 @@
import { stringify } from '../util/underscore'
export const url_decode = (x: string) => decodeURIComponent(stringify(x).replace(/\+/g, ' '))
export const url_decode = (x: string) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ')
export const url_encode = (x: string) => encodeURIComponent(stringify(x)).replace(/%20/g, '+')
export const cgi_escape = (x: string) => encodeURIComponent(stringify(x))
.replace(/%20/g, '+')
+1 -1
View File
@@ -11,7 +11,7 @@ export { Context, Scope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output, Variable, VariableLocation, VariableSegments, Variables, StaticAnalysis, StaticAnalysisOptions, analyze, analyzeSync, Arguments, PartialScope } from './template'
export type { TagRenderReturn } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
export type { RangeToken, LiteralToken, QuotedToken, PropertyAccessToken, NumberToken, FilteredValueToken } from './tokens'
export type { RangeToken, LiteralToken, QuotedToken, PropertyAccessToken, NumberToken } from './tokens'
export { TokenKind, Tokenizer, ParseStream, Parser } from './parser'
export { filters } from './filters'
export * from './tags'
+1 -8
View File
@@ -38,10 +38,7 @@ 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.
* 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.
*/
/** Hide scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates. */
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;
@@ -87,8 +84,6 @@ export interface LiquidOptions {
operators?: Operators;
/** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */
orderedFilterParameters?: boolean;
/** Allow parenthesized expressions as operands in conditions and loops, e.g. `{% if (foo | upcase) == "BAR" %}`. This is a non-standard extension to Liquid. Defaults to `false`. */
groupedExpressions?: boolean;
/** For DoS handling, limit total length of templates parsed in one `parse()` call. A typical PC can handle 1e8 (100M) characters without issues. */
parseLimit?: number;
/** For DoS handling, limit total time (in ms) for each `render()` call. */
@@ -164,7 +159,6 @@ export interface NormalizedFullOptions extends NormalizedOptions {
globals: object;
keepOutputType: boolean;
operators: Operators;
groupedExpressions: boolean;
parseLimit: number;
renderLimit: number;
memoryLimit: number;
@@ -201,7 +195,6 @@ export const defaultOptions: NormalizedFullOptions = {
globals: {},
keepOutputType: false,
operators: defaultOperators,
groupedExpressions: false,
memoryLimit: Infinity,
parseLimit: Infinity,
renderLimit: Infinity
+3 -6
View File
@@ -31,7 +31,7 @@ export class Liquid {
}
public _render (tpl: Template[], scope: Context | object | undefined, renderOptions: RenderOptions): IterableIterator<any> {
const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions, { liquid: this })
const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions)
return this.renderer.renderTemplates(tpl, ctx)
}
public async render (tpl: Template[], scope?: object, renderOptions?: RenderOptions): Promise<any> {
@@ -41,7 +41,7 @@ export class Liquid {
return toValueSync(this._render(tpl, scope, { ...renderOptions, sync: true }))
}
public renderToNodeStream (tpl: Template[], scope?: object, renderOptions: RenderOptions = {}): NodeJS.ReadableStream {
const ctx = new Context(scope, this.options, renderOptions, { liquid: this })
const ctx = new Context(scope, this.options, renderOptions)
return this.renderer.renderTemplatesToNodeStream(tpl, ctx)
}
@@ -88,7 +88,7 @@ export class Liquid {
public _evalValue (str: string, scope?: object | Context): IterableIterator<any> {
const value = new Value(str, this)
const ctx = scope instanceof Context ? scope : new Context(scope, this.options, {}, { liquid: this })
const ctx = scope instanceof Context ? scope : new Context(scope, this.options)
return value.value(ctx)
}
public async evalValue (str: string, scope?: object | Context): Promise<any> {
@@ -101,9 +101,6 @@ export class Liquid {
public registerFilter (name: string, filter: FilterImplOptions) {
this.filters[name] = filter
}
public unregisterFilter (name: string) {
delete this.filters[name]
}
public registerTag (name: string, tag: TagClass | TagImplOptions) {
this.tags[name] = isFunction(tag) ? tag : createTagClass(tag)
}
+1 -1
View File
@@ -33,7 +33,7 @@ export class Parser {
public parse (html: string, filepath?: string): Template[] {
html = String(html)
this.parseLimit.use(html.length)
const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath, undefined, this.liquid.options.groupedExpressions)
const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath)
const tokens = tokenizer.readTopLevelTokens(this.liquid.options)
return this.parseTokens(tokens)
}
-1
View File
@@ -12,6 +12,5 @@ export enum TokenKind {
Quoted = 1024,
Operator = 2048,
FilteredValue = 4096,
GroupedExpression = 8192,
Delimited = Tag | Output
}
+9 -108
View File
@@ -1,4 +1,4 @@
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken, FilteredValueToken } from '../tokens'
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken } from '../tokens'
import { Tokenizer } from './tokenizer'
import { defaultOperators } from '../render/operator'
import { createTrie } from '../util/operator-trie'
@@ -229,115 +229,24 @@ describe('Tokenizer', function () {
})
describe('#readRange()', () => {
it('should read `(1..3)`', () => {
const range = new Tokenizer('(1..3)').readGroupOrRange()
expect(range).toBeDefined()
const range = new Tokenizer('(1..3)').readRange()
expect(range).toBeInstanceOf(RangeToken)
expect(range!.getText()).toEqual('(1..3)')
expect((range as RangeToken).lhs).toBeInstanceOf(NumberToken)
expect((range as RangeToken).lhs.getText()).toBe('1')
expect((range as RangeToken).rhs).toBeInstanceOf(NumberToken)
expect((range as RangeToken).rhs.getText()).toBe('3')
const { lhs, rhs } = range!
expect(lhs).toBeInstanceOf(NumberToken)
expect(lhs.getText()).toBe('1')
expect(rhs).toBeInstanceOf(NumberToken)
expect(rhs.getText()).toBe('3')
})
it('should throw for `(..3)`', () => {
expect(() => new Tokenizer('(..3)').readGroupOrRange()).toThrow('unexpected token "..3)", value expected')
expect(() => new Tokenizer('(..3)').readRange()).toThrow('unexpected token "..3)", value expected')
})
it('should read `(a.b..c["..d"])`', () => {
const range = new Tokenizer('(a.b..c["..d"])').readGroupOrRange()
expect(range).toBeDefined()
const range = new Tokenizer('(a.b..c["..d"])').readRange()
expect(range).toBeInstanceOf(RangeToken)
expect(range!.getText()).toEqual('(a.b..c["..d"])')
})
})
describe('#readGroupedExpression()', () => {
function createGrouped (input: string): Tokenizer {
const t = new Tokenizer(input, defaultOperators)
t.groupedExpressions = true
return t
}
it('should read `(foo | upcase)` as FilteredValueToken', () => {
const token = createGrouped('(foo | upcase)').readValue()
expect(token).toBeInstanceOf(FilteredValueToken)
const grouped = token as FilteredValueToken
expect(grouped.getText()).toBe('(foo | upcase)')
expect(grouped.initial.postfix).toHaveLength(1)
expect(grouped.filters).toHaveLength(1)
expect(grouped.filters[0].name).toBe('upcase')
})
it('should read `(foo | append: "!")` with filter argument', () => {
const token = createGrouped('(foo | append: "!")').readValue()
expect(token).toBeInstanceOf(FilteredValueToken)
const grouped = token as FilteredValueToken
expect(grouped.filters).toHaveLength(1)
expect(grouped.filters[0].name).toBe('append')
expect(grouped.filters[0].args).toHaveLength(1)
})
it('should read nested `((foo | append: "!") | upcase)`', () => {
const token = createGrouped('((foo | append: "!") | upcase)').readValue()
expect(token).toBeInstanceOf(FilteredValueToken)
const grouped = token as FilteredValueToken
expect(grouped.filters).toHaveLength(1)
expect(grouped.filters[0].name).toBe('upcase')
expect(grouped.initial.postfix).toHaveLength(1)
expect(grouped.initial.postfix[0]).toBeInstanceOf(FilteredValueToken)
})
it('should parse `(a | upcase) == "BAR"` as expression', () => {
const exp = [...createGrouped('(a | upcase) == "BAR"').readExpressionTokens()]
expect(exp).toHaveLength(3)
expect(exp[0]).toBeInstanceOf(FilteredValueToken)
expect(exp[1]).toBeInstanceOf(OperatorToken)
expect(exp[1].getText()).toBe('==')
expect(exp[2]).toBeInstanceOf(QuotedToken)
})
it('should read `((a | upcase) > 3)` as outer FilteredValueToken with comparison inside parens', () => {
const token = createGrouped('((a | upcase) > 3)').readValue()
expect(token).toBeInstanceOf(FilteredValueToken)
const outer = token as FilteredValueToken
expect(outer.filters).toHaveLength(0)
expect(outer.getText()).toBe('((a | upcase) > 3)')
const [first, second, third] = outer.initial.postfix
expect(first).toBeInstanceOf(FilteredValueToken)
expect(second).toBeInstanceOf(NumberToken)
expect(third).toBeInstanceOf(OperatorToken)
expect((first as FilteredValueToken).filters[0].name).toBe('upcase')
})
it('should read `(1 < 3)` as grouped comparison with no filters', () => {
const token = createGrouped('(1 < 3)').readValue() as FilteredValueToken
expect(token.filters).toHaveLength(0)
expect(token.initial.postfix).toHaveLength(3)
expect(token.initial.postfix[0]).toBeInstanceOf(NumberToken)
expect(token.initial.postfix[1]).toBeInstanceOf(NumberToken)
expect((token.initial.postfix[2] as OperatorToken).operator).toBe('<')
})
it('should read redundant parens `(x)` as FilteredValueToken', () => {
const token = createGrouped('(x)').readValue() as FilteredValueToken
expect(token.filters).toHaveLength(0)
expect(token.initial.postfix).toHaveLength(1)
})
it('should read expression plus filters inside parens `(a == b | default: "x")`', () => {
const token = createGrouped('(a == b | default: "x")').readValue() as FilteredValueToken
expect(token.filters).toHaveLength(1)
expect(token.filters[0].name).toBe('default')
expect(token.initial.postfix.map((t) => t.getText()).join(' ')).toMatch(/a.*b.*==/)
})
it('should parse `((a | upcase) > 3) and (1 < 3)` as three expression tokens', () => {
const exp = [...createGrouped('((a | upcase) > 3) and (1 < 3)').readExpressionTokens()]
expect(exp).toHaveLength(3)
expect(exp[0]).toBeInstanceOf(FilteredValueToken)
expect(exp[1]).toBeInstanceOf(OperatorToken)
expect(exp[1].getText()).toBe('and')
expect(exp[2]).toBeInstanceOf(FilteredValueToken)
})
it('should still parse `(1..3)` as RangeToken', () => {
const token = createGrouped('(1..3)').readValue()
expect(token).toBeInstanceOf(RangeToken)
})
it('should throw for unclosed parens', () => {
expect(() => createGrouped('(foo | upcase').readValue()).toThrow('unbalanced parentheses')
})
it('should fall back to readRange when flag is off', () => {
expect(() => new Tokenizer('(foo | upcase)', defaultOperators).readValue()).toThrow('invalid range syntax')
})
})
describe('#readFilter()', () => {
it('should read a simple filter', function () {
const tokenizer = new Tokenizer('| plus')
@@ -613,14 +522,6 @@ 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\'')

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