Compare commits

..

3 Commits

Author SHA1 Message Date
Tristan Hume
10173c3315 Add line numbers to warnings 2014-08-14 16:23:38 -04:00
Tristan Hume
1c1aa4094a Remove parser switching duplication 2014-08-14 15:06:54 -04:00
Florian Weingarten
627ef9e29d Optional line numbers for liquid errors 2014-08-14 16:11:21 +00:00
17 changed files with 84 additions and 178 deletions

View File

@@ -3,8 +3,6 @@
## 3.0.0 / not yet released / branch "master"
* ...
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
* Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]

View File

@@ -15,9 +15,7 @@ module Liquid
'>'.freeze => :>,
'>='.freeze => :>=,
'<='.freeze => :<=,
'contains'.freeze => lambda { |cond, left, right|
left && right && left.respond_to?(:include?) ? left.include?(right) : false
}
'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
}
def self.operators

View File

@@ -16,14 +16,12 @@ module Liquid
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = resource_limits || Template.default_resource_limits
@resource_limits[:render_score_current] = 0
@resource_limits[:assign_score_current] = 0
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments
@@ -94,11 +92,8 @@ module Liquid
end
def handle_error(e, token=nil)
if e.is_a?(Liquid::Error)
e.set_line_number_from_token(token)
end
def handle_error(e, token)
e = Liquid::Error.error_with_line_number(e, token)
errors.push(e)
raise if exception_handler && exception_handler.call(e)
Liquid::Error.render(e)

View File

@@ -1,51 +1,34 @@
module Liquid
class Error < ::StandardError
attr_accessor :line_number
attr_accessor :markup_context
def to_s(with_prefix=true)
str = ""
str << message_prefix if with_prefix
str << super()
if markup_context
str << " "
str << markup_context
def self.render(e)
msg = if e.is_a?(Liquid::Error) && e.line_number
"#{e.line_number}: #{e.message}"
else
e.message
end
str
case e
when SyntaxError
"Liquid syntax error: #{msg}"
else
"Liquid error: #{msg}"
end
end
def self.error_with_line_number(e, token)
if e.is_a?(Liquid::Error)
e.set_line_number_from_token(token)
end
e
end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
self.line_number = token.line_number
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e.to_s}"
end
end
private
def message_prefix
str = ""
if is_a?(SyntaxError)
str << "Liquid syntax error"
else
str << "Liquid error"
end
if line_number
str << " (line #{line_number})"
end
str << ": "
str
end
end
class ArgumentError < Error; end

View File

@@ -8,7 +8,7 @@ module Liquid
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
e.set_line_number_from_token(markup)
e.line_number = markup.line_number if markup.is_a?(Token)
@warnings ||= []
@warnings << e
return lax_parse(markup)
@@ -20,12 +20,12 @@ module Liquid
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.markup_context = markup_context(markup)
e.message << markup_context(markup)
raise e
end
def markup_context(markup)
"in \"#{markup.strip}\""
" in \"#{markup.strip}\""
end
end
end
end

View File

@@ -42,10 +42,6 @@ module Liquid
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
def url_encode(input)
CGI.escape(input) rescue input
end
def slice(input, offset, length=nil)
offset = Integer(offset)
length = length ? Integer(length) : 1

View File

@@ -69,7 +69,7 @@ module Liquid
return cached
end
source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source, pass_options)
partial = Liquid::Template.parse(source)
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
@@ -88,16 +88,6 @@ module Liquid
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end
end
def pass_options
dont_pass = @options[:include_options_blacklist]
return {locale: @options[:locale]} if dont_pass == true
opts = @options.merge(included: true, include_options_blacklist: false)
if dont_pass.is_a?(Array)
dont_pass.each {|o| opts.delete(o)}
end
opts
end
end
Template.register_tag('include'.freeze, Include)

View File

@@ -86,10 +86,6 @@ module Liquid
Strainer.global_filter(mod)
end
def default_resource_limits
@default_resource_limits ||= {}
end
# creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
@@ -99,16 +95,16 @@ module Liquid
end
end
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
def initialize
@resource_limits = self.class.default_resource_limits.dup
@resource_limits = {}
end
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@options = options
@profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling
@profiling = options.delete(:profile)
@line_numbers = options.delete(:line_numbers) || @profiling
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil
self
@@ -201,7 +197,7 @@ module Liquid
end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
context.handle_error(e, nil)
ensure
@errors = context.errors
end
@@ -239,7 +235,7 @@ module Liquid
end
def with_profiling
if @profiling && !@options[:included]
if @profiling
@profiler = Profiler.new
@profiler.start

View File

@@ -30,7 +30,7 @@ module Liquid
end
def markup_context(markup)
"in \"{{#{markup}}}\""
" in \"{{#{markup}}}\""
end
def lax_parse(markup)

View File

@@ -1,14 +1,17 @@
require 'benchmark'
require 'benchmark/ips'
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
N = 100
Benchmark.bmbm do |x|
x.report("parse:") { N.times { profiler.parse } }
x.report("marshal load:") { N.times { profiler.marshal_load } }
x.report("render:") { N.times { profiler.render } }
x.report("marshal load & render:") { N.times { profiler.load_and_render } }
x.report("parse & render:") { N.times { profiler.parse_and_render } }
Benchmark.ips do |x|
x.time = 60
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run }
end

View File

@@ -3,13 +3,13 @@ require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.parse_and_render
profiler.run
[:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type.to_s} mode..."
results = StackProf.run(mode: profile_type) do
100.times do
profiler.parse_and_render
profiler.run
end
end
StackProf::Report.new(results).print_text(false, 20)

View File

@@ -32,59 +32,45 @@ class ThemeRunner
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
end.compact
@parsed = @tests.map do |liquid, layout, template_name|
[Liquid::Template.parse(liquid), Liquid::Template.parse(layout), template_name]
end
@marshaled = @parsed.map do |liquid, layout, template_name|
[Marshal.dump(liquid), Marshal.dump(layout), template_name]
end
end
def parse
def compile
# Dup assigns because will make some changes to them
@tests.each do |liquid, layout, template_name|
Liquid::Template.parse(liquid)
Liquid::Template.parse(layout)
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end
end
def marshal_load
@marshaled.each do |liquid, layout, template_name|
Marshal.load(liquid)
Marshal.load(layout)
end
end
def render
@parsed.each do |liquid, layout, template_name|
render_once(liquid, layout, template_name)
end
end
def load_and_render
@marshaled.each do |liquid, layout, template_name|
render_once(Marshal.load(liquid), Marshal.load(layout), template_name)
end
end
def parse_and_render
@tests.each do |liquid, layout, template_name|
render_once(Liquid::Template.parse(liquid), Liquid::Template.parse(layout), template_name)
end
end
def render_once(template, layout, template_name)
def run
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
assigns['page_title'] = 'Page title'
assigns['template'] = File.basename(template_name, File.extname(template_name))
template.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_name))
@tests.each do |liquid, layout, template_name|
content_for_layout = template.render!(assigns)
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name)
end
end
def compile_and_render(template, layout, assigns, page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
layout.render!(assigns)
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end

View File

@@ -40,13 +40,13 @@ class ErrorHandlingTest < Minitest::Test
expected = <<-TEXT
Hello,
Liquid error (line 3): standard error will raise a standard error.
Liquid error: 3: standard error will raise a standard error.
Bla bla test.
Liquid syntax error (line 7): syntax error will raise a syntax error.
Liquid syntax error: 7: syntax error will raise a syntax error.
This is an argument error: Liquid error (line 9): argument error
This is an argument error: Liquid error: 9: argument error
Bla.
TEXT
@@ -104,28 +104,25 @@ class ErrorHandlingTest < Minitest::Test
err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
assert_equal 'Unexpected character = in "1 =! 2"', err.message
err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
end
def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal '', template.render
end
def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal 3, template.warnings.size
assert_equal [1,2,3], template.warnings.map(&:line_number)
end

View File

@@ -48,18 +48,6 @@ class RenderProfilingTest < Minitest::Test
assert_equal 2, t.profiler[1].line_number
end
def test_profiling_includes_line_numbers_of_included_partials
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!
included_children = t.profiler[0].children
# {% assign template_name = 'a_template' %}
assert_equal 1, included_children[0].line_number
# {{ template_name }}
assert_equal 2, included_children[1].line_number
end
def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!

View File

@@ -118,11 +118,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')
end
def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal nil, @filters.url_encode(nil)
end
def test_truncatewords
assert_equal 'one two three', @filters.truncatewords('one two three', 4)
assert_equal 'one two...', @filters.truncatewords('one two three', 2)

View File

@@ -132,7 +132,7 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError, SystemStackError) do
assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render!
end
@@ -209,19 +209,4 @@ class IncludeTagTest < Minitest::Test
a.render!
assert_empty a.errors
end
def test_passing_options_to_included_templates
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).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 }}')
end
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).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 }}')
end
end
end # IncludeTagTest

View File

@@ -80,10 +80,6 @@ class ConditionUnitTest < Minitest::Test
assert_evalutes_false "0", 'contains', 'not_assigned'
end
def test_contains_return_false_on_wrong_data_type
assert_evalutes_false "1", 'contains', '0'
end
def test_or_condition
condition = Condition.new('1', '==', '2')