Add try_parse_tag_token that parses {%...%} tag tokens using
getbyte/byteslice + ByteTables lookup arrays instead of the
FullToken regex with 4 capture groups. Allocates only the 2
strings needed (tag_name, markup) vs 4+ from regex captures.
Uses ByteTables::WORD (no hyphen) for tag name scanning,
matching TagName = /#|\w+/ exactly. Falls back to FullToken
regex when the fast path returns nil.
Skip the expensive recursive VariableParser regex for simple
lookups like 'product.title' (~90% of real-world cases).
SIMPLE_LOOKUP_RE validates the input is a plain a.b.c chain
(no brackets, no quotes). On match, byte-walks on dots to split
segments instead of invoking the regex engine. Falls through to
the original VariableParser scan for complex inputs.
parse_number: replace INTEGER_REGEX/FLOAT_REGEX matching and
StringScanner loop with a single byte-walking pass using
ByteTables::DIGIT. Avoids MatchData allocation and StringScanner
reset on every call.
Expression.parse: only call String#strip when leading/trailing
whitespace is actually present (checked via ByteTables::WHITESPACE).
Avoids allocating a new String on ~4,464 calls per compile.
ByteTables provides pre-computed 256-entry boolean lookup arrays for
byte classification (IDENT_START, IDENT_CONT, WORD, DIGIT, WHITESPACE)
and named constants for delimiter bytes (NEWLINE, DASH, DOT, HASH).
bench_quick.rb measures parse µs, render µs, and object allocations
for the theme benchmark suite.
* feat: add cumulative resource score tracking across partial renders
Add cumulative_render_score and cumulative_assign_score counters to
ResourceLimits that accumulate across reset() calls, with optional
cumulative_render_score_limit and cumulative_assign_score_limit to
cap total work across all partial renders.
Also add a reached? check in BlockBody's render loop so that once a
cumulative limit triggers, the parent template stops processing
further nodes.
Bump version to 5.12.0.
* refactor: move cumulative limit enforcement into reset()
Instead of checking reached? in BlockBody's render loop, enforce
cumulative limits in reset() itself. Since reset() is called before
the begin/rescue MemoryError block in Template#render, the raise
propagates to the parent naturally — no changes to BlockBody needed.
- Implement liquid_blank? and liquid_empty? methods in Condition
to emulate ActiveSupport's behavior when it's not loaded
- This ensures templates like `{% if x == blank %}` work identically
whether ActiveSupport is loaded or not
- Update liquid-spec adapters for new API (ctx parameter)
- Add rake spec task for running liquid-spec matrix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- nil is NOT empty (but IS blank) - matches Shopify production
- String first/last returns '' for empty strings, not nil - matches ActiveSupport
- Add test for nil not being empty
Implement ActiveSupport-compatible behaviors internally so Liquid works
correctly without ActiveSupport being loaded:
1. String first/last via property access (name.first, name.last)
- VariableLookup now handles string[0] and string[-1] for first/last
2. String first/last via filters (name | first, name | last)
- StandardFilters#first and #last now handle strings
3. blank?/empty? comparisons for types without these methods
- Condition now implements liquid_blank? and liquid_empty? internally
- blank? matches ActiveSupport: nil, false, empty/whitespace strings,
empty arrays/hashes are all blank
- empty? checks length == 0 only (whitespace is NOT empty)
This fixes spec failures for templates like:
- {{ name.first }} / {{ name | first }} on strings
- {% if x == blank %} for whitespace strings, empty hashes/arrays
- {% case ' ' %}{% when blank %} matching whitespace
## Summary
- BigDecimal values now stringify using `to_s("F")` format instead of the default `to_s`
## Why
Ruby's default `BigDecimal#to_s` produces engineering/scientific notation for certain values:
```ruby
BigDecimal("0.00001").to_s # => "0.1E-4"
BigDecimal("12345678.9").to_s # => "0.123456789E8"
```
This is rarely the desired output in templates. Using `to_s("F")` produces the expected floating-point format:
```ruby
BigDecimal("0.00001").to_s("F") # => "0.00001"
BigDecimal("12345678.9").to_s("F") # => "12345678.9"
```
Previously, strict2 case/when used `safe_parse_expression`
to parse when expressions causing `blank`/`empty` to be
treated as string literals (Expression::LITERALS maps 'empty' => ''),
rather than method literals
This caused unexpected behavior:
```
{%- case empty_obj -%}
{%- when empty -%}
previously: doesn't render (empty_obj == '' is false)
now: renders (empty_obj.empty? is true)
{%- endcase -%}
```
This commit instead calls `Condition.parse_expression`
with `safe: true`, which will correctly handle `blank`
and `empty`
This commit reverts the Inline Snippets tag (#2001) and bumps
Liquid to 5.11. For now, the inclusion of Inline Snippets
in the latest Liquid release is being treated as a bug.
While #2001 does implement the scope contained in RFC#1916,
we need to take a step back to make sure we’re setting our
sights high enough with this feature, and that we’re truly
supporting theme developers in the ways they need.
If you have any feedback, please leave a comment on RFC#1916.
- Liquid Developer Tools
Previously if you set `strict_variables` to `true` on the context using
`key?('key_name')` would raise a `Liquid::UndefinedVariable` error.
Raising this error makes sense if you're trying to access the variable
directly with something like `context['key_name']` but by using `key?`
you're safely checking if it exists first.
You should be able to enable `strict_variables` and use `key?` in
combination with each other to ensure code safety.
This commit updates the render method to share parts
of the snippet and block rendering logic to enable
inline snippets to support `with`, `for`, and `as`
syntax
Currently, snippet files identified by strings. This
PR makes changes to render to allow for new inline
snippets to use variables as identifiers instead
Previously, inline snippets syntax looked a bit
different, they:
- used strings as tag identifiers
- defined tag arguments {% snippet "input" |type| %}
This PR updates snippets to better reflect
the currently proposed syntax
Co-authored-by: Orlando Qiu <[email protected]>
Most of changes update this:
```
[:lax, :strict].each do |mode|
with_error_mode(mode) do
assert_template_result(...
```
to be this:
```
with_error_mode(:lax, :strict) do
assert_template_result(...
```
* lax_parse - no changes
* strict_parse - uses the `lax_parse_filter_expressions` (as it was doing before)
* rigid_parse - uses the `rigid_parse_filter_expressions`
* Remove development helpers from parse context
* Simplify strict_parse_with_error_mode_fallback and update
documentation
* Add unit tests for `Liquid::Expression` and `Liquid::ParseContext`
* Update test helpers to work better with the `:rigid` mode