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" ## 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 uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and 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] * 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 => :>=, '>='.freeze => :>=,
'<='.freeze => :<=, '<='.freeze => :<=,
'contains'.freeze => lambda { |cond, left, right| 'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
left && right && left.respond_to?(:include?) ? left.include?(right) : false
}
} }
def self.operators def self.operators

View File

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

View File

@@ -1,51 +1,34 @@
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
attr_accessor :markup_context
def to_s(with_prefix=true) def self.render(e)
str = "" msg = if e.is_a?(Liquid::Error) && e.line_number
str << message_prefix if with_prefix "#{e.line_number}: #{e.message}"
str << super() else
e.message
if markup_context
str << " "
str << markup_context
end 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 end
def set_line_number_from_token(token) def set_line_number_from_token(token)
return unless token.respond_to?(:line_number) return unless token.respond_to?(:line_number)
self.line_number = token.line_number self.line_number = token.line_number
end 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 end
class ArgumentError < Error; end class ArgumentError < Error; end

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ module Liquid
return cached return cached
end end
source = read_template_from_file_system(context) source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source, pass_options) partial = Liquid::Template.parse(source)
cached_partials[template_name] = partial cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials context.registers[:cached_partials] = cached_partials
partial partial
@@ -88,16 +88,6 @@ module Liquid
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)" raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end end
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 end
Template.register_tag('include'.freeze, Include) Template.register_tag('include'.freeze, Include)

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,13 @@ require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.parse_and_render profiler.run
[:cpu, :object].each do |profile_type| [:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type.to_s} mode..." puts "Profiling in #{profile_type.to_s} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
100.times do 100.times do
profiler.parse_and_render profiler.run
end end
end end
StackProf::Report.new(results).print_text(false, 20) 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] [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
end.compact 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 end
def parse def compile
# Dup assigns because will make some changes to them
@tests.each do |liquid, layout, template_name| @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
end end
def marshal_load def run
@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)
# Dup assigns because will make some changes to them # Dup assigns because will make some changes to them
assigns = Database.tables.dup assigns = Database.tables.dup
assigns['page_title'] = 'Page title' @tests.each do |liquid, layout, template_name|
assigns['template'] = File.basename(template_name, File.extname(template_name))
template.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(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 if layout
assigns['content_for_layout'] = content_for_layout assigns['content_for_layout'] = content_for_layout
layout.render!(assigns) tmpl.parse(layout).render!(assigns)
else else
content_for_layout content_for_layout
end end

View File

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

View File

@@ -48,18 +48,6 @@ class RenderProfilingTest < Minitest::Test
assert_equal 2, t.profiler[1].line_number assert_equal 2, t.profiler[1].line_number
end 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 def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", :profile => true) t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render! 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>') assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')
end 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 def test_truncatewords
assert_equal 'one two three', @filters.truncatewords('one two three', 4) assert_equal 'one two three', @filters.truncatewords('one two three', 4)
assert_equal 'one two...', @filters.truncatewords('one two three', 2) 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 Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError, SystemStackError) do assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}").render!
end end
@@ -209,19 +209,4 @@ class IncludeTagTest < Minitest::Test
a.render! a.render!
assert_empty a.errors assert_empty a.errors
end 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 end # IncludeTagTest

View File

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