Skip to content
Draft

WIP3 #2021

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
aa11d34
Remove :lax, :strict and :warn error modes
charlespwd Nov 6, 2025
db61632
Rename strict2_parse to parse_markup
charlespwd Nov 6, 2025
f0f63d4
Remove :error_mode
charlespwd Dec 2, 2025
c12fc1b
Update changelog with planned changes for 6.0.0
charlespwd Dec 1, 2025
d22ead1
Remove warnings system
charlespwd Dec 3, 2025
c688f70
Move `safe_parse_expression` into `Parser.expression_node`
charlespwd Dec 2, 2025
158feb0
Move parse_expression to Parser.unsafe_parse_expression
charlespwd Dec 2, 2025
44106dc
Rename Parser#expression -> Parser#expression_string
charlespwd Dec 2, 2025
123c2e3
Rename Parser#argument -> argument_string
charlespwd Dec 3, 2025
8222a1c
Rename Parser#expression_node -> Parser#expression
charlespwd Dec 2, 2025
58cb885
Remove Expression#safe_parse
charlespwd Dec 2, 2025
8848065
Extract Parser#string out of Expression.parse
charlespwd Dec 2, 2025
96e1595
Extract Parser#number parse out of Expression.parse
charlespwd Dec 2, 2025
7b37ba6
Simplify parse_number
charlespwd Dec 2, 2025
58ce63e
Replace RangeLookup.parse with RangeLookup.create
charlespwd Dec 2, 2025
1023f3f
Move VariableLookup parsing logic to .parse instead of initializer
charlespwd Dec 2, 2025
0d80117
Extract Parser#variable_lookup out of Expression.parse
charlespwd Dec 2, 2025
ab89281
Extract Parser#range_lookup out of Expression.parse
charlespwd Dec 3, 2025
5e3d8f4
Replace Context[]'s Expression.parse with Parser#expression
charlespwd Dec 3, 2025
82f10b7
Make Parser.expression parse comparisons
charlespwd Dec 3, 2025
1e70fed
Make Parser.expression parse equality expressions
charlespwd Dec 3, 2025
1d42d9c
Use BinaryExpression instead of Condition for comparisons
charlespwd Dec 3, 2025
f49de89
Add boolean expressions as variable unit tests
charlespwd Dec 3, 2025
585270e
Make Condition unit tests go through BinaryExpression
charlespwd Dec 4, 2025
c6e297d
Remove Condition.operators feature that let you define operators
charlespwd Dec 4, 2025
1130dc7
Remove Condition.op, Condition.right
charlespwd Dec 4, 2025
7275dfe
Add support for logical expressions
charlespwd Dec 4, 2025
0613db4
Remove Condition#{child_relation,and,or}
charlespwd Dec 4, 2025
34bba2b
Prepare logical for grouping
charlespwd Dec 5, 2025
3895bca
Add support for parenthesized expressions
charlespwd Dec 5, 2025
fd3bd81
Update History.md
charlespwd Dec 4, 2025
a33443c
Annotate Parser with grammar rules
charlespwd Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Liquid Change Log

## 6.0.0

### Features
* Add support for boolean expressions everywhere
* As variable output `{{ a or b }}`
* As filter argument `{{ collection | where: 'prop', a or b }}`
* As tag argument `{% render 'snip', enabled: a or b %}`
* As conditional tag argument `{% if cond %}` (extending previous behaviour)
* Add support for subexpression prioritization and associativity
* In ascending order of priority:
* Logical: `and`, `or` (right to left)
* Equality: `==`, `!=`, `<>` (left to right)
* Comparison: `>`, `>=`, `<`, `<=`, `contains` (left to right)
* Groupings: `( expr )`
- For example, this is now supported
* `{{ a > b == c < d or e == f }}` which is equivalent to
* `{{ ((a > b) == (c < d)) or (e == f) }}`
- Add support for parenthesized expressions
* e.g. `(a or b) == c`

### Architectural changes
* `parse_expression` and `safe_parse_expression` have been removed from `Tag` and `ParseContext`
* `Parser` methods now produce AST nodes instead of strings
* `Parser#expression` produces a value,
* `Parser#string` produces a string,
* etc.

### Breaking changes
* The Environment's `error_mode` option has been removed.
* `:warn` is no longer supported
* `:lax` and `lax_parse` is no longer supported
* `:strict` and `strict_parse` is no longer supported
* `strict2_parse` is renamed to `parse_markup`
* Expressions
* The `warnings` system has been removed.
* `Parser#expression` is renamed to `Parser#expression_string`
* `safe_parse_expression` methods are replaced by `Parser#expression`
* `parse_expression` methods are replaced by `Parser#unsafe_parse_expression`
* `Condition`
* `new(expr)` no longer accepts an `op` or `right`. Logic moved to BinaryExpression.
* `Condition#or` and `Condition#and` were replaced by `BinaryExpression`.
* `Condition#child_relation` replaced by `BinaryExpression`.
* `Condition.operations` was removed.
* `Condtion::MethodLiteral` was moved to the `Liquid` namespace

### Migrating from `^5.11.0`
- In custom tags that include `ParserSwitching`, rename `strict2_parse` to `parse_markup`
- Remove code depending on `:error_mode`
- Replace `safe_parse_expression` calls with `Parser#expression`

## 5.11.0
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
* Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
Expand Down
25 changes: 0 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,31 +93,6 @@ LIQUID

By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.

### Error Modes

Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.

Liquid also comes with different parsers that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:

```ruby
Liquid::Environment.default.error_mode = :strict2 # Raises a SyntaxError when invalid syntax is used in all tags
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
```

If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby
Liquid::Template.parse(source, error_mode: :strict)
```
This is useful for doing things like enabling strict mode only in the theme editor.

It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.

### Undefined variables and filters

By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
Expand Down
43 changes: 4 additions & 39 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,12 @@ task :rubocop do
end
end

desc('runs test suite with lax, strict, and strict2 parsers')
desc('runs test suite')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke

ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke

ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke

if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke

ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke

ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
Expand All @@ -78,24 +61,11 @@ task release: :build do
end

namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :lax do
ruby "./performance/benchmark.rb lax"
end

desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
end

desc "Run the liquid benchmark with strict2 parsing"
task :strict2 do
ruby "./performance/benchmark.rb strict2"
desc "Run the liquid benchmark"
task :run do
ruby "./performance/benchmark.rb"
end

desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
task run: [:lax, :strict, :strict2]

desc "Run unit benchmarks"
namespace :unit do
task :all do
Expand Down Expand Up @@ -126,11 +96,6 @@ namespace :profile do
task :run do
ruby "./performance/profile.rb"
end

desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
end
end

namespace :memory_profile do
Expand Down
2 changes: 2 additions & 0 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ module Liquid
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer'
require 'liquid/method_literal'
require 'liquid/binary_expression'
require 'liquid/parser'
require 'liquid/i18n'
require 'liquid/drop'
Expand Down
94 changes: 94 additions & 0 deletions lib/liquid/binary_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

module Liquid
class BinaryExpression
attr_reader :operator
attr_accessor :left_node, :right_node

def initialize(left, operator, right)
@left_node = left
@operator = operator
@right_node = right
end

def evaluate(context)
left = value(left_node, context)

# logical relation short circuiting
if operator == 'and'
return left && value(right_node, context)
elsif operator == 'or'
return left || value(right_node, context)
end

right = value(right_node, context)

case operator
when '>'
left > right if can_compare?(left, right)
when '>='
left >= right if can_compare?(left, right)
when '<'
left < right if can_compare?(left, right)
when '<='
left <= right if can_compare?(left, right)
when '=='
equal_variables(left, right)
when '!=', '<>'
!equal_variables(left, right)
when 'contains'
contains(left, right)
else
raise(Liquid::ArgumentError, "Unknown operator #{operator}")
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message
end

def to_s
"(#{left_node.inspect} #{operator} #{right_node.inspect})"
end

private

def value(expr, context)
Utils.to_liquid_value(context.evaluate(expr))
end

def can_compare?(left, right)
left.respond_to?(operator) && right.respond_to?(operator) && !left.is_a?(Hash) && !right.is_a?(Hash)
end

def contains(left, right)
if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end

def apply_method_literal(node, other)
other.send(node.method_name) if other.respond_to?(node.method_name)
end

def equal_variables(left, right)
return apply_method_literal(left, right) if left.is_a?(MethodLiteral)
return apply_method_literal(right, left) if right.is_a?(MethodLiteral)

left == right
end

class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left_node,
@node.right_node,
]
end
end
end
end
Loading