Compare commits

...
Author SHA1 Message Date
Chris AtLee 7997c11c1a WIP - Implement indentation whitespace control 2024-11-26 18:03:11 -05:00
Ian Ker-Seymer 42b6763546 Bump to v5.6.0.rc1 2024-11-04 15:26:56 -05:00
Michael GoandGitHub e5d18c83bb Merge pull request #1848 from Shopify/env-warn-cleanup
clean up all warnings by using new Environment
2024-11-04 16:17:17 -04:00
Michael Go c77ff68573 clean up all warnings by using new Environment 2024-11-04 16:15:05 -04:00
Ian Ker-SeymerandGitHub b0cba0bfd2 Remove Liquid.cache_classes option (#1847) 2024-11-04 14:41:56 -05:00
Michael GoandGitHub 8d8661349a Merge pull request #1843 from Shopify/empty-array
avoid allocating new empty array
2024-11-04 15:36:46 -04:00
Michael Go 1f3ea7322b avoid allocating new empty array 2024-11-04 15:35:45 -04:00
Michael GoandGitHub 06f44226c0 Merge pull request #1846 from Shopify/env-propgating
propagate Environment on new Context creation
2024-11-04 15:33:50 -04:00
Michael GoandGitHub 4bd22a26dc Merge pull request #1845 from Shopify/remove-tag-registry
remove TagRegistry
2024-11-04 15:33:30 -04:00
Michael Go 3ed54bfdf9 propagate Environment on new Context creation 2024-11-04 15:32:07 -04:00
Michael Go 29986d3704 remove TagRegistry 2024-11-04 15:22:56 -04:00
Michael GoandGitHub 8e40f8050a Merge pull request #1838 from Shopify/quirky-lexer-parsing
fix parsing quirky incomplete expressions
2024-10-30 13:52:47 -03:00
Michael Go ffce6de8bb avoid using StringScanner eos 2024-10-30 13:45:10 -03:00
Michael Go f00670cb01 refactor lexer unit test 2024-10-30 13:44:36 -03:00
Michael Go f6a3e25e2e fix parsing quirky incomplete expressions 2024-10-30 13:44:35 -03:00
Michael GoandGitHub f6ffc37cf2 Merge pull request #1840 from Shopify/fix-lexer-contains-as-id
fix lexer parsing ID 'contains' as comparison
2024-10-30 13:43:48 -03:00
Michael Go 1375a9e4dc fix lexer parsing ID 'contains' as comparison 2024-10-30 13:39:55 -03:00
Michael GoandGitHub c626dfa1a1 Merge pull request #1839 from Shopify/lexer-parse-error-with-utf8
raise syntax error from lexer parser with UTF-8 character
2024-10-30 13:39:05 -03:00
Michael Go 8a9f33a060 raise syntax error from lexer parser with utf8 character 2024-10-29 22:04:37 -03:00
Michael GoandGitHub 1943441361 Merge pull request #1835 from Shopify/fix-multibyte-variable-parsing
fix parsing Variable blockbody with multibyte character
2024-10-28 19:31:21 -03:00
Michael GoandGitHub 36251e640c Merge pull request #1837 from Shopify/lexer-comparison-fix
fix lexer parsing comparison without whitespaces
2024-10-28 19:31:11 -03:00
Michael Go d94293a464 fix lexer parsing comparison without whitespaces 2024-10-28 19:30:12 -03:00
Michael Go 6c13805a60 fix parsing Variable blockbody with multibyte character 2024-10-28 17:33:55 -03:00
Michael GoandGitHub b4196489c2 Merge pull request #1833 from Shopify/fast-variable-parse
Faster Variable BlockBody Matching
2024-10-28 15:28:04 -03:00
Gray GilmoreandGitHub 6d58c41440 Merge pull request #1831 from Shopify/gg-add-named-params-docs
Update liquid docs for named parameters
2024-10-28 09:20:55 -07:00
Michael Go fb6ac72520 use byteslice to create Variable BlockBody 2024-10-25 15:41:35 -03:00
Michael Go cb16219552 faster BlockBody variable matching 2024-10-25 15:22:56 -03:00
Gray Gilmore 8d7ed706f4 Update liquid docs for named parameters
The YARD liquid gem now supports specifying named parameters. For the
core liquid tags and filters this is the only object I could find that
needed to be updated.
2024-10-24 09:56:50 -07:00
Ian Ker-SeymerandGitHub b3553787c8 Speed up the lexer for Ruby 3.4+ (#1832)
* Speed up lexing

* Bump msrv to 3.0 (from 2.7)

* Normalize test for ruby-head compat

* Fix bug when parsing negative numbers
2024-10-23 14:15:33 -04:00
Ian Ker-SeymerandGitHub b233b3d081 Bump to v5.6.0.alpha (#1819) 2024-08-15 16:12:16 -04:00
Alex CocoandGitHub ac91d31268 Merge pull request #1818 from Shopify/tablerow-interrupt
Handle interrupts in table row
2024-08-15 14:04:35 -04:00
Alex Coco 9067e5167a Handle interrupts in table row 2024-08-15 11:56:29 -04:00
Ian Ker-SeymerandGitHub fb6634f454 Add concept of Liquid::Environment (#1815)
* Add concept of `Liquid::World`

* Rename `World` to `Environment`
2024-08-07 15:00:44 -04:00
Ian Ker-SeymerandIan Ker-Seymer a0411e0927 Bump to v5.5.1 2024-07-22 23:25:56 -04:00
Ian Ker-SeymerandGitHub ed421202e2 Merge pull request #1811 from Shopify/marco-cycle-update
Add named? method to Cycle class for checking if the cycle is named
2024-07-16 15:58:21 -04:00
Ian Ker-Seymer d6ca569e8a Require base64 2024-07-16 15:56:47 -04:00
Marco Concetto RudilossoandIan Ker-Seymer d36937d17f Add named? method to Cycle class for checking if the cycle is named 2024-07-16 15:52:25 -04:00
Peter ZhuandGitHub 77bc56a1c2 Merge pull request #1792 from Shopify/centralize_ruby_version
Centralize Ruby Version to `.ruby-version`
2024-04-11 11:12:08 -04:00
Jenny Shen 88d013c8da Use latest compatible Bundler of each Ruby version run 2024-04-05 10:33:27 -04:00
Jenny Shen 36c7fc8e07 Remove ruby version definition in CI/CD
it will be read from .ruby-version
2024-04-05 10:33:06 -04:00
Jenny Shen 2b4810006b Remove TargetRubyVersion in RuboCop config
reads from required_ruby_version in RuboCop 1.61+
2024-04-05 10:23:39 -04:00
Jenny Shen fc4f19471e Update rubocop to 1.61.0 2024-04-05 10:23:37 -04:00
Jenny Shen 8596bb2e38 Commit a .ruby-version 2024-04-05 10:23:05 -04:00
Ian Ker-SeymerandGitHub 6bf18775e7 Merge pull request #1791 from Shopify/v5.5.0
Bump to v5.5.0
2024-03-21 15:50:59 -04:00
Ian Ker-Seymer 56a0b7c42b Bump to v5.5.0 2024-03-21 15:48:54 -04:00
Ian Ker-SeymerandGitHub dba733084e Merge pull request #1760 from mtasaka/nil-nil-comparison-fix
change: make nil_safe_casecmp judge compatible for nil-nil comparison
2024-03-21 15:38:13 -04:00
Michael GoandGitHub 4a4fe3c72a Merge pull request #1781 from Shopify/contextualize-before-to-liquid
update variable's context before invoking its to_liquid
2024-02-12 16:58:54 -04:00
Michael Go 4f35b0bc66 update liquid-c 2024-02-12 16:49:52 -04:00
Michael Go a5e5fab82a update variable's context before invoking its to_liquid 2024-02-12 16:48:20 -04:00
Ian Ker-SeymerandGitHub 02ecaab9d1 Merge pull request #1783 from Shopify/contains-encoding
Fallback to binary comparison when `contains` RHS is  UTF8 encoded
2024-01-31 11:14:01 -05:00
Ian Ker-Seymer 1b2b62964e Allow for binary comparison of incompatible strings 2024-01-30 19:03:05 -05:00
Michael GoandGitHub 9b38a15282 Merge pull request #1776 from Shopify/refactor/invalid-encoding-error
add new TemplateEncodingError
2024-01-12 12:38:34 -04:00
Michael Go 7b25b770af add new TemplateEncodingError 2024-01-12 12:17:20 -04:00
Michael GoandGitHub cf76c0bbec Merge pull request #1775 from Shopify/allow-nil-template-source
allow non-string template source
2024-01-12 10:48:30 -04:00
Michael Go 6a0fe3f7e3 convert template source to string to ensure encoding validity 2024-01-10 16:50:31 -04:00
Michael Go 730ad3684a allow nil template source 2024-01-10 16:32:08 -04:00
Michael GoandGitHub 3ac7e470e6 Merge pull request #1774 from Shopify/check-utf8-validity
check template UTF8 validity before parsing
2024-01-10 15:27:49 -04:00
Michael GoandGitHub f5ed5404b5 Merge pull request #1773 from Shopify/liquid-tag-whitespace-control-with-comment
don't reset Liquid tag's whitespace control from comment tag
2024-01-05 10:54:32 -04:00
Mamoru TASAKA 4924822c88 change: make nil_safe_casecmp judge compatible for nil-nil comparison
Ruby returns 0 (not nil) for nil <=> nil, i.e. nil and nil are judged
as equal for comparison, and so returns nil_safe_compare .
ref: https://github.com/Shopify/liquid/pull/1476

To make the behavior of nil_safe_casecmp consistent with
nil_safe_compare , change nil_safe_casecmp so that comparison between
nil <=> nil return 0 (equal).

Also change testsuite to reflect this change.

Fixes #1759 .
2023-12-15 15:09:57 +09:00
68 changed files with 1245 additions and 410 deletions
+5 -4
View File
@@ -11,9 +11,10 @@ jobs:
strategy: strategy:
matrix: matrix:
entry: entry:
- { ruby: 2.7, allowed-failure: false } # minimum supported - { ruby: 3.0, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false } # latest - { ruby: 3.2, allowed-failure: false }
- { ruby: ruby-head, allowed-failure: true } - { ruby: 3.3, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: false }
name: Test Ruby ${{ matrix.entry.ruby }} name: Test Ruby ${{ matrix.entry.ruby }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -21,6 +22,7 @@ jobs:
with: with:
ruby-version: ${{ matrix.entry.ruby }} ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true bundler-cache: true
bundler: latest
- run: bundle exec rake - run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }} continue-on-error: ${{ matrix.entry.allowed-failure }}
@@ -30,6 +32,5 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: 2.7
bundler-cache: true bundler-cache: true
- run: bundle exec rake memory_profile:run - run: bundle exec rake memory_profile:run
+1 -2
View File
@@ -4,7 +4,6 @@
pkg pkg
*.rbc *.rbc
.rvmrc .rvmrc
.ruby-version
Gemfile.lock
.bundle .bundle
.byebug_history .byebug_history
Gemfile.lock
-1
View File
@@ -10,7 +10,6 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.7
NewCops: disable NewCops: disable
SuggestExtensions: false SuggestExtensions: false
Exclude: Exclude:
+1
View File
@@ -0,0 +1 @@
3.3.4
+4 -2
View File
@@ -7,6 +7,8 @@ end
gemspec gemspec
gem "base64"
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
@@ -18,11 +20,11 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.44.0' gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
end end
end end
+4
View File
@@ -1,5 +1,9 @@
# Liquid Change Log # Liquid Change Log
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
## 5.4.0 2022-07-29 ## 5.4.0 2022-07-29
### Breaking Changes ### Breaking Changes
+45 -3
View File
@@ -52,6 +52,47 @@ For standard use you can just pass it the content of a file and call render with
@template.render('name' => 'tobi') # => "hi tobi" @template.render('name' => 'tobi') # => "hi tobi"
``` ```
### Concept of Environments
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
By using environments, you can:
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
Here's an example of how you can define and use Environments in Liquid:
```ruby
user_environment = Liquid::Environment.build do |environment|
environment.register_tag("renderobj", RenderObjTag)
end
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
{% renderobj src: "path/to/model.obj" %}
LIQUID
```
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
Similarly, you can define another environment for a different context, such as email templates:
```ruby
email_environment = Liquid::Environment.build do |environment|
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
end
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
{% unsubscribe_footer %}
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.
### Error Modes ### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
@@ -62,9 +103,10 @@ Liquid also comes with a stricter parser that can be used when editing templates
when templates are invalid. You can enable this new parser like this: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Environment.default.error_mode = :strict
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. 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`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
+8
View File
@@ -81,6 +81,14 @@ namespace :benchmark do
task :strict do task :strict do
ruby "./performance/benchmark.rb strict" ruby "./performance/benchmark.rb strict"
end end
desc "Run unit benchmarks"
task :unit do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
end end
namespace :profile do namespace :profile do
+13 -15
View File
@@ -26,7 +26,9 @@ module Liquid
ArgumentSeparator = ',' ArgumentSeparator = ','
FilterArgumentSeparator = ':' FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.' VariableAttributeSeparator = '.'
WhitespaceControl = '-' WhitespaceControl = '(?:[-~])'
WhitespaceTrim = '-'
WhitespaceTrimIndent = '~'
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
TagName = /#|\w+/ TagName = /#|\w+/
@@ -44,13 +46,20 @@ module Liquid
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise } RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end end
require "liquid/version" require "liquid/version"
require "liquid/deprecations"
require "liquid/const"
require 'liquid/standardfilters'
require 'liquid/file_system'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/parse_tree_visitor' require 'liquid/parse_tree_visitor'
require 'liquid/interrupts'
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
@@ -61,23 +70,16 @@ require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_template' require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits' require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
@@ -86,7 +88,3 @@ require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/registers' require 'liquid/registers'
require 'liquid/template_factory' require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+22 -10
View File
@@ -52,7 +52,7 @@ module Liquid
next parse_liquid_tag(markup, parse_context) next parse_liquid_tag(markup, parse_context)
end end
unless (tag = registered_tags[tag_name]) unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -147,7 +147,7 @@ module Liquid
next next
end end
unless (tag = registered_tags[tag_name]) unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -164,6 +164,7 @@ module Liquid
token.lstrip! token.lstrip!
end end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
parse_context.strip_trailing = false
@nodelist << token @nodelist << token
@blank &&= token.match?(WhitespaceOrNothing) @blank &&= token.match?(WhitespaceOrNothing)
end end
@@ -174,7 +175,8 @@ module Liquid
end end
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl parse_context.indentation = nil
if token[2] == WhitespaceTrim
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0) first_byte = previous_token.getbyte(0)
@@ -183,8 +185,15 @@ module Liquid
previous_token << first_byte previous_token << first_byte
end end
end end
elsif token[2] == WhitespaceTrimIndent
previous_token = @nodelist.last
if previous_token.is_a?(String)
start_of_line = previous_token.rindex("\n") || 0
parse_context.indentation = previous_token[start_of_line + 1..]
end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceTrim)
parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent)
end end
def blank? def blank?
@@ -246,10 +255,17 @@ module Liquid
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
if token =~ ContentOfVariable if token.end_with?("}}")
markup = Regexp.last_match(1) i = 2
i = 3 if token[i] == "-" or token[i] == "~"
parse_end = token.length - 3
parse_end -= 1 if token[parse_end] == "-"
markup_end = parse_end - i + 1
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
@@ -262,9 +278,5 @@ module Liquid
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags
Template.tags
end
end end
end end
+3
View File
@@ -24,6 +24,9 @@ module Liquid
else else
false false
end end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end, end,
} }
+8
View File
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
+17 -11
View File
@@ -15,24 +15,25 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, environment, &block)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
@environment = environment
@environments = [environments] @environments = [environments]
@environments.flatten! @environments.flatten!
@static_environments = [static_environments].flatten(1).freeze @static_environments = [static_environments].flatten(1).freeze
@scopes = [(outer_scope || {})] @scopes = [outer_scope || {}]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers) @registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@@ -40,10 +41,10 @@ module Liquid
@disabled_tags = {} @disabled_tags = {}
@registers.static[:cached_partials] ||= {} @registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= Liquid::Template.file_system @registers.static[:file_system] ||= environment.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new @registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = environment.exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
@@ -60,7 +61,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= StrainerFactory.create(self, @filters) @strainer ||= @environment.create_strainer(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -142,6 +143,7 @@ module Liquid
check_overflow check_overflow
self.class.build( self.class.build(
environment: @environment,
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: Registers.new(registers), registers: Registers.new(registers),
@@ -197,10 +199,14 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
variable = variable.to_liquid # update variable's context before invoking #to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable liquid_variable = variable.to_liquid
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
liquid_variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require "set"
module Liquid
class Deprecations
class << self
attr_accessor :warned
Deprecations.warned = Set.new
def warn(name, alternative)
return if warned.include?(name)
warned << name
caller_location = caller_locations(2, 1).first
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
end
end
end
end
+159
View File
@@ -0,0 +1,159 @@
# frozen_string_literal: true
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
# The strainer template which is used to store filters that are available to
# use in templates.
attr_accessor :strainer_template
# The exception renderer that is used to render exceptions that are raised
# when rendering a template
attr_accessor :exception_renderer
# The default file system that is used to load templates from.
attr_accessor :file_system
# The default resource limits that are used to limit the resources that a
# template can consume.
attr_accessor :default_resource_limits
class << self
# Creates a new environment instance.
#
# @param tags [Hash] The tags that are available to use in
# 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 :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)
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
end
# Returns the default environment instance.
#
# @return [Environment] The default environment instance.
def default
@default ||= new
end
# Sets the default environment instance for the duration of the block
#
# @param environment [Environment] The environment instance to use as the default for the
# duration of the block.
# @yield
# @return [Object] The return value of the block.
def dangerously_override(environment)
original_default = @default
@default = environment
yield
ensure
@default = original_default
end
end
# Initializes a new environment instance.
# @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
@exception_renderer = ->(exception) { exception }
@file_system = BlankFileSystem.new
@default_resource_limits = Const::EMPTY_HASH
@strainer_template_class_cache = {}
end
# Registers a new tag with the environment.
#
# @param name [String] The name of the tag.
# @param klass [Liquid::Tag] The class that implements the tag.
# @return [void]
def register_tag(name, klass)
@tags[name] = klass
end
# Registers a new filter with the environment.
#
# @param filter [Module] The module that contains the filter methods.
# @return [void]
def register_filter(filter)
@strainer_template_class_cache.clear
@strainer_template.add_filter(filter)
end
# Registers multiple filters with this environment.
#
# @param filters [Array<Module>] The modules that contain the filter methods.
# @return [self]
def register_filters(filters)
@strainer_template_class_cache.clear
filters.each { |f| @strainer_template.add_filter(f) }
self
end
# Creates a new strainer instance with the given filters, caching the result
# for faster lookup.
#
# @param context [Liquid::Context] The context that the strainer will be
# used in.
# @param filters [Array<Module>] The filters that the strainer will have
# access to.
# @return [Liquid::Strainer] The new strainer instance.
def create_strainer(context, filters = Const::EMPTY_ARRAY)
return @strainer_template.new(context) if filters.empty?
strainer_template = @strainer_template_class_cache[filters] ||= begin
klass = Class.new(@strainer_template)
filters.each { |f| klass.add_filter(f) }
klass
end
strainer_template.new(context)
end
# Returns the names of all the filter methods that are available to use in
# the strainer template.
#
# @return [Array<String>] The names of all the filter methods.
def filter_method_names
@strainer_template.filter_method_names
end
# Returns the tag class for the given tag name.
#
# @param name [String] The name of the tag.
# @return [Liquid::Tag] The tag class.
def tag_for_name(name)
@tags[name]
end
def freeze
@tags.freeze
# TODO: freeze the tags, currently this is not possible because of liquid-c
# @strainer_template.freeze
super
end
end
end
+16 -15
View File
@@ -40,19 +40,20 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) ArgumentError = Class.new(Error)
ContextError = Class.new(Error) ContextError = Class.new(Error)
FileSystemError = Class.new(Error) FileSystemError = Class.new(Error)
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
MemoryError = Class.new(Error) MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error) ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error) FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error) UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error) UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error) UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error) DisabledError = Class.new(Error)
InternalError = Class.new(Error) InternalError = Class.new(Error)
TemplateEncodingError = Class.new(Error)
end end
+177 -1
View File
@@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
require "strscan" require "strscan"
module Liquid module Liquid
class Lexer class Lexer1
SPECIALS = { SPECIALS = {
'|' => :pipe, '|' => :pipe,
'.' => :dot, '.' => :dot,
@@ -58,4 +59,179 @@ module Liquid
@output << [:end_of_string] @output << [:end_of_string]
end end
end end
class Lexer2
CLOSE_ROUND = [:close_round, ")"].freeze
CLOSE_SQUARE = [:close_square, "]"].freeze
COLON = [:colon, ":"].freeze
COMMA = [:comma, ","].freeze
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
COMPARISON_EQUAL = [:comparison, "=="].freeze
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
DASH = [:dash, "-"].freeze
DOT = [:dot, "."].freeze
DOTDOT = [:dotdot, ".."].freeze
DOT_ORD = ".".ord
DOUBLE_STRING_LITERAL = /"[^\"]*"/
EOS = [:end_of_string].freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
OPEN_ROUND = [:open_round, "("].freeze
OPEN_SQUARE = [:open_square, "["].freeze
PIPE = [:pipe, "|"].freeze
QUESTION = [:question, "?"].freeze
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
SINGLE_STRING_LITERAL = /'[^\']*'/
WHITESPACE_OR_NOTHING = /\s*/
SINGLE_COMPARISON_TOKENS = [].tap do |table|
table["<".ord] = COMPARISON_LESS_THAN
table[">".ord] = COMPARISON_GREATER_THAN
end
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
table["=".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_EQUAL
sub_table.freeze
end
table["!".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISION_NOT_EQUAL
sub_table.freeze
end
end
COMPARISON_JUMP_TABLE = [].tap do |table|
table["<".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
sub_table.freeze
end
table[">".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
sub_table.freeze
end
table.freeze
end
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
"a".upto("z") do |c|
table[c.ord] = [:id, IDENTIFIER].freeze
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
end
table["_".ord] = [:id, IDENTIFIER].freeze
"0".upto("9") do |c|
table[c.ord] = [:number, NUMBER_LITERAL].freeze
end
table["-".ord] = [:number, NUMBER_LITERAL].freeze
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
table.freeze
end
SPECIAL_TABLE = [].tap do |table|
table["|".ord] = PIPE
table[".".ord] = DOT
table[":".ord] = COLON
table[",".ord] = COMMA
table["[".ord] = OPEN_SQUARE
table["]".ord] = CLOSE_SQUARE
table["(".ord] = OPEN_ROUND
table[")".ord] = CLOSE_ROUND
table["?".ord] = QUESTION
table["-".ord] = DASH
end
NUMBER_TABLE = [].tap do |table|
"0".upto("9") do |c|
table[c.ord] = true
end
table.freeze
end
def initialize(input)
@ss = StringScanner.new(input)
end
# rubocop:disable Metrics/BlockNesting
def tokenize
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
start_pos = @ss.pos
peeked = @ss.peek_byte
if (special = SPECIAL_TABLE[peeked])
@ss.scan_byte
# Special case for ".."
if special == DOT && @ss.peek_byte == DOT_ORD
@ss.scan_byte
@output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
@ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)]
else
@output << special
end
else
@output << special
end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
raise_syntax_error(start_pos)
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
@output << SINGLE_COMPARISON_TOKENS[peeked]
end
else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
if type && (t = @ss.scan(pattern))
# Special case for "contains"
@output << if type == :id && t == "contains" && @output.last&.first != :dot
COMPARISON_CONTAINS
else
[type, t]
end
else
raise_syntax_error(start_pos)
end
end
end
# rubocop:enable Metrics/BlockNesting
@output << EOS
end
def raise_syntax_error(start_pos)
@ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{@ss.getch}"
end
end
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end end
+5 -4
View File
@@ -2,10 +2,11 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth, :indentation, :strip_trailing
attr_reader :partial, :warnings, :error_mode attr_reader :partial, :warnings, :error_mode, :environment
def initialize(options = {}) def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@@ -35,7 +36,7 @@ module Liquid
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || @environment.error_mode
end end
def partial_options def partial_options
+1 -1
View File
@@ -36,7 +36,7 @@ module Liquid
protected protected
def children def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : [] @node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
end end
end end
end end
+1 -1
View File
@@ -53,7 +53,7 @@ module Liquid
str = consume str = consume
str << variable_lookups str << variable_lookups
when :open_square when :open_square
str = consume str = consume.dup
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)
str << variable_lookups str << variable_lookups
+3 -3
View File
@@ -877,7 +877,7 @@ module Liquid
# - [`nil`](/docs/api/liquid/basics#nil) # - [`nil`](/docs/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable # @liquid_syntax variable | default: variable
# @liquid_return [untyped] # @liquid_return [untyped]
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default. # @liquid_optional_param allow_false: [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {}) def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash) options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input) false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
@@ -944,6 +944,8 @@ module Liquid
def nil_safe_casecmp(a, b) def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil? if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s) a.to_s.casecmp(b.to_s)
elsif a.nil? && b.nil?
0
else else
a.nil? ? 1 : -1 a.nil? ? 1 : -1
end end
@@ -999,6 +1001,4 @@ module Liquid
end end
end end
end end
Template.register_filter(StandardFilters)
end end
-41
View File
@@ -1,41 +0,0 @@
# frozen_string_literal: true
module Liquid
# StrainerFactory is the factory for the filters system.
module StrainerFactory
extend self
def add_global_filter(filter)
strainer_class_cache.clear
GlobalCache.add_filter(filter)
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
def global_filter_names
GlobalCache.filter_method_names
end
GlobalCache = Class.new(StrainerTemplate)
private
def strainer_from_cache(filters)
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
def strainer_class_cache
@strainer_class_cache ||= {}
end
end
end
+3
View File
@@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
+47
View File
@@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative "tags/table_row"
require_relative "tags/echo"
require_relative "tags/if"
require_relative "tags/break"
require_relative "tags/inline_comment"
require_relative "tags/for"
require_relative "tags/assign"
require_relative "tags/ifchanged"
require_relative "tags/case"
require_relative "tags/include"
require_relative "tags/continue"
require_relative "tags/capture"
require_relative "tags/decrement"
require_relative "tags/unless"
require_relative "tags/increment"
require_relative "tags/comment"
require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
module Liquid
module Tags
STANDARD_TAGS = {
'cycle' => Cycle,
'render' => Render,
'raw' => Raw,
'comment' => Comment,
'increment' => Increment,
'unless' => Unless,
'decrement' => Decrement,
'capture' => Capture,
'continue' => Continue,
'include' => Include,
'case' => Case,
'ifchanged' => Ifchanged,
'assign' => Assign,
'for' => For,
'#' => InlineComment,
'break' => Break,
'if' => If,
'echo' => Echo,
'tablerow' => TableRow,
}.freeze
end
end
-2
View File
@@ -72,6 +72,4 @@ module Liquid
end end
end end
end end
Template.register_tag('assign', Assign)
end end
-2
View File
@@ -26,6 +26,4 @@ module Liquid
output output
end end
end end
Template.register_tag('break', Break)
end end
-2
View File
@@ -39,6 +39,4 @@ module Liquid
true true
end end
end end
Template.register_tag('capture', Capture)
end end
-2
View File
@@ -123,6 +123,4 @@ module Liquid
end end
end end
end end
Template.register_tag('case', Case)
end end
+5 -6
View File
@@ -63,10 +63,11 @@ module Liquid
comment_tag_depth -= 1 comment_tag_depth -= 1
end end
if comment_tag_depth.zero? next unless comment_tag_depth.zero?
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
return false parse_context.trim_whitespace = (token[-3] == WhitespaceTrim) unless tokenizer.for_liquid_tag
end parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent) unless tokenizer.for_liquid_tag
return false
end end
raise_tag_never_closed(block_name) raise_tag_never_closed(block_name)
@@ -85,6 +86,4 @@ module Liquid
raise_tag_never_closed("raw") raise_tag_never_closed("raw")
end end
end end
Template.register_tag('comment', Comment)
end end
-2
View File
@@ -17,6 +17,4 @@ module Liquid
output output
end end
end end
Template.register_tag('continue', Continue)
end end
+6 -2
View File
@@ -26,14 +26,20 @@ module Liquid
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1)) @name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@is_named = !@name.match?(/\w+:0x\h{8}/)
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
end end
def named?
@is_named
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
@@ -72,6 +78,4 @@ module Liquid
end end
end end
end end
Template.register_tag('cycle', Cycle)
end end
-2
View File
@@ -35,6 +35,4 @@ module Liquid
output output
end end
end end
Template.register_tag('decrement', Decrement)
end end
-2
View File
@@ -36,6 +36,4 @@ module Liquid
end end
end end
end end
Template.register_tag('echo', Echo)
end end
-2
View File
@@ -201,6 +201,4 @@ module Liquid
end end
end end
end end
Template.register_tag('for', For)
end end
+1 -3
View File
@@ -111,7 +111,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while (op = (p.id?('and') || p.id?('or'))) while (op = p.id?('and') || p.id?('or'))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -135,6 +135,4 @@ module Liquid
end end
end end
end end
Template.register_tag('if', If)
end end
-2
View File
@@ -14,6 +14,4 @@ module Liquid
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged)
end end
+13 -4
View File
@@ -37,6 +37,7 @@ module Liquid
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = parse_expression(template_name)
@attributes = {} @attributes = {}
@indentation = options.indentation
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = parse_expression(value)
@@ -80,15 +81,25 @@ module Liquid
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
end end
partial_output = +''
if variable.is_a?(Array) if variable.is_a?(Array)
variable.each do |var| variable.each do |var|
context[context_variable_name] = var context[context_variable_name] = var
partial.render_to_output_buffer(context, output) partial.render_to_output_buffer(context, partial_output)
end end
else else
context[context_variable_name] = variable context[context_variable_name] = variable
partial.render_to_output_buffer(context, output) partial.render_to_output_buffer(context, partial_output)
end end
if @indentation
partial_output = partial_output.lines.map.with_index do |line, i|
next line if i == 0
@indentation + line
end.join
end
output << partial_output
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
@@ -110,6 +121,4 @@ module Liquid
end end
end end
end end
Template.register_tag('include', Include)
end end
-2
View File
@@ -35,6 +35,4 @@ module Liquid
output output
end end
end end
Template.register_tag('increment', Increment)
end end
-2
View File
@@ -25,6 +25,4 @@ module Liquid
true true
end end
end end
Template.register_tag('#', InlineComment)
end end
+2 -3
View File
@@ -25,7 +25,8 @@ module Liquid
@body = +'' @body = +''
while (token = tokens.shift) while (token = tokens.shift)
if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceTrim)
parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent)
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return return
end end
@@ -56,6 +57,4 @@ module Liquid
end end
end end
end end
Template.register_tag('raw', Raw)
end end
+14 -3
View File
@@ -46,6 +46,8 @@ module Liquid
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR) @is_for_loop = (with_or_for == FOR)
@indentation = options.indentation
@strip_trailing = options.strip_trailing
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@@ -84,7 +86,18 @@ module Liquid
inner_context[key] = context.evaluate(value) inner_context[key] = context.evaluate(value)
end end
inner_context[context_variable_name] = var unless var.nil? inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output) partial_output = +''
partial.render_to_output_buffer(inner_context, partial_output)
if @indentation
partial_output = partial_output.lines.map.with_index do |line, i|
next line if i == 0
@indentation + line
end.join
end
if @strip_trailing
partial_output.rstrip!
end
output << partial_output
forloop&.send(:increment!) forloop&.send(:increment!)
} }
@@ -108,6 +121,4 @@ module Liquid
end end
end end
end end
Template.register_tag('render', Render)
end end
+6 -2
View File
@@ -65,6 +65,12 @@ module Liquid
super super
output << '</td>' output << '</td>'
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
end
if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end end
@@ -91,6 +97,4 @@ module Liquid
raise Liquid::ArgumentError, "invalid integer" raise Liquid::ArgumentError, "invalid integer"
end end
end end
Template.register_tag('tablerow', TableRow)
end end
-2
View File
@@ -44,6 +44,4 @@ module Liquid
output output
end end
end end
Template.register_tag('unless', Unless)
end end
+57 -61
View File
@@ -18,42 +18,6 @@ module Liquid
attr_accessor :root, :name attr_accessor :root, :name
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
class TagRegistry
include Enumerable
def initialize
@tags = {}
@cache = {}
end
def [](tag_name)
return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def delete(tag_name)
@tags.delete(tag_name)
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
Object.const_get(name)
end
end
attr_reader :profiler attr_reader :profiler
class << self class << self
@@ -61,55 +25,81 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_accessor :error_mode def error_mode=(mode)
Template.error_mode = :lax Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end end
attr_accessor :file_system def error_mode
Template.file_system = BlankFileSystem.new Environment.default.error_mode
end
attr_accessor :tags def default_exception_renderer=(renderer)
Template.tags = TagRegistry.new Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
private :tags= Environment.default.exception_renderer = renderer
end
def default_exception_renderer
Environment.default.exception_renderer
end
def file_system=(file_system)
Deprecations.warn("Template.file_system=", "Environment#file_system=")
Environment.default.file_system = file_system
end
def file_system
Environment.default.file_system
end
def tags
Environment.default.tags
end
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass Deprecations.warn("Template.register_tag", "Environment#register_tag")
Environment.default.register_tag(name, klass)
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
StrainerFactory.add_global_filter(mod) Deprecations.warn("Template.register_filter", "Environment#register_filter")
Environment.default.register_filter(mod)
end end
attr_accessor :default_resource_limits private def default_resource_limits=(limits)
Template.default_resource_limits = {} Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
private :default_resource_limits= Environment.default.default_resource_limits = limits
end
def default_resource_limits
Environment.default.default_resource_limits
end
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
new.parse(source, options) environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
end end
end end
def initialize def initialize(environment: Environment.default)
@environment = environment
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits) @resource_limits = ResourceLimits.new(environment.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
parse_context = configure_options(options) parse_context = configure_options(options)
source = source.to_s.to_str
unless source.valid_encoding? unless source.valid_encoding?
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_template_encoding") raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
end end
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@@ -161,11 +151,11 @@ module Liquid
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
else else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
@@ -225,8 +215,14 @@ module Liquid
@options = options @options = options
@profiling = profiling @profiling = profiling
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) parse_context = if options.is_a?(ParseContext)
@warnings = parse_context.warnings options
else
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
ParseContext.new(opts)
end
@warnings = parse_context.warnings
parse_context parse_context
end end
+1 -1
View File
@@ -5,7 +5,7 @@ module Liquid
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source.to_s.to_str @source = source
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@offset = 0 @offset = 0
+19 -3
View File
@@ -29,6 +29,8 @@ module Liquid
@name = nil @name = nil
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
@indentation = parse_context.indentation
@strip_trailing = parse_context.strip_trailing
strict_parse_with_error_mode_fallback(markup) strict_parse_with_error_mode_fallback(markup)
end end
@@ -68,7 +70,7 @@ module Liquid
@name = parse_context.parse_expression(p.expression) @name = parse_context.parse_expression(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -96,13 +98,27 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
obj = render(context) obj = render(context)
obj_output = ''
if obj.is_a?(Array) if obj.is_a?(Array)
output << obj.join obj_output = obj.join
elsif obj.nil? elsif obj.nil?
else else
output << obj.to_s obj_output = obj.to_s
end end
if @indentation
obj_output = obj_output.lines.map.with_index do |line, i|
next line if i == 0
@indentation + line
end.join
end
if @strip_trailing
obj_output.rstrip!
end
output << obj_output
output output
end end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.4.0" VERSION = "5.6.0.rc1"
end end
+4 -1
View File
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.7.0" s.required_ruby_version = ">= 3.0.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org' s.metadata['allowed_push_host'] = 'https://rubygems.org'
@@ -28,6 +28,9 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_dependency("strscan")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0') s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end
+3 -1
View File
@@ -3,7 +3,9 @@
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
+8 -7
View File
@@ -11,11 +11,12 @@ require_relative 'shop_filter'
require_relative 'tag_filter' require_relative 'tag_filter'
require_relative 'weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag('paginate', Paginate) default_environment = Liquid::Environment.default
Liquid::Template.register_tag('form', CommentForm) default_environment.register_tag('paginate', Paginate)
default_environment.register_tag('form', CommentForm)
Liquid::Template.register_filter(JsonFilter) default_environment.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter) default_environment.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter) default_environment.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter) default_environment.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter) default_environment.register_filter(TagFilter)
+62
View File
@@ -0,0 +1,62 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
EXPRESSIONS = [
"foo[1..2].baz",
"12.0",
"foo.bar.based",
"21 - 62",
"foo.bar.baz",
"foo > 12",
"foo < 12",
"foo <= 12",
"foo >= 12",
"foo <> 12",
"foo == 12",
"foo != 12",
"foo contains 12",
"foo contains 'bar'",
"foo != 'bar'",
"'foo' contains 'bar'",
'234089',
"foo | default: -1",
]
EXPRESSIONS.each do |expr|
lexer_1_result = Liquid::Lexer1.new(expr).tokenize
lexer_2_result = Liquid::Lexer2.new(expr).tokenize
next if lexer_1_result == lexer_2_result
warn "Lexer1 and Lexer2 results are different for expression: #{expr}"
warn "expected: #{lexer_1_result}"
warn "got: #{lexer_2_result}"
abort
end
Benchmark.ips do |x|
x.config(time: 10, warmup: 5)
x.report("Liquid::Lexer1#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer1.new(expr)
l.tokenize
end
end
x.report("Liquid::Lexer2#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer2.new(expr)
l.tokenize
end
end
x.compare!
end
+52
View File
@@ -36,6 +36,24 @@ class Category
end end
end end
class ProductsDrop < Liquid::Drop
def initialize(products)
@products = products
end
def size
@products.size
end
def to_liquid
if @context["forloop"]
@products.first(@context["forloop"].length)
else
@products
end
end
end
class CategoryDrop < Liquid::Drop class CategoryDrop < Liquid::Drop
attr_accessor :category, :context attr_accessor :category, :context
@@ -635,6 +653,40 @@ class ContextTest < Minitest::Test
assert_equal(:my_value, c.registers[:my_register]) assert_equal(:my_value, c.registers[:my_register])
end end
def test_variable_to_liquid_returns_contextual_drop
context = {
"products" => ProductsDrop.new(["A", "B", "C", "D", "E"]),
}
template = Liquid::Template.parse(<<~LIQUID)
{%- for i in (1..3) -%}
for_loop_products_count: {{ products | size }}
{% endfor %}
unscoped_products_count: {{ products | size }}
LIQUID
result = template.render(context)
assert_includes(result, "for_loop_products_count: 3")
assert_includes(result, "unscoped_products_count: 5")
end
def test_new_isolated_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
subcontext = context.new_isolated_subcontext
assert_equal(global_environment, subcontext.environment)
end
def test_newly_built_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
assert_equal(global_environment, context.environment)
assert(context.environment.tags.each.to_a.empty?)
end
private private
def assert_no_object_allocations def assert_no_object_allocations
+22 -14
View File
@@ -203,20 +203,34 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { default_exception_renderer = ->(e) {
exceptions << e exceptions << e
'' ''
} }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output) assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class)) assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
ensure end
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
def test_setting_exception_renderer_on_environment
exceptions = []
exception_renderer = ->(e) do
exceptions << e
''
end
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
@@ -242,16 +256,10 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_included_template_name_with_line_numbers def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system environment = Liquid::Environment.build(file_system: TestFileSystem.new)
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
page = template.render('errors' => ErrorDrop.new)
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", template.errors.first.template_name)
end end
+12
View File
@@ -131,4 +131,16 @@ class ParsingQuirksTest < Minitest::Test
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true }) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end end
def test_incomplete_expression
with_error_mode(:lax) do
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
end
end
end # ParsingQuirksTest end # ParsingQuirksTest
+1 -1
View File
@@ -33,7 +33,7 @@ class ProfilerTest < Minitest::Test
end end
def setup def setup
Liquid::Template.file_system = ProfilingFileSystem.new Liquid::Environment.default.file_system = ProfilingFileSystem.new
end end
def test_template_allows_flagging_profiling def test_template_allows_flagging_profiling
+2 -2
View File
@@ -32,7 +32,7 @@ class TestDrop < Liquid::Drop
attr_reader :value attr_reader :value
def registers def registers
{ @value => @context.registers[@value] } "{#{@value.inspect}=>#{@context.registers[@value].inspect}}"
end end
end end
@@ -331,8 +331,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "handle" => "delta" },
] ]
assert_equal(expectation, @filters.sort_natural(input, "price")) assert_equal(expectation, @filters.sort_natural(input, "price"))
end end
+17 -20
View File
@@ -49,14 +49,6 @@ end
class IncludeTagTest < Minitest::Test class IncludeTagTest < Minitest::Test
include Liquid include Liquid
def setup
@default_file_system = Liquid::Template.file_system
end
def teardown
Liquid::Template.file_system = @default_file_system
end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal( assert_equal(
'from OtherFileSystem', 'from OtherFileSystem',
@@ -182,10 +174,10 @@ class IncludeTagTest < Minitest::Test
end end
end end
Liquid::Template.file_system = infinite_file_system.new env = Liquid::Environment.build(file_system: infinite_file_system.new)
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}", environment: env).render!
end end
end end
@@ -214,9 +206,10 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
environment = Liquid::Environment.build(file_system: file_system)
assert_equal( assert_equal(
'from CountingFileSystemfrom CountingFileSystem', 'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }), Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
) )
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
end end
@@ -271,26 +264,27 @@ class IncludeTagTest < Minitest::Test
end end
def test_does_not_add_error_in_strict_mode_for_missing_variable def test_does_not_add_error_in_strict_mode_for_missing_variable
Liquid::Template.file_system = TestFileSystem.new env = Liquid::Environment.build(file_system: TestFileSystem.new)
a = Liquid::Template.parse(' {% include "nested_template" %}') a = Liquid::Template.parse(' {% include "nested_template" %}', environment: env)
a.render! a.render!
assert_empty(a.errors) assert_empty(a.errors)
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
Liquid::Template.file_system = TestFileSystem.new env = Liquid::Environment.build(file_system: TestFileSystem.new)
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')) assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
end end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')) assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
end end
end end
@@ -341,8 +335,11 @@ class IncludeTagTest < Minitest::Test
end end
def test_including_with_strict_variables def test_including_with_strict_variables
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" }) env = Liquid::Environment.build(
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) file_system: StubFileSystem.new('simple' => 'simple'),
)
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
assert_equal([], template.errors) assert_equal([], template.errors)
+8 -5
View File
@@ -82,19 +82,22 @@ class RenderTagTest < Minitest::Test
end end
def test_recursively_rendered_template_does_not_produce_endless_loop def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}') env = Liquid::Environment.build(
file_system: StubFileSystem.new('loop' => '{% render "loop" %}'),
)
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop" %}').render! Template.parse('{% render "loop" %}', environment: env).render!
end end
end end
def test_sub_contexts_count_towards_the_same_recursion_limit def test_sub_contexts_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new( env = Liquid::Environment.build(
'loop_render' => '{% render "loop_render" %}', file_system: StubFileSystem.new('loop_render' => '{% render "loop_render" %}'),
) )
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop_render" %}').render! Template.parse('{% render "loop_render" %}', environment: env).render!
end end
end end
+48
View File
@@ -207,4 +207,52 @@ class TableRowTest < Minitest::Test
render_errors: true, render_errors: true,
) )
end end
def test_table_row_handles_interrupts
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
'{% tablerow n in (1...3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
)
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
'{% tablerow n in (1...3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
)
end
def test_table_row_does_not_leak_interrupts
template = <<~LIQUID
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{% tablerow k in (1..3) %}{% break %}{% endtablerow -%}
loop j={{ j }}
{% endfor -%}
loop i={{ i }}
{% endfor -%}
after loop
LIQUID
expected = <<~STR
<tr class="row1">
<td class="col1"></td></tr>
loop j=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=2
loop i=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=1
<tr class="row1">
<td class="col1"></td></tr>
loop j=2
loop i=2
after loop
STR
assert_template_result(
expected,
template,
)
end
end end
+8 -2
View File
@@ -339,7 +339,7 @@ class TemplateTest < Minitest::Test
end end
def test_raises_error_with_invalid_utf8 def test_raises_error_with_invalid_utf8
e = assert_raises(SyntaxError) do e = assert_raises(TemplateEncodingError) do
Template.parse(<<~LIQUID) Template.parse(<<~LIQUID)
{% comment %} {% comment %}
\xC0 \xC0
@@ -347,6 +347,12 @@ class TemplateTest < Minitest::Test
LIQUID LIQUID
end end
assert_equal('Liquid syntax error: Invalid template encoding', e.message) assert_equal('Liquid error: Invalid template encoding', e.message)
end
def test_allows_non_string_values_as_source
assert_equal('', Template.parse(nil).render)
assert_equal('1', Template.parse(1).render)
assert_equal('true', Template.parse(true).render)
end end
end end
+135
View File
@@ -555,4 +555,139 @@ class TrimModeTest < Minitest::Test
def test_trim_blank def test_trim_blank
assert_template_result('foobar', 'foo {{-}} bar') assert_template_result('foobar', 'foo {{-}} bar')
end end
def test_trim_indent_variable
text = <<-END_TEMPLATE
<div>
<p>
{{~ 'Hello\nWorld' ~}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_indent_variable_trims_trailing_whitespace
text = <<-END_TEMPLATE
<div>
<p>
{{~ 'Hello\nWorld\n' ~}}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_indent_tags
text = <<-END_TEMPLATE
<div>
<p>
{%~ echo 'Hello\nWorld' ~%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text)
end
def test_trim_indent_include
text = <<-END_TEMPLATE
<div>
<p>
{%~ include "snippet" ~%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld"})
end
def test_trim_indent_render
text = <<-END_TEMPLATE
<div>
<p>
{%~ render "snippet" ~%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld"})
end
def test_trim_indent_render_trim_trailing_whitespace
text = <<-END_TEMPLATE
<div>
<p>
{%~ render "snippet" ~%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text, partials: {"snippet" => "Hello\nWorld\n"})
end
def test_trim_indent_nested_render
text = <<-END_TEMPLATE
<div>
<p>
{%~ render "snippet" ~%}
</p>
</div>
END_TEMPLATE
expected = <<-END_EXPECTED
<div>
<p>
Hello
inside
World
</p>
</div>
END_EXPECTED
assert_template_result(expected, text, partials: {"snippet" => "Hello\n {%~ render \"snippet2\" ~%}\nWorld", "snippet2" => "inside"})
end
end # TrimModeTest end # TrimModeTest
+17 -33
View File
@@ -13,7 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
puts "-- #{env_mode.upcase} ERROR MODE" puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym mode = env_mode.to_sym
end end
Liquid::Template.error_mode = mode Liquid::Environment.default.error_mode = mode
if ENV['LIQUID_C'] == '1' if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C" puts "-- LIQUID C"
@@ -42,10 +42,11 @@ module Minitest
message: nil, partials: nil, error_mode: nil, render_errors: false, message: nil, partials: nil, error_mode: nil, render_errors: false,
template_factory: nil template_factory: nil
) )
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials || {}) file_system = StubFileSystem.new(partials || {})
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory) registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers) context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
output = template.render(context) output = template.render(context)
assert_equal(expected, output, message) assert_equal(expected, output, message)
end end
@@ -78,44 +79,27 @@ module Minitest
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}") assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end end
def with_global_filter(*globals) def with_global_filter(*globals, &blk)
original_global_cache = Liquid::StrainerFactory::GlobalCache environment = Liquid::Environment.build do |w|
Liquid::StrainerFactory.send(:remove_const, :GlobalCache) w.register_filters(globals)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate)) end
globals.each do |global| Environment.dangerously_override(environment, &blk)
Liquid::Template.register_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end end
def with_error_mode(mode) def with_error_mode(mode)
old_mode = Liquid::Template.error_mode old_mode = Liquid::Environment.default.error_mode
Liquid::Template.error_mode = mode Liquid::Environment.default.error_mode = mode
yield yield
ensure ensure
Liquid::Template.error_mode = old_mode Liquid::Environment.default.error_mode = old_mode
end end
def with_custom_tag(tag_name, tag_class) def with_custom_tag(tag_name, tag_class, &block)
old_tag = Liquid::Template.tags[tag_name] environment = Liquid::Environment.default.dup
begin environment.register_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class)
yield Environment.dangerously_override(environment, &block)
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end end
end end
end end
+6
View File
@@ -32,6 +32,12 @@ class BlockUnitTest < Minitest::Test
assert_equal(String, template.root.nodelist[2].class) assert_equal(String, template.root.nodelist[2].class)
end end
def test_variable_with_multibyte_character
template = Liquid::Template.parse("{{ '❤️' }}")
assert_equal(1, template.root.nodelist.size)
assert_equal(Variable, template.root.nodelist[0].class)
end
def test_variable_many_embedded_fragments def test_variable_many_embedded_fragments
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal(7, template.root.nodelist.size) assert_equal(7, template.root.nodelist.size)
+7 -2
View File
@@ -55,6 +55,11 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_false('bob', 'contains', '---') assert_evaluates_false('bob', 'contains', '---')
end end
def test_contains_binary_encoding_compatibility_with_utf8
assert_evaluates_true('🙈'.b, 'contains', '🙈')
assert_evaluates_true('🙈', 'contains', '🙈'.b)
end
def test_invalid_comparation_operator def test_invalid_comparation_operator
assert_evaluates_argument_error(1, '~~', 0) assert_evaluates_argument_error(1, '~~', 0)
end end
@@ -166,14 +171,14 @@ class ConditionUnitTest < Minitest::Test
def assert_evaluates_true(left, op, right) def assert_evaluates_true(left, op, right)
assert( assert(
Condition.new(left, op, right).evaluate(@context), Condition.new(left, op, right).evaluate(@context),
"Evaluated false: #{left} #{op} #{right}", "Evaluated false: #{left.inspect} #{op} #{right.inspect}",
) )
end end
def assert_evaluates_false(left, op, right) def assert_evaluates_false(left, op, right)
assert( assert(
!Condition.new(left, op, right).evaluate(@context), !Condition.new(left, op, right).evaluate(@context),
"Evaluated true: #{left} #{op} #{right}", "Evaluated true: #{left.inspect} #{op} #{right.inspect}",
) )
end end
@@ -2,7 +2,7 @@
require 'test_helper' require 'test_helper'
class StrainerFactoryUnitTest < Minitest::Test class EnvironmentFilterTest < Minitest::Test
include Liquid include Liquid
module AccessScopeFilters module AccessScopeFilters
@@ -16,8 +16,6 @@ class StrainerFactoryUnitTest < Minitest::Test
private :private_filter private :private_filter
end end
StrainerFactory.add_global_filter(AccessScopeFilters)
module LateAddedFilter module LateAddedFilter
def late_added_filter(_input) def late_added_filter(_input)
"filtered" "filtered"
@@ -25,24 +23,28 @@ class StrainerFactoryUnitTest < Minitest::Test
end end
def setup def setup
@context = Context.build @environment = Liquid::Environment.build do |env|
env.register_filter(AccessScopeFilters)
end
@context = Context.build(environment: @environment)
end end
def test_strainer def test_strainer
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_equal(5, strainer.invoke('size', 'input')) assert_equal(5, strainer.invoke('size', 'input'))
assert_equal("public", strainer.invoke("public_filter")) assert_equal("public", strainer.invoke("public_filter"))
end end
def test_stainer_raises_argument_error def test_stainer_raises_argument_error
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1) strainer.invoke("public_filter", 1)
end end
end end
def test_stainer_argument_error_contains_backtrace def test_stainer_argument_error_contains_backtrace
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1) strainer.invoke("public_filter", 1)
@@ -57,7 +59,7 @@ class StrainerFactoryUnitTest < Minitest::Test
end end
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_equal(false, strainer.class.invokable?('__test__')) assert_equal(false, strainer.class.invokable?('__test__'))
assert_equal(false, strainer.class.invokable?('test')) assert_equal(false, strainer.class.invokable?('test'))
assert_equal(false, strainer.class.invokable?('instance_eval')) assert_equal(false, strainer.class.invokable?('instance_eval'))
@@ -66,18 +68,18 @@ class StrainerFactoryUnitTest < Minitest::Test
end end
def test_strainer_returns_nil_if_no_filter_method_found def test_strainer_returns_nil_if_no_filter_method_found
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_nil(strainer.invoke("private_filter")) assert_nil(strainer.invoke("private_filter"))
assert_nil(strainer.invoke("undef_the_filter")) assert_nil(strainer.invoke("undef_the_filter"))
end end
def test_strainer_returns_first_argument_if_no_method_and_arguments_given def test_strainer_returns_first_argument_if_no_method_and_arguments_given
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_equal("password", strainer.invoke("undef_the_method", "password")) assert_equal("password", strainer.invoke("undef_the_method", "password"))
end end
def test_strainer_only_allows_methods_defined_in_filters def test_strainer_only_allows_methods_defined_in_filters
strainer = StrainerFactory.create(@context) strainer = @environment.create_strainer(@context)
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1")) assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom")) assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke")) assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
@@ -86,7 +88,9 @@ class StrainerFactoryUnitTest < Minitest::Test
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a = Module.new a = Module.new
b = Module.new b = Module.new
strainer = StrainerFactory.create(@context, [a, b])
strainer = @environment.create_strainer(@context, [a, b])
assert_kind_of(StrainerTemplate, strainer) assert_kind_of(StrainerTemplate, strainer)
assert_kind_of(a, strainer) assert_kind_of(a, strainer)
assert_kind_of(b, strainer) assert_kind_of(b, strainer)
@@ -94,8 +98,10 @@ class StrainerFactoryUnitTest < Minitest::Test
end end
def test_add_global_filter_clears_cache def test_add_global_filter_clears_cache
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input')) assert_equal('input', @environment.create_strainer(@context).invoke('late_added_filter', 'input'))
StrainerFactory.add_global_filter(LateAddedFilter)
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input')) @environment.register_filter(LateAddedFilter)
assert_equal('filtered', @environment.create_strainer(nil).invoke('late_added_filter', 'input'))
end end
end end
+105 -19
View File
@@ -6,48 +6,134 @@ class LexerUnitTest < Minitest::Test
include Liquid include Liquid
def test_strings def test_strings
tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize assert_equal(
assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens) [[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]],
tokenize(%( 'this is a test""' "wat 'lol'")),
)
end end
def test_integer def test_integer
tokens = Lexer.new('hi 50').tokenize assert_equal(
assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens) [[:id, 'hi'], [:number, '50'], [:end_of_string]],
tokenize('hi 50'),
)
end end
def test_float def test_float
tokens = Lexer.new('hi 5.0').tokenize assert_equal(
assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens) [[:id, 'hi'], [:number, '5.0'], [:end_of_string]],
tokenize('hi 5.0'),
)
end end
def test_comparison def test_comparison
tokens = Lexer.new('== <> contains ').tokenize assert_equal(
assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens) [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]],
tokenize('== <> contains '),
)
end
def test_comparison_without_whitespace
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '0'], [:end_of_string]],
tokenize('1>0'),
)
end
def test_comparison_with_negative_number
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '-1'], [:end_of_string]],
tokenize('1>-1'),
)
end
def test_raise_for_invalid_comparison
assert_raises(SyntaxError) do
tokenize('1>!1')
end
assert_raises(SyntaxError) do
tokenize('1=<1')
end
assert_raises(SyntaxError) do
tokenize('1!!1')
end
end end
def test_specials def test_specials
tokens = Lexer.new('| .:').tokenize assert_equal(
assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens) [[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]],
tokens = Lexer.new('[,]').tokenize tokenize('| .:'),
assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens) )
assert_equal(
[[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]],
tokenize('[,]'),
)
end end
def test_fancy_identifiers def test_fancy_identifiers
tokens = Lexer.new('hi five?').tokenize assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokenize('hi five?'))
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens)
tokens = Lexer.new('2foo').tokenize assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokenize('2foo'))
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens)
end end
def test_whitespace def test_whitespace
tokens = Lexer.new("five|\n\t ==").tokenize assert_equal(
assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens) [[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]],
tokenize("five|\n\t =="),
)
end end
def test_unexpected_character def test_unexpected_character
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Lexer.new("%").tokenize tokenize("%")
end end
end end
def test_negative_numbers
assert_equal(
[[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]],
tokenize("foo | default: -1"),
)
end
def test_greater_than_two_digits
assert_equal(
[[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]],
tokenize("foo > 12"),
)
end
def test_error_with_utf8_character
error = assert_raises(SyntaxError) do
tokenize("1 < 1Ø")
end
assert_equal(
'Liquid syntax error: Unexpected character Ø',
error.message,
)
end
def test_contains_as_attribute_name
assert_equal(
[[:id, "a"], [:dot, "."], [:id, "contains"], [:dot, "."], [:id, "b"], [:end_of_string]],
tokenize("a.contains.b"),
)
end
def test_tokenize_incomplete_expression
assert_equal([[:id, "false"], [:dash, "-"], [:end_of_string]], tokenize("false -"))
assert_equal([[:id, "false"], [:comparison, "<"], [:end_of_string]], tokenize("false <"))
assert_equal([[:id, "false"], [:comparison, ">"], [:end_of_string]], tokenize("false >"))
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
end
private
def tokenize(input)
Lexer.new(input).tokenize
end
end end
+13 -8
View File
@@ -25,11 +25,13 @@ class StrainerTemplateUnitTest < Minitest::Test
end end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter) Liquid::Environment.build do |env|
env.register_filter(PublicMethodOverrideFilter)
env.register_filter(PrivateMethodOverrideFilter)
end
end end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end end
@@ -42,11 +44,13 @@ class StrainerTemplateUnitTest < Minitest::Test
end end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter) Liquid::Environment.build do |env|
env.register_filter(PublicMethodOverrideFilter)
env.register_filter(ProtectedMethodOverrideFilter)
end
end end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end end
@@ -58,8 +62,9 @@ class StrainerTemplateUnitTest < Minitest::Test
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
with_global_filter do with_global_filter do
strainer = Context.new.strainer context = Context.new
strainer.class.add_filter(PublicMethodOverrideFilter) context.add_filters([PublicMethodOverrideFilter])
strainer = context.strainer
assert(strainer.class.send(:filter_methods).include?('public_filter')) assert(strainer.class.send(:filter_methods).include?('public_filter'))
end end
end end
+4 -53
View File
@@ -20,62 +20,13 @@ class TemplateUnitTest < Minitest::Test
assert_equal(fixture("en_locale.yml"), locale.path) assert_equal(fixture("en_locale.yml"), locale.path)
end end
def test_with_cache_classes_tags_returns_the_same_class
original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = true
original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass)
Template.register_tag('custom', CustomTag)
Object.send(:remove_const, :CustomTag)
new_klass = Class.new
Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(original_klass))
ensure
Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting
end
def test_without_cache_classes_tags_reloads_the_class
original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = false
original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass)
Template.register_tag('custom', CustomTag)
Object.send(:remove_const, :CustomTag)
new_klass = Class.new
Object.send(:const_set, :CustomTag, new_klass)
assert(Template.tags['custom'].equal?(new_klass))
ensure
Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting
end
class FakeTag; end class FakeTag; end
def test_tags_delete
Template.register_tag('fake', FakeTag)
assert_equal(FakeTag, Template.tags['fake'])
Template.tags.delete('fake')
assert_nil(Template.tags['fake'])
end
def test_tags_can_be_looped_over def test_tags_can_be_looped_over
Template.register_tag('fake', FakeTag) with_custom_tag('fake', FakeTag) do
result = Template.tags.map { |name, klass| [name, klass] } result = Template.tags.map { |name, klass| [name, klass] }
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"])) assert(result.include?(["fake", TemplateUnitTest::FakeTag]))
ensure end
Template.tags.delete('fake')
end end
class TemplateSubclass < Liquid::Template class TemplateSubclass < Liquid::Template