Compare commits

...

16 Commits

Author SHA1 Message Date
Tom Burns
9559d69e11 blacklist inspect et al from being called on drops 2012-12-18 13:50:02 -05:00
Dylan Smith
50bd34fd78 Merge pull request #161 from Shopify/fix-filter-parser-regex
Fix filter parser regex for filter args without separating spaces.
2012-12-18 10:13:21 -08:00
Dylan Smith
ee41b3f4a3 Fix filter parser regex for filter args without separating spaces.
The regex was using \S+ to match the comma between the filters
arguments, but would continue to match idependent quote characters and
filter separators. This can result in multiple filters being interpreted as
a single one with many arguments.
2012-12-18 01:23:31 -05:00
Tobias Lütke
05d9976e16 fix benchmark 2012-10-29 16:47:57 -04:00
Tom Burns
6c2fde5eea Instantiate blank string once instead of at every comparison 2012-10-25 11:54:19 -04:00
Tobias Lütke
ce76dbf8d9 fixed the performance suite 2012-10-20 10:53:53 -04:00
Steven Soroka
661ff2ccdf Merge pull request #140 from binarycleric/feature/break_for_loop
Added break and continue statements
2012-08-21 13:22:24 -07:00
Jon Daniel
9c183bea83 added interrupt class for continue/break statements
When a continue or break statement is executed it pushes an interrupt to a
stack in context. If any non-handled interrupts are present blocks will cease
to execute. The for loop can handle the most recent interrupt in the stack.
2012-08-21 13:14:27 -04:00
Jon Daniel
484fd18612 added break and continue tags 2012-08-21 00:00:02 -04:00
Jonathan Rudenberg
bf86459456 Merge pull request #139 from pjb3/fix_block_test_name
Class name does not match file name
2012-08-19 15:07:59 -07:00
Paul Barry
d2827c561b Class name does not match file name 2012-08-19 07:44:35 -04:00
Tobias Lütke
16c34595a4 fix mergeconflict 2012-08-07 13:21:31 -04:00
Tobias Lütke
6e091909ee Merge branch 'master' of github.com:Shopify/liquid 2012-08-07 13:20:37 -04:00
Tobias Lütke
d7cb39ccb3 release 2.4.0 2012-08-07 13:20:23 -04:00
Jonathan Rudenberg
f8d46804fd Fix rake test for broken version of rake on travis 2012-08-07 09:49:55 -04:00
Jonathan Rudenberg
5c6de2d919 Fix typo 2012-08-07 09:37:19 -04:00
21 changed files with 285 additions and 35 deletions

View File

@@ -8,7 +8,7 @@
* Ruby 1.8, 1.9, and Rubinius compatibility fixes
* Add support for `quoted['references']` in `tablerow`
* Add support for Enumerable to `tablerow`
* `strip\_html` filter removes html comments
* `strip_html` filter removes html comments
## 2.3.0 / 2011-10-16

View File

@@ -9,7 +9,7 @@ task :default => 'test'
Rake::TestTask.new(:test) do |t|
t.libs << '.' << 'lib' << 'test'
t.pattern = 'test/liquid/**/*_test.rb'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.verbose = false
end
@@ -27,7 +27,7 @@ namespace :benchmark do
desc "Run the liquid benchmark"
task :run do
ruby "performance/benchmark.rb"
ruby "./performance/benchmark.rb"
end
end
@@ -37,12 +37,12 @@ namespace :profile do
desc "Run the liquid profile/performance coverage"
task :run do
ruby "performance/profile.rb"
ruby "./performance/profile.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
end
end

View File

@@ -48,6 +48,7 @@ end
require 'liquid/drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/context'
require 'liquid/tag'

View File

@@ -89,13 +89,27 @@ module Liquid
end
def render_all(list, context)
list.collect do |token|
output = []
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
token.respond_to?(:render) ? token.render(context) : token
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a? Continue or token.is_a? Break
context.push_interrupt(token.interrupt)
break
end
output << (token.respond_to?(:render) ? token.render(context) : token)
rescue ::StandardError => e
context.handle_error(e)
output << (context.handle_error(e))
end
end.join
end
output.join
end
end
end

View File

@@ -22,6 +22,8 @@ module Liquid
@errors = []
@rethrow_errors = rethrow_errors
squash_instance_assigns_with_environments
@interrupts = []
end
def strainer
@@ -41,6 +43,21 @@ module Liquid
end
end
# are there any not handled interrupts?
def has_interrupt?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e)
@interrupts.push(e)
end
# pop an interrupt from the stack
def pop_interrupt
@interrupts.pop
end
def handle_error(e)
errors.push(e)
raise if @rethrow_errors

View File

@@ -20,6 +20,9 @@ module Liquid
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all.
class Drop
EMPTY_STRING = ''.freeze
METHOD_BLACKLIST = [:dup, :clone, :singleton_class, :eval, :class_eval, :`, :inspect]
attr_writer :context
# Catch all for the method
@@ -29,7 +32,7 @@ module Liquid
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if method_or_key && method_or_key != '' && self.class.public_method_defined?(method_or_key.to_s.to_sym)
if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s.to_sym)
send(method_or_key.to_s.to_sym)
else
before_method(method_or_key)
@@ -45,5 +48,9 @@ module Liquid
end
alias :[] :invoke_drop
METHOD_BLACKLIST.each do |blacklisted|
define_method(blacklisted) {nil}
end
end
end

View File

@@ -8,4 +8,4 @@ module Liquid
class StandardError < Error; end
class SyntaxError < Error; end
class StackLevelError < Error; end
end
end

17
lib/liquid/interrupts.rb Normal file
View File

@@ -0,0 +1,17 @@
module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt
attr_reader :message
def initialize(message=nil)
@message = message || "interrupt"
end
end
# Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < Interrupt; end
# Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end
end

21
lib/liquid/tags/break.rb Normal file
View File

@@ -0,0 +1,21 @@
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% break %}
# {% endif %}
# {% endfor %}
#
class Break < Tag
def interrupt
BreakInterrupt.new
end
end
Template.register_tag('break', Break)
end

View File

@@ -0,0 +1,21 @@
module Liquid
# Continue tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag
def interrupt
ContinueInterrupt.new
end
end
Template.register_tag('continue', Continue)
end

View File

@@ -69,7 +69,7 @@ module Liquid
@nodelist = @else_block = []
end
def render(context)
def render(context)
context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name]
@@ -101,8 +101,8 @@ module Liquid
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'] = {
'name' => @name,
@@ -115,6 +115,13 @@ module Liquid
'last' => (index == length - 1) }
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result

View File

@@ -11,7 +11,7 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?!(?:#{FilterSeparator}))(?:#{QuotedFragment}|\S+)\s*)+)/o
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
attr_accessor :filters, :name
def initialize(markup)

View File

@@ -2,7 +2,7 @@
Gem::Specification.new do |s|
s.name = "liquid"
s.version = "2.4.0"
s.version = "2.4.1"
s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"]

View File

@@ -5,6 +5,7 @@ require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
Benchmark.bmbm do |x|
x.report("parse & run:") { 10.times { profiler.run(false) } }
x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end

View File

@@ -6,14 +6,14 @@ profiler = ThemeRunner.new
puts 'Running profiler...'
results = profiler.run(true)
results = profiler.run
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/liquid.#{klass.name.downcase}.txt")
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/callgrind.liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp) }
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
end

View File

@@ -27,9 +27,34 @@ class ThemeRunner
end.compact
end
def compile
# Dup assigns because will make some changes to them
def run(profile = false)
RubyProf.measure_mode = RubyProf::WALL_TIME if profile
@tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end
end
def run
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# 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)
end
end
def run_profile
RubyProf.measure_mode = RubyProf::WALL_TIME
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@@ -40,26 +65,27 @@ class ThemeRunner
html = nil
page_template = File.basename(template_name, File.extname(template_name))
# Profile compiling and rendering both
if profile
RubyProf.resume do
html = compile_and_render(liquid, layout, assigns, page_template)
end
else
html = compile_and_render(liquid, layout, assigns, page_template)
unless @started
RubyProf.start
RubyProf.pause
@started = true
end
html = nil
RubyProf.resume
html = compile_and_render(liquid, layout, assigns, page_template)
RubyProf.pause
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)] if profile
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
# Uncomment to dump html files to /tmp so that you can inspect for errors
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
end
RubyProf.stop if profile
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template)

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class VariableTest < Test::Unit::TestCase
class BlockTest < Test::Unit::TestCase
include Liquid
def test_blankspace

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class BreakTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if break is called outside of a
# block
def test_break_with_no_block
assigns = {'i' => 1}
markup = '{% break %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class ContinueTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if continue is called outside of a
# block
def test_continue_with_no_block
assigns = {}
markup = '{% continue %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -168,6 +168,88 @@ HERE
assert_template_result(expected,markup,assigns)
end
def test_for_with_break
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234"
assert_template_result(expected,markup,assigns)
# tests to ensure it only breaks out of the local for loop
# and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% break %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '3456'
assert_template_result(expected, markup, assigns)
# test break does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_with_continue
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245"
assert_template_result(expected,markup,assigns)
# tests to ensure it only continues the local for loop and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% continue %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '23456'
assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_tag_string
# ruby 1.8.7 "String".each => Enumerator with single "String" element.

View File

@@ -66,6 +66,10 @@ class VariableTest < Test::Unit::TestCase
var = Variable.new('hello|textileze|paragraph')
assert_equal 'hello', var.name
assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
var = Variable.new("hello|replace:'foo','bar'|textileze")
assert_equal 'hello', var.name
assert_equal [[:replace, ["'foo'", "'bar'"]], [:textileze, []]], var.filters
end
def test_symbol