Compare commits

..

3 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
9b6344f407 Port liquid-c bug compatible whitespace trimming 2020-09-08 21:03:27 -04:00
Dylan Thacker-Smith
a372baa9cf Rename Liquid::Block#unknown_tag parameters for clarity 2020-09-08 14:07:20 -04:00
Dylan Thacker-Smith
60075ddda2 Pass the tag markup and tokenizer to Document#unknown_tag
The parse_context no longer needs to be passed in because it is available
through through an attr_reader on the instance. However, the markup and
tokenizer weren't made available.  This refactor also makes the parameters
given to Document#unknown_tag consistent with Block#unknown_tag.
2020-09-08 14:07:20 -04:00
17 changed files with 120 additions and 177 deletions

View File

@@ -59,7 +59,7 @@ module Liquid
# @api public # @api public
def new_body def new_body
parse_context.new_block_body BlockBody.new
end end
# @api public # @api public

View File

@@ -71,24 +71,13 @@ module Liquid
# @api private # @api private
def self.render_node(context, output, node) def self.render_node(context, output, node)
node.render_to_output_buffer(context, output) node.render_to_output_buffer(context, output)
rescue => exc rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
blank_tag = !node.instance_of?(Variable) && node.blank? context.handle_error(e, node.line_number)
rescue_render_node(context, output, node.line_number, exc, blank_tag) rescue MemoryError
end raise
rescue ::StandardError => e
# @api private line_number = node.is_a?(String) ? nil : node.line_number
def self.rescue_render_node(context, output, line_number, exc, blank_tag) output << context.handle_error(e, line_number)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
end end
private def parse_liquid_tag(markup, parse_context) private def parse_liquid_tag(markup, parse_context)

View File

@@ -27,28 +27,10 @@ module Liquid
end, end,
} }
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
end
@@method_literals = {
'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze,
}
def self.operators def self.operators
@@operators @@operators
end end
def self.parse_expression(markup)
@@method_literals[markup] || Expression.parse(markup)
end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
@@ -109,7 +91,7 @@ module Liquid
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(MethodLiteral) if left.is_a?(Liquid::Expression::MethodLiteral)
if right.respond_to?(left.method_name) if right.respond_to?(left.method_name)
return right.send(left.method_name) return right.send(left.method_name)
else else
@@ -117,7 +99,7 @@ module Liquid
end end
end end
if right.is_a?(MethodLiteral) if right.is_a?(Liquid::Expression::MethodLiteral)
if left.respond_to?(right.method_name) if left.respond_to?(right.method_name)
return left.send(right.method_name) return left.send(right.method_name)
else else

View File

@@ -47,7 +47,7 @@ module Liquid
private private
def new_body def new_body
parse_context.new_block_body Liquid::BlockBody.new
end end
def parse_body(tokenizer) def parse_body(tokenizer)

View File

@@ -2,12 +2,25 @@
module Liquid module Liquid
class Expression class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = { LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => '', 'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => '' 'empty' => MethodLiteral.new(:empty?, '').freeze
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m SINGLE_QUOTED_STRING = /\A'(.*)'\z/m

View File

@@ -19,10 +19,6 @@ module Liquid
@options[option_key] @options[option_key]
end end
def new_block_body
Liquid::BlockBody.new
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options

View File

@@ -68,7 +68,7 @@ module Liquid
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Condition.parse_expression(Regexp.last_match(1))) block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end

View File

@@ -67,22 +67,18 @@ module Liquid
block.attach(new_body) block.attach(new_body)
end end
def parse_expression(markup)
Condition.parse_expression(markup)
end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
@@ -110,9 +106,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = parse_expression(p.expression) a = Expression.parse(p.expression)
if (op = p.consume?(:comparison)) if (op = p.consume?(:comparison))
b = parse_expression(p.expression) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)

View File

@@ -11,48 +11,4 @@ class BlockTest < Minitest::Test
end end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif") assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end end
def test_with_custom_tag
with_custom_tag('testtag', Block) do
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
end
end
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Block) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal 'hello', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal 'foohellobar', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end
end end

View File

@@ -261,12 +261,4 @@ class ErrorHandlingTest < Minitest::Test
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
def test_bug_compatible_silencing_of_errors_in_blank_nodes
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end
end end

View File

@@ -43,22 +43,15 @@ class SecurityTest < Minitest::Test
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)) assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
end end
def test_does_not_permanently_add_filters_to_symbol_table def test_does_not_add_filters_to_symbol_table
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
# MRI imprecisely marks objects found on the C stack, which can result test = %( {{ "some_string" | a_bad_filter }} )
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
GC.start template = Template.parse(test)
assert_equal([], (Symbol.all_symbols - current_symbols))
template.render!
assert_equal([], (Symbol.all_symbols - current_symbols)) assert_equal([], (Symbol.all_symbols - current_symbols))
end end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class TagTest < Minitest::Test
include Liquid
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Tag) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'hello', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'foohellobar', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end
end

View File

@@ -529,17 +529,6 @@ class TrimModeTest < Minitest::Test
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_pre_trim_blank_preceding_text
template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_equal("", template.render)
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end
def test_bug_compatible_pre_trim def test_bug_compatible_pre_trim
template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true) template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render) assert_equal("\n", template.render)

View File

@@ -45,9 +45,53 @@ class BlockUnitTest < Minitest::Test
assert_equal(3, template.root.nodelist.size) assert_equal(3, template.root.nodelist.size)
end end
def test_with_custom_tag
with_custom_tag('testtag', Block) do
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
end
end
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Block) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal 'hello', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal 'foohellobar', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end
private private
def block_types(nodelist) def block_types(nodelist)
nodelist.collect(&:class) nodelist.collect(&:class)
end end
end end # VariableTest

View File

@@ -65,7 +65,7 @@ class ArrayLike
end end
end end
class ContextTest < Minitest::Test class ContextUnitTest < Minitest::Test
include Liquid include Liquid
def setup def setup

View File

@@ -20,4 +20,42 @@ class TagUnitTest < Minitest::Test
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some_tag', tag.tag_name) assert_equal('some_tag', tag.tag_name)
end end
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Tag) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'hello', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'foohellobar', template.render
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end
end end