Skip to content
Open

WIP #2017

Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Liquid Change Log

## 6.0.0

### Architectural changes

### Features
* (TODO) 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)
* (TODO) Add support for subexpression prioritization and associativity
* In ascending order of priority:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not missing from this list?

Copy link
Contributor Author

@charlespwd charlespwd Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep pull :D It's not in scope (yet?), but, unlike last time, we wouldn't be held back by lax anymore and the system would leave space for a UnaryExpression node with an operator and an expression.

* Logical: `and`, `or` (right to left)
* Equality: `==`, `!=`, `<>` (left to right)
* Comparison: `>`, `>=`, `<`, `<=`, `contains` (left to right)
- For example, this is now supported
* `{{ a > b == c < d or e == f }}` which is equivalent to
* `{{ ((a > b) == (c < d)) or (e == f) }}`
- (TODO) Add support for parenthesized expressions
* e.g. `(a or b) and c`

### 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`
* The `warnings` system has been removed.

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

## 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
7 changes: 1 addition & 6 deletions lib/liquid/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_erro
end
# rubocop:enable Metrics/ParameterLists

def warnings
@warnings ||= []
end

def strainer
@strainer ||= @environment.create_strainer(self, @filters)
end
Expand Down Expand Up @@ -157,7 +153,6 @@ def new_isolated_subcontext
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
Expand Down Expand Up @@ -244,7 +239,7 @@ def tag_disabled?(tag_name)

protected

attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
attr_writer :base_scope_depth, :errors, :strainer, :filters, :disabled_tags

private

Expand Down
10 changes: 1 addition & 9 deletions lib/liquid/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ module Liquid
# The Environment is the container for all configuration options of Liquid, such as
# the registered tags, filters, and the default error mode.
class Environment
# The default error mode for all templates. This can be overridden on a
# per-template basis.
attr_accessor :error_mode

# The tags that are available to use in the template.
attr_accessor :tags

Expand All @@ -33,17 +29,14 @@ class << self
# the template.
# @param file_system The default file system that is used
# to load templates from.
# @param error_mode [Symbol] The default error mode for all templates
# (either :strict2, :strict, :warn, or :lax).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
# @return [Environment] The new environment instance.
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
def build(tags: nil, file_system: nil, exception_renderer: nil)
ret = new
ret.tags = tags if tags
ret.file_system = file_system if file_system
ret.error_mode = error_mode if error_mode
ret.exception_renderer = exception_renderer if exception_renderer
yield ret if block_given?
ret.freeze
Expand Down Expand Up @@ -75,7 +68,6 @@ def dangerously_override(environment)
# @api private
def initialize
@tags = Tags::STANDARD_TAGS.dup
@error_mode = :lax
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
klass.add_filter(StandardFilters)
end
Expand Down
3 changes: 0 additions & 3 deletions lib/liquid/expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ class Expression
'false' => false,
'blank' => '',
'empty' => '',
# in lax mode, minus sign can be a VariableLookup
# For simplicity and performace, we treat it like a literal
'-' => VariableLookup.parse("-", nil).freeze,
}.freeze

DOT = ".".ord
Expand Down
23 changes: 8 additions & 15 deletions lib/liquid/parse_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment
attr_reader :partial, :environment

def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {}

@locale = @template_options[:locale] ||= I18n.new
@warnings = []
@locale = @template_options[:locale] ||= I18n.new

# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
Expand Down Expand Up @@ -55,25 +54,19 @@ def safe_parse_expression(parser)
end

def parse_expression(markup, safe: false)
if !safe && @error_mode == :strict2
# parse_expression is a widely used API. To maintain backward
# compatibility while raising awareness about strict2 parser standards,
# the safe flag supports API users make a deliberate decision.
#
# In strict2 mode, markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead.
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
end
# markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead. The `safe` opt-in
# exists to ensure it is not accidentally still called with
# the result of a regex.
raise Liquid::InternalError, "unsafe parse_expression cannot be used" unless safe

Expression.parse(markup, @string_scanner, @expression_cache)
end

def partial=(value)
@partial = value
@options = value ? partial_options : @template_options

@error_mode = @options[:error_mode] || @environment.error_mode
end

def partial_options
Expand Down
63 changes: 2 additions & 61 deletions lib/liquid/parser_switching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,15 @@

module Liquid
module ParserSwitching
# Do not use this.
#
# It's basically doing the same thing the {#parse_with_selected_parser},
# except this will try the strict parser regardless of the error mode,
# and fall back to the lax parser if the error mode is lax or warn,
# except when in strict2 mode where it uses the strict2 parser.
#
# @deprecated Use {#parse_with_selected_parser} instead.
def strict_parse_with_error_mode_fallback(markup)
return strict2_parse_with_error_context(markup) if strict2_mode?

strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :rigid
rigid_warn
raise
when :strict2
raise
when :strict
raise
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end

def parse_with_selected_parser(markup)
case parse_context.error_mode
when :rigid then rigid_warn && strict2_parse_with_error_context(markup)
when :strict2 then strict2_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict2_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
lax_parse(markup)
end
end
end

def strict2_mode?
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
end

private

def rigid_warn
Deprecations.warn(':rigid', ':strict2')
end

def strict2_parse_with_error_context(markup)
strict2_parse(markup)
parse_markup(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end

def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
private

def markup_context(markup)
"in \"#{markup.strip}\""
Expand Down
2 changes: 1 addition & 1 deletion lib/liquid/partial_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials]
cache_key = "#{template_name}:#{parse_context.error_mode}"
cache_key = template_name.to_s
cached = cached_partials[cache_key]
return cached if cached

Expand Down
Loading