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
21 changed files with 277 additions and 117 deletions
+3 -5
View File
@@ -26,7 +26,9 @@ module Liquid
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
WhitespaceControl = '-'
WhitespaceControl = '(?:[-~])'
WhitespaceTrim = '-'
WhitespaceTrimIndent = '~'
TagStart = /\{\%/
TagEnd = /\%\}/
TagName = /#|\w+/
@@ -44,9 +46,6 @@ module Liquid
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end
require "liquid/version"
@@ -71,7 +70,6 @@ require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/tag'
+12 -3
View File
@@ -164,6 +164,7 @@ module Liquid
token.lstrip!
end
parse_context.trim_whitespace = false
parse_context.strip_trailing = false
@nodelist << token
@blank &&= token.match?(WhitespaceOrNothing)
end
@@ -174,7 +175,8 @@ module Liquid
end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
parse_context.indentation = nil
if token[2] == WhitespaceTrim
previous_token = @nodelist.last
if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
@@ -183,8 +185,15 @@ module Liquid
previous_token << first_byte
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
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
parse_context.trim_whitespace = (token[-3] == WhitespaceTrim)
parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent)
end
def blank?
@@ -248,7 +257,7 @@ module Liquid
def create_variable(token, parse_context)
if token.end_with?("}}")
i = 2
i = 3 if token[i] == "-"
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
+1 -1
View File
@@ -2,7 +2,7 @@
module Liquid
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, :environment
def initialize(options = Const::EMPTY_HASH)
-23
View File
@@ -1,23 +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, environment = Environment.default)
Deprecations.warn("StrainerFactory.add_global_filter", "Environment#register_filter")
environment.register_filter(filter)
end
def create(context, filters = Const::EMPTY_ARRAY, environment = Environment.default)
Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer")
environment.create_strainer(context, filters)
end
def global_filter_names(environment = Environment.default)
Deprecations.warn("StrainerFactory.global_filter_names", "Environment#filter_method_names")
Environment.strainer_template.filter_method_names
end
end
end
+5 -4
View File
@@ -63,10 +63,11 @@ module Liquid
comment_tag_depth -= 1
end
if comment_tag_depth.zero?
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
return false
end
next unless comment_tag_depth.zero?
parse_context.trim_whitespace = (token[-3] == WhitespaceTrim) unless tokenizer.for_liquid_tag
parse_context.strip_trailing = (token[-3] == WhitespaceTrimIndent) unless tokenizer.for_liquid_tag
return false
end
raise_tag_never_closed(block_name)
+13 -2
View File
@@ -37,6 +37,7 @@ module Liquid
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@attributes = {}
@indentation = options.indentation
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
@@ -80,15 +81,25 @@ module Liquid
context[key] = context.evaluate(value)
end
partial_output = +''
if variable.is_a?(Array)
variable.each do |var|
context[context_variable_name] = var
partial.render_to_output_buffer(context, output)
partial.render_to_output_buffer(context, partial_output)
end
else
context[context_variable_name] = variable
partial.render_to_output_buffer(context, output)
partial.render_to_output_buffer(context, partial_output)
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
ensure
context.template_name = old_template_name
+2 -1
View File
@@ -25,7 +25,8 @@ module Liquid
@body = +''
while (token = tokens.shift)
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) != ""
return
end
+14 -1
View File
@@ -46,6 +46,8 @@ module Liquid
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@indentation = options.indentation
@strip_trailing = options.strip_trailing
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@@ -84,7 +86,18 @@ module Liquid
inner_context[key] = context.evaluate(value)
end
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!)
}
+18 -2
View File
@@ -29,6 +29,8 @@ module Liquid
@name = nil
@parse_context = parse_context
@line_number = parse_context.line_number
@indentation = parse_context.indentation
@strip_trailing = parse_context.strip_trailing
strict_parse_with_error_mode_fallback(markup)
end
@@ -96,13 +98,27 @@ module Liquid
def render_to_output_buffer(context, output)
obj = render(context)
obj_output = ''
if obj.is_a?(Array)
output << obj.join
obj_output = obj.join
elsif obj.nil?
else
output << obj.to_s
obj_output = obj.to_s
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
end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.6.0.alpha"
VERSION = "5.6.0.rc1"
end
+2 -1
View File
@@ -4,7 +4,8 @@ require 'benchmark/ips'
require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
+8 -7
View File
@@ -11,11 +11,12 @@ require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag('form', CommentForm)
default_environment = Liquid::Environment.default
default_environment.register_tag('paginate', Paginate)
default_environment.register_tag('form', CommentForm)
Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter)
default_environment.register_filter(JsonFilter)
default_environment.register_filter(MoneyFilter)
default_environment.register_filter(WeightFilter)
default_environment.register_filter(ShopFilter)
default_environment.register_filter(TagFilter)
+4 -5
View File
@@ -203,20 +203,19 @@ class ErrorHandlingTest < Minitest::Test
end
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
Liquid::Template.default_exception_renderer = ->(e) {
default_exception_renderer = ->(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)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end
def test_setting_exception_renderer_on_environment
+1 -1
View File
@@ -33,7 +33,7 @@ class ProfilerTest < Minitest::Test
end
def setup
Liquid::Template.file_system = ProfilingFileSystem.new
Liquid::Environment.default.file_system = ProfilingFileSystem.new
end
def test_template_allows_flagging_profiling
+15 -11
View File
@@ -174,10 +174,10 @@ class IncludeTagTest < Minitest::Test
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
Template.parse("{% include 'loop' %}").render!
Template.parse("{% include 'loop' %}", environment: env).render!
end
end
@@ -264,26 +264,27 @@ class IncludeTagTest < Minitest::Test
end
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!
assert_empty(a.errors)
end
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
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
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
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
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
@@ -334,8 +335,11 @@ class IncludeTagTest < Minitest::Test
end
def test_including_with_strict_variables
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
env = Liquid::Environment.build(
file_system: StubFileSystem.new('simple' => 'simple'),
)
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
template.render(nil, strict_variables: true)
assert_equal([], template.errors)
+8 -5
View File
@@ -82,19 +82,22 @@ class RenderTagTest < Minitest::Test
end
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
Template.parse('{% render "loop" %}').render!
Template.parse('{% render "loop" %}', environment: env).render!
end
end
def test_sub_contexts_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}',
env = Liquid::Environment.build(
file_system: StubFileSystem.new('loop_render' => '{% render "loop_render" %}'),
)
assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop_render" %}').render!
Template.parse('{% render "loop_render" %}', environment: env).render!
end
end
+135
View File
@@ -555,4 +555,139 @@ class TrimModeTest < Minitest::Test
def test_trim_blank
assert_template_result('foobar', 'foo {{-}} bar')
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
+4 -4
View File
@@ -13,7 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym
end
Liquid::Template.error_mode = mode
Liquid::Environment.default.error_mode = mode
if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
@@ -88,11 +88,11 @@ module Minitest
end
def with_error_mode(mode)
old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode
old_mode = Liquid::Environment.default.error_mode
Liquid::Environment.default.error_mode = mode
yield
ensure
Liquid::Template.error_mode = old_mode
Liquid::Environment.default.error_mode = old_mode
end
def with_custom_tag(tag_name, tag_class, &block)
@@ -2,7 +2,7 @@
require 'test_helper'
class StrainerFactoryUnitTest < Minitest::Test
class EnvironmentFilterTest < Minitest::Test
include Liquid
module AccessScopeFilters
@@ -16,8 +16,6 @@ class StrainerFactoryUnitTest < Minitest::Test
private :private_filter
end
StrainerFactory.add_global_filter(AccessScopeFilters)
module LateAddedFilter
def late_added_filter(_input)
"filtered"
@@ -25,24 +23,28 @@ class StrainerFactoryUnitTest < Minitest::Test
end
def setup
@context = Context.build
@environment = Liquid::Environment.build do |env|
env.register_filter(AccessScopeFilters)
end
@context = Context.build(environment: @environment)
end
def test_strainer
strainer = StrainerFactory.create(@context)
strainer = @environment.create_strainer(@context)
assert_equal(5, strainer.invoke('size', 'input'))
assert_equal("public", strainer.invoke("public_filter"))
end
def test_stainer_raises_argument_error
strainer = StrainerFactory.create(@context)
strainer = @environment.create_strainer(@context)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_stainer_argument_error_contains_backtrace
strainer = StrainerFactory.create(@context)
strainer = @environment.create_strainer(@context)
exception = assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
@@ -57,7 +59,7 @@ class StrainerFactoryUnitTest < Minitest::Test
end
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?('instance_eval'))
@@ -66,18 +68,18 @@ class StrainerFactoryUnitTest < Minitest::Test
end
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("undef_the_filter"))
end
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"))
end
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("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
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
a = 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(a, strainer)
assert_kind_of(b, strainer)
@@ -94,8 +98,10 @@ class StrainerFactoryUnitTest < Minitest::Test
end
def test_add_global_filter_clears_cache
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input'))
StrainerFactory.add_global_filter(LateAddedFilter)
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input'))
assert_equal('input', @environment.create_strainer(@context).invoke('late_added_filter', 'input'))
@environment.register_filter(LateAddedFilter)
assert_equal('filtered', @environment.create_strainer(nil).invoke('late_added_filter', 'input'))
end
end
+10 -6
View File
@@ -25,11 +25,13 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
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
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
@@ -42,11 +44,13 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
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
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
-19
View File
@@ -20,25 +20,6 @@ class TemplateUnitTest < Minitest::Test
assert_equal(fixture("en_locale.yml"), locale.path)
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)
Liquid.cache_classes = original_cache_setting
end
class FakeTag; end
def test_tags_can_be_looped_over