liquid 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,9 +1,13 @@
1
+ * Ruby 1.9.1 bugfixes
2
+
3
+ * Fix LiquidView for Rails 2.2. Fix local assigns for all versions of Rails
4
+
1
5
  * Fixed gem install rake task
2
6
  * Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins
3
7
 
4
8
  * Added If with or / and expressions
5
9
 
6
- * Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
10
+ * Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
7
11
 
8
12
  * Added more tags to standard library
9
13
 
@@ -22,17 +26,17 @@
22
26
  * Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
23
27
 
24
28
  * Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
25
-
26
- {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
27
-
28
29
 
29
- * Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
30
+ {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
31
+
32
+
33
+ * Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
30
34
 
31
35
  class ProductDrop < Liquid::Drop
32
36
  def top_sales
33
37
  Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
34
38
  end
35
- end
39
+ end
36
40
  t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
37
41
  t.render('product' => ProductDrop.new )
38
42
 
data/Rakefile CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'rubygems'
3
3
  require 'rake'
4
- require 'hoe'
4
+ require 'rake/testtask'
5
+ require 'rake/gempackagetask'
5
6
 
6
- PKG_VERSION = "2.0.0"
7
- PKG_NAME = "liquid"
8
- PKG_DESC = "A secure non evaling end user template engine with aesthetic markup."
7
+ task :default => 'test'
9
8
 
10
9
  Rake::TestTask.new(:test) do |t|
11
10
  t.libs << "lib"
@@ -14,11 +13,27 @@ Rake::TestTask.new(:test) do |t|
14
13
  t.verbose = false
15
14
  end
16
15
 
17
- Hoe.new(PKG_NAME, PKG_VERSION) do |p|
18
- p.rubyforge_name = PKG_NAME
19
- p.summary = PKG_DESC
20
- p.description = PKG_DESC
21
- p.author = "Tobias Luetke"
22
- p.email = "tobi@leetsoft.com"
23
- p.url = "http://www.liquidmarkup.org"
24
- end
16
+ gemspec = eval(File.read('liquid.gemspec'))
17
+ Rake::GemPackageTask.new(gemspec) do |pkg|
18
+ pkg.gem_spec = gemspec
19
+ end
20
+
21
+ namespace :profile do
22
+
23
+
24
+ task :default => [:run]
25
+
26
+ desc "Run the liquid profile/perforamce coverage"
27
+ task :run do
28
+
29
+ ruby "performance/shopify.rb"
30
+
31
+ end
32
+
33
+ desc "Run KCacheGrind"
34
+ task :grind => :run do
35
+ system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
36
+ end
37
+ end
38
+
39
+
@@ -5,32 +5,43 @@
5
5
  #
6
6
  # ActionView::Base::register_template_handler :liquid, LiquidView
7
7
  class LiquidView
8
+ PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
9
+ _response url _request _cookies variables_added _flash params _headers request cookies
10
+ ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
11
+ PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
12
+ @helpers @assigns_added @template @_render_stack @template_format @assigns )
13
+
14
+ def self.call(template)
15
+ "LiquidView.new(self).render(template, local_assigns)"
16
+ end
8
17
 
9
- def initialize(action_view)
10
- @action_view = action_view
18
+ def initialize(view)
19
+ @view = view
11
20
  end
12
21
 
13
-
14
- def render(template, local_assigns_for_rails_less_than_2_1_0 = nil)
15
- @action_view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
16
- assigns = @action_view.assigns.dup
22
+ def render(template, local_assigns = nil)
23
+ @view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
17
24
 
18
- # template is a Template object in Rails >=2.1.0, a source string previously.
19
- if template.respond_to? :source
20
- source = template.source
21
- local_assigns = template.locals
25
+ # Rails 2.2 Template has source, but not locals
26
+ if template.respond_to?(:source) && !template.respond_to?(:locals)
27
+ assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
28
+ hash[ivar[1..-1]] = @view.instance_variable_get(ivar)
29
+ hash
30
+ end
22
31
  else
23
- source = template
24
- local_assigns = local_assigns_for_rails_less_than_2_1_0
32
+ assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
25
33
  end
26
-
27
- if content_for_layout = @action_view.instance_variable_get("@content_for_layout")
34
+
35
+ source = template.respond_to?(:source) ? template.source : template
36
+ local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
37
+
38
+ if content_for_layout = @view.instance_variable_get("@content_for_layout")
28
39
  assigns['content_for_layout'] = content_for_layout
29
40
  end
30
- assigns.merge!(local_assigns)
41
+ assigns.merge!(local_assigns.stringify_keys)
31
42
 
32
43
  liquid = Liquid::Template.parse(source)
33
- liquid.render(assigns, :filters => [@action_view.controller.master_helper_module], :registers => {:action_view => @action_view, :controller => @action_view.controller})
44
+ liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
34
45
  end
35
46
 
36
47
  def compilable?
data/lib/liquid.rb CHANGED
@@ -29,11 +29,11 @@ module Liquid
29
29
  TagStart = /\{\%/
30
30
  TagEnd = /\%\}/
31
31
  VariableSignature = /\(?[\w\-\.\[\]]\)?/
32
- VariableSegment = /[\w\-]\??/
32
+ VariableSegment = /[\w\-]/
33
33
  VariableStart = /\{\{/
34
34
  VariableEnd = /\}\}/
35
35
  VariableIncompleteEnd = /\}\}?/
36
- QuotedString = /"[^"]+"|'[^']+'/
36
+ QuotedString = /"[^"]*"|'[^']*'/
37
37
  QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/
38
38
  StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s,\|,\:,\,]+/
39
39
  FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/
@@ -44,7 +44,7 @@ module Liquid
44
44
  AnyStartingTag = /\{\{|\{\%/
45
45
  PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
46
46
  TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
47
- VariableParser = /\[[^\]]+\]|#{VariableSegment}+/
47
+ VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
48
48
  end
49
49
 
50
50
  require 'liquid/drop'
data/lib/liquid/block.rb CHANGED
@@ -1,19 +1,23 @@
1
1
  module Liquid
2
-
2
+
3
3
  class Block < Tag
4
+ IsTag = /^#{TagStart}/
5
+ IsVariable = /^#{VariableStart}/
6
+ FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
7
+ ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/
4
8
 
5
9
  def parse(tokens)
6
10
  @nodelist ||= []
7
11
  @nodelist.clear
8
12
 
9
- while token = tokens.shift
13
+ while token = tokens.shift
10
14
 
11
15
  case token
12
- when /^#{TagStart}/
13
- if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
16
+ when IsTag
17
+ if token =~ FullToken
14
18
 
15
19
  # if we found the proper block delimitor just end parsing here and let the outer block
16
- # proceed
20
+ # proceed
17
21
  if block_delimiter == $1
18
22
  end_tag
19
23
  return
@@ -23,33 +27,33 @@ module Liquid
23
27
  if tag = Template.tags[$1]
24
28
  @nodelist << tag.new($1, $2, tokens)
25
29
  else
26
- # this tag is not registered with the system
30
+ # this tag is not registered with the system
27
31
  # pass it to the current block for special handling or error reporting
28
32
  unknown_tag($1, $2, tokens)
29
- end
33
+ end
30
34
  else
31
35
  raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
32
36
  end
33
- when /^#{VariableStart}/
37
+ when IsVariable
34
38
  @nodelist << create_variable(token)
35
39
  when ''
36
40
  # pass
37
41
  else
38
42
  @nodelist << token
39
43
  end
40
- end
41
-
42
- # Make sure that its ok to end parsing in the current block.
43
- # Effectively this method will throw and exception unless the current block is
44
- # of type Document
44
+ end
45
+
46
+ # Make sure that its ok to end parsing in the current block.
47
+ # Effectively this method will throw and exception unless the current block is
48
+ # of type Document
45
49
  assert_missing_delimitation!
46
- end
47
-
48
- def end_tag
50
+ end
51
+
52
+ def end_tag
49
53
  end
50
54
 
51
55
  def unknown_tag(tag, params, tokens)
52
- case tag
56
+ case tag
53
57
  when 'else'
54
58
  raise SyntaxError, "#{block_name} tag does not expect else tag"
55
59
  when 'end'
@@ -61,14 +65,14 @@ module Liquid
61
65
 
62
66
  def block_delimiter
63
67
  "end#{block_name}"
64
- end
68
+ end
65
69
 
66
70
  def block_name
67
71
  @tag_name
68
72
  end
69
73
 
70
74
  def create_variable(token)
71
- token.scan(/^#{VariableStart}(.*)#{VariableEnd}$/) do |content|
75
+ token.scan(ContentOfVariable) do |content|
72
76
  return Variable.new(content.first)
73
77
  end
74
78
  raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
@@ -77,7 +81,7 @@ module Liquid
77
81
  def render(context)
78
82
  render_all(@nodelist, context)
79
83
  end
80
-
84
+
81
85
  protected
82
86
 
83
87
  def assert_missing_delimitation!
@@ -86,12 +90,12 @@ module Liquid
86
90
 
87
91
  def render_all(list, context)
88
92
  list.collect do |token|
89
- begin
93
+ begin
90
94
  token.respond_to?(:render) ? token.render(context) : token
91
- rescue Exception => e
95
+ rescue Exception => e
92
96
  context.handle_error(e)
93
- end
94
- end
97
+ end
98
+ end
95
99
  end
96
- end
97
- end
100
+ end
101
+ end
@@ -3,7 +3,7 @@ module Liquid
3
3
  #
4
4
  # Example:
5
5
  #
6
- # c = Condition.new('1', '==', '1')
6
+ # c = Condition.new('1', '==', '1')
7
7
  # c.evaluate #=> true
8
8
  #
9
9
  class Condition #:nodoc:
@@ -15,35 +15,35 @@ module Liquid
15
15
  '>' => :>,
16
16
  '>=' => :>=,
17
17
  '<=' => :<=,
18
- 'contains' => lambda { |cond, left, right| left.include?(right) },
18
+ 'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
19
19
  }
20
-
20
+
21
21
  def self.operators
22
22
  @@operators
23
23
  end
24
24
 
25
25
  attr_reader :attachment
26
26
  attr_accessor :left, :operator, :right
27
-
27
+
28
28
  def initialize(left = nil, operator = nil, right = nil)
29
29
  @left, @operator, @right = left, operator, right
30
30
  @child_relation = nil
31
31
  @child_condition = nil
32
32
  end
33
-
33
+
34
34
  def evaluate(context = Context.new)
35
- result = interpret_condition(left, right, operator, context)
36
-
35
+ result = interpret_condition(left, right, operator, context)
36
+
37
37
  case @child_relation
38
- when :or
38
+ when :or
39
39
  result || @child_condition.evaluate(context)
40
- when :and
40
+ when :and
41
41
  result && @child_condition.evaluate(context)
42
42
  else
43
43
  result
44
- end
45
- end
46
-
44
+ end
45
+ end
46
+
47
47
  def or(condition)
48
48
  @child_relation, @child_condition = :or, condition
49
49
  end
@@ -51,25 +51,25 @@ module Liquid
51
51
  def and(condition)
52
52
  @child_relation, @child_condition = :and, condition
53
53
  end
54
-
54
+
55
55
  def attach(attachment)
56
56
  @attachment = attachment
57
57
  end
58
-
58
+
59
59
  def else?
60
60
  false
61
- end
62
-
61
+ end
62
+
63
63
  def inspect
64
64
  "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
65
65
  end
66
-
66
+
67
67
  private
68
-
68
+
69
69
  def equal_variables(left, right)
70
70
  if left.is_a?(Symbol)
71
71
  if right.respond_to?(left)
72
- return right.send(left.to_s)
72
+ return right.send(left.to_s)
73
73
  else
74
74
  return nil
75
75
  end
@@ -77,47 +77,44 @@ module Liquid
77
77
 
78
78
  if right.is_a?(Symbol)
79
79
  if left.respond_to?(right)
80
- return left.send(right.to_s)
80
+ return left.send(right.to_s)
81
81
  else
82
82
  return nil
83
83
  end
84
84
  end
85
85
 
86
- left == right
87
- end
86
+ left == right
87
+ end
88
88
 
89
89
  def interpret_condition(left, right, op, context)
90
-
91
- # If the operator is empty this means that the decision statement is just
92
- # a single variable. We can just poll this variable from the context and
90
+ # If the operator is empty this means that the decision statement is just
91
+ # a single variable. We can just poll this variable from the context and
93
92
  # return this as the result.
94
- return context[left] if op == nil
93
+ return context[left] if op == nil
95
94
 
96
95
  left, right = context[left], context[right]
97
-
98
96
 
99
97
  operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
100
98
 
101
99
  if operation.respond_to?(:call)
102
100
  operation.call(self, left, right)
103
- elsif left.respond_to?(operation) and right.respond_to?(operation)
101
+ elsif left.respond_to?(operation) and right.respond_to?(operation)
104
102
  left.send(operation, right)
105
103
  else
106
104
  nil
107
105
  end
108
- end
109
- end
106
+ end
107
+ end
110
108
 
111
109
 
112
110
  class ElseCondition < Condition
113
-
114
- def else?
111
+ def else?
115
112
  true
116
113
  end
117
-
114
+
118
115
  def evaluate(context)
119
116
  true
120
117
  end
121
118
  end
122
119
 
123
- end
120
+ end
@@ -13,14 +13,15 @@ module Liquid
13
13
  #
14
14
  # context['bob'] #=> nil class Context
15
15
  class Context
16
- attr_reader :scopes
17
- attr_reader :errors, :registers
16
+ attr_reader :scopes, :errors, :registers, :environments
18
17
 
19
- def initialize(assigns = {}, registers = {}, rethrow_errors = false)
20
- @scopes = [(assigns || {})]
21
- @registers = registers
22
- @errors = []
18
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
19
+ @environments = [environments].flatten
20
+ @scopes = [(outer_scope || {})]
21
+ @registers = registers
22
+ @errors = []
23
23
  @rethrow_errors = rethrow_errors
24
+ squash_instance_assigns_with_environments
24
25
  end
25
26
 
26
27
  def strainer
@@ -61,9 +62,9 @@ module Liquid
61
62
  end
62
63
 
63
64
  # push new local scope on the stack. use <tt>Context#stack</tt> instead
64
- def push
65
+ def push(new_scope={})
65
66
  raise StackLevelError, "Nesting too deep" if @scopes.length > 100
66
- @scopes.unshift({})
67
+ @scopes.unshift(new_scope)
67
68
  end
68
69
 
69
70
  # merge a hash of variables in the current local scope
@@ -86,9 +87,9 @@ module Liquid
86
87
  # end
87
88
  # context['var] #=> nil
88
89
  #
89
- def stack(&block)
90
+ def stack(new_scope={},&block)
90
91
  result = nil
91
- push
92
+ push(new_scope)
92
93
  begin
93
94
  result = yield
94
95
  ensure
@@ -96,6 +97,10 @@ module Liquid
96
97
  end
97
98
  result
98
99
  end
100
+
101
+ def clear_instance_assigns
102
+ @scopes[0] = {}
103
+ end
99
104
 
100
105
  # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
101
106
  def []=(key, value)
@@ -133,9 +138,6 @@ module Liquid
133
138
  :blank?
134
139
  when 'empty'
135
140
  :empty?
136
- # filtered variables
137
- when SpacelessFilter
138
- filtered_variable(key)
139
141
  # Single quoted strings
140
142
  when /^'(.*)'$/
141
143
  $1.to_s
@@ -159,16 +161,21 @@ module Liquid
159
161
  # fetches an object starting at the local scope and then moving up
160
162
  # the hierachy
161
163
  def find_variable(key)
162
- @scopes.each do |scope|
163
- if scope.has_key?(key)
164
- variable = scope[key]
165
- variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
166
- variable = variable.to_liquid
167
- variable.context = self if variable.respond_to?(:context=)
168
- return variable
164
+ scope = @scopes.find { |s| s.has_key?(key) }
165
+ if scope.nil?
166
+ @environments.each do |e|
167
+ if variable = lookup_and_evaluate(e, key)
168
+ scope = e
169
+ break
170
+ end
169
171
  end
170
172
  end
171
- nil
173
+ scope ||= @environments.last || @scopes.last
174
+ variable ||= lookup_and_evaluate(scope, key)
175
+
176
+ variable = variable.to_liquid
177
+ variable.context = self if variable.respond_to?(:context=)
178
+ return variable
172
179
  end
173
180
 
174
181
  # resolves namespaced queries gracefully.
@@ -200,8 +207,7 @@ module Liquid
200
207
  (object.respond_to?(:fetch) and part.is_a?(Integer)))
201
208
 
202
209
  # if its a proc we will replace the entry with the proc
203
- res = object[part]
204
- res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
210
+ res = lookup_and_evaluate(object, part)
205
211
  object = res.to_liquid
206
212
 
207
213
  # Some special cases. If the part wasn't in square brackets and
@@ -225,8 +231,24 @@ module Liquid
225
231
  object
226
232
  end
227
233
 
228
- def filtered_variable(markup)
229
- Variable.new(markup).render(self)
234
+ def lookup_and_evaluate(obj, key)
235
+ if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
236
+ obj[key] = value.call(self)
237
+ else
238
+ value
239
+ end
230
240
  end
241
+
242
+ def squash_instance_assigns_with_environments
243
+ @scopes.last.each_key do |k|
244
+ @environments.each do |env|
245
+ if env.has_key?(k)
246
+ scopes.last[k] = lookup_and_evaluate(env, k)
247
+ break
248
+ end
249
+ end
250
+ end
251
+ end
252
+
231
253
  end
232
254
  end