url_decode decoded the percent-encoding first and only then replaced
"+" with a space, so a "%2B" became "+" and was immediately turned into
a space. Any literal "+" was therefore lost when round-tripped through
url_encode. I now replace "+" with a space before decodeURIComponent,
which lines up with Ruby's CGI.unescape used by Shopify.
* Add support of inner expressions enclosed by parentheses
* Add support of inner expressions enclosed by parentheses
Made-with: Cursor
* simplify implementation
* fix lint
* fix test
* Enhance tests for parenthesized filter chains in Liquid tags. Added scenarios for enabled and disabled grouped expressions in case, for, if, unless tags, ensuring proper handling of expressions and error throwing for invalid syntax.
* test: remove duplicate readGroupedExpression test block
The readGroupedExpression() test suite was duplicated twice in the spec file. Removed the duplicate block to avoid redundant test execution.
* refactor: extract extractGroupedExpressionTokenVariables helper
Extract inline grouped expression variable extraction logic into a dedicated
function for consistency with other extractors (extractFilteredValueVariables,
extractPropertyAccessVariable).
This addresses PR #863 comment 7 - improves code organization and
maintainability.
* refactor(types): explicit type for collection in for tag
collection: ValueToken | GroupedExpressionToken
Addresses PR #863 comment 5.
* refactor: evaluate grouped expressions at render time with resolvedFilters
Addresses PR review comments 4, 6, 8, 9 - moves grouped expression evaluation
from parse-time resolution to render-time lazy evaluation following the
generator-based async/sync duality pattern used throughout liquidjs.
Key changes:
- Replace resolvedValue (Value instance) with resolvedFilters (Filter[])
- Rename resolveGroupedExpressions() to resolveGroupedExpressionFilters()
- Move evaluation logic to evalGroupedExpressionToken() at render time
- Build Filter instances at parse time (carry liquid reference for render)
- Evaluate expression and apply filters lazily via generators
- Add support for tablerow tag with grouped expressions
- Remove duplicate getFilter() method in Value class
Maintains proper layering (tokens → render → templates) and consistency
with Value.value() pattern. Filter resolution still happens at parse time
since it requires liquid.filters access, but actual evaluation is deferred
to render time.
Tags that store raw ValueToken (for, case when-values, tablerow) still need
explicit resolveGroupedExpressionFilters() calls. Tags that wrap with
new Value() get automatic recursive resolution via Value constructor.
* refactor: reuse FilteredValueToken and fix architectural layering
Replace GroupedExpressionToken with existing FilteredValueToken to avoid
code duplication and fix layering violation where tokens depended on
templates (Filter instances).
Key changes:
- Reuse FilteredValueToken instead of GroupedExpressionToken
- Simplify readGroupOrRange() to return FilteredValueToken | RangeToken
- Add liquid reference to Context for runtime filter resolution
- Build Filter instances at render time in evalFilteredValueToken()
- Remove resolveGroupedExpressionFilters() and parse-time resolution
- Remove explicit resolution calls from tag constructors
This maintains proper architectural layering (tokens → render → templates)
with no backward dependencies, as requested in PR review feedback.
All 1537 tests pass.
* revert redundant'
* refactor: make getFilter private and improve code organization
* test: fix test name in case.spec.ts for when disabled block
* refactor: no need for Deprecated flag
* test: fix test name and logic to properly test if tag with nested expressions
* feat: support real parenthesis grouping in grouped expressions
Allow arbitrary expressions inside parentheses (e.g. ((a | upcase) > 3)
and (1 < 3)) when groupedExpressions is enabled, reusing readFilteredValue
for the general case while keeping range and filter-chain fast paths.
* feat: enhance expression tokenization with new generator methods
Added `readExpressionTokensFromHere` and `readGroupedExpressionTokens` methods to improve the handling of expression tokens. This refactor simplifies the token reading process and maintains compatibility with existing grouped expressions, ensuring proper evaluation and filtering.
* add tests
* address comments
---------
Co-authored-by: Omri Rosner <[email protected]>
The %s handler read LiquidDate.getTime(), which returns the
displayDate deliberately shifted by the display timezone offset for
wall-clock getters. With a timezone argument or timezoneOffset
option set, %s produced an epoch shifted by (server offset - display
offset) instead of the true Unix timestamp.
Expose the unshifted time as LiquidDate.dateValue() and use it for
%s. Also switch Math.round to Math.floor so fractional seconds
truncate toward the epoch like Ruby strftime.
Fixes#931
%N renders the fractional part of the second. The milliseconds returned by
getMilliseconds() are the three most significant digits of that fraction and
must be zero-padded to three digits before use, otherwise sub-100ms values
lose their leading zeros:
50ms => strftime("%N") returned "500000000", expected "050000000"
5ms => strftime("%3N") returned "500", expected "005"
Pad the milliseconds to three digits before slicing to the requested width.
Ruby/Shopify `slice` returns nil (rendered as an empty string or array) when
the begin offset falls outside the negative range or when the length is
negative. liquidjs forwarded the adjusted indices straight to
Array/String.prototype.slice, whose own negative-index handling produced
non-empty, incorrect output:
{{ "hello" | slice: -10, 2 }} => "he" (expected "")
{{ "Liquid" | slice: 1, -2 }} => "iqui" (expected "")
Guard the adjusted begin and the length before slicing.
* fix(filters): charge join/array_to_sentence_string by output size
join charged memoryLimit by array element count, not by the string it
produces, letting concat doubling (cheap reference copies) inflate an
array's element count and then materialize a huge string via join far
past the configured memoryLimit (GHSA-4r6h-5v86-94p3). Charge by the
sum of stringified element lengths plus separators before allocating.
Apply the same fix to the sibling array_to_sentence_string filter.
Co-authored-by: Cursor <[email protected]>
* refactor(filters): simplify join output-size accounting
Sum stringified element lengths in a single pass and keep the guarded
Array.prototype.join for the result, instead of building an intermediate
parts array.
Co-authored-by: Cursor <[email protected]>
* fix(filters): charge json/jsonify/inspect serialization to memoryLimit
json/jsonify/inspect serialized values without charging memoryLimit, so
a concat-doubled array (cheap reference copies) could be materialized
into a huge JSON string past the configured limit — the same unbounded
class as the join bug (GHSA-4r6h-5v86-94p3). Charge via a JSON.stringify
replacer that accounts string lengths as it walks, aborting mid-
serialization instead of allocating the full blob first.
Co-authored-by: Cursor <[email protected]>
* fix(memory): charge rendered output to memoryLimit at emission
Move output-length accounting into the emitters, which charge each
written chunk against ctx.memoryLimit right before it reaches the
result string or stream. Filters/tags now only pre-charge the extra
working memory they allocate apart from that output, so join drops its
bespoke output-size counting and charges array.length like its siblings.
The block.super capture emitter intentionally omits the limiter to
avoid double-counting content that is re-emitted through the final
emitter.
Co-authored-by: Cursor <[email protected]>
* refactor(filters): rely on emitter output charge for json/inspect/array_to_sentence_string
With rendered output charged at emission, these filters no longer need
bespoke output-size counting: the emitted case is covered by the final
emitter. Revert json/inspect to their original form and array_to_sentence_string
to its element-count charge, dropping the non-emitted `| size` guards.
Co-authored-by: Cursor <[email protected]>
* revert(memory): drop emitter output charge, restore filter output-size accounting
join/array_to_sentence_string/json/inspect charge memoryLimit by the
string they materialize (not element count), so discarded results like
{% assign out = a | join %}{{ out | size }} are still bounded.
Remove the emitter-level limiter added in 2f343f063; it cannot catch
materialized-but-not-emitted values.
Co-authored-by: Cursor <[email protected]>
* fix(filters): charge json/inspect replacer by serialized node size
Replace the flat 1-unit charge for non-string JSON nodes with per-type
estimates (primitives via JSON.stringify length, containers by structure).
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* fix: enforce ownPropertyOnly for inherited array indices
Route array index access (including negative indices, first/last, and the
first/last filters) through a shared readArrayElement helper so that
ownPropertyOnly hides prototype-inherited array indices, closing the
GHSA-fwxr-j5w2-587m bypass. The option's scope (property/index access
only, not filter transforms or iteration) is documented on the option.
Co-authored-by: Cursor <[email protected]>
* fix(filters): invoke Array.prototype methods on unsanitized array values
Call built-ins via Array.prototype.<m>.call(...) for values that come
from scope (join, compact, concat, slice, where/reject) so an overridden
instance method on unsanitized data cannot hijack filter behavior.
Methods on freshly-created arrays are left as-is.
Co-authored-by: Cursor <[email protected]>
* fix(filters): use String.prototype.slice for the string branch of slice
Route the non-array branch through String.prototype.slice.call so the
slice filter never dispatches through a possibly-overridden instance
method, matching the Array.prototype guard.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
The `modulo` filter used JavaScript's `%` (truncated remainder, sign
follows the dividend). Shopify/Ruby Liquid uses floored modulo, where the
result takes the sign of the divisor. Since liquidjs advertises Shopify
compatibility, negative operands produced the wrong sign.
Use `((v % arg) + arg) % arg` to match Ruby's `%`. Positive-operand
results are unchanged.
* docs(readme): lead with quick start and scannable structure
Restructure the README to match common OSS conventions: tagline and
badges above the fold, copy-paste Quick start, Features list, and a
compact Used by section. Remove the star plea, centered logo, and
per-project marketing blurbs that pushed useful content down.
Co-authored-by: Cursor <[email protected]>
* docs(readme): playground GIF, used-by grid, and docs homepage sync
Add data/used-by.json with build:used-by for README and docs homepage, playground demo capture, and shared home-section layout. Used by lists products with site logos; Financial Support keeps org and individual sponsors.
Co-authored-by: Cursor <[email protected]>
* chore: use .local for playground capture scratch files
Co-authored-by: Cursor <[email protected]>
* fix: satisfy eslint in build-used-by and capture scripts
Co-authored-by: Cursor <[email protected]>
* chore: drop one-off playground capture script
Co-authored-by: Cursor <[email protected]>
* refactor(docs): copy Used by from README like financial contributors
Drop data/used-by.json and build-used-by.js; build-contributors.js now extracts USED-BY-BEGIN/END to used-by.swig.
Co-authored-by: Cursor <[email protected]>
* refactor(docs): inline Used by section, drop home-section partial
Co-authored-by: Cursor <[email protected]>
* fix(docs): drop redundant logo styles from .contributors
Co-authored-by: Cursor <[email protected]>
* fix(docs): build liquid bundle before hexo serve
Co-authored-by: Cursor <[email protected]>
* refactor(docs): drop playground window chrome from capture demo
Co-authored-by: Cursor <[email protected]>
* refactor(docs): revert playground capture changes to master behavior
Restore Ace output pane, drop Prism and output-preview styling. Simplify docs:dev to rely on docs prestart.
Co-authored-by: Cursor <[email protected]>
* refactor(docs): rely on docs prebuild for liquid bundle and contributors
Co-authored-by: Cursor <[email protected]>
* feat(docs): show playground output as Prism-highlighted HTML code
Co-authored-by: Cursor <[email protected]>
* feat(docs): polish playground layout and regenerate README demo GIF
* fix(docs): align playground GIF capture with live editor styling
* fix(docs): unify playground pane padding and hold output on errors
Match editor inset to the output panel, drop Prism from output preview,
keep the last render while typing invalid template/context, and refresh
the README demo GIF.
* fix(docs): regenerate playground GIF with held output during typing
* feat(docs): sync Used by logos and polish playground
Inline README Used by grid on the docs homepage, refine playground layout and live output behavior, and drop the unused build-used-by script from package scripts.
Co-authored-by: Cursor <[email protected]>
* fix(docs): restore Rock RMS logo and remove duplicate entry
Restore the official Rock RMS wordmark (GetImage.ashx?id=72534) instead of the SparkDevNetwork GitHub org avatar that was wrongly substituted for it.
Co-authored-by: Cursor <[email protected]>
* fix(docs): regenerate playground GIF with live indicator states
Restore the capture script for the new pane-indicator layout so the README demo shows correct idle/active/pending/ok colors and pulsing animations while typing.
* fix(docs): static playground GIF with correct indicator colors
Capture one frame per keystroke with animations disabled so dot states
(idle/active/pending/ok) match the live playground without pulsing.
Co-authored-by: Cursor <[email protected]>
* docs: use square Rock RMS icon in Used by section
Co-authored-by: Cursor <[email protected]>
* fix(docs): ensure capture indicator colors apply instantly
Disable indicator transitions and cancel active animations before
setting data-state so pending yellow is not stuck on the prior ok green.
Co-authored-by: Cursor <[email protected]>
* docs: point Microsoft Used by link to microsoft.com
The merged tile title covers Power Pages and Azure API Management; href should go to Microsoft home, not Power Pages only.
Co-authored-by: Cursor <[email protected]>
* docs: remove Dailycontributors from Used by section
No evidence they run on LiquidJS; they are an OpenCollective sponsor only.
Co-authored-by: Cursor <[email protected]>
* docs: reword intro to say Liquid, not Shopify Liquid
Move Shopify into the compatibility list and drop the shopify/liquid link from README; align package.json description.
Co-authored-by: Cursor <[email protected]>
* fix(docs): restore playground output as Prism-highlighted HTML
Co-authored-by: Cursor <[email protected]>
* docs: add extensible to README intro and package description
EOF
Co-authored-by: Cursor <[email protected]>
* chore: move playground capture script to .local
Co-authored-by: Cursor <[email protected]>
* fix(docs): drop unused Ace mode-html from playground
Output pane uses Prism, not Ace; template and context editors still need liquid/json modes and basePath for themes.
Co-authored-by: Cursor <[email protected]>
* docs: sync intro sentence across package and site metadata
Align package.json, docs config, manifest, llms.txt, and AGENTS.md tagline to the README canonical description.
Co-authored-by: Cursor <[email protected]>
* docs: trim verbose intro in intro-to-liquid tutorial
Remove README tagline and repo-purpose copy duplicated by the recent metadata sync.
Co-authored-by: Cursor <[email protected]>
* docs: shorten homepage banner subtitle
Trim docs site banner and short taglines after the em dash; keep full description for meta tags and npm/README.
* docs: simplify playground GIF caption in README
Co-authored-by: Cursor <[email protected]>
* docs: dedupe homepage subtitle and description into _config.yml
Remove redundant front matter from index.pug; theme falls back to site config for banner and meta tags.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
The Tokenizer constructor calls createTrie(operators) and
createTrie(literalValues) on every instantiation, and liquidjs builds a
fresh Tokenizer per output/tag while parsing. On typical templates this
rebuilt the same prefix-tries dozens of times and showed up as a large
share of parse CPU in profiling.
Memoize createTrie with a module-level WeakMap keyed on the input object.
The inputs (operators, literalValues) are stable references and the trie
is only ever read afterward (via matchTrie), never mutated, so caching by
reference is behavior-preserving. WeakMap (not Map) lets short-lived,
per-instance operator objects and their tries be garbage collected.
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]>
* 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]>
* 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]>
* 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]>
- 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]>
* 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]>
* 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]>
* 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]>
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]>
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]>
* 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
Use _getFromScope for property access in sort/sort_natural filters to respect the ownPropertyOnly security option, preventing prototype chain traversal that could leak sensitive inherited properties.
Also extract shared sortBy helper, add orderedCompare with nil handling consistent with caseInsensitiveCompare and Ruby Liquid.
Made-with: Cursor
* fix: use realpath for fs.contains
* chore: reset file mode changes
Made-with: Cursor
* fix: Windows compat for contains/containsSync and toLiquidAsync arg order
Made-with: Cursor
DISCLAIMER: This may not be the proper way to approach the issue.
Although the Markdown code is proper, on the website itself it is incorrectly rendered as "{{` and `}}".
The reason for the disclaimer above is that I’m imagining this may be an issue at the content rendering level, and my fix here is just a workaround of that issue. — Given this, I’ll not be offended if this PR of mine is rejected and closed. 🙂
* feat: static variable analysis
* Accept any iterable from `children`, `arguments`, etc.
* Test analysis of standard tags
* Use `TagToken.tokenizer` instead of creating a new one
* Test analysis of netsted tags
* Group variables by their root value
* Test analysis of nested globals and locals
* Analyze included and rendered templates WIP
* Use existing tokenizer when constructing `Hash`
* Improve test coverage
* Analyze variables from `layout` and `block` tags
* Test analysis of Jekyll style includes
* Handle variables that start with a nested variable
* Async analysis
* Test non-standard tag end to end
* Implement convenience analysis methods on the `Liquid` class
* More analysis convenience methods
* Accept string or template array
* Draft static analysis docs
* Deduplicate variables names
* Fix isolated scope global variable map
* Coerce variables to strings instead of extending String
* Private map instead of extending Map
* Fix e2e test
* Tentatively implement analysis of aliased variables
* Fix nested variable segments array
* Update docs sidebar
* fix: allow liquidMethodMissing to return any supported value type
* Update src/drop/drop.ts
Co-authored-by: Jun Yang <[email protected]>
---------
Co-authored-by: Jun Yang <[email protected]>
* fix: only render the first 'else' template in the case of multiples
* fix: empty else block and cases with when conditions
* fix: don't render elsif after else
* chore: Update src/tags/unless.ts
* chore: Update src/tags/if.ts
* chore: Update src/tags/case.ts
---------
Co-authored-by: Jun Yang <[email protected]>
* fix: rely on equal for computing contains
This change allows contains to be computed using the toValue of the items in the array.
Before this change if I had an array of "Drop" `contains` would not match against the value.
* Apply suggestions from code review
Co-authored-by: Jun Yang <[email protected]>
---------
Co-authored-by: Jun Yang <[email protected]>
* feat(filters): add array sum filter
* docs(filters): array sum filter
* docs: update versoin number in source/filters/sum.md
---------
Co-authored-by: Jun Yang <[email protected]>
As written, the strftime module's `getSuffix` method (responsible for
returning a date's ordinal string) works correctly for all dates in a
month _except_ 11, 12, and 13.
The existing code would return a string based on the last digit of the
date. In American English, these three dates use the `th` suffix rather
than `st`, `nd`, or `rd`.
This refactor/bug fix draws inspiration from Rails' ActiveSupport
ordinal inflector:
https://github.com/rails/rails/blob/main/activesupport/lib/active_support/locale/en.rb
The Ruby code above is designed to work with any number, so the bug fix
to this library is simpler.
The test suite is also updated with additional examples to verify the
correctness of these changes.
Once #615 is merged, the build script should work on macOS just as well as it does on Ubuntu and with this GitHub Actions matrix we can keep ensuring that it continues to work as it should.
This should help anyone looking to test their changes they made to LiquidJS via the Playground site also provided in the repository.
I have also added basic information on where the Playground is and how to run it in general.
* fix: sed invocations to work out of the box on macOS
These fixes make the sed invocations run on macOS but I am not sure if they will break the GNU sed invocations in return…
* fix: add macOS branches to the shell scripts
The sed command line was difficult (impossible?) to be made portable so I just added macOS conditions.
* Prototype the solution using an alias
It seems that this will not work because I keep getting "sedi: command not found".
* fix: use a Bash function instead of an alias to run sed portably
This seems to work on my local so let's see if it runs in the CI on GitHub.
* fix: use 2 spaces like the original scripts did
Not sure why VS Code went with 4.
* fix: use sedi for the build-changelog `1i\` part as well
I am curious if Ubuntu will handle the newline…
* fix: sed invocations to work out of the box on macOS
These fixes make the sed invocations run on macOS but I am not sure if they will break the GNU sed invocations in return…
* fix: add macOS branches to the shell scripts
The sed command line was difficult (impossible?) to be made portable so I just added macOS conditions.
* Prototype the solution using an alias
It seems that this will not work because I keep getting "sedi: command not found".
* fix: use a Bash function instead of an alias to run sed portably
This seems to work on my local so let's see if it runs in the CI on GitHub.
* fix: use 2 spaces like the original scripts did
Not sure why VS Code went with 4.
* fix: use sedi for the build-changelog `1i\` part as well
I am curious if Ubuntu will handle the newline…
* Add support for the Jekyll sample filter
See https://jekyllrb.com/docs/liquid/filters
I am sorting the array randomly and then picking the first N items or all items if there is no sample limit.
* Add tests for `push`
I thought `push` was broken because it doesn't work on the Playground but that's not the case. Here are the tests to prove it. Taken from the `concat` tests.
* Remove the sample experiment
This got in from another branch lol.
* Recommend the build:dist command over the whole build
This one works for me locally whereas build:docs is reporting some issues.
* Return the full `build` command and explain how to use `commitlint` to workshop the message
I am working on a PR to make `build` run on macOS because it is a part of the pre-commit hook anyway so all contributors should make it work for them.
I have also shown how to check your messages against `commitlint` from the CLI because it is faster than using the VS Code GUI.
* Add support for the Jekyll sample filter
See https://jekyllrb.com/docs/liquid/filters
I am sorting the array randomly and then picking the first N items or all items if there is no sample limit.
* Remove incorrect spaces before parens
Typo and copy-paste meet
* docs: add echo and liquid tags chinese translation
* Update docs/source/zh-cn/tags/echo.md
Co-authored-by: Jun Yang <[email protected]>
* Update docs/source/zh-cn/tags/echo.md
Co-authored-by: Jun Yang <[email protected]>
* Update docs/source/zh-cn/tags/echo.md
Co-authored-by: Jun Yang <[email protected]>
* Update Liquid.md
* Update Liquid.md
Co-authored-by: Jun Yang <[email protected]>
- `relativeReference` is enabled by default, set to `false` to disable
- Referenced files are still constrained within root/partias/layouts
- fix: relative filenames are not constrained (which allows arbitrary filesystem read)
Example Usage:
{% render "../foo/bar.html" %}
Note:
../foo/bar.html' should also be within `partials` (or `root` if `partials` not set)
* fix: spelling
* fix: respect param order for reversed
* docs: add for-reversed order details
* perf: improve performance by 4x by simplified parseFile
BREAKING CHANGES:
- previously deprecated `getTemplate()` and `getTemplateSync()` not no longer supported
- `opts` no longer support dynamic set in `parseFile()`, `renderFile()` arguments
* perf: parse filenames in parse() insteadof render()
* docs: update description of LiquidJS
Co-authored-by: harttle <[email protected]>
- previously deprecated `getTemplate()` and `getTemplateSync()` not no longer supported
- `opts` no longer support dynamic set in `parseFile()`, `renderFile()` arguments
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support. TypeScript in `src/`, bundles in `dist/`. Docs site in `docs/` (Hexo, `navy` theme).
-`toValueSync(generator)` — sync driver; passes yielded values through as-is
Never duplicate logic into separate async and sync methods. One generator serves both paths.
When wrapping an async+sync pair (e.g. `contains`/`containsSync`, `readFile`/`readFileSync`), use `toLiquidAsync(asyncFn, syncFn?)` — returns a `LiquidAsync<F>` that picks sync or async via a leading `sync: boolean` arg. `yield` the result inside a generator. See `src/util/async.ts`.
## Style
Make minimal changes only. Avoid sweeping edits. Always check after you made changes.
- Change only what the task requires. No drive-by refactors, test harnesses, or extra files unless asked.
- Match existing patterns in the file you edit.
- Repro, PoC, and scratch files go in `.local/` — not tracked `poc/` folders or one-off scripts under `docs/`.
### Comments
- Do not add narrative comments. Code should be clear from structure and naming; if it needs explanation, refactor instead.
- Comments follow existing repo usage only: non-obvious invariants, `@deprecated`, JSDoc on public API where TypeDoc needs it. Not for explaining changes to the author, migration history, or restating what the code already says.
- Comments document the code; they do not fix unclear code.
### Tests
- Assert observable behavior, not internal implementation details.
- Avoid duplicate coverage; keep test diffs minimal.
- **E2E** (`test/e2e/`): import from the package root (resolves to `dist/` via `package.json`). Do not import from `src/` — e2e must match what npm consumers get.
- **Integration/unit** (`test/integration/`, etc.): may import from `src/` against current TypeScript sources.
### Docs site
- Reuse existing asset paths under `docs/source/` and `docs/themes/navy/` — no new asset directories unless asked.
- Front matter `title:` is plain text (no backticks).
-`docs/source/llms.txt` — deployed to https://liquidjs.com/llms.txt for web agents (llms.txt spec).
- After theme/markdown changes: build or serve locally, check in a browser (light and dark), not only curl or editor preview.
### README
- Research original sources before reordering contributors, logos, or lists.
## Verify
- Do not commit, push, amend, or open a PR unless asked.
- After changes: verify yourself via CLI or UI (tests, `cd docs && npm start`, browser) before reporting done. Do not tell the user to check instead.
- Before push on sweeping changes: run `npm run check`.
- Confirm facts from `.github/workflows`, `package.json`, and library docs — not stale human docs or assumptions.
- When replacing or integrating a library: read its docs and understand what the previous setup did before changing behavior.
### Security fixes
- Reproduce on current `master` first. Smallest fix that addresses the reported issue.
- If Shopify/Ruby Liquid behaves the same, document unsafe usage in filter/docs instead of changing behavior.
## Docs
- Published: https://liquidjs.com
- Repo agent instructions: this file (`AGENTS.md`)
A simple, expressive, safe and [Shopify][shopify/liquid] compatible template engine in pure JavaScript.
**The purpose of this repo** is to provide a standard Liquid implementation for the JavaScript community so that [Jekyll sites](https://jekyllrb.com), [Github Pages](https://pages.github.com/) and [Shopify templates](https://themes.shopify.com/) can be ported to Node.js without pain.
A simple, expressive, extensible Liquid template engine for JavaScript — Shopify, Jekyll and GitHub Pages compatible, for Node.js, browsers, and the CLI, with TypeScript support.
* [Documentation][doc]
* Please star [LiquidJS on GitHub][github]!
* Support [LiquidJS on Open Collective][oc] or [Patreon][patreon]
<img src="docs/source/playground-demo.gif" alt="LiquidJS playground: edit a template and context, see live HTML output" width="980" style="display: block; margin: 0 auto;" />
</a>
<p align="center"><sub>Try the <a href="https://liquidjs.com/playground.html">online playground</a>.</sub></p>
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Liquid"}'
```
* [gulp-liquidjs](https://www.npmjs.com/package/@tuanpham-dev/gulp-liquidjs): A shopify compatible Liquid template engine for Gulp using liquidjs.
* [grunt-liquify](https://www.npmjs.com/package/grunt-liquify): A Grunt task to process Liquid using liquidjs. Use it to add Liquid magic to your scripts and css assets.
* [react-liquid](https://github.com/aquibm/react-liquid#readme): Liquid templating language component for React
* [@11ty/eleventy](https://www.npmjs.com/package/@11ty/eleventy): A simpler static site generator. An alternative to Jekyll. Written in JavaScript. Transforms a directory of templates (of varying types) into HTML.
See the [setup guide][setup] for partials, layouts, caching, and other options.
## Backers
## Used by
If you love LiquidJS or your company is using LiquidJS? Please consider [support us on Open Collective or Patreon][financial-support].

Products and projects running on LiquidJS. [Open a PR](https://github.com/harttle/liquidjs/edit/master/README.md) to add yours.
## Financial Support
If you personally love LiquidJS or it's benefiting your business, please consider financially support us via [GitHub Sponsors](https://github.com/sponsors/harttle). Special thanks to our sponsors!
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.