1951 Commits

Author SHA1 Message Date
Guilherme Carreiro
248f3a412f Bump to 5.10.0 v5.10.0 2025-10-30 12:28:52 +01:00
Guilherme Carreiro
a16ec56a40 Update error handling for keeping backward-compatibility on error messages in the render tag 2025-10-30 12:00:44 +01:00
Julia Boutin
12fd93fbe2 Missing inline snippets should display same error as filebased 2025-10-30 12:00:44 +01:00
Julia Boutin
5ceb0e9cec Raise error on invalid snippet name 2025-10-30 12:00:44 +01:00
Julia Boutin
98fbd985d8 Remove unneeded read method 2025-10-30 12:00:44 +01:00
Julia Boutin
ae05ba071c Add liquid_public_docs yard tag to snippet tag 2025-10-30 12:00:44 +01:00
Julia Boutin
4205131148 Extract snippet resource scoring logic into assign_score_of 2025-10-30 12:00:44 +01:00
Julia Boutin
db350c54ff Allow render tag to recognize drops that respond to to_partial 2025-10-30 12:00:44 +01:00
Julia Boutin
0cc6cdd553 Remove ... syntax references 2025-10-30 12:00:44 +01:00
Julia Boutin
40e45e32ac Raise syntax error on incorrect render identifier type 2025-10-30 12:00:44 +01:00
Julia Boutin
d4d2237b90 Support prop spreading 2025-10-30 12:00:44 +01:00
Julia Boutin
0ceeefba02 Implement resource limits and remove leftover string references 2025-10-30 12:00:44 +01:00
Julia Boutin
65fb80a347 Render arguments should maintain correct precedence 2025-10-30 12:00:44 +01:00
Julia Boutin
489a03118c Remove inline snippet specific example files 2025-10-30 12:00:44 +01:00
Julia Boutin
99116638fd Support with, for, and as inline snippet syntax
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
2025-10-30 12:00:44 +01:00
Julia Boutin
9bcfd32e65 Support ... inline snippet syntax 2025-10-30 12:00:44 +01:00
Julia Boutin
c7ad1c90ca Change inline snippet identifier from string to variable
Currently, snippet files identified by strings. This
PR makes changes to render to allow for new inline
snippets to use variables as identifiers instead
2025-10-30 12:00:44 +01:00
Julia Boutin
12bbbc4537 Create SnippetDrop and set in scope 2025-10-30 12:00:44 +01:00
Julia Boutin
1eca707c4a Update inline snippets syntax
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 <orlando.qiu@shopify.com>
2025-10-30 12:00:44 +01:00
Josh Faigan
ed9c4e31c4 Introduce new inline snippets tag
Inline snippets will reduce code duplication and
improve the developer experience, eliminating the
need for one-off snippet files
2025-10-30 12:00:44 +01:00
Guilherme Carreiro
c357f91e0c Bump to 5.9.0 v5.9.0 2025-10-27 17:25:36 +01:00
Guilherme Carreiro
1f58216f48 Add unit test mixing positional and kwargs arguments 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
d38795168b Update test/integration/tags/table_row_test.rb
Co-authored-by: Gray Gilmore <graygilmore@gmail.com>
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
51f312b220 Update README.md
Co-authored-by: Gray Gilmore <graygilmore@gmail.com>
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
44f0429c08 Extract /\w+:0x\h{8}/ regex to UNNAMED_CYCLE_PATTERN constant 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
e57b7efe4e Simplify render/include tags following PR review feedback 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
15430c0770 Add rigid mode to rake benchmark task
The benchmark results show that rigid mode performs a bit better than both strict
and lax modes across most metrics, including tokenization, parsing, rendering,
and their combined operations. Rigid mode consistently delivers the highest
number of iterations per second, with performance differences staying within
1–2% compared to the other modes

```
================================================================================
/opt/rubies/3.4.1/bin/ruby ./performance/benchmark.rb lax

Running benchmark for 20 seconds (with 10 seconds warmup).

ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
           tokenize:   332.000 i/100ms
              parse:    14.000 i/100ms
             render:    61.000 i/100ms
     parse & render:    11.000 i/100ms
Calculating -------------------------------------
           tokenize:      3.325k (± 1.0%) i/s  (300.73 μs/i) -    66.732k in  20.070562s
              parse:    148.166 (± 0.7%) i/s    (6.75 ms/i)  -     2.968k in  20.032971s
             render:    654.428 (± 4.0%) i/s    (1.53 ms/i)  -    13.115k in  20.090452s
     parse & render:    116.108 (± 1.7%) i/s    (8.61 ms/i)  -     2.332k in  20.089221s

================================================================================
/opt/rubies/3.4.1/bin/ruby ./performance/benchmark.rb strict

Running benchmark for 20 seconds (with 10 seconds warmup).

ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
           tokenize:   332.000 i/100ms
              parse:    14.000 i/100ms
             render:    61.000 i/100ms
     parse & render:    11.000 i/100ms
Calculating -------------------------------------
           tokenize:      3.332k (± 0.2%) i/s  (300.14 μs/i) -    66.732k in  20.029095s
              parse:    145.674 (± 0.0%) i/s    (6.86 ms/i)  -     2.926k in  20.086104s
             render:    656.711 (± 4.6%) i/s    (1.52 ms/i)  -    13.115k in  20.050810s
     parse & render:    114.705 (± 0.0%) i/s    (8.72 ms/i)  -     2.299k in  20.043028s

================================================================================
/opt/rubies/3.4.1/bin/ruby ./performance/benchmark.rb rigid

Running benchmark for 20 seconds (with 10 seconds warmup).

ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin23]
Warming up --------------------------------------
           tokenize:   333.000 i/100ms
              parse:    14.000 i/100ms
             render:    62.000 i/100ms
     parse & render:    11.000 i/100ms
Calculating -------------------------------------
           tokenize:      3.334k (± 0.3%) i/s  (299.93 μs/i) -    66.933k in  20.075484s
              parse:    148.349 (± 2.0%) i/s    (6.74 ms/i)  -     2.968k in  20.019775s
             render:    663.752 (± 2.6%) i/s    (1.51 ms/i)  -    13.268k in  20.010303s
     parse & render:    116.464 (± 2.6%) i/s    (8.59 ms/i)  -     2.332k in  20.037869s
liquid$
```
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
6aa4041c0f Rename with_error_mode(...) to with_error_modes(...) 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
d63dd104de Update test/integration/tags/render_tag_test.rb
Co-authored-by: Alok Swamy <alok.swamy@shopify.com>
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
1733586242 Update test/unit/tags/case_tag_unit_test.rb
Co-authored-by: Alok Swamy <alok.swamy@shopify.com>
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
5a210fc8f6 Update Rakefile
Co-authored-by: Alok Swamy <alok.swamy@shopify.com>
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
8d7bb9b6f8 Update History.md (5.8.8 -> 5.9.0) 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
4f35764d44 Update History.md 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
018442b7d1 Covered changes with more tests, remove redundant cases, and the new with_error_mode(*modes)
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(...
```
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
a23c71e40b Fix variable to keep it backward-compatible in strict mode
* 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`
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
738540a601 Update infrastructure that handles parsing switching:
* 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
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
4cd367d971 * Update bin/render script to present an error when no template is passed
* Remove `bin/example.liquid` as it's not executable
2025-10-27 16:33:31 +01:00
Charles-P. Clermont
34ab3bdc8e Fix assert_template_result tests not picking up Liquid::Environment.default.error_mode
The `rake test` command gave us the impression that we were running all the tests
on all the error modes, that was false.
2025-10-27 16:33:31 +01:00
Charles-P. Clermont
1be1e36a8d Fixup cycle rigid parsing to be backwards compatible 2025-10-27 16:33:31 +01:00
Charles-P. Clermont
902ff978a6 Fixup include parsing of with expression 2025-10-27 16:33:31 +01:00
Charles-P. Clermont
2ba81b3f1a Fix alias parsing 2025-10-27 16:33:31 +01:00
Charles-P. Clermont
5660ce6945 render end of string is not optional 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
5ec3008b37 Add rigid parser to tablerow tag 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
5248025439 Remove redundant tests where rigid and strict modes have the same
behavior
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
0bc69b86b7 No longer test ParseContext directly on RigidModeUnitTest as
now the `safe: true` calls are considered safe

Test the entire template instead
2025-10-27 16:33:31 +01:00
Charles-P. Clermont
b5fbad08c6 rigid set_attribute in for parsing 2025-10-27 16:33:31 +01:00
Charles-P. Clermont
b8958f626d Stricter 1:1 refactor of strict_parse for Variable 2025-10-27 16:33:31 +01:00
Charles-P. Clermont
94bbf6ca32 Make it possible to safe_parse subsets of expressions
e.g. sometimes you want to only accept strings | lookups.
{% render snippetName %} for example. snippetName is a string right now.
We don't want safe_parse_expression because this would allow snippetName
to be a number, a boolean, etc. But we still want to strict parse this.

So what we'll do is use parse_expression(string, safe: true), this is
an optional opt-in to say "I know what I'm doing". Usually that's
because you're using the output of Parser#something as the input of
parse_expression.

It is true that Parser#expression is subset of Expression.parse, it is
not true of the opposite (e.g. Expression.parse doesn't care about .5
and happily parses that as a global lookup of the variable named "5",
Parser#expression throws for that.)

diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb
index e5c321dc..9ab350f0 100644
--- a/lib/liquid/condition.rb
+++ b/lib/liquid/condition.rb
@@ -48,8 +48,8 @@ module Liquid
       @@operators
     end

-    def self.parse_expression(parse_context, markup)
-      @@method_literals[markup] || parse_context.parse_expression(markup)
+    def self.parse_expression(parse_context, markup, safe: false)
+      @@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
     end

     attr_reader :attachment, :child_condition
diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb
index 1c59fe4a..82cf5768 100644
--- a/lib/liquid/parse_context.rb
+++ b/lib/liquid/parse_context.rb
@@ -51,13 +51,13 @@ module Liquid
     end

     def safe_parse_expression(parser)
-      Expression.safe_parse(parser)
+      Expression.safe_parse(parser, @string_scanner, @expression_cache)
     end

-    def parse_expression(markup)
+    def parse_expression(markup, safe: false)
       # todo(guilherme): remove this once rigid mode is fully using safe_parse_expression
-      # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if @error_mode == :rigid
-      puts("🚨 parse_expression used in rigid mode") if @error_mode == :rigid
+      # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if !safe && @error_mode == :rigid
+      puts("🚨 parse_expression used in rigid mode") if !safe && @error_mode == :rigid

       Expression.parse(markup, @string_scanner, @expression_cache)
     end
diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb
index 656d2e47..374ee511 100644
--- a/lib/liquid/tag.rb
+++ b/lib/liquid/tag.rb
@@ -72,8 +72,8 @@ module Liquid
       parse_context.safe_parse_expression(parser)
     end

-    def parse_expression(markup)
-      parse_context.parse_expression(markup)
+    def parse_expression(markup, safe: false)
+      parse_context.parse_expression(markup, safe: safe)
     end
   end
 end
diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb
index c2be5db1..3182983b 100644
--- a/lib/liquid/tags/for.rb
+++ b/lib/liquid/tags/for.rb
@@ -93,7 +93,7 @@ module Liquid
       raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')

       collection_name  = p.expression
-      @collection_name = parse_expression(collection_name)
+      @collection_name = parse_expression(collection_name, safe: true)

       @name     = "#{@variable_name}-#{collection_name}"
       @reversed = p.id?('reversed')
diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb
index 342374f1..e25d6250 100644
--- a/lib/liquid/tags/if.rb
+++ b/lib/liquid/tags/if.rb
@@ -81,8 +81,8 @@ module Liquid
       block.attach(new_body)
     end

-    def parse_expression(markup)
-      Condition.parse_expression(parse_context, markup)
+    def parse_expression(markup, safe: false)
+      Condition.parse_expression(parse_context, markup, safe: safe)
     end

     def lax_parse(markup)
@@ -124,9 +124,9 @@ module Liquid
     end

     def parse_comparison(p)
-      a = parse_expression(p.expression)
+      a = parse_expression(p.expression, safe: true)
       if (op = p.consume?(:comparison))
-        b = parse_expression(p.expression)
+        b = parse_expression(p.expression, safe: true)
         Condition.new(a, op, b)
       else
         Condition.new(a)
diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb
index 6cdbfd6f..b72a235b 100644
--- a/lib/liquid/tags/include.rb
+++ b/lib/liquid/tags/include.rb
@@ -87,10 +87,11 @@ module Liquid
     def rigid_parse(markup)
       p = @parse_context.new_parser(markup)

-      template_name = p.expression
+      @template_name_expr = safe_parse_expression(p)
       with_or_for = p.id?("for") || p.id?("with") || nil
+      @variable_name_expr = nil
       if with_or_for
-        variable_name = p.expression
+        @variable_name_expr = parse_expression(p.consume(:id), safe: true)
       end

       alias_name = nil
@@ -98,8 +99,6 @@ module Liquid
         alias_name = p.consume(:id)
       end

-      @template_name_expr = parse_expression(template_name)
-      @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
       @alias_name = alias_name

       # optional comma
@@ -109,7 +108,7 @@ module Liquid
       while p.look(:id)
         key = p.consume
         p.consume(:colon)
-        @attributes[key] = parse_expression(p.expression)
+        @attributes[key] = safe_parse_expression(p)
         p.consume?(:comma) # optional comma
       end
     end
diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb
index 89c11063..4f716b24 100644
--- a/lib/liquid/tags/render.rb
+++ b/lib/liquid/tags/render.rb
@@ -88,10 +88,11 @@ module Liquid
     def rigid_parse(markup)
       p = @parse_context.new_parser(markup)

-      template_name = rigid_template_name(p)
+      @template_name_expr = parse_expression(rigid_template_name(p), safe: true)
+      @variable_name_expr = nil
       with_or_for = p.id?("for") || p.id?("with") || nil
       if with_or_for
-        variable_name = p.expression
+        @variable_name_expr = safe_parse_expression(p)
       end

       alias_name = nil
@@ -99,8 +100,6 @@ module Liquid
         alias_name = p.consume(:id)
       end

-      @template_name_expr = parse_expression(template_name)
-      @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
       @alias_name = alias_name
       @is_for_loop = (with_or_for == FOR)

@@ -111,7 +110,7 @@ module Liquid
       while p.look(:id)
         key = p.consume
         p.consume(:colon)
-        @attributes[key] = parse_expression(p.expression)
+        @attributes[key] = safe_parse_expression(p)
         p.consume?(:comma) # optional comma
       end
     end
diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb
index 20957065..a3623bc5 100644
--- a/lib/liquid/variable.rb
+++ b/lib/liquid/variable.rb
@@ -65,11 +65,11 @@ module Liquid

       return if p.look(:end_of_string)

-      @name = parse_context.parse_expression(p.expression)
+      @name = parse_context.safe_parse_expression(p)
       while p.consume?(:pipe)
         filtername = p.consume(:id)
         filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
-        @filters << parse_filter_expressions(filtername, filterargs)
+        @filters << parse_filter_expressions(filtername, filterargs, safe: true)
       end
       p.consume(:end_of_string)
     end
@@ -122,15 +122,15 @@ module Liquid

     private

-    def parse_filter_expressions(filter_name, unparsed_args)
+    def parse_filter_expressions(filter_name, unparsed_args, safe: false)
       filter_args  = []
       keyword_args = nil
       unparsed_args.each do |a|
-        if (matches = a.match(JustTagAttributes))
+        if (matches = a.match(JustTagAttributes)) # we'll need to fix this
           keyword_args           ||= {}
-          keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
+          keyword_args[matches[1]] = parse_context.parse_expression(matches[2], safe: false)
         else
-          filter_args << parse_context.parse_expression(a)
+          filter_args << parse_context.parse_expression(a, safe: safe)
         end
       end
       result = [filter_name, filter_args]
2025-10-27 16:33:31 +01:00
Guilherme Carreiro
4f7dafbcac Use safe_parse_expression instead of parse_expression 2025-10-27 16:33:31 +01:00
Guilherme Carreiro
d77662cd0e Remove unnecessary skips 2025-10-27 16:33:31 +01:00