Compare commits

...
Author SHA1 Message Date
Yang Jun c7808f1a9b Revert "docs(readme): lead with quick start and scannable structure"
This reverts commit 4e31549faa.
2026-06-20 00:03:57 +08:00
Yang JunandCursor 4e31549faa 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]>
2026-06-20 00:03:31 +08:00
Yang JunandCursor 491edf240f docs(security): route vulnerability reports to GitHub Advisories
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-19 23:57:26 +08:00
8a0c74a7fc fix(security): charge pop filter allocation to memoryLimit (#907)
* fix(security): charge pop filter allocation to memoryLimit (CWE-770)

The `pop` array filter cloned the input via `[...toArray(v)]` without
charging `this.context.memoryLimit.use(...)`, bypassing the memoryLimit
DoS guard that its sibling filters (shift, unshift, compact, etc.) apply.
Mirror `shift` to account for the O(N) allocation.

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

* fix(security): charge sample filter full clone allocation to memoryLimit (CWE-770)

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

---------

Co-authored-by: Cursor <[email protected]>
2026-06-14 15:50:50 +08:00
ed15a52c26 docs: revisit wording & style for liquidjs.com (#906)
* docs: polish theme, playground, and reference pages

Improve readability of the docs site with updated light/dark tokens, shared
code-block styling, and playground editors that follow system color scheme.
Skip CookieHub on localhost, serve the browser bundle from theme source, and
use backtick titles on filter/tag reference pages for consistent navigation.

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

* docs: highlight npx in bash blocks and polish English copy

Use Prism insertBefore for CLI commands like npx, tighten tutorial and reference wording, and keep YAML titles free of backticks so sidebar and page headings stay correct.

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

* docs: restore lowercase filter and tag titles

Titles should match actual filter/tag identifiers (e.g. abs, append), not capitalized English labels.

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

---------

Co-authored-by: Cursor <[email protected]>
2026-06-08 00:26:52 +08:00
499b221f33 docs: remove translations & update homepage (#904)
* docs: add GitHub buttons and improve option docs

* chore: replace husky with prepush check

* 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]>

* 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]>

* fix: resolve TS errors without deprecated tsconfig options

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

---------

Co-authored-by: Cursor <[email protected]>
2026-06-07 01:37:51 +08:00
Yang JunandGitHub 705e5d1b6e docs: add consent banner and GA (#903) 2026-06-06 14:04:21 +08:00
semantic-release-bot a8fd734b5e chore(release): 10.27.0 [skip ci]
# [10.27.0](https://github.com/harttle/liquidjs/compare/v10.26.0...v10.27.0) (2026-05-15)

### Features

* **context:** null-prototype scope frames via createScope ([#899](https://github.com/harttle/liquidjs/issues/899)) ([47d3f1b](https://github.com/harttle/liquidjs/commit/47d3f1b1cf33be91fe587821f288d1c9d8e1ace7))
2026-05-15 18:21:41 +00:00
47d3f1b1cf feat(context): null-prototype scope frames via createScope (#899)
- Add createScope() building Object.create(null) with optional own props

- Initialize context stack bottom with createScope() for assign/capture

- Push null-proto scopes from for, tablerow, block, layout, include (incl. Jekyll)

Co-authored-by: Cursor <[email protected]>
2026-05-16 02:20:03 +08:00
semantic-release-bot c20c0af02d chore(release): 10.26.0 [skip ci]
# [10.26.0](https://github.com/harttle/liquidjs/compare/v10.25.7...v10.26.0) (2026-05-14)

### Bug Fixes

* **date:** cap strftime widths and account padding in memoryLimit ([#895](https://github.com/harttle/liquidjs/issues/895)) ([3129d46](https://github.com/harttle/liquidjs/commit/3129d46dc95efa357b00e5a57ee1af80a13d72ed))
* enforce renderLimit for empty renderTemplates calls ([#894](https://github.com/harttle/liquidjs/issues/894)) ([5b9c346](https://github.com/harttle/liquidjs/commit/5b9c3469085e01c79e2d0af28e2a13f730e1793d))
* propagate ownPropertyOnly into Context.spawn() for {% render %} ([#893](https://github.com/harttle/liquidjs/issues/893)) ([dbbf628](https://github.com/harttle/liquidjs/commit/dbbf6288030591bf6da28d8c1cce5a17bca97bb6))
* **security:** block Object.prototype filter/tag lookups (RCE) ([#897](https://github.com/harttle/liquidjs/issues/897)) ([457fae0](https://github.com/harttle/liquidjs/commit/457fae0736c3ec862539b9dbf7f477e6c08fb6c6))
* strip html newline tags ([#892](https://github.com/harttle/liquidjs/issues/892)) ([26ea285](https://github.com/harttle/liquidjs/commit/26ea2856c7a90aec892b98d94a9b7a3e18539045))
* **strip_html:** rewrite as linear single-pass scan to avoid ReDoS ([#896](https://github.com/harttle/liquidjs/issues/896)) ([3616a74](https://github.com/harttle/liquidjs/commit/3616a744b9abeb425c217b340a2397d46176afb8))

### Features

* add sha256 and hmac_sha256 filters for cryptographic operations ([#889](https://github.com/harttle/liquidjs/issues/889)) ([1c816d4](https://github.com/harttle/liquidjs/commit/1c816d4fc3bcd2cba011f7a84f56a4251fca0622))
2026-05-14 14:23:44 +00:00
457fae0736 fix(security): block Object.prototype filter/tag lookups (RCE) (#897)
* fix(security): block Object.prototype filter/tag lookups (RCE)

`liquid.filters` and `liquid.tags` were plain `{}` so bracket access on
template-controlled keys inherited from `Object.prototype`. Most damaging:
`{{ x | valueOf }}` resolved to `Object.prototype.valueOf`, which the
filter pipeline called as a handler with `this = FilterImpl`; valueOf
returns its receiver, leaking `context`, `liquid`, `token` (and via them
parser, loader, fs) into the template — chain that with `group_by`/`where`
gadgets and an attacker reaches `Function`/`child_process` for RCE.
Same shape on the tag side: `{% constructor %}` bypassed the
"tag not found" assertion and crashed with a confusing message.

Use null-prototype storage so `liquid.filters[name]` / `liquid.tags[name]`
only resolve to explicitly registered entries. The existing
`assert(impl || !strictFilters)` and `assert(TagClass, ...)` now do the
right thing for `valueOf`, `toString`, `constructor`, `__proto__`,
`hasOwnProperty`, `isPrototypeOf`, `__defineGetter__`, etc.

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

* test: fold prototype-registry regressions into register + e2e

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

* test: assert null-prototype registries vs all Object.prototype keys

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

* test: dedupe registry checks; merge filter prototype loop

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

* fix(context): use null-prototype scope and register objects

Add createScope(); use for bottom scope, spawn default, getAll merge, ctx.push frames, filter loops, include/layout blocks registers, and cycle groups. registers uses Object.create(null) and getRegister uses ??.

For-loop continue register defaults to 0 (not {}): Array.slice coerces plain {} but not null-prototype objects.

Export createScope from the package entry.

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

* revert(context): plain {} registers and getRegister ||

Registers are only mutated by tag implementations, not templates; keep null-prototype scopes/createScope for push frames.

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

* test(context): assert scope isolation without probing prototypes

Replace Object.getPrototypeOf checks for bottom() and getAll() with
'in' checks on typical Object.prototype names plus a merge assertion.

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

* test(e2e): assert constructor filter/tag lookups (node + UMD)

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

* test(context): cover Object.prototype keys under ownPropertyOnly

- Add getSync cases for constructor and valueOf on plain objects
- Remove scope storage tests that used the in operator

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

* refactor: remove createScope helper

Drop the exported helper and finish migrating call sites. Revert incidental context/for/include/layout churn so behavior matches mainline aside from the removal. Trim duplicate e2e and heavy Object.prototype loops in registry tests.

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

* docs: document ownPropertyOnly and Drop security in security model

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

* docs(zh-cn): sync security model with ownPropertyOnly and Drop notes

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

---------

Co-authored-by: Cursor <[email protected]>
2026-05-14 22:18:10 +08:00
3616a744b9 fix(strip_html): rewrite as linear single-pass scan to avoid ReDoS (#896)
* fix(strip_html): rewrite as linear single-pass scan to avoid ReDoS

The previous strip_html regex
  /<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<[\s\S]*?>|<!--[\s\S]*?-->/g
contains lazy alternatives that backtrack O(n^2) on inputs with many
unclosed `<script` / `<style` openers. A 350KB payload of
`'<script'.repeat(50000)` blocked the Node.js event loop for ~10s, and
cost grew quadratically with input size. memoryLimit only charged
str.length, which does not bound regex CPU.

Replace the regex with an indexOf-based single-pass scan. For each `<`
we:
- if `<script` opener: find next `</script>` and skip the whole block;
  cache "no closer after pos k" so subsequent unclosed `<script`
  openers do not re-scan the tail.
- same for `<style` / `</style>`.
- otherwise treat as a generic `<...>` tag (matches the original
  behavior, where the `<[\s\S]*?>` alternative also caught comments).
- if no closing `>` exists, emit the tail as literal text and stop.

Total work is O(n). All existing strip_html test cases pass unchanged.

Add regression tests covering the PoCs (`<script` / `<style` repeats,
and `<script>foo` repeats with `>` but no `</script>`) plus a
memoryLimit assertion.

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

* refactor(strip_html): factor block kinds into a small table

Same algorithm and complexity, fewer lines. Document why a regex-only
solution can't be O(n) in V8 (no atomic groups / possessive quantifiers
/ memoization, so unrolled-loop patterns are still O(n^2) on unclosed
openers — empirically confirmed: original 280KB ~4s, Friedl unrolled
~14s, atomic lookahead ~7s; tokenizer ~1ms).

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

* refactor(strip_html): inline block kinds to match file style

Drop the module-level STRIP_BLOCKS table; the rest of the file keeps
each filter self-contained (only escapeMap/unescapeMap are top-level
maps shared across filters). Two openers don't justify a table.

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

* refactor(strip_html): unify raw-text blocks; treat <!--...--> as opaque

In HTML5, <script>, <style>, and <!-- --> are all raw-text blocks: their
content is opaque until the matching closer, so a `>` inside CSS, JS, or
a comment must not be treated as a tag end. The previous code only had
this special handling for <script> and <style>; comments containing `>`
fell through to the generic `<...>` branch and were partially stripped
(e.g. `<!-- a > b -->` left `b -->` in the output).

Match Shopify Liquid's STRIP_HTML_BLOCKS set (script + style + comment),
and consolidate the three near-identical branches into a small
opener/closer table inside the function.

Algorithm and complexity unchanged (O(n) via indexOf + cached closer
positions). Add a regression test for `>` inside a comment.

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

* refactor(strip_html): drop position cache, delete dead blocks from Set

Once `indexOf(closer, X)` returns -1, all subsequent searches (with
monotonically increasing start) also return -1. So tracking absence is
enough; storing positions is unnecessary. Make `blocks` a Set and
delete a kind once its closer is known absent — no parallel `dead`
bookkeeping. Use Jest's per-test timeout for the ReDoS regressions
instead of manual Date.now() bookkeeping.

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

* refactor(strip_html): treat '<...>' as a catch-all block kind

Adding ['<', '>'] as the lowest-priority entry of `blocks` lets the
inner loop subsume the generic-tag fallback: the `end` sentinel and
its `< 0` / `<= 0` follow-up checks disappear, the "no terminator"
exit becomes a single `i === lt` test, and Set<[string, string]>
collapses to Map<string, string>.

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

---------

Co-authored-by: Cursor <[email protected]>
2026-05-11 23:59:40 +08:00
3129d46dc9 fix(date): cap strftime widths and account padding in memoryLimit (#895)
* fix(date): cap strftime widths and account padding in memoryLimit

- Clamp numeric strftime pad widths to MAX_STRFTIME_PAD (1024)
- Export estimateStrftimePaddingMemory for the date filter to charge memoryLimit
- Replace unbounded pad() concatenation loop with ch.repeat + single concat
- Add regression tests for clamping and memoryLimit on huge %width directives

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

* fix(date): harden strftime memory accounting and document security model

Move strftime memory charging into the same formatting path used for padding, enforce pre-allocation checks, and add regression tests for non-string date format PoCs. Add dedicated docs clarifying that memoryLimit is cooperative DoS mitigation and not strict heap isolation.

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

* docs(zh-cn): add security model docs for DoS limits

Add a Chinese security-model tutorial and link it from the Chinese DoS guide to clarify that memoryLimit is cooperative accounting, list uncounted custom conversion cases, and recommend avoiding fully user-defined templates in online services.

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

* docs: consolidate DoS docs into security-model pages

Merge DoS guidance into security-model docs in both English and Chinese, and remove the placeholder dos.md pages to avoid duplicate/redirect-only docs.

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

* docs: merge DoS details into security-model docs

Move the detailed parseLimit/renderLimit/memoryLimit explanations and examples into the English and Chinese security-model pages so content from the removed dos pages is preserved.

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

* docs: reorganize security-model structure for clarity

Restructure English and Chinese security-model docs into a consistent flow: security boundary, limits overview, per-limit details, and online service guidance.

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

* refactor(strftime): simplify %N width parsing logic

Use regex-backed width assumptions to simplify %N width normalization and padding memory accounting while keeping behavior equivalent.

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

* refactor(strftime): rely on memoryLimit for width control

Remove MAX_STRFTIME_PAD hard capping and rely on memoryLimit enforcement before padding allocation. Update strftime/date tests and security-model docs to match the new boundary and renderLimit caveats.

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

* fix(strftime): use add() once for padding, minimize churn

- pad(): replace per-char loop with a single add(str, ch.repeat(n)) call.
  The earlier `probe[0] === ch` heuristic was wrong when ch happened to
  equal a leading char of 'probe' (e.g. ch === 'p').
- strftime.ts: revert unrelated typing/structural refactors so the diff
  contains only the memoryLimit threading and the %N memory charge.
- docs: rewire the deleted dos.html sidebar entry to security-model.html
  (with localized labels) so the deleted page does not 404 from the
  sidebar.

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

---------

Co-authored-by: Cursor <[email protected]>
2026-05-10 14:35:28 +08:00
5b9c346908 fix: enforce renderLimit for empty renderTemplates calls (#894)
renderLimit was only checked inside the per-template loop, so
renderTemplates([], ...) skipped it. Empty {% for %} and {% tablerow %}
bodies call that path once per iteration (tablerow still does emitter
work for <tr>/<td>), bypassing the documented time budget. Check the
limiter at renderTemplates entry before the loop.

Add regression tests for empty for-body and empty tablerow-body.

Co-authored-by: Cursor <[email protected]>
2026-05-07 23:03:43 +08:00
dbbf628803 fix: propagate ownPropertyOnly into Context.spawn() for {% render %} (#893)
Child contexts from spawn() re-derived ownPropertyOnly from Liquid opts
only, dropping per-render RenderOptions overrides. That broke the contract
that parseAndRender(..., { ownPropertyOnly: true }) locks down a single
render, including partials loaded via {% render %}.

Add regression test matching prototype-chain leak PoC.

Co-authored-by: Cursor <[email protected]>
2026-05-03 22:35:31 +08:00
26ea2856c7 fix: strip html newline tags (#892)
* docs: add @talboren as financial contributor

* fix(strip_html): match tags that span newlines inside angle brackets

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

---------

Co-authored-by: Cursor <[email protected]>
2026-05-03 21:36:09 +08:00
a55f543f49 docs(readme): add Freshet to Who's Using LiquidJS (#888)
Co-authored-by: MattAltermatt <[email protected]>
2026-05-03 12:05:12 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
d1d517d1ec docs: add VladimirFilonov as a contributor for code (#891)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-05-03 12:03:52 +08:00
Vladimir FilonovandGitHub 1c816d4fc3 feat: add sha256 and hmac_sha256 filters for cryptographic operations (#889) 2026-05-03 12:03:26 +08:00
semantic-release-bot 34877950bf chore(release): 10.25.7 [skip ci]
## [10.25.7](https://github.com/harttle/liquidjs/compare/v10.25.6...v10.25.7) (2026-04-23)

### Bug Fixes

* **filters:** support Buffer input in base64_encode to prevent binary data corruption ([#881](https://github.com/harttle/liquidjs/issues/881)) ([0ee6dbb](https://github.com/harttle/liquidjs/commit/0ee6dbb511aa926f6d490293282060abf3bab37f))
2026-04-23 13:41:11 +00:00
Yang JunandGitHub 75c815a4d7 docs: add @talboren as financial contributor (#886) 2026-04-23 21:39:50 +08:00
allcontributors[bot]GitHuballcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
f1f896c29d docs: add talboren as a contributor for code (#885)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-04-23 21:31:50 +08:00
TalandGitHub 0ee6dbb511 fix(filters): support Buffer input in base64_encode to prevent binary data corruption (#881)
* fix: support Buffer input in base64_encode filter

When binary data (e.g. images, PDFs) is passed through the template
context as a Node.js Buffer, the base64_encode filter would call
stringify() on it first, which internally does String(value). This
triggers Buffer.toString() with the default 'utf-8' encoding, which
is a lossy conversion for non-UTF-8 byte sequences — invalid bytes
get replaced with U+FFFD, permanently destroying the original data.

The fix checks for Buffer.isBuffer() before stringify, and calls
buffer.toString('base64') directly, bypassing the lossy UTF-8
intermediate step. String inputs continue through the existing path
unchanged.

Made-with: Cursor

* fix: handle Buffer in filter layer to fix browser build

Move Buffer handling from base64-impl.ts (which gets swapped for the
browser impl at build time) into base64.ts (the filter layer). This
avoids a type error during the browser rollup build where the browser
impl only accepts string.

Also guard Buffer.isBuffer() with typeof Buffer !== 'undefined' for
safety in browser environments.

Made-with: Cursor
2026-04-23 21:30:58 +08:00
241 changed files with 1267 additions and 6429 deletions
+18
View File
@@ -820,6 +820,24 @@
"contributions": [
"code"
]
},
{
"login": "talboren",
"name": "Tal",
"avatar_url": "https://avatars.githubusercontent.com/u/68807791?v=4",
"profile": "https://github.com/talboren",
"contributions": [
"code"
]
},
{
"login": "VladimirFilonov",
"name": "Vladimir Filonov",
"avatar_url": "https://avatars.githubusercontent.com/u/813224?v=4",
"profile": "https://filonov.dev",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+1 -1
View File
@@ -11,7 +11,7 @@ coverage/
node_modules/
# tmp
docs/public/js/liquid.browser.min.js
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
dist/
+31
View File
@@ -1,3 +1,34 @@
# [10.27.0](https://github.com/harttle/liquidjs/compare/v10.26.0...v10.27.0) (2026-05-15)
### Features
* **context:** null-prototype scope frames via createScope ([#899](https://github.com/harttle/liquidjs/issues/899)) ([47d3f1b](https://github.com/harttle/liquidjs/commit/47d3f1b1cf33be91fe587821f288d1c9d8e1ace7))
# [10.26.0](https://github.com/harttle/liquidjs/compare/v10.25.7...v10.26.0) (2026-05-14)
### Bug Fixes
* **date:** cap strftime widths and account padding in memoryLimit ([#895](https://github.com/harttle/liquidjs/issues/895)) ([3129d46](https://github.com/harttle/liquidjs/commit/3129d46dc95efa357b00e5a57ee1af80a13d72ed))
* enforce renderLimit for empty renderTemplates calls ([#894](https://github.com/harttle/liquidjs/issues/894)) ([5b9c346](https://github.com/harttle/liquidjs/commit/5b9c3469085e01c79e2d0af28e2a13f730e1793d))
* propagate ownPropertyOnly into Context.spawn() for {% render %} ([#893](https://github.com/harttle/liquidjs/issues/893)) ([dbbf628](https://github.com/harttle/liquidjs/commit/dbbf6288030591bf6da28d8c1cce5a17bca97bb6))
* **security:** block Object.prototype filter/tag lookups (RCE) ([#897](https://github.com/harttle/liquidjs/issues/897)) ([457fae0](https://github.com/harttle/liquidjs/commit/457fae0736c3ec862539b9dbf7f477e6c08fb6c6))
* strip html newline tags ([#892](https://github.com/harttle/liquidjs/issues/892)) ([26ea285](https://github.com/harttle/liquidjs/commit/26ea2856c7a90aec892b98d94a9b7a3e18539045))
* **strip_html:** rewrite as linear single-pass scan to avoid ReDoS ([#896](https://github.com/harttle/liquidjs/issues/896)) ([3616a74](https://github.com/harttle/liquidjs/commit/3616a744b9abeb425c217b340a2397d46176afb8))
### Features
* add sha256 and hmac_sha256 filters for cryptographic operations ([#889](https://github.com/harttle/liquidjs/issues/889)) ([1c816d4](https://github.com/harttle/liquidjs/commit/1c816d4fc3bcd2cba011f7a84f56a4251fca0622))
## [10.25.7](https://github.com/harttle/liquidjs/compare/v10.25.6...v10.25.7) (2026-04-23)
### Bug Fixes
* **filters:** support Buffer input in base64_encode to prevent binary data corruption ([#881](https://github.com/harttle/liquidjs/issues/881)) ([0ee6dbb](https://github.com/harttle/liquidjs/commit/0ee6dbb511aa926f6d490293282060abf3bab37f))
## [10.25.6](https://github.com/harttle/liquidjs/compare/v10.25.5...v10.25.6) (2026-04-19)
+4
View File
@@ -64,6 +64,7 @@ For more details, refer to the [Setup Guide][setup].
- [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.
Feel free to create a PR or contact me to add your use case into this list!
@@ -82,6 +83,7 @@ If you personally love LiquidJS or it's benefiting your business, please conside
<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/>
<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://github.com/talboren" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://avatars.githubusercontent.com/u/68807791?v=4&s=100" height="80" style="vertical-align: middle;" alt="Tal" title="Tal (@talboren)"/></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://dailycontributors.com/" style="display: inline-block; vertical-align: middle; margin: 8px;"><img src="https://images.opencollective.com/dailycontributors/3c2e057/logo/256.png?height=50&width=100" width="120" style="vertical-align: middle;" alt="Dailycontributors" title="Dailycontributors"/></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>
@@ -221,6 +223,8 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
<td align="center" valign="top" width="14.28%"><a href="https://doruk.ch"><img src="https://avatars.githubusercontent.com/u/77903714?v=4?s=100" width="100px;" alt="Peak Twilight"/><br /><sub><b>Peak Twilight</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=peaktwilight" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joecottam"><img src="https://avatars.githubusercontent.com/u/44173086?v=4?s=100" width="100px;" alt="Joe Cottam"/><br /><sub><b>Joe Cottam</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=joecottam" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timbze"><img src="https://avatars.githubusercontent.com/u/35117769?v=4?s=100" width="100px;" alt="Timmy Braun"/><br /><sub><b>Timmy Braun</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=timbze" title="Code">💻</a></td>
<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>
</tbody>
</table>
+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
@@ -18,7 +18,5 @@ content = content
.replace(/\{\{/g, '{% raw %}{{{% endraw %}')
const enFrontmatter = '---\ntitle: Changelog\nauto: true\n---\n\n'
const zhFrontmatter = '---\ntitle: 更新日志\nauto: true\n---\n\n'
fs.writeFileSync(path.join(root, 'docs/source/tutorials/changelog.md'), enFrontmatter + content)
fs.writeFileSync(path.join(root, 'docs/source/zh-cn/tutorials/changelog.md'), zhFrontmatter + content)
+3 -5
View File
@@ -1,10 +1,8 @@
title: LiquidJS
subtitle: "A simple, expressive and safe template engine."
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 for JavaScript."
description: "LiquidJS is a simple, expressive, and safe template engine for JavaScript, compatible with Shopify and GitHub Pages."
author: Harttle
language:
- en
- zh-cn
language: en
timezone: UTC
url: https://liquidjs.com
+2
View File
@@ -2,6 +2,8 @@
'use strict'
require('./prism-bash-extend')
const { resolve, basename } = require('path')
const { readFileSync } = require('fs')
const cheerio = require('cheerio')
+34
View File
@@ -0,0 +1,34 @@
'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']
}
})
-2
View File
@@ -1,3 +1 @@
en: English
zh-cn:
name: 简体中文
-24
View File
@@ -1,24 +0,0 @@
-
url: https://opencollective.com/liquidjs/#section-contribute
date: '2020-02-26'
title:
zh-cn: '赞助人:第一个 backer 通过 Open Collective 贡献于 LiquidJS。'
en: 'Backers: the first backer contributed to LiquidJS via Open Collective.'
-
url: https://github.com/harttle/liquidjs/pull/202
date: '2020-03-11'
title:
zh-cn: '内存优化:用更精细的手法重写了解析器,来避免临时字符串的生成,内存占用降低 57.7% 以上。'
en: 'Memory Optimization: a more elaborate parser reducing the memory footprint by 57.7%.'
-
url: https://github.com/harttle/liquidjs/pull/205
date: '2020-03-15'
title:
zh-cn: '性能提升:引入 AST 并重新设计 Token 类型系统,使渲染性能平均提升 100.3%。'
en: 'Performance Boost: a simple AST to improve render performance by 100.3%.'
-
url: https://github.com/harttle/liquidjs/milestone/3?closed=1
date: '2021-09-30'
title:
zh-cn: '流式渲染:4 倍渲染速度,并增加了对流式渲染的支持。'
en: 'Streamed Rendering: now render is 4x faster and support streamed rendering.'
+2 -2
View File
@@ -19,7 +19,7 @@ tutorials:
plugins: plugins.html
operators: operators.html
truth: truthy-and-falsy.html
dos: dos.html
security_model: security-model.html
static_analysis: static-analysis.html
miscellaneous:
migration9: migrate-to-9.html
@@ -114,7 +114,7 @@ filters:
tags:
overview: overview.html
"#": inline_comment.html
"# (inline comment)": inline_comment.html
assign: assign.html
capture: capture.html
case: case.html
+6 -6
View File
@@ -3,18 +3,18 @@ title: date
---
{% since %}v1.9.1{% endsince %}
Date filter is used to convert a timestamp into the specified format.
The `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're 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 are 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 firstly converted to `Date` object via [new Date()][jsDate], that means literal values are considered in runtime's time zone by default.
* 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.
* 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 Liquid instance. Defaults to `Intl.DateTimeFormat().resolvedOptions.locale`).
* [`locale`](/api/interfaces/LiquidOptions.html#locale) can be set when creating a Liquid instance. Defaults to `Intl.DateTimeFormat().resolvedOptions().locale`.
### Examples
```liquid
@@ -26,10 +26,10 @@ Date filter is used to convert a timestamp into the specified format.
```
# TimeZone
* During output, LiquidJS uses local timezone which can override by:
* During output, LiquidJS uses the local timezone, which can be overridden by:
* setting a timezone in-place when calling `date` filter, or
* setting the [`timezoneOffset`](/api/interfaces/LiquidOptions.html#timezoneOffset) LiquidJS option
* It defaults to runtime's time one.
* It defaults to the runtime's timezone.
* Offset can be set as,
* minutes: `-360` means `'+06:00'` and `360` means `'-06:00'`
* timeZone ID: `Asia/Colombo` or `America/New_York`
+20
View File
@@ -0,0 +1,20 @@
---
title: hmac_sha256
---
{% since %}vNEXT{% endsince %}
Converts a string into an SHA-256 hash using a hash message authentication code (HMAC). The secret key is passed as the filter argument. The output is a lowercase hexadecimal string.
Input
```liquid
{%- assign secret_potion = 'Polyjuice' | hmac_sha256: 'Polina' -%}
My secret potion: {{ secret_potion }}
```
Output
```text
My secret potion: 8e0d5d65cff1242a4af66c8f4a32854fd5fb80edcc8aabe9b302b29c7c71dc20
```
+1 -1
View File
@@ -4,7 +4,7 @@ title: json
{% since %}v9.10.0{% endsince %}
Convert values to string via `JSON.stringify()`, for debug purpose.
Convert values to string via `JSON.stringify()`, for debugging purposes.
Input
```liquid
+9 -8
View File
@@ -5,16 +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's 40+ filters supported by LiquidJS. These filters can be categorized into these groups:
There are 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
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
+20
View File
@@ -0,0 +1,20 @@
---
title: sha256
---
{% since %}vNEXT{% endsince %}
Converts a string into an SHA-256 hash. The output is a lowercase hexadecimal string.
Input
```liquid
{%- assign secret_potion = 'Polyjuice' | sha256 -%}
My secret potion: {{ secret_potion }}
```
Output
```text
My secret potion: 44ac1d7a2936e30a5de07082fd65d6fe9b1fb658a1a98bfe65bc5959beac5dd0
```
+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
+8 -8
View File
@@ -7,23 +7,23 @@ ul#intro-feature-list
.intro-feature
.intro-feature-icon
i.icon-shield
h3.intro-feature-title Safe Rendering
p.intro-feature-desc Liquid templates are highly readable and fault-tolerant thus suitable for designers and customers. Operators and expressions are parsed to AST and no #[code eval] or #[code new Function] are used.
h3.intro-feature-title Safe &amp; Typed
p.intro-feature-desc Templates are readable and fault-tolerant, parsed to an AST with no #[code eval] or #[code new Function]. The whole repo is written in TypeScript strict mode, so types stay precise and docs accurate.
li.intro-feature-wrap
.intro-feature
.intro-feature-icon
i.icon-rocket
h3.intro-feature-title Pure JavaScript
p.intro-feature-desc Written with pure JavaScript with no native bindings, available in both Node.js and browsers. All of the CMD, ESM and CJS bundles are available on CDN.
p.intro-feature-desc Written in pure JavaScript with no native bindings, running in both Node.js and the browser. The CMD, ESM and CJS bundles are all available on CDN.
li.intro-feature-wrap
.intro-feature
.intro-feature-icon
i.icon-shopify
h3.intro-feature-title Shopify Compatible
p.intro-feature-desc All filters and tags from Ruby #[a(href="https://github.com/shopify/liquid") shopify/liquid] are supported by LiquidJS. #[a(href="https://jekyllrb.com/") Jekyll sites], #[a(href="https://pages.github.com/") GitHub Pages] and #[a(href="https://themes.shopify.com/") Shopify templates] can be ported to Node.js without pain.
h3.intro-feature-title Shopify &amp; Jekyll
p.intro-feature-desc All filters and tags from Ruby #[a(href="https://github.com/shopify/liquid") shopify/liquid] are supported, so #[a(href="https://themes.shopify.com/") Shopify templates] work out of the box — as do #[a(href="https://jekyllrb.com/") Jekyll] sites and #[a(href="https://pages.github.com/") GitHub Pages].
li.intro-feature-wrap
.intro-feature
.intro-feature-icon
i.icon-typescript
h3.intro-feature-title TypeScript Strict
p.intro-feature-desc The whole repo is re-written in TypeScript strict mode to ensure a smooth experience using this lib and the document is precise and always up to date.
i.icon-network
h3.intro-feature-title Streaming
p.intro-feature-desc Render directly to a Node.js stream with #[code renderToNodeStream], emitting output as it's produced — for a faster time to first byte and low memory usage on large pages.
+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` compares its values.
Creates a switch statement to compare a variable with different values. `case` initializes the switch statement, and `when` tags compare 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 a 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 an included template.
## Passing Variables
Variables defined in parent's scope can be passed to a the partial template by listing them as parameters on the `include` tag:
Variables defined in the parent's scope can be passed to 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 following content:
`article.html` with the 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`. Will output following:
Note that we're referencing the first parameter by `include.header` instead of `header`. It will output the 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 <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.
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.
{% 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 fol
## Default Block Contents
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:
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:
```liquid
// default-layout.liquid
@@ -68,7 +68,7 @@ In the above layout files, blocks has empty contents. But it's not necessarily b
## Passing Variables
Variables defined in current template can be passed to a the layout template by listing them as parameters on the `layout` tag:
Variables defined in the current template can be passed to 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're a dozen of tags supported by LiquidJS, with all tags in [shopify/liquid][shopify/liquid]. These tags can be categorized into these groups:
There are a dozen tags supported by LiquidJS, including 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 | # (inline comment), 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 | `#`, `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 parent's scope can be passed to a the partial template by listing them as parameters on the render tag:
Variables defined in the parent's scope can be passed to 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: Table Row
title: tablerow
---
{% 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 filter function like:
As covered in [Register Filters/Tags][register-filters], we can access filter arguments directly in a 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 transform a URL path to full URL, we'll need to access a `origin` in current scope:
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:
```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 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.
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.
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 string or files. The result template can be then rendered multiple times with different context.
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.
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 softwares, 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 software, 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, please consider contribute 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, consider contributing 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're 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 are 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 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.
* **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.
* **[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.
-57
View File
@@ -1,57 +0,0 @@
---
title: DoS Prevention
---
When the template or data context cannot be trusted, enabling DoS prevention options is crucial. LiquidJS provides 3 options for this purpose: `parseLimit`, `renderLimit`, and `memoryLimit`.
## TL;DR
Setting these options can largely ensure that your LiquidJS instance won't hang for extended periods or consume excessive memory. These limits are based on the available JavaScript APIs, so they are not precise hard limits but thresholds to help prevent your process from failing or hanging.
```typescript
const liquid = new Liquid({
parseLimit: 1e8, // typical size of your templates in each render
renderLimit: 1000, // limit each render to be completed in 1s
memoryLimit: 1e9, // memory available for LiquidJS (1e9 for 1GB)
})
```
When a `parse()` or `render()` cannot be completed within given resource, it throws.
## parseLimit
[parseLimit][parseLimit] restricts the size (character length) of templates parsed in each `.parse()` call, including referenced partials and layouts. Since LiquidJS parses template strings in near O(n) time, limiting total template length is usually sufficient.
A typical PC handles `1e8` (100M) characters without issues.
## renderLimit
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) -%}
order: {{i}}
{%- endfor -%}
```
Render time is checked on a per-template basis (before rendering each template). In the above example, there are 2 templates in the loop: `order: ` and `{{i}}`, render time will be checked 10000000x2 times.
For time-consuming tags and filters within a single template, the process can still hang. For fully controlled rendering, consider using a process manager like [paralleljs][paralleljs].
## memoryLimit
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: "," %}
{% for i in (1..32) %}
{% assign array = array | concat: array %}
{% endfor %}
```
[memoryLimit][memoryLimit] restricts memory-sensitive filters to prevent excessive memory allocation. As [JavaScript uses GC to manage memory](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management), `memoryLimit` limits only the total number of objects allocated by memory sensitive filters in LiquidJS thus may not reflect the actual memory footprint.
[paralleljs]: https://www.npmjs.com/package/paralleljs
[parseLimit]: /api/interfaces/LiquidOptions.html#parseLimit
[renderLimit]: /api/interfaces/LiquidOptions.html#renderLimit
[memoryLimit]: /api/interfaces/LiquidOptions.html#memoryLimit
+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 itself to render the 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 the object itself when rendering 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. While escaping has 2 different meanings for a template engine:
Escaping is important in all languages, including LiquidJS. Escaping has two 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's considered special in Liquid language. This will be useful when you're writing an article in Liquid template to introduce Liquid language.
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.
## HTML Escape
@@ -55,7 +55,7 @@ In LiquidJS, {{ this | escape }} will be HTML-escaped, but
{{{ that }}} will not.
```
Within strings literals in LiquidJS template, `\` can be used to escape special characters in string syntax. For example:
Within string literals in a LiquidJS template, `\` can be used to escape special characters in string syntax. For example:
Input
```liquid
+3 -3
View File
@@ -5,7 +5,7 @@ describe: A short introduction to the Liquid template language and some simple d
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].
LiquidJS syntax is relatively simple. There're 2 types of markups in LiquidJS:
LiquidJS syntax is relatively simple. 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 `}}`.
@@ -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, inter-op with other templates, etc. For example `assign` can be used to define a variable which can be later used in the template:
**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:
```liquid
{% assign foo = "FOO" %}
@@ -50,7 +50,7 @@ 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` not equals "FOO"
Variable `foo` does not equal "FOO"
{% endif %}
```
+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 improvement due to higher target(see #137). There're also some breaking changes.
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.
## 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 await-ed some times.
* Promise typed Drops are not always awaited.
## Performance
* Performance Improvements due to targeting to Node.js 8, see [#137](https://github.com/harttle/liquidjs/issues/137);
* Performance Improvements due to targeting 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're 2 types of operators supported:
LiquidJS operators are very simple and different. There are 2 types of operators supported:
* Comparison operators: `==`, `!=`, `>`, `<`, `>=`, `<=`
* Logic operators: `not`, `or`, `and`, `contains`
* Logical operators: `not`, `or`, `and`, `contains`
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.
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.
## Logic Operators
## Logical 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 logic operators have the same (lowest) precedence.
3. `or` and `and` operators. These logical operators have the same (lowest) precedence.
## Associativity
Logic operators are evaluated from right to left, see [shopify docs][operator-order].
Logical 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 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>.
{% 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>.
{% endnote %}
## cache
**cache** is used to improve performance by caching previously parsed template structures, specially in cases when we're repeatedly parse or render files.
**cache** is used to improve performance by caching previously parsed template structures, especially in cases when we repeatedly parse or render files.
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.
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.
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 lookup and read template files. Can be a single string and an array of strings. See [Render Files][render-file] for details.
**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.
**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.
**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.
**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.
**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.
**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.
**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.
## 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`. More details please check out [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`. For more details, see [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 lookup 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 look up 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 the Shopify.
**jsTruthy** is used to use standard JavaScript truthiness rather than Shopify's.
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 evalu
- 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 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.
- 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.
## 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 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`.
**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`.
**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 is available on `tagToken.args` as a raw string. For example:
As covered in [Register Filters/Tags][register-tags], tag parameters are 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'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.
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.
```liquid
{% random from:2, to:max %}
@@ -25,7 +25,7 @@ color: 'red' shape: 'circle'
color: 'yellow' shape: 'square'
```
More details please refer to the [render](../tags/render.html) tag.
For more details, see 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
```
More details please refer to the [layout](../tags/layout.html) tag.
For more details, see 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 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].
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].
Now we'll make a plugin to upper case every letter of the input, save the following snippet to `upup.js`:
Now we'll make a plugin to uppercase every letter of the input. Save the following snippet to `upup.js`:
```javascript
/**
@@ -62,7 +62,7 @@ See existing filter implementations here: <https://github.com/harttle/liquidjs/t
## Unregister Tags/Filters
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.
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 that throws a corresponding Error.
```javascript
// disable a tag
+2 -2
View File
@@ -38,7 +38,7 @@ name: alice
## Template Lookup
Template files names passed to [renderFile][renderFile], [parseFile][parseFile], [renderFileSync][renderFileSync], [parseFileSync][parseFileSync] APIs,
Template file 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 w/o 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 without 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 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.
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.
## Render Tag Content
@@ -22,12 +22,12 @@ Expected output:
</div>
```
Firstly, [register][register-tags] a tag with name `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`,
Firstly, [register][register-tags] a tag named `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 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`.
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`.
```javascript
engine.registerTag('wrap', {
@@ -57,11 +57,11 @@ engine.registerTag('wrap', {
})
```
`.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/>
`.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/>
## 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` and does exactly the same as above example.
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.
```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 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).
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).
```liquid
{% repeat %}
+89
View File
@@ -0,0 +1,89 @@
---
title: Security Model
---
LiquidJS provides DoS-oriented limits (`parseLimit`, `renderLimit`, `memoryLimit`) to reduce risk. This page summarizes those limits, [`ownPropertyOnly`][ownPropertyOnly], custom [`Drop`][drop] usage, and the security boundary to assume in production.
## Security boundary
The built-in limits are cooperative safeguards, not strict runtime isolation.
- They do **not** equal process RSS/heap usage.
- They do **not** sandbox JavaScript execution.
- They should be combined with process/container limits and request timeouts for defense in depth.
## Limits at a glance
- [parseLimit][parseLimit]: limit total template size per `parse()` call.
- [renderLimit][renderLimit]: limit total render time per `render()` call.
- [memoryLimit][memoryLimit]: cooperatively limit memory-sensitive allocations counted by LiquidJS.
## Limit details
### parseLimit
[parseLimit][parseLimit] restricts the size (character length) of templates parsed in each `.parse()` call, including referenced partials and layouts. Since LiquidJS parses template strings in near O(n) time, limiting total template length is usually sufficient.
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.
```liquid
{%- for i in (1..10000000) -%}
order: {{i}}
{%- endfor -%}
```
Render time is checked on a per-template basis (before rendering each template). In the above example, there are 2 templates in the loop: `order: ` and `{{i}}`, render time will be checked 10000000x2 times.
`renderLimit` is not a hard CPU limiter. It is checked between template renders, so compute-intensive filters/tags/user-defined functions or deeply nested template execution between checks can still cause DoS.
### memoryLimit
`memoryLimit` only limits operations that LiquidJS explicitly counts.
- Counted: memory-sensitive LiquidJS operations that call internal memory accounting.
- Not guaranteed counted: arbitrary user object behavior such as custom `toValue()`/`toString()` chains, or other host-side code that allocates outside LiquidJS accounting points.
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:
```liquid
{% assign array = "1,2,3" | split: "," %}
{% for i in (1..32) %}
{% assign array = array | concat: array %}
{% endfor %}
```
As [JavaScript uses GC to manage memory](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management), `memoryLimit` may not reflect the actual memory footprint.
## `ownPropertyOnly` and scope data
With [`ownPropertyOnly`][ownPropertyOnly] `true`, plain scope objects only expose **own** properties (no inherited / `Object.prototype` keys). Default `false` follows normal JS property access. Use `true` for untrusted or polluted objects; add [`strictVariables`][strictVariables] if missing paths should error. Override per render via [`RenderOptions`][renderOwnPropertyOnly]. This is a read policy for scope data—not a sandbox for filters, tags, or your code.
## Custom `Drop` classes
[`Drop`][drop] values are not restricted the same way: LiquidJS still reads the prototype chain and may call [`liquidMethodMissing`][liquidMethodMissing]. **You** control what a drop exposes; narrow APIs and never feed unsafe data into drops unless the class is built for template access. `ownPropertyOnly` alone does not harden custom drops—audit them like any privileged code.
## Online service guidance
If you run an online service, avoid rendering fully user-defined templates whenever possible.
- Prefer curated templates or a restricted template subset.
- If user-defined templates are required, isolate rendering (worker/process/container), enforce OS/container memory and CPU limits, and apply request rate limits.
- Treat `parseLimit`/`renderLimit`/`memoryLimit` as one layer in a broader DoS defense strategy.
For heavy single-template operations, process-level isolation is still recommended (for example with [paralleljs][paralleljs]).
[paralleljs]: https://www.npmjs.com/package/paralleljs
[parseLimit]: /api/interfaces/LiquidOptions.html#parseLimit
[renderLimit]: /api/interfaces/LiquidOptions.html#renderLimit
[memoryLimit]: /api/interfaces/LiquidOptions.html#memoryLimit
[ownPropertyOnly]: /api/interfaces/LiquidOptions.html#ownPropertyOnly
[renderOwnPropertyOnly]: /api/interfaces/RenderOptions.html#ownPropertyOnly
[strictVariables]: /api/interfaces/LiquidOptions.html#strictVariables
[drop]: /api/classes/Drop.html
[liquidMethodMissing]: /api/classes/Drop.html#liquidMethodMissing
+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 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 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 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. And internal structures returned can be changed w/o a major version bump.
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.
{% 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 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.
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.
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 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.
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.
## Sync and Async API
All major methods on [Liquid][Liquid] supports both sync and async. These methods return Promises:
All major methods on [Liquid][Liquid] support both sync and async. These methods return Promises:
- `render()`
- `renderFile()`
@@ -44,11 +44,11 @@ engine.registerTag('upper', class UpperTag extends Tag {
})
```
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:
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:
- declare render function as `* render()`, in which
- do not directly `return <Promise>`, and
- do not call any APIs that returns a Promise.
- do not call any APIs that return 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 intend 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 intended 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're [certain differences][diff] with [the Ruby version][ruby], one of which is the `truthy` value.
Though [Liquid][sl] is platform-independent, there are [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 use Shopify's truthiness by default. But it can be toggled to used standard JavaScript truthiness by setting the **jsTruthy** option to `true`.
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`.
value | truthy | falsy
--- | --- | ---
+2 -2
View File
@@ -2,7 +2,7 @@
title: Use in Express.js
---
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:
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:
```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 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 a 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 link):
Outputs (note the blank line):
```
harttle
```
We can include hyphens in your tag syntax (`{% raw %}{{-{% endraw %}`, `-}}`, `{% raw %}{%-{% endraw %}`, `-%}`) to strip whitespace from left or right. For example:
You can include hyphens in tag syntax (`{% raw %}{{-{% endraw %}`, `-}}`, `{% raw %}{%-{% endraw %}`, `-%}`) to strip whitespace from the left or right. For example:
```liquid
{% assign author = "harttle" -%}
-39
View File
@@ -1,39 +0,0 @@
---
title: abs
---
{% since %}v1.9.1{% endsince %}
返回数字的绝对值。
输入
```liquid
{{ -17 | abs }}
```
输出
```text
17
```
输入
```liquid
{{ 4 | abs }}
```
输出
```text
4
```
对于只包含数字的字符串也好使:
输入
```liquid
{{ "-19.86" | abs }}
```
输出
```text
19.86
```
-31
View File
@@ -1,31 +0,0 @@
---
title: append
---
{% since %}v1.9.1{% endsince %}
连接两个字符串并返回结果。
输入
```liquid
{{ "/my/fancy/url" | append: ".html" }}
```
输出
```text
/my/fancy/url.html
```
也可以用于变量。
输入
```liquid
{% assign filename = "/index.html" %}
{{ "website.com" | append: filename }}
```
输出
```text
website.com/index.html
```
@@ -1,27 +0,0 @@
---
title: array_to_sentence_string
---
{% since %}v10.13.0{% endsince %}
把数组转化为句子,用于做标签列表。有一个可选的连接词参数。
输入
```liquid
{{ "foo,bar,baz" | split: "," | array_to_sentence_string }}
```
输出
```text
foo, bar, and baz
```
输入
```liquid
{{ "foo,bar,baz" | split: "," | array_to_sentence_string: "or" }}
```
输出
```text
foo, bar, or baz
```
-27
View File
@@ -1,27 +0,0 @@
---
title: at_least
---
{% since %}v8.4.0{% endsince %}
限制数字到某个最小值。
输入
```liquid
{{ 4 | at_least: 5 }}
```
输出
```text
5
```
输入
```liquid
{{ 4 | at_least: 3 }}
```
输出
```text
4
```
-27
View File
@@ -1,27 +0,0 @@
---
title: at_most
---
{% since %}v8.4.0{% endsince %}
限制数字到某个最大值。
输入
```liquid
{{ 4 | at_most: 5 }}
```
输出
```text
4
```
输入
```liquid
{{ 4 | at_most: 3 }}
```
输出
```text
3
```
-29
View File
@@ -1,29 +0,0 @@
---
title: capitalize
---
{% since %}v1.9.1{% endsince %}
把字符串首字母改为大写。
输入
```liquid
{{ "title" | capitalize }}
```
输出
```text
Title
```
`capitalize` 只会大写首字母,因此后续单词的不会受影响:
Input
```liquid
{{ "my great title" | capitalize }}
```
输出
```text
My great title
```
-49
View File
@@ -1,49 +0,0 @@
---
title: ceil
---
{% since %}v1.9.1{% endsince %}
向上取整,取整前 LiquidJS 会首先把输入转换为数字。
输入
```liquid
{{ 1.2 | ceil }}
```
输出
```text
2
```
输入
```liquid
{{ 2.0 | ceil }}
```
输出
```text
2
```
输入
```liquid
{{ 183.357 | ceil }}
```
输出
```text
184
```
下面的例子中输入是字符串:
输入
```liquid
{{ "3.5" | ceil }}
```
输出
```text
4
```
-17
View File
@@ -1,17 +0,0 @@
---
title: cgi_escape
---
{% since %}v10.13.0{% endsince %}
把字符串 CGI 转义,用于 URL。用对应的 `%XX` 替换特殊字符,空格会被转义为 `+` 号。
输入
```liquid
{{ "foo, bar; baz?" | cgi_escape }}
```
输出
```text
foo%2C+bar%3B+baz%3F
```
-49
View File
@@ -1,49 +0,0 @@
---
title: compact
---
{% since %}v9.22.0{% endsince %}
从数组里移除任何 `null``undefined` 值。
假设 `site.pages` 是网页列表,有些网页包含 `category` 属性用来标明类别。如果把它们 `map` 到数组里,那么对于没有 `category` 属性的元素就会是 `undefined`
输入
```liquid
{% assign site_categories = site.pages | map: "category" %}
{% for category in site_categories %}
- {{ category }}
{% endfor %}
```
输出
```text
- business
- celebrities
-
- lifestyle
- sports
-
- technology
```
使用 `compact` 创建 `site_categories` 数组,可以移除所有 `null``undefined` 值。
输入
```liquid
{% assign site_categories = site.pages | map: "category" | compact %}
{% for category in site_categories %}
- {{ category }}
{% endfor %}
```
输出
```text
- business
- celebrities
- lifestyle
- sports
- technology
```
-55
View File
@@ -1,55 +0,0 @@
---
title: concat
---
{% since %}v2.0.0{% endsince %}
连接多个数组,返回的数组包含所有传入数组的元素。
输入
```liquid
{% assign fruits = "apples, oranges, peaches" | split: ", " %}
{% assign vegetables = "carrots, turnips, potatoes" | split: ", " %}
{% assign everything = fruits | concat: vegetables %}
{% for item in everything %}
- {{ item }}
{% endfor %}
```
输出
```text
- apples
- oranges
- peaches
- carrots
- turnips
- potatoes
```
可以链式地使用 `concat` 过滤器来连接多个数组:
输入
```liquid
{% assign furniture = "chairs, tables, shelves" | split: ", " %}
{% assign everything = fruits | concat: vegetables | concat: furniture %}
{% for item in everything %}
- {{ item }}
{% endfor %}
```
输出
```text
- apples
- oranges
- peaches
- carrots
- turnips
- potatoes
- chairs
- tables
- shelves
```
-86
View File
@@ -1,86 +0,0 @@
---
title: date
---
{% since %}v1.9.1{% endsince %}
把时间戳转换为字符串。LiquidJS 尝试跟 Shopify/Liquid 保持一致,它用的是 Ruby 核心的 [Time#strftime(string)](http://www.ruby-doc.org/core/Time.html#method-i-strftime)。此外 LiquidJS 会先通过 [new Date()][newDate] 尝试把输入转换为 Date 对象。
但 LiquidJS 支持的格式与 [Ruby 的 flag](https://ruby-doc.org/core/strftime_formatting_rdoc.html) 有些不同:
* `%Z`(自 v10.11.1 起支持)只有在传入了时区时才起作用(可以通过 `LiquidOption` 传入,也可以在创建日期时单独传入,见下文)。如果传入的时区是个数字,那么它的表现将会与 `%z` 相同。如果没有传入时区,将会返回 [运行时默认时区](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone)。
* LiquidJS 提供额外的 `%q` 用来处理序数:`{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
* 日期字面量会通过 [new Date()][jsDate] 转化为 `Date` 对象,这意味着字面量默认使用运行时默认时区。
* 格式字参数是可选的:
* 如果不传,默认为 `%A, %B %-e, %Y at %-l:%M %P %z`
* 上述默认值可以通过 [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) 参数覆盖。
输入
```liquid
{{ article.published_at | date: "%a, %b %d, %y" }}
```
输出
```text
Fri, Jul 17, 15
```
{% note info 时区 %}
日期在输出时会转换为当地时区,设置 `timezoneOffset` LiquidJS 参数可以指定一个不同的时区。或者设置 `preserveTimezones``true` 来保持字面量时间戳的时区,数据中的日期对象不受此参数的影响。
{% endnote %}
你也可以在使用 `date` 时再设置时区:
输入
```liquid
{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", 360}} // 等价于设置 `options.timezoneOffset` to `360`.
{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", "Asia/Colombo" }}
```
输出
```liquid
1990-12-31T17:00:00
1991-01-01T04:30:00
```
输入
```liquid
{{ article.published_at | date: "%Y" }}
```
输出
```text
2015
```
输入也可以是符合 JavaScript `Date` 格式的字符串::
输入
```liquid
{{ "March 14, 2016" | date: "%b %d, %y" }}
```
输出
```text
Mar 14, 16
```
{% note info 时间戳字符串 %}
LiquidJS 使用 JavaScript [Date][newDate] 来解析输入字符串,意味着支持 [IETF-compliant RFC 2822 时间戳](https://datatracker.ietf.org/doc/html/rfc2822#page-14) 和 [特定版本的 ISO8601](https://www.ecma-international.org/ecma-262/11.0/#sec-date.parse)。
{% endnote %}
可以用特殊值 `"now"`(或`"today"`)来获取当前时间:
输入
```liquid
This page was last updated at {{ "now" | date: "%Y-%m-%d %H:%M" }}.
```
输出
```text
This page was last updated at 2020-03-25 15:57.
```
{% note info 当前时间 %}注意得到的当前时间是模板渲染时的时间,如果你在用静态站点生成器或者模板有被缓存这一时间可能与用户看到的时间不同。{% endnote %}
[newDate]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date
@@ -1,31 +0,0 @@
---
title: date_to_long_string
---
{% since %}v10.13.0{% endsince %}
把日期转换为长格式(只支持 US/UK 两种),与 Jekyll 的 `date_to_long_string` 过滤器一样。
输入
```liquid
{{ site.time | date_to_long_string }}
```
输出
```text
07 November 2008
```
输入
```liquid
{{ site.time | date_to_long_string: "ordinal" }}
```
输出
```text
7th November 2008
```
注意 JavaScript `Date` 没有时区信息,详情请参考 [date][date] 过滤器。
[date]: ./date.html
@@ -1,20 +0,0 @@
---
title: date_to_rfc822
---
{% since %}v10.13.0{% endsince %}
把日期转换为 RFC-822 格式用于 RSS feed,与 Jekyll 的 `date_to_rfc822` 过滤器一样。
输入
```liquid
{{ site.time | date_to_rfc822 }}
```
输入
```text
Mon, 07 Nov 2008 13:07:54 -0800
```
注意 JavaScript `Date` 没有时区信息,详情请参考 [date][date] 过滤器。
[date]: ./date.html
@@ -1,30 +0,0 @@
---
title: date_to_string
---
{% since %}v10.13.0{% endsince %}
把日期转换为短格式(只支持 US/UK 两种),与 Jekyll 的 `date_to_string` 过滤器一样。
输入
```liquid
{{ site.time | date_to_string }}
```
输出
```text
07 Nov 2008
```
输入
```liquid
{{ site.time | date_to_string: "ordinal", "US" }}
```
输出
```text
Nov 7th, 2008
```
注意 JavaScript `Date` 没有时区信息,详情请参考 [date][date] 过滤器。
[date]: ./date.html
@@ -1,20 +0,0 @@
---
title: date_to_xmlschema
---
{% since %}v10.13.0{% endsince %}
把日期转换为 XML Schema (ISO 8601) 格式,与 Jekyll 的 `date_to_xmlschema` 过滤器一样。
输入
```liquid
{{ site.time | date_to_xmlschema }}
```
输出
```text
2008-11-07T13:07:54-08:00
```
注意 JavaScript `Date` 没有时区信息,详情请参考 [date][date] 过滤器。
[date]: ./date.html
-64
View File
@@ -1,64 +0,0 @@
---
title: default
---
{% since %}v1.9.1{% endsince %}
在值不存在时给一个默认值,如果左侧是 [falsy][falsy] 或空(`string``Array`)就会使用这个默认值。下面的例子中 `product_price` 没有定义,因此使用了默认值。
输入
```liquid
{{ product_price | default: 2.99 }}
```
输出
```text
2.99
```
下面的例子中定义了 `product_price` 所以没有使用默认值。
输入
```liquid
{% assign product_price = 4.99 %}
{{ product_price | default: 2.99 }}
```
输出
```text
4.99
```
下面例子中 `product_price` 为空,所以使用了默认值。
输入
```liquid
{% assign product_price = "" %}
{{ product_price | default: 2.99 }}
```
输出
```text
2.99
```
## 允许 `false`
{% since %}v9.32.0{% endsince %}
为了允许让 `false` 直接输出而不是用默认值,可以用 `allow_false` 参数。
输入
```liquid
{% assign display_price = false %}
{{ display_price | default: true, allow_false: true }}
```
输出
```text
false
```
[falsy]: ../tutorials/truthy-and-falsy.html
-48
View File
@@ -1,48 +0,0 @@
---
title: divided_by
---
{% since %}v1.9.1{% endsince %}
两数相除返回商,返回结果数字在 JavaScript 中 `.toString()` 得到的字符串。
输入
```liquid
{{ 16 | divided_by: 4 }}
```
输出
```text
4
```
输入
```liquid
{{ 5 | divided_by: 3 }}
```
输出
```text
1.6666666666666667
```
在 JavaScript 里数字没有浮点和整数的区分,它们的类型都是 `number`
```javascript
// always true
5.0 === 5
```
因此如果需要做整数运算,需要传入额外的 `integerArithmetic` 参数:
Input
```liquid
{{ 5 | divided_by: 3, true }}
```
Output
```text
1
```
[floor]: ./floor.html
-27
View File
@@ -1,27 +0,0 @@
---
title: downcase
---
{% since %}v1.9.1{% endsince %}
字符串中每个字符都转为小写,对已经是小写的字符没有影响。
输入
```liquid
{{ "Parker Moore" | downcase }}
```
输出
```text
parker moore
```
输入
```liquid
{{ "apple" | downcase }}
```
输出
```text
apple
```
-27
View File
@@ -1,27 +0,0 @@
---
title: escape
---
{% since %}v1.9.1{% endsince %}
把字符串中的 HTML 特殊字符转义,对不需要转义的字符串不会产生影响。
输入
```liquid
{{ "Have you read 'James & the Giant Peach'?" | escape }}
```
输出
<pre class="highlight">
{{"Have you read &#39;James &amp; the Giant Peach&#39;?" | escape}}
</pre>
输入
```liquid
{{ "Tetsuro Takara" | escape }}
```
输出
```text
Tetsuro Takara
```
-27
View File
@@ -1,27 +0,0 @@
---
title: escape_once
---
{% since %}v1.9.1{% endsince %}
把字符串中的特殊字符转义得到可用在 URL 里的字符串,对已经转义过的字符串和不需要转义的字符串不会产生影响。
输入
```liquid
{{ "1 < 2 & 3" | escape_once }}
```
输出
<pre class="highlight">
{{"1 &lt; 2 &amp; 3" | escape}}
</pre>
输入
<pre class="highlight">
&#x7B;&#x7B; "{{"1 &lt; 2 &amp; 3" | escape}}" | escape_once }}
</pre>
输出
<pre class="highlight">
{{"1 &lt; 2 &amp; 3" | escape}}
</pre>
-25
View File
@@ -1,25 +0,0 @@
---
title: find
---
{% since %}v10.11.0{% endsince %}
在数组中找到给定的属性为给定的值的第一个元素并返回;如果没有这样的元素则返回 `nil`。对于 `members` 数组:
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
输入
```liquid
{{ members | find: "graduation_year", 2014 | json }}
```
输出
```text
{"graduation_year":2014,"name":"John"}
```
-25
View File
@@ -1,25 +0,0 @@
---
title: find_exp
---
{% since %}v10.11.0{% endsince %}
找到数组中给定的表达式值为 `true` 的第一个元素,如果没有这样的元素则返回 `nil`。对于下面的 `members` 数组:
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2014, name: 'Jack' }
]
```
输入
```liquid
{{ members | find_exp: "item", "item.graduation_year == 2014" | json }}
```
输出
```text
{"graduation_year":2014,"name":"John"}
```
-37
View File
@@ -1,37 +0,0 @@
---
title: first
---
{% since %}v1.9.1{% endsince %}
返回数组的第一个元素。
输入
```liquid
{{ "Ground control to Major Tom." | split: " " | first }}
```
输出
```text
Ground
```
输入
```liquid
{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}
{{ my_array.first }}
```
输出
```text
zebra
```
需要在标签中使用的时候,可以用点来计算 `first`
```liquid
{% if my_array.first == "zebra" %}
Here comes a zebra!
{% endif %}
```
-49
View File
@@ -1,49 +0,0 @@
---
title: floor
---
{% since %}v1.9.1{% endsince %}
数字下取整,LiquidJS 会尝试把输入转换为数字再做下取整操作。
输入
```liquid
{{ 1.2 | floor }}
```
输出
```text
1
```
输入
```liquid
{{ 2.0 | floor }}
```
输出
```text
2
```
输入
```liquid
{{ 183.357 | floor }}
```
输出
```text
183
```
下面的例子中输入是个数字:
输入
```liquid
{{ "3.5" | floor }}
```
输出
```text
3
```
-48
View File
@@ -1,48 +0,0 @@
---
title: group_by
---
{% since %}v10.11.0{% endsince %}
把数组元素按照给定的属性的值分组。对于 `members` 数组:
```javascript
const members = [
{ graduation_year: 2003, name: 'Jay' },
{ graduation_year: 2003, name: 'John' },
{ graduation_year: 2004, name: 'Jack' }
]
```
输入
```liquid
{{ members | group_by: "graduation_year" | json: 2 }}
```
输出
```text
[
{
"name": 2003,
"items": [
{
"graduation_year": 2003,
"name": "Jay"
},
{
"graduation_year": 2003,
"name": "John"
}
]
},
{
"name": 2004,
"items": [
{
"graduation_year": 2004,
"name": "Jack"
}
]
}
]
```
-48
View File
@@ -1,48 +0,0 @@
---
title: group_by_exp
---
{% since %}v10.11.0{% endsince %}
把数组元素按照给定的 Liquid 表达式的值分组。对于 `members` 数组:
```javascript
const members = [
{ graduation_year: 2013, name: 'Jay' },
{ graduation_year: 2014, name: 'John' },
{ graduation_year: 2009, name: 'Jack' }
]
```
输入
```liquid
{{ members | group_by_exp: "item", "item.graduation_year | truncate: 3, ''" | json: 2 }}
```
输出
```text
[
{
"name": "201",
"items": [
{
"graduation_year": 2013,
"name": "Jay"
},
{
"graduation_year": 2014,
"name": "John"
}
]
},
{
"name": "200",
"items": [
{
"graduation_year": 2009,
"name": "Jack"
}
]
}
]
```
-42
View File
@@ -1,42 +0,0 @@
---
title: inspect
---
{% since %}v10.13.0{% endsince %}
类似于 `json`,但可以处理循环引用的情况。例如对于上下文:
```
const foo = {
bar: 'BAR'
}
foo.foo = foo
const scope = { foo }
```
输入
```liquid
{% foo | inspect %}
```
输出
```text
{"bar":"BAR","foo":"[Circular]"}
```
## 格式化
可以指定一个 `space` 参数来缩进长度。
输入
```liquid
{{ foo | inspect: 4 }}
```
输出
```text
{
"bar": "BAR",
"foo": "[Circular]"
}
```
-19
View File
@@ -1,19 +0,0 @@
---
title: join
---
{% since %}v1.9.1{% endsince %}
把数组中的元素连接成为一个字符串,以传入的参数作为分隔符。
输入
```liquid
{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}
{{ beatles | join: " and " }}
```
输出
```text
John and Paul and George and Ringo
```
-39
View File
@@ -1,39 +0,0 @@
---
title: json
---
{% since %}v9.10.0{% endsince %}
通过 `JSON.stringify()` 把值转换为字符串,多用于调试用途。
输入
```liquid
{% assign arr = "foo bar coo" | split: " " %}
{{ arr | json }}
```
输出
```text
["foo","bar","coo"]
```
## 格式化
{% since %}v10.11.0{% endsince %}
可以指定一个 `space` 参数来格式化 JSON。
输入
```liquid
{% assign arr = "foo bar coo" | split: " " %}
{{ arr | json: 4 }}
```
输出
```text
[
"foo",
"bar",
"coo"
]
```
-9
View File
@@ -1,9 +0,0 @@
---
title: jsonify
---
{% since %}v10.13.0{% endsince %}
见 [json][json]。
[json]: ./json.html
-37
View File
@@ -1,37 +0,0 @@
---
title: last
---
{% since %}v1.9.1{% endsince %}
返回数组的最后一个元素。
输入
```liquid
{{ "Ground control to Major Tom." | split: " " | last }}
```
输出
```text
Tom.
```
输入
```liquid
{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}
{{ my_array.last }}
```
输出
```text
tiger
```
需要在标签中使用的时候,可以用点来计算 `last`
```liquid
{% if my_array.last == "tiger" %}
There goes a tiger!
{% endif %}
```
-17
View File
@@ -1,17 +0,0 @@
---
title: lstrip
---
{% since %}v1.9.1{% endsince %}
移除字符串左侧的空白字符(制表符、空格、换行),不影响词之间的空格。
输入
```liquid
BEGIN{{ " So much room for activities! " | lstrip }}END
```
输出
```text
BEGINSo much room for activities! END
```
-27
View File
@@ -1,27 +0,0 @@
---
title: map
---
{% since %}v1.9.1{% endsince %}
按照属性名提取对象的属性形成另一个数组并返回。
下面的例子中假设 `site.pages` 包含了站点的所有网页元信息。使用 `assign``map` 过滤器创建了一个 `site.pages` 中所有对象的 `category` 属性的值构成的数组。
输入
```liquid
{% assign all_categories = site.pages | map: "category" %}
{% for item in all_categories %}
- {{ item }}
{% endfor %}
```
输出
```text
- business
- celebrities
- lifestyle
- sports
- technology
```
-37
View File
@@ -1,37 +0,0 @@
---
title: minus
---
{% since %}v1.9.1{% endsince %}
两数相减。
输入
```liquid
{{ 4 | minus: 2 }}
```
输出
```text
2
```
输入
```liquid
{{ 16 | minus: 4 }}
```
输出
```text
12
```
输入
```liquid
{{ 183.357 | minus: 12 }}
```
输出
```text
171.357
```
-37
View File
@@ -1,37 +0,0 @@
---
title: modulo
---
{% since %}v1.9.1{% endsince %}
返回两数相除的余数。
输入
```liquid
{{ 3 | modulo: 2 }}
```
输出
```text
1
```
输入
```liquid
{{ 24 | modulo: 7 }}
```
输出
```text
3
```
输入
```liquid
{{ 183.357 | modulo: 12 }}
```
输出
```text
3.3569999999999993
```
@@ -1,23 +0,0 @@
---
title: newline_to_br
---
{% since %}v1.9.1{% endsince %}
把字符串里的所有换行符(`\n`)替换为 HTML 换行(`<br />`)。
输入
```liquid
{% capture string_with_newlines %}
Hello
there
{% endcapture %}
{{ string_with_newlines | newline_to_br }}
```
输出
```html
<br/>Hello<br/>there<br/>
```
@@ -1,17 +0,0 @@
---
title: normalize_whitespace
---
{% since %}v10.13.0{% endsince %}
把连续的空白字符替换为单个空格。
输入
```liquid
{{ "a \n b" | normalize_whitespace }}
```
输出
```html
a b
```
@@ -1,49 +0,0 @@
---
title: number_of_words
---
{% since %}v10.13.0{% endsince %}
计算文本中的单词数。此过滤器接受一个可选参数,用于控制输入字符串中汉字-日语-韩语(CJK)字符的处理方式:
- `'cjk'`:将每个检测到的 CJK 字符计为一个单词,无论是否由空格分隔。
- `'auto'`:与 `'cjk'` 类似,但如果过滤器用于可能包含或不包含 CJK 字符的字符串,则性能更好。
输入
```liquid
{{ "Hello world!" | number_of_words }}
```
输出
```text
2
```
输入
```liquid
{{ "你好hello世界world" | number_of_words }}
```
输出
```text
1
```
输入
```liquid
{{ "你好hello世界world" | number_of_words: "cjk" }}
```
输出
```text
6
```
输入
```liquid
{{ "你好hello世界world" | number_of_words: "auto" }}
```
输出
```text
6
```
-19
View File
@@ -1,19 +0,0 @@
---
title: 过滤器
description: 每个 Liquid 过滤器的描述和示例
---
LiquidJS 支持 Liquid 语法中具体业务无关的过滤器,基本上 [shopify/liquid 核心][shopify/liquid] 支持的 LiquidJS 都支持。这部分包含了所有 LiquidJS 支持的过滤器的文档和使用示例。
LiquidJS 共支持 40+ 个过滤器,可以分为如下几类:
类别 | 过滤器
--- | ---
数学 | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most
字符串 | 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
数组 | 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_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
其他 | default, json, jsonify, inspect, raw, to_integer
[shopify/liquid]: https://github.com/Shopify/liquid
-37
View File
@@ -1,37 +0,0 @@
---
title: plus
---
{% since %}v1.9.1{% endsince %}
两数相加。
输入
```liquid
{{ 4 | plus: 2 }}
```
输出
```text
6
```
输入
```liquid
{{ 16 | plus: 4 }}
```
输出
```text
20
```
输入
```liquid
{{ 183.357 | plus: 12 }}
```
输出
```text
195.357
```
-24
View File
@@ -1,24 +0,0 @@
---
title: pop
---
{% since %}v10.11.0{% endsince %}
从数组末尾弹出一个元素。注意该操作不会改变原数组,而是在一份拷贝上操作。
输入
```liquid
{% assign fruits = "apples, oranges, peaches" | split: ", " %}
{% assign everything = fruits | pop %}
{% for item in everything %}
- {{ item }}
{% endfor %}
```
输出
```text
- apples
- oranges
```

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