Compare commits

...
Author SHA1 Message Date
semantic-release-bot ed489865b6 chore(release): 10.27.1 [skip ci]
## [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))
2026-06-23 10:43:45 +00:00
afec88b04c docs(readme): README overhaul, used-by grid, and docs homepage (#914)
* docs(readme): lead with quick start and scannable structure

Restructure the README to match common OSS conventions: tagline and
badges above the fold, copy-paste Quick start, Features list, and a
compact Used by section. Remove the star plea, centered logo, and
per-project marketing blurbs that pushed useful content down.

Co-authored-by: Cursor <[email protected]>

* docs(readme): playground GIF, used-by grid, and docs homepage sync

Add data/used-by.json with build:used-by for README and docs homepage, playground demo capture, and shared home-section layout. Used by lists products with site logos; Financial Support keeps org and individual sponsors.

Co-authored-by: Cursor <[email protected]>

* chore: use .local for playground capture scratch files

Co-authored-by: Cursor <[email protected]>

* fix: satisfy eslint in build-used-by and capture scripts

Co-authored-by: Cursor <[email protected]>

* chore: drop one-off playground capture script

Co-authored-by: Cursor <[email protected]>

* refactor(docs): copy Used by from README like financial contributors

Drop data/used-by.json and build-used-by.js; build-contributors.js now extracts USED-BY-BEGIN/END to used-by.swig.

Co-authored-by: Cursor <[email protected]>

* refactor(docs): inline Used by section, drop home-section partial

Co-authored-by: Cursor <[email protected]>

* fix(docs): drop redundant logo styles from .contributors

Co-authored-by: Cursor <[email protected]>

* fix(docs): build liquid bundle before hexo serve

Co-authored-by: Cursor <[email protected]>

* refactor(docs): drop playground window chrome from capture demo

Co-authored-by: Cursor <[email protected]>

* refactor(docs): revert playground capture changes to master behavior

Restore Ace output pane, drop Prism and output-preview styling. Simplify docs:dev to rely on docs prestart.

Co-authored-by: Cursor <[email protected]>

* refactor(docs): rely on docs prebuild for liquid bundle and contributors

Co-authored-by: Cursor <[email protected]>

* feat(docs): show playground output as Prism-highlighted HTML code

Co-authored-by: Cursor <[email protected]>

* feat(docs): polish playground layout and regenerate README demo GIF

* fix(docs): align playground GIF capture with live editor styling

* fix(docs): unify playground pane padding and hold output on errors

Match editor inset to the output panel, drop Prism from output preview,
keep the last render while typing invalid template/context, and refresh
the README demo GIF.

* fix(docs): regenerate playground GIF with held output during typing

* feat(docs): sync Used by logos and polish playground

Inline README Used by grid on the docs homepage, refine playground layout and live output behavior, and drop the unused build-used-by script from package scripts.

Co-authored-by: Cursor <[email protected]>

* fix(docs): restore Rock RMS logo and remove duplicate entry

Restore the official Rock RMS wordmark (GetImage.ashx?id=72534) instead of the SparkDevNetwork GitHub org avatar that was wrongly substituted for it.

Co-authored-by: Cursor <[email protected]>

* fix(docs): regenerate playground GIF with live indicator states

Restore the capture script for the new pane-indicator layout so the README demo shows correct idle/active/pending/ok colors and pulsing animations while typing.

* fix(docs): static playground GIF with correct indicator colors

Capture one frame per keystroke with animations disabled so dot states
(idle/active/pending/ok) match the live playground without pulsing.

Co-authored-by: Cursor <[email protected]>

* docs: use square Rock RMS icon in Used by section

Co-authored-by: Cursor <[email protected]>

* fix(docs): ensure capture indicator colors apply instantly

Disable indicator transitions and cancel active animations before
setting data-state so pending yellow is not stuck on the prior ok green.

Co-authored-by: Cursor <[email protected]>

* docs: point Microsoft Used by link to microsoft.com

The merged tile title covers Power Pages and Azure API Management; href should go to Microsoft home, not Power Pages only.

Co-authored-by: Cursor <[email protected]>

* docs: remove Dailycontributors from Used by section

No evidence they run on LiquidJS; they are an OpenCollective sponsor only.

Co-authored-by: Cursor <[email protected]>

* docs: reword intro to say Liquid, not Shopify Liquid

Move Shopify into the compatibility list and drop the shopify/liquid link from README; align package.json description.

Co-authored-by: Cursor <[email protected]>

* fix(docs): restore playground output as Prism-highlighted HTML

Co-authored-by: Cursor <[email protected]>

* docs: add extensible to README intro and package description

EOF

Co-authored-by: Cursor <[email protected]>

* chore: move playground capture script to .local

Co-authored-by: Cursor <[email protected]>

* fix(docs): drop unused Ace mode-html from playground

Output pane uses Prism, not Ace; template and context editors still need liquid/json modes and basePath for themes.

Co-authored-by: Cursor <[email protected]>

* docs: sync intro sentence across package and site metadata

Align package.json, docs config, manifest, llms.txt, and AGENTS.md tagline to the README canonical description.

Co-authored-by: Cursor <[email protected]>

* docs: trim verbose intro in intro-to-liquid tutorial

Remove README tagline and repo-purpose copy duplicated by the recent metadata sync.

Co-authored-by: Cursor <[email protected]>

* docs: shorten homepage banner subtitle

Trim docs site banner and short taglines after the em dash; keep full description for meta tags and npm/README.

* docs: simplify playground GIF caption in README

Co-authored-by: Cursor <[email protected]>

* docs: dedupe homepage subtitle and description into _config.yml

Remove redundant front matter from index.pug; theme falls back to site config for banner and meta tags.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-06-23 17:39:19 +08:00
Max MedveandGitHub 3a0d80d1f4 perf(parser): memoize createTrie to avoid rebuilding tries per Tokenizer (#911)
The Tokenizer constructor calls createTrie(operators) and
createTrie(literalValues) on every instantiation, and liquidjs builds a
fresh Tokenizer per output/tag while parsing. On typical templates this
rebuilt the same prefix-tries dozens of times and showed up as a large
share of parse CPU in profiling.

Memoize createTrie with a module-level WeakMap keyed on the input object.
The inputs (operators, literalValues) are stable references and the trie
is only ever read afterward (via matchTrie), never mutated, so caching by
reference is behavior-preserving. WeakMap (not Map) lets short-lived,
per-instance operator objects and their tries be garbage collected.
2026-06-22 20:11:06 +08:00
Timmy BraunandGitHub 956b51ea95 fix: improve round function; improvement to #873 (#901) 2026-06-22 20:07:27 +08:00
Yang JunandGitHub 5c3522f339 fix(strip_html): infinite loop for strip_html 2026-06-22 02:28:07 +08:00
6d00257e15 docs: add AGENTS.md and llms.txt for AI agents (#919)
Co-authored-by: Cursor <[email protected]>
2026-06-22 02:24:54 +08:00
03a30e6dc4 docs: replace CookieHub with cookieconsent (#918)
Co-authored-by: Cursor <[email protected]>
2026-06-22 01:34:53 +08:00
4775227358 docs(security): route vulnerability reports to GitHub Advisories (#913)
Replace the private email contact with GitHub Security Advisories and
set the common-case fix expectation to within a month.

Co-authored-by: Cursor <[email protected]>
2026-06-20 00:00:05 +08:00
40 changed files with 783 additions and 220 deletions
-17
View File
@@ -1,17 +0,0 @@
---
description: Architecture overview for liquidjs internals
alwaysApply: true
---
## Async/sync duality via generators
All core logic is written once as a `Generator` function (`function *`). Use `yield` where you'd normally `await` a potentially async value.
- `toPromise(generator)` drives it **asynchronously** — awaits yielded promises.
- `toValueSync(generator)` drives it **synchronously** — passes yielded values through as-is.
Never duplicate logic into separate async and sync methods. A single generator serves both paths.
When wrapping an async+sync function pair (e.g. `contains`/`containsSync`, `exists`/`existsSync`, `readFile`/`readFileSync`), use `toLiquidAsync(asyncFn, syncFn?)` which returns a `LiquidAsync<F>` — one function that picks the sync or async implementation based on a leading `sync: boolean` arg. Then `yield` the result inside a generator to let the driver handle it in both modes.
See `src/util/async.ts`.
-6
View File
@@ -1,6 +0,0 @@
---
description: Project conventions for liquidjs
alwaysApply: true
---
- Keep edits minimal: change only what the task requires, match existing style.
-17
View File
@@ -1,17 +0,0 @@
---
description: Testing conventions — e2e uses built dist, integration uses src
globs: test/**/*.ts
alwaysApply: false
---
# Testing
## End-to-end tests (`test/e2e`)
- **Use the built package**, not TypeScript sources under `src/`.
- Import the public API from the package root (for example `import { Liquid } from '../..'`), which resolves through `package.json` to **`dist/`** (`main`, `module`, etc.).
- **Avoid** `import … from '../../src/liquid'` (or other `src/` paths) in `test/e2e/**` so e2e matches what consumers get from npm and you do not depend on an unbuilt tree.
## Integration and unit tests
- Tests under `test/integration/`, `src/**/*.spec.ts`, and similar may import from **`src/`** when the suite is meant to run against the current TypeScript sources (typical for this repos Jest setup).
+2
View File
@@ -11,9 +11,11 @@ coverage/
node_modules/
# tmp
.local/
docs/themes/navy/source/js/liquid.browser.min.js
docs/themes/navy/layout/partial/all-contributors.swig
docs/themes/navy/layout/partial/financial-contributors.swig
docs/themes/navy/layout/partial/used-by.swig
dist/
demo/*/yarn.json
+94
View File
@@ -0,0 +1,94 @@
# LiquidJS
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support. TypeScript in `src/`, bundles in `dist/`. Docs site in `docs/` (Hexo, `navy` theme).
## Layout
| Path | Contents |
| --- | --- |
| `src/parser`, `src/render`, `src/tags`, `src/filters` | Template parse and render |
| `src/context`, `src/template`, `src/tokens` | Scope, templates, token stream |
| `src/util/async.ts` | `toPromise`, `toValueSync`, `toLiquidAsync` |
| `test/` | Jest |
| `docs/source/` | Doc markdown; sidebar in `docs/source/_data/sidebar.yml` |
| `docs/themes/navy/` | Layout, CSS, JS |
| `.local/` | Scratch, repro, PoC (gitignored) |
## Commands
```
npm run build # after src/ changes, before npm test
npm test
npm run lint
npm run check # build + build:docs + test + lint + perf:diff (manual)
npm run build:docs
cd docs && npm start # http://localhost:4000
npm run perf:diff
```
PR CI (`pull_request`): build, lint, test, coverage, performance. Docs build runs on push to `master` only.
PR titles: conventional format (`feat:`, `fix:`, `docs:`, …) — checked by CI. Releases on `master` use semantic-release from merged commits.
Backward-compatible API changes expected unless doing an intentional major break.
## Architecture
All core logic is one `function *` per feature. Use `yield` where you'd normally `await` a potentially async value.
- `toPromise(generator)` — async driver; awaits yielded promises
- `toValueSync(generator)` — sync driver; passes yielded values through as-is
Never duplicate logic into separate async and sync methods. One generator serves both paths.
When wrapping an async+sync pair (e.g. `contains`/`containsSync`, `readFile`/`readFileSync`), use `toLiquidAsync(asyncFn, syncFn?)` — returns a `LiquidAsync<F>` that picks sync or async via a leading `sync: boolean` arg. `yield` the result inside a generator. See `src/util/async.ts`.
## Style
Make minimal changes only. Avoid sweeping edits. Always check after you made changes.
- Change only what the task requires. No drive-by refactors, test harnesses, or extra files unless asked.
- Match existing patterns in the file you edit.
- Repro, PoC, and scratch files go in `.local/` — not tracked `poc/` folders or one-off scripts under `docs/`.
### Comments
- Do not add narrative comments. Code should be clear from structure and naming; if it needs explanation, refactor instead.
- Comments follow existing repo usage only: non-obvious invariants, `@deprecated`, JSDoc on public API where TypeDoc needs it. Not for explaining changes to the author, migration history, or restating what the code already says.
- Comments document the code; they do not fix unclear code.
### Tests
- Assert observable behavior, not internal implementation details.
- Avoid duplicate coverage; keep test diffs minimal.
- **E2E** (`test/e2e/`): import from the package root (resolves to `dist/` via `package.json`). Do not import from `src/` — e2e must match what npm consumers get.
- **Integration/unit** (`test/integration/`, etc.): may import from `src/` against current TypeScript sources.
### Docs site
- Reuse existing asset paths under `docs/source/` and `docs/themes/navy/` — no new asset directories unless asked.
- Front matter `title:` is plain text (no backticks).
- `docs/source/llms.txt` — deployed to https://liquidjs.com/llms.txt for web agents (llms.txt spec).
- After theme/markdown changes: build or serve locally, check in a browser (light and dark), not only curl or editor preview.
### README
- Research original sources before reordering contributors, logos, or lists.
## Verify
- Do not commit, push, amend, or open a PR unless asked.
- After changes: verify yourself via CLI or UI (tests, `cd docs && npm start`, browser) before reporting done. Do not tell the user to check instead.
- Before push on sweeping changes: run `npm run check`.
- Confirm facts from `.github/workflows`, `package.json`, and library docs — not stale human docs or assumptions.
- When replacing or integrating a library: read its docs and understand what the previous setup did before changing behavior.
### Security fixes
- Reproduce on current `master` first. Smallest fix that addresses the reported issue.
- If Shopify/Ruby Liquid behaves the same, document unsafe usage in filter/docs instead of changing behavior.
## Docs
- Published: https://liquidjs.com
- Repo agent instructions: this file (`AGENTS.md`)
+14
View File
@@ -1,3 +1,17 @@
## [10.27.1](https://github.com/harttle/liquidjs/compare/v10.27.0...v10.27.1) (2026-06-23)
### Bug Fixes
* improve round function; improvement to [#873](https://github.com/harttle/liquidjs/issues/873) ([#901](https://github.com/harttle/liquidjs/issues/901)) ([956b51e](https://github.com/harttle/liquidjs/commit/956b51ea953eb52d9eba7409b7f51e379023fec4))
* **security:** charge pop filter allocation to memoryLimit ([#907](https://github.com/harttle/liquidjs/issues/907)) ([8a0c74a](https://github.com/harttle/liquidjs/commit/8a0c74a7fcb1671aa1dcb71ec82ba0602dc90d04))
* **strip_html:** infinite loop for strip_html ([5c3522f](https://github.com/harttle/liquidjs/commit/5c3522f33928aae66f0fe85c36e1d9015c768fe2))
### Performance Improvements
* **parser:** memoize createTrie to avoid rebuilding tries per Tokenizer ([#911](https://github.com/harttle/liquidjs/issues/911)) ([3a0d80d](https://github.com/harttle/liquidjs/commit/3a0d80d1f4526af0fbca2bb2e0a9c51669d2fd3e))
# [10.27.0](https://github.com/harttle/liquidjs/compare/v10.26.0...v10.27.0) (2026-05-15)
+58 -42
View File
@@ -1,72 +1,86 @@
# liquidjs
# LiquidJS
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.
[![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)
[![Coverage](https://img.shields.io/coveralls/harttle/liquidjs.svg?style=flat-square)](https://coveralls.io/github/harttle/liquidjs?branch=master)
[![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)
[![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)
A simple, expressive and safe [Shopify][shopify/liquid] / GitHub Pages compatible template engine in pure JavaScript.
**The purpose of this repo** is to provide a standard Liquid implementation for the JavaScript community so that [Jekyll sites](https://jekyllrb.com), [GitHub Pages](https://pages.github.com/) and [Shopify templates](https://themes.shopify.com/) can be ported to Node.js without pain.
[Documentation][doc] · [Playground](https://liquidjs.com/playground.html) · [Setup guide][setup] · [Contributing][contribution]
* [Documentation][doc]
* Please star [LiquidJS on GitHub][github]!
* Financial support via [GitHub Sponsors](https://github.com/sponsors/harttle).
<a href="https://liquidjs.com/playground.html">
<img src="docs/source/playground-demo.gif" alt="LiquidJS playground: edit a template and context, see live HTML output" width="980" style="display: block; margin: 0 auto;" />
</a>
<p align="center"><a href="https://liquidjs.com"><img height="155px" width="155px" src="https://liquidjs.com/icon/mstile-310x310.png" alt="logo"></a></p>
<p align="center"><sub>Try the <a href="https://liquidjs.com/playground.html">online playground</a>.</sub></p>
## What's it like?
## Quick start
Basically there're two types of Liquid syntax: tags enclosed by `{% %}` and outputs enclosed by `{{ }}`. A Liquid template looks like:
```js
import { Liquid } from 'liquidjs'
```liquid
{% if username %}
{{ username | append: ", welcome to LiquidJS!" | capitalize }}
{% endif %}
const engine = new Liquid()
const html = await engine.parseAndRender(
'Hello, {{ name | capitalize }}!',
{ name: 'liquid' }
)
//=> 'Hello, Liquid!'
```
[A live demo](https://liquidjs.com/playground.html) is also available and here's a [quick tutorial](https://liquidjs.com/tutorials/intro-to-liquid.html) for Liquid syntax.
## Installation
Install from npm in Node.js:
**Node.js**
```bash
npm install liquidjs
```
Or use the UMD bundle from jsDelivr:
**Browser** (jsDelivr UMD bundle)
```html
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
```
Or render directly from CLI using npx:
**CLI**
```bash
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Liquid"}'
```
For more details, refer to the [Setup Guide][setup].
See the [setup guide][setup] for partials, layouts, caching, and other options.
## Who's Using LiquidJS?
## Used by
- [Eleventy](https://www.11ty.dev/): Eleventy, a simpler static site generator.
- [Github Docs](https://github.com/github/docs): The open-source repo for docs.github.com.
- [Kibana](https://github.com/elastic/kibana): Elastic's analytics and visualization platform for Elasticsearch; workflow features use LiquidJS for Liquid templates.
- [Opensense](https://www.opensense.com/): The smarter way to send email.
- [Directus](https://docs.directus.io/): an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
- [Rock](https://www.rockrms.com/): An open source CMS, Relationship Management System (RMS) and Church Management System (ChMS) all rolled into one.
- [Mitosis](https://github.com/BuilderIO/mitosis): Write components once, run everywhere. Compiles to React, Vue, Qwik, Solid, Angular, Svelte, and more.
- [Pattern Lab](https://patternlab.io/): a frontend workshop environment that helps you build, view, test, and showcase your design system's UI components.
- [Builder.io](https://www.builder.io/m/developers): the first and only headless CMS with a visual editor that lets you drag and drop with your components, directly within your current site or app. Completely API-driven, for cleaner code and simpler workflows.
- [Microsoft Power Pages](https://learn.microsoft.com/en-us/power-pages/introduction): a secure, enterprise-grade, low-code software as a service (SaaS) platform for creating, hosting, and administering modern external-facing business websites.
- [Azure API Management developer portal](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal): an automatically generated, fully customizable website with the documentation of your APIs.
- [WISMOlabs](https://wismolabs.com/): Post Purchase Experience platform for eCommerce retailers enhancing customer satisfaction by using LiquidJS to provide customizable post-purchase experiences through programmable email, SMS, order tracking pages, and webhooks.
- [Freshet](https://chromewebstore.google.com/detail/freshet/mpclplhdencffbilobpcapccnihpelcg): *JSON in, page out* — a Chrome extension that uses LiquidJS templates per URL pattern, so the JSON becomes a rendered, useful page.
<!-- USED-BY-BEGIN -->
<p align="center" style="line-height: 2.5;">
<a href="https://www.11ty.dev/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/35147177?v=4&s=100" height="80" style="vertical-align: middle;" alt="Eleventy" title="Eleventy"/></a>
<a href="https://www.opensense.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Opensense" title="Opensense"/></a>
<a href="https://www.microsoft.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" height="80" style="vertical-align: middle;" alt="Power Pages, Azure API Management developer portal" title="Power Pages, Azure API Management developer portal"/></a>
<a href="https://docs.github.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/9919?v=4&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 -->
Feel free to create a PR or contact me to add your use case into this list!
Products and projects running on LiquidJS. [Open a PR](https://github.com/harttle/liquidjs/edit/master/README.md) to add yours.
## Financial Support
@@ -78,7 +92,7 @@ If you personally love LiquidJS or it's benefiting your business, please conside
<a href="https://www.opensense.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/opensense-inc/bf840ae/logo/256.png?height=100" height="80" style="vertical-align: middle;" alt="Opensense Inc." title="Opensense"/></a>
<a href="https://github.com/microsoft" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/6154722?v=4&s=100" height="80" style="vertical-align: middle;" alt="Microsoft" title="Microsoft"/></a>
<a href="https://sentry.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1396951?v=4&s=100" height="80" style="vertical-align: middle;" alt="Sentry" title="Sentry"/></a>
<a href="https://www.checkoutblocks.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/114603307?v=4&s=100" height="80" style="vertical-align: middle;" alt="Checkout Blocks" title="Checkout Blocks"/></a>
<a href="https://github.com/Checkout-Blocks" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/114603307?v=4&s=100" height="80" style="vertical-align: middle;" alt="Checkout Blocks" title="Checkout Blocks"/></a>
<a href="https://customer.io/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/1152079?v=4&s=100" height="80" style="vertical-align: middle;" alt="Customer IO" title="Customer IO"/></a>
<a href="https://syntax.fm/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/130389858?v=4&s=100" height="80" style="vertical-align: middle;" alt="Syntax Podcast" title="Syntax Podcast"/></a>
<br/>
@@ -234,8 +248,10 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
<!-- ALL-CONTRIBUTORS-LIST:END -->
[shopify/liquid]: https://shopify.github.io/liquid/
[plugins]: https://liquidjs.com/tutorials/plugins.html#Plugin-List
## License
[MIT](LICENSE) © [Jun Yang](https://github.com/harttle)
[setup]: https://liquidjs.com/tutorials/setup.html
[doc]: https://liquidjs.com
[github]: https://github.com/harttle/liquidjs
+5 -3
View File
@@ -6,8 +6,10 @@ Only the latest major version is supported with security updates. It can be chan
## Reporting a Vulnerability
Please contact harttleharttle@gmail.com to report a vulnerability or change request.
**Please do not report security vulnerabilities through public GitHub issues.**
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within 1 week).
Report them via [GitHub Security Advisories — Report a vulnerability](https://github.com/harttle/liquidjs/security/advisories/new).
- If the vulnerability in question affects common use cases, it will be treated as a bug and fixed very soon (typically within a month).
- Otherwise, it'll be scheduled in the same priority of feature request (which is lower than bugs).
- If the request is declined, you'll receive a reply email anyway (most likely there will be a discussion).
- If the request is declined, you'll receive a reply anyway (most likely there will be a discussion).
+2
View File
@@ -33,7 +33,9 @@ function transformFinancial (html) {
const allContributors = transformContributors(extractSection(readme, 'ALL-CONTRIBUTORS-LIST:START', 'ALL-CONTRIBUTORS-LIST:END'))
const financialContributors = transformFinancial(extractSection(readme, 'FINANCIAL-CONTRIBUTORS-BEGIN', 'FINANCIAL-CONTRIBUTORS-END'))
const usedBy = transformFinancial(extractSection(readme, 'USED-BY-BEGIN', 'USED-BY-END'))
const outDir = path.join(root, 'docs/themes/navy/layout/partial')
fs.writeFileSync(path.join(outDir, 'all-contributors.swig'), allContributors)
fs.writeFileSync(path.join(outDir, 'financial-contributors.swig'), financialContributors)
fs.writeFileSync(path.join(outDir, 'used-by.swig'), usedBy)
+2 -2
View File
@@ -1,6 +1,6 @@
title: LiquidJS
subtitle: "A simple, expressive, and safe template engine for JavaScript."
description: "LiquidJS is a simple, expressive, and safe template engine for JavaScript, compatible with Shopify and GitHub Pages."
subtitle: "A simple, expressive, extensible Liquid template engine for JavaScript"
description: "A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support."
author: Harttle
language: en
timezone: UTC
+8
View File
@@ -6,6 +6,10 @@ title: strip_html
Removes any HTML tags from a string.
{% note warn Not safe for HTML output %}
This filter removes tags by string scanning; it does not parse HTML5 the way a browser does, and it is not a sanitizer. The result may still be unsafe when inserted into HTML. Use [escape][escape], [escape_once][escape_once], or [`outputEscape: "escape"`][outputEscape] for untrusted output.
{% endnote %}
Input
```liquid
{{ "Have <em>you</em> read <strong>Ulysses</strong>?" | strip_html }}
@@ -15,3 +19,7 @@ Output
```text
Have you read Ulysses?
```
[escape]: ./escape.html
[escape_once]: ./escape.html
[outputEscape]: ../tutorials/options.html#outputEscape
-2
View File
@@ -1,6 +1,4 @@
layout: index
description: LiquidJS is a simple, expressive and safe Shopify / GitHub Pages compatible template engine in pure JavaScript.
subtitle: A simple, expressive and safe template engine.
---
ul#intro-feature-list
li.intro-feature-wrap
+28
View File
@@ -0,0 +1,28 @@
# LiquidJS
> A simple, expressive, extensible Liquid template engine for JavaScript
## Tutorials
- [Introduction to Liquid](https://liquidjs.com/tutorials/intro-to-liquid.html)
- [Setup](https://liquidjs.com/tutorials/setup.html)
- [Options](https://liquidjs.com/tutorials/options.html)
- [Render files](https://liquidjs.com/tutorials/render-file.html)
- [Partials and layouts](https://liquidjs.com/tutorials/partials-and-layouts.html)
- [Express.js](https://liquidjs.com/tutorials/use-in-expressjs.html)
- [Register filters and tags](https://liquidjs.com/tutorials/register-filters-tags.html)
- [Plugins](https://liquidjs.com/tutorials/plugins.html)
- [Sync and async](https://liquidjs.com/tutorials/sync-and-async.html)
- [Operators](https://liquidjs.com/tutorials/operators.html)
- [Truthy and falsy](https://liquidjs.com/tutorials/truthy-and-falsy.html)
- [Security model](https://liquidjs.com/tutorials/security-model.html)
- [Differences from Shopify Liquid](https://liquidjs.com/tutorials/differences.html)
- [Migrate to v9](https://liquidjs.com/tutorials/migrate-to-9.html)
- [Changelog](https://liquidjs.com/tutorials/changelog.html)
## Reference
- [Tags](https://liquidjs.com/tags/overview.html)
- [Filters](https://liquidjs.com/filters/overview.html)
- [API (TypeDoc)](https://liquidjs.com/api/)
- [Playground](https://liquidjs.com/playground.html)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"short_name": "LiquidJS",
"name": "LiquidJS",
"description": "A simple, expressive and safe Shopify / GitHub Pages compatible template engine in pure JavaScript.",
"description": "A simple, expressive, extensible Liquid template engine for JavaScript",
"icons": [
{
"src": "/icon/apple-touch-icon-57x57.png",
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+2 -3
View File
@@ -3,9 +3,9 @@ title: The Liquid Template Language
describe: A short introduction to the Liquid template language and some simple demos.
---
LiquidJS is a simple, expressive and safe [Shopify][shopify/liquid] / GitHub Pages compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by GitHub Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff].
Liquid is a template language originally implemented in Ruby and used by Shopify, Jekyll, and GitHub Pages. LiquidJS implements it in JavaScript; see [Differences with Shopify/liquid][diff] for compatibility notes.
LiquidJS syntax is relatively simple. There are 2 types of markups in LiquidJS:
There are 2 types of markups in LiquidJS:
- **Tags**. A tag consists of a tag name and optional arguments wrapped between `{%raw%}{%{%endraw%}` and `%}`.
- **Outputs**. An output consists of a value and a list of filters, which is optional, wrapped between `{%raw%}{{{%endraw%}` and `}}`.
@@ -56,5 +56,4 @@ Typically tags appear in pairs with a start tag and a corresponding end tag. For
A complete list of tags supported by LiquidJS can be found [here](../tags/overview.html).
[shopify/liquid]: https://github.com/Shopify/liquid
[diff]: ./differences.html
+5 -1
View File
@@ -13,10 +13,14 @@ 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: 'If you personally love LiquidJS or it&#39;s benefiting your business, please <a href="https://github.com/sponsors/harttle">sponsor us</a>!'
description: 'Organizations and individuals who <a href="https://github.com/sponsors/harttle">sponsor LiquidJS</a>. Thank you!'
used_by:
title: Used by
description: 'Products and projects running on LiquidJS. <a href="https://github.com/harttle/liquidjs/edit/master/README.md">Open a PR</a> to add yours.'
playground:
title: Playground
lead: Edit a template and context JSON — rendered HTML updates as you type.
loading: Loading...
page:
+18 -5
View File
@@ -1,6 +1,6 @@
<header id="banner" class="wrapper">
<div class="inner inner-content">
<h2 id="banner-title">{{ page.subtitle }}</h2>
<h2 id="banner-title">{{ page.subtitle | default(config.subtitle) }}</h2>
<div id="banner-share">{{ partial('partial/share') }}</div>
<div id="banner-start">
<code id="banner-start-command">npm install liquidjs</code><a id="banner-start-link" href="./tutorials/setup.html"><i class="icon-arrow-right"></i></a>
@@ -14,15 +14,15 @@
</div>
</div>
</div>
<div id="contributors-wrap">
<div id="used-by-wrap">
<div class="wrapper">
<div class="inner inner-content">
<div class="section-header">
<h3>{{__('index.contributors.title')}}</h3>
<p class="description">{{__('index.contributors.description')}}</p>
<h3>{{__('index.used_by.title')}}</h3>
<p class="description">{{__('index.used_by.description')}}</p>
</div>
<div class="contributors">
{{ partial('partial/all-contributors') }}
{{ partial('partial/used-by') }}
</div>
</div>
</div>
@@ -40,3 +40,16 @@
</div>
</div>
</div>
<div id="contributors-wrap">
<div class="wrapper">
<div class="inner inner-content">
<div class="section-header">
<h3>{{__('index.contributors.title')}}</h3>
<p class="description">{{__('index.contributors.description')}}</p>
</div>
<div class="contributors">
{{ partial('partial/all-contributors') }}
</div>
</div>
</div>
</div>
+6 -3
View File
@@ -1,6 +1,11 @@
{% if page.layout === 'playground' %}
{{ js('js/liquid.browser.min.js') }}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/ace.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/mode-liquid.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src-min/mode-json.js"></script>
<script>ace.config.set('basePath', 'https://cdn.jsdelivr.net/npm/[email protected]/src-min/');</script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/components/prism-markup.min.js"></script>
{% endif %}
{{ js('js/main') }}
@@ -17,12 +22,10 @@ debug: false
});
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GM713991QQ"></script>
<script>
<script type="text/plain" data-category="analytics">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-GM713991QQ');
</script>
+1 -5
View File
@@ -1,7 +1,3 @@
{
"people": [
"alice",
"bob",
"carol"
]
"name": "liquid"
}
+1 -9
View File
@@ -1,9 +1 @@
<ul>
{%- for person in people %}
<li>
<a href="{{person | prepend: "https://example.com/"}}">
{{ person | capitalize }}
</a>
</li>
{%- endfor%}
</ul>
<p>Hello, {{ name | capitalize }}!</p>
+1
View File
@@ -5,6 +5,7 @@
{{__('footer.license')}}
</div>
<div id="footer-links">
<button type="button" class="footer-link cookie-preferences" data-cc="show-consentModal" title="Manage cookie preferences"><i class="icon-shield"></i></button>
<a href="https://twitter.com/{{ config.twitter }}" class="footer-link" target="_blank"><i class="icon-twitter"></i></a>
<a href="https://opencollective.com/{{ config.oc }}" class="footer-link" target="_blank"><i class="icon-opencollective"></i></a>
<a href="https://github.com/{{ config.github }}" class="footer-link" target="_blank"><i class="icon-github"></i></a>
+3 -8
View File
@@ -32,12 +32,7 @@
<meta name="msapplication-TileImage" content="{{ url_for('icon/mstile-144x144.png') }}">
{{ css('css/navy') }}
{{ feed_tag('atom.xml') }}
<script src="https://cdn.cookiehub.eu/c2/e8e44c93.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return;
var cpm = {};
window.cookiehub.load(cpm);
});
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orestbida/[email protected]/dist/cookieconsent.css">
<script src="https://cdn.jsdelivr.net/gh/orestbida/[email protected]/dist/cookieconsent.umd.js"></script>
{{ js('js/cookieconsent-config') }}
</head>
+37 -13
View File
@@ -1,21 +1,45 @@
<div id="playground" role="main">
<div class="wrapper">
<h1 class="inner">{{__('playground.title')}}</h1>
<header class="playground-hero inner">
<div class="playground-hero-text">
<h1>{{__('playground.title')}}</h1>
<p class="playground-lead">{{__('playground.lead')}}</p>
</div>
<p class="playground-version version"></p>
</header>
<div class="loader" role=status aria-busy=true></div>
<div id="editors" class="inner hide" aria-hide=true>
<div class="area-tpl editor-wrapper">
<h2>Template</h2>
<div class="editor" id="editorEl">{{ raw('partial/demo.liquid') }}</div>
</div>
<div class="area-data editor-wrapper">
<h2>Context</h2>
<div class="editor" id="dataEl">{{ raw('partial/demo.json') }}</div>
</div>
<div class="area-output editor-wrapper">
<h2>Output</h2>
<div class="editor" id="previewEl">{{__('playground.loading')}}</div>
<div class="playground-workspace">
<div class="playground-pane area-tpl">
<div class="pane-head">
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
<h2>Template</h2>
</div>
<div class="pane-body">
<div class="editor" id="editorEl">{{ raw('partial/demo.liquid') }}</div>
</div>
</div>
<div class="playground-pane area-data">
<div class="pane-head">
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
<h2>Context</h2>
</div>
<div class="pane-body">
<div class="editor" id="dataEl">{{ raw('partial/demo.json') }}</div>
</div>
</div>
<div class="playground-pane area-output">
<div class="pane-head">
<span class="pane-indicator" data-state="idle" aria-hidden="true"></span>
<h2>Output</h2>
</div>
<div class="pane-body">
<div class="output-preview" id="previewEl">
<pre class="highlight"><code class="language-markup" id="previewCode">{{__('playground.loading')}}</code></pre>
</div>
</div>
</div>
</div>
</div>
<p class="inner version"></p>
</div>
</div>
+15
View File
@@ -0,0 +1,15 @@
#cc-main
--cc-btn-primary-bg: var(--color-link)
--cc-btn-primary-color: #fff
--cc-btn-primary-border-color: var(--color-link)
--cc-btn-primary-hover-bg: var(--color-link-hover)
--cc-btn-primary-hover-border-color: var(--color-link-hover)
--cc-btn-secondary-bg: #eaeff2
--cc-btn-secondary-color: var(--color-default)
--cc-btn-secondary-border-color: #eaeff2
--cc-toggle-on-bg: var(--color-link)
.cc--darkmode #cc-main
--cc-btn-secondary-bg: #3a4248
--cc-btn-secondary-color: var(--color-default)
--cc-btn-secondary-border-color: #3a4248
+7
View File
@@ -42,6 +42,13 @@
@media mq-normal
font-size: 30px
.cookie-preferences
background: none
border: 0
padding: 0
cursor: pointer
color: inherit
.icon-oc
height: 36px
width: 30px
+2 -2
View File
@@ -126,7 +126,7 @@
background: var(--color-link-hover)
color: #fff
#sponsors-wrap, #contributors-wrap
#used-by-wrap, #sponsors-wrap, #contributors-wrap
background: var(--color-navy-lighter)
border-top: 1px solid #161d24
border-bottom: 1px solid #161d24
@@ -181,7 +181,7 @@
#contributors-wrap
border: none
overflow: hidden;
overflow: hidden
.contributors
tr
+260 -55
View File
@@ -1,107 +1,312 @@
#playground
--playground-gap: 12px
--playground-radius: 10px
--playground-inset: 16px
background: var(--color-content-bg)
overflow: hidden
box-shadow: var(--panel-shadow)
.wrapper
margin-bottom: 40px
margin-bottom: 32px
@media mq-mobile
margin-bottom: 20px
.playground-hero
display: flex
flex-wrap: wrap
align-items: flex-end
justify-content: space-between
gap: 16px 24px
padding-top: 32px
padding-bottom: 20px
@media mq-mobile
padding-top: 16px
padding-bottom: 12px
gap: 10px
align-items: flex-start
.playground-hero-text
flex: 1 1 280px
min-width: 0
h1
font-size: 36px
font-weight: 300
margin-top: 40px
margin-bottom: 24px
color: var(--color-default)
h2
font-size: 0.8125rem
font-size: 28px
font-weight: 600
text-transform: uppercase
letter-spacing: 0.04em
color: var(--color-gray)
letter-spacing: -0.02em
margin: 0 0 8px
color: var(--color-default)
@media mq-mobile
font-size: 22px
margin-bottom: 4px
.playground-lead
margin: 0
font-size: 15px
line-height: 1.5
color: var(--color-gray)
@media mq-mobile
font-size: 14px
line-height: 1.45
.playground-version
flex: 0 0 auto
margin: 0
font-size: 12px
line-height: 1.4
font-family: font-mono
padding: 6px 12px
border-radius: 999px
background: var(--playground-surface)
border: 1px solid var(--color-border)
color: var(--color-gray)
a
color: var(--color-default)
text-decoration: none
font-weight: 500
&:hover
color: var(--color-link)
text-decoration: none
#editors
display: grid
overflow: hidden
margin-bottom: 0
height: 75vh
min-height: 480px
.playground-workspace
display: grid
gap: var(--playground-gap)
grid-template-columns: 1fr 1fr
grid-template-rows: 3fr 2fr
grid-gap: 16px
align-items: stretch
@media mq-normal
overflow: hidden
height: 75vh
max-height: unquote('calc(100vh - 200px)')
min-height: 520px
@media mq-mobile
height: auto
min-height: 0
grid-template-columns: 1fr
grid-template-rows: auto
grid-gap: 20px
gap: 12px
.area-tpl
grid-row: 1
grid-column: 1
min-height: 0
--pane-dot: var(--color-link)
.area-data
grid-row: 2
grid-column: 1
min-height: 0
--pane-dot: var(--highlight-orange)
.area-output
grid-column: 2
grid-row: 1 / -1
min-height: 0
min-width: 0
--pane-dot: var(--highlight-green)
@media mq-mobile
grid-row: auto
grid-column: 1
.editor-wrapper
.playground-pane
display: flex
gap: 8px
flex-direction: column
min-height: 0
overflow: hidden
.editor
flex: 1 1 auto
min-height: 0
position: relative
code-block-chrome()
overflow: hidden
@media mq-mobile
min-height: 240px
.ace_editor
font-family: font-mono
font-size: 14px
line-height: 1.5
border-radius: 6px
.ace_scrollbar
z-index: 2
background: var(--playground-pane-head)
border: 1px solid var(--code-border)
border-radius: var(--playground-radius)
box-shadow: var(--code-shadow)
.version
font-size: 0.8125rem
line-height: 1.5
.pane-head
display: flex
align-items: center
gap: 10px
flex-shrink: 0
height: 36px
padding: 0 var(--playground-inset)
border-bottom: 1px solid var(--code-border)
@media mq-mobile
height: 32px
padding: 0 10px
h2
font-size: 13px
font-weight: 600
letter-spacing: 0.01em
text-transform: none
color: var(--color-default)
margin: 0
@media mq-mobile
font-size: 12px
.pane-indicator
width: 8px
height: 8px
border-radius: 50%
flex-shrink: 0
background: unquote('color-mix(in srgb, var(--pane-dot) 38%, var(--color-border))')
transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease
&[data-state="active"]
background: var(--pane-dot)
animation: playground-dot-typing 0.85s ease-in-out infinite
&[data-state="pending"]
background: var(--highlight-yellow)
&[data-state="ok"]
background: var(--highlight-green)
animation: playground-dot-ok 0.45s ease-out
&[data-state="error"]
background: var(--highlight-red)
animation: playground-dot-error 0.35s ease-out
.area-output .pane-indicator
&[data-state="pending"]
animation: playground-dot-pending 0.55s ease-in-out infinite
@keyframes playground-dot-typing
0%, 100%
transform: scale(1)
box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--pane-dot) 0%, transparent)')
50%
transform: scale(1.2)
box-shadow: 0 0 0 4px unquote('color-mix(in srgb, var(--pane-dot) 28%, transparent)')
@keyframes playground-dot-pending
0%, 100%
transform: scale(1)
opacity: 0.75
50%
transform: scale(1.12)
opacity: 1
@keyframes playground-dot-ok
0%
transform: scale(0.85)
box-shadow: 0 0 0 0 unquote('color-mix(in srgb, var(--highlight-green) 50%, transparent)')
70%
transform: scale(1.15)
box-shadow: 0 0 0 5px unquote('color-mix(in srgb, var(--highlight-green) 0%, transparent)')
100%
transform: scale(1)
box-shadow: none
@keyframes playground-dot-error
0%, 100%
transform: translateX(0)
20%
transform: translateX(-2px)
40%
transform: translateX(2px)
60%
transform: translateX(-1px)
80%
transform: translateX(1px)
.pane-body
flex: 1 1 auto
min-height: 0
min-width: 0
display: flex
flex-direction: column
overflow: hidden
background: var(--highlight-background)
.area-tpl .pane-body,
.area-data .pane-body
padding: var(--playground-inset)
box-sizing: border-box
@media mq-mobile
padding: 12px
.area-tpl .ace_gutter,
.area-data .ace_gutter
display: none
width: 0
min-width: 0
.area-tpl .ace_editor,
.area-data .ace_editor,
.area-tpl .ace_scroller,
.area-data .ace_scroller,
.area-tpl .ace_content,
.area-data .ace_content,
.area-tpl .ace_text-layer,
.area-data .ace_text-layer
background: transparent
.editor
flex: 1 1 auto
min-height: 0
position: relative
overflow: hidden
@media mq-mobile
min-height: 180px
.output-preview
flex: 1 1 auto
min-height: 0
min-width: 0
width: 100%
overflow: auto
@media mq-mobile
min-height: 120px
pre.highlight
margin: 0
min-height: 100%
width: 100%
box-sizing: border-box
padding: var(--playground-inset)
border: none
box-shadow: none
border-radius: 0
background: transparent
color: var(--highlight-foreground)
overflow-x: hidden
overflow-y: auto
white-space: pre-wrap
overflow-wrap: break-word
@media mq-mobile
padding: 12px
code
display: block
width: 100%
box-sizing: border-box
font-family: font-mono
font-size: 14px
line-height: 1.55
color: var(--highlight-foreground)
background: transparent
padding: 0
white-space: inherit
overflow-wrap: inherit
@media mq-mobile
font-size: 13px
.ace_editor
font-family: font-mono
color: var(--color-gray)
margin-top: 20px
margin-bottom: 32px
a
color: inherit
text-decoration: none
&:hover
color: var(--color-link)
text-decoration: underline
font-size: 14px
line-height: 1.55
border-radius: 0
@media mq-mobile
font-size: 13px
.ace_scrollbar
z-index: 2
.hide
display: none
.loader
width: 48px
height: 48px
margin: 150px auto 200px
border: 3px solid var(--color-border)
width: 40px
height: 40px
margin: 120px auto 160px
border: 2px solid var(--color-border)
border-top-color: var(--color-link)
border-radius: 50%
animation: spin 0.8s infinite linear
animation: playground-spin 0.7s infinite linear
@media mq-mobile
margin: 60px auto 80px
@keyframes spin
@keyframes playground-spin
100%
transform: rotate(360deg)
+4
View File
@@ -37,6 +37,8 @@ vendor-prefixes = webkit moz ms official
--highlight-aqua: #0550ae
--highlight-blue: #0550ae
--highlight-purple: #8250df
--playground-surface: #f3f4f6
--playground-pane-head: #fff
}
@media (prefers-color-scheme: dark) {
@@ -75,6 +77,8 @@ vendor-prefixes = webkit moz ms official
--highlight-aqua: #79c0ff
--highlight-blue: #79c0ff
--highlight-purple: #d2a8ff
--playground-surface: hsl(218, 26%, 10%)
--playground-pane-head: hsl(218, 22%, 16%)
}
}
+1
View File
@@ -9,6 +9,7 @@
@import "_partial/page"
@import "_partial/mobile_nav"
@import "_partial/footer"
@import "_partial/cookieconsent"
@import "_partial/highlight"
@import "_partial/icomoon.css"
@import "_partial/docsearch.min.css"
+49
View File
@@ -0,0 +1,49 @@
(function () {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('cc--darkmode');
}
var config = {
guiOptions: {
consentModal: {
equalWeightButtons: false
}
},
categories: {
necessary: {
enabled: true,
readOnly: true
},
analytics: {
enabled: false
}
},
language: {
default: 'en',
translations: {
en: {
consentModal: {
title: 'We use cookies',
description: 'This site uses cookies for analytics and to improve your experience.',
acceptAllBtn: 'Accept',
acceptNecessaryBtn: 'Reject'
}
}
}
}
};
if (/^localhost$|^127\.0\.0\.1$/i.test(location.hostname)) {
config.cookie = { secure: false };
}
function run() {
CookieConsent.run(config);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
}());
+119 -16
View File
@@ -38,8 +38,8 @@
(function() {
// playground
/* global liquidjs, ace */
if (!location.pathname.match(/playground.html$/)) return;
/* global liquidjs, ace, Prism */
if (!/\/playground(?:\.html)?$/.test(location.pathname)) return;
updateVersion(liquidjs.version);
const engine = new liquidjs.Liquid({
memoryLimit: 1e5,
@@ -48,14 +48,19 @@
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
const editor = createEditor('editorEl', 'liquid');
const dataEditor = createEditor('dataEl', 'json');
const preview = createEditor('previewEl', 'html');
preview.setReadOnly(true);
preview.renderer.setShowGutter(false);
preview.renderer.setPadding(16);
const previewCode = document.getElementById('previewCode');
const indicatorTpl = document.querySelector('.area-tpl .pane-indicator');
const indicatorData = document.querySelector('.area-data .pane-indicator');
const indicatorOutput = document.querySelector('.area-output .pane-indicator');
const editors = [editor, dataEditor, preview];
const editors = [editor, dataEditor];
let previewValue = '';
let hadPreview = false;
let renderTimer = null;
const RENDER_DELAY = 180;
colorScheme.addEventListener('change', function() {
editors.forEach(applyEditorTheme);
if (previewValue) setPreview(previewValue);
});
const init = parseArgs(location.hash.slice(1));
@@ -63,9 +68,11 @@
editor.setValue(init.tpl, 1);
dataEditor.setValue(init.data, 1);
}
editor.on('change', update);
dataEditor.on('change', update);
update();
editor.on('change', onTemplateChange);
dataEditor.on('change', onContextChange);
editor.on('focus', function () { setIndicator(indicatorTpl, 'active'); });
dataEditor.on('focus', function () { setIndicator(indicatorData, 'active'); });
scheduleUpdate();
ready();
function ready() {
@@ -87,6 +94,8 @@
function applyEditorTheme(editor) {
editor.setTheme(getEditorTheme());
editor.renderer.setPadding(0);
editor.container.style.background = 'transparent';
}
function createEditor(id, lang) {
@@ -96,15 +105,61 @@
fontFamily: '"Source Code Pro", ui-monospace, Monaco, Menlo, Consolas, monospace',
fontSize: '14px',
showPrintMargin: false,
showGutter: false,
highlightActiveLine: false,
tabSize: 2,
useSoftTabs: true,
scrollPastEnd: 0.25
scrollPastEnd: 0
});
editor.getSession().setMode('ace/mode/' + lang);
editor.renderer.setScrollMargin(8, 8, 0, 0);
editor.renderer.setShowGutter(false);
if (editor.renderer.$gutter) {
editor.renderer.$gutter.style.display = 'none';
}
editor.renderer.setScrollMargin(0, 0, 0, 0);
bindClipboard(editor);
return editor;
}
function bindClipboard(editor) {
editor.commands.addCommand({
name: 'copy',
bindKey: {win: 'Ctrl-C', mac: 'Command-C'},
exec: function (ed) {
const text = ed.getCopyText();
if (!text) return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text);
}
},
readOnly: true
});
editor.commands.addCommand({
name: 'cut',
bindKey: {win: 'Ctrl-X', mac: 'Command-X'},
exec: function (ed) {
const text = ed.getCopyText();
if (!text) return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function () {
ed.insert('');
});
}
}
});
editor.commands.addCommand({
name: 'paste',
bindKey: {win: 'Ctrl-V', mac: 'Command-V'},
exec: function (ed) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.readText().then(function (text) {
ed.insert(text);
});
}
}
});
}
function parseArgs(hash) {
if (!hash) return;
try {
@@ -118,16 +173,64 @@
return utoa(obj.tpl) + ',' + utoa(obj.data);
}
function setPreview(value) {
previewValue = value;
previewCode.textContent = value;
if (window.Prism) {
delete previewCode.dataset.highlighted;
window.Prism.highlightElement(previewCode);
}
}
function setIndicator(indicator, state) {
if (indicator) indicator.dataset.state = state;
}
function onTemplateChange() {
setIndicator(indicatorTpl, 'active');
if (indicatorData.dataset.state !== 'error') setIndicator(indicatorData, 'idle');
setIndicator(indicatorOutput, 'pending');
scheduleUpdate();
}
function onContextChange() {
setIndicator(indicatorData, 'active');
if (indicatorTpl.dataset.state !== 'error') setIndicator(indicatorTpl, 'idle');
setIndicator(indicatorOutput, 'pending');
scheduleUpdate();
}
function scheduleUpdate() {
clearTimeout(renderTimer);
renderTimer = setTimeout(update, RENDER_DELAY);
}
async function update() {
const tpl = editor.getValue();
const data = dataEditor.getValue();
history.replaceState({}, '', '#' + serializeArgs({tpl, data}));
let parsed;
try {
const html = await engine.parseAndRender(tpl, JSON.parse(data));
preview.setValue(html, 1);
parsed = JSON.parse(data);
} catch (err) {
preview.setValue(err.stack, 1);
throw err;
setIndicator(indicatorData, 'error');
setIndicator(indicatorTpl, 'idle');
setIndicator(indicatorOutput, 'error');
return;
}
try {
const html = await engine.parseAndRender(tpl, parsed);
if (html !== '' || !hadPreview) {
setPreview(html);
if (html !== '') hadPreview = true;
}
setIndicator(indicatorTpl, 'idle');
setIndicator(indicatorData, 'idle');
setIndicator(indicatorOutput, 'ok');
} catch (err) {
setIndicator(indicatorTpl, 'error');
setIndicator(indicatorData, 'idle');
setIndicator(indicatorOutput, 'error');
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "liquidjs",
"version": "10.27.0",
"version": "10.27.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "liquidjs",
"version": "10.27.0",
"version": "10.27.1",
"license": "MIT",
"dependencies": {
"commander": "^10.0.0"
+7 -4
View File
@@ -1,8 +1,8 @@
{
"name": "liquidjs",
"version": "10.27.0",
"version": "10.27.1",
"sideEffects": false,
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"description": "A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.",
"main": "dist/liquid.node.js",
"module": "dist/liquid.node.mjs",
"es2015": "dist/liquid.browser.mjs",
@@ -29,12 +29,15 @@
"build:min": "BUNDLES=min rollup -c rollup.config.mjs",
"build:umd": "BUNDLES=umd rollup -c rollup.config.mjs",
"build:charmap": "./bin/character-gen.js > src/util/character.ts",
"build:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog build:docs-hexo",
"prepare:docs": "run-s build:docs-liquid build:contributors build:apidoc build:changelog",
"build:docs": "run-s prepare:docs build:docs-hexo",
"build:docs-liquid": "cross-env BUNDLES=min rollup -c rollup.config.mjs && shx cp dist/liquid.browser.min.js docs/themes/navy/source/js/",
"build:contributors": "node bin/build-contributors.js",
"build:apidoc": "shx rm -rf docs/source/api && typedoc --plugin typedoc-plugin-missing-exports ./src --gitRevision master --out docs/source/api",
"build:changelog": "node bin/build-changelog.js",
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/"
"build:docs-hexo": "cd docs && npm ci && npm run build && shx cp CNAME public/",
"serve:docs": "cd docs && npm run start",
"dev:docs": "run-s prepare:docs serve:docs"
},
"bin": {
"liquidjs": "./bin/liquid.js",
+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
}
+2 -3
View File
@@ -15,7 +15,6 @@ export function round (v: number, arg = 0) {
v = toNumber(v)
arg = toNumber(arg)
const amp = Math.pow(10, arg)
const scaled = v * amp
// Round half away from zero
return Math.sign(v) * Math.round(Math.abs(scaled)) / amp
const scaled = (v * amp) * (1 + Number.EPSILON)
return Math.round(scaled) / amp
}
+8
View File
@@ -522,6 +522,14 @@ describe('Tokenizer', function () {
expect(new Tokenizer('contains b').matchTrie(opTrie)).toBe(8)
})
})
describe('#createTrie()', function () {
it('should return the same trie for the same input', () => {
expect(createTrie(defaultOperators)).toBe(createTrie(defaultOperators))
})
it('should return distinct tries for distinct inputs', () => {
expect(createTrie({ foo: 1 })).not.toBe(createTrie({ foo: 1 }))
})
})
describe('#readLiquidTagTokens', () => {
it('should read newline terminated tokens', () => {
const tokenizer = new Tokenizer('echo \'hello\'')
+10
View File
@@ -10,7 +10,16 @@ export type Trie<T> = {
needBoundary?: true
} & Record<string, any>
// Tries are built once per input object and reused: the Tokenizer rebuilds them
// on every instantiation, but `input` (operators/literalValues) is a stable
// reference. WeakMap-keying by `input` lets short-lived operator objects (and
// their tries) be garbage collected. The returned trie is treated as read-only
// by callers (matchTrie only reads it); do not mutate it.
const trieCache = new WeakMap<TrieInput<any>, Trie<any>>()
export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
const cached = trieCache.get(input)
if (cached) return cached
const trie: Trie<T> = {}
for (const [name, data] of Object.entries(input)) {
let node = trie
@@ -29,5 +38,6 @@ export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
node.data = data
node.end = true
}
trieCache.set(input, trie)
return trie
}
+4
View File
@@ -85,5 +85,9 @@ describe('filters/html', function () {
expect(liquid.parseAndRenderSync('{{"<img\rsrc=x\ronerror=alert(1)>" | strip_html}}')).toBe('')
expect(liquid.parseAndRenderSync('{{"<svg\nonload=alert(1)>" | strip_html}}')).toBe('')
})
it('should not loop on unclosed openers (GHSA-m7fp-h3p4-hr49)', function () {
expect(liquid.parseAndRenderSync('{{ "a<" | strip_html }}')).toBe('a<')
expect(liquid.parseAndRenderSync('{{ "hello<world<again" | strip_html }}')).toBe('hello<world<again')
})
})
})
+4
View File
@@ -70,6 +70,10 @@ describe('filters/math', function () {
it('should return "183.36" for 183.357,2',
() => test('{{183.357|round: 2}}', '183.36'))
it('should convert string to number', () => test('{{"2.7"|round}}', '3'))
it('odd number 1.005 should round correctly', () => test('{{1.005|round:2}}', '1.01'))
it('odd number -1.005 should round correctly', () => test('{{num|round:2}}', { num: -1.005 }, '-1.01'))
it('odd number 9.075 should round correctly', () => test('{{9.075|round:2}}', '9.08'))
it('odd number -9.075 should round correctly', () => test('{{num|round:2}}', { num: -9.075 }, '-9.08'))
})
describe('times', function () {
it('should return "6" for 3,2', () => test('{{ 3 | times: 2 }}', '6'))