locomotivecms-liquid 2.6.0 → 4.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +62 -5
  3. data/README.md +4 -4
  4. data/lib/liquid.rb +16 -12
  5. data/lib/liquid/block.rb +37 -118
  6. data/lib/liquid/block_body.rb +131 -0
  7. data/lib/liquid/condition.rb +28 -17
  8. data/lib/liquid/context.rb +94 -146
  9. data/lib/liquid/document.rb +16 -10
  10. data/lib/liquid/drop.rb +8 -5
  11. data/lib/liquid/drops/inherited_block_drop.rb +24 -0
  12. data/lib/liquid/errors.rb +44 -5
  13. data/lib/liquid/expression.rb +33 -0
  14. data/lib/liquid/file_system.rb +17 -6
  15. data/lib/liquid/i18n.rb +2 -2
  16. data/lib/liquid/interrupts.rb +1 -1
  17. data/lib/liquid/lexer.rb +11 -9
  18. data/lib/liquid/locales/en.yml +2 -4
  19. data/lib/liquid/parser.rb +2 -1
  20. data/lib/liquid/parser_switching.rb +31 -0
  21. data/lib/liquid/profiler.rb +162 -0
  22. data/lib/liquid/profiler/hooks.rb +23 -0
  23. data/lib/liquid/range_lookup.rb +22 -0
  24. data/lib/liquid/resource_limits.rb +23 -0
  25. data/lib/liquid/standardfilters.rb +142 -67
  26. data/lib/liquid/strainer.rb +14 -4
  27. data/lib/liquid/tag.rb +22 -41
  28. data/lib/liquid/tags/assign.rb +15 -10
  29. data/lib/liquid/tags/break.rb +1 -1
  30. data/lib/liquid/tags/capture.rb +7 -9
  31. data/lib/liquid/tags/case.rb +28 -19
  32. data/lib/liquid/tags/comment.rb +2 -2
  33. data/lib/liquid/tags/continue.rb +1 -4
  34. data/lib/liquid/tags/cycle.rb +10 -14
  35. data/lib/liquid/tags/decrement.rb +3 -4
  36. data/lib/liquid/tags/extends.rb +28 -44
  37. data/lib/liquid/tags/for.rb +64 -42
  38. data/lib/liquid/tags/if.rb +30 -19
  39. data/lib/liquid/tags/ifchanged.rb +4 -4
  40. data/lib/liquid/tags/include.rb +30 -20
  41. data/lib/liquid/tags/increment.rb +3 -8
  42. data/lib/liquid/tags/inherited_block.rb +54 -56
  43. data/lib/liquid/tags/raw.rb +18 -10
  44. data/lib/liquid/tags/table_row.rb +72 -0
  45. data/lib/liquid/tags/unless.rb +5 -7
  46. data/lib/liquid/template.rb +113 -53
  47. data/lib/liquid/token.rb +18 -0
  48. data/lib/liquid/utils.rb +13 -4
  49. data/lib/liquid/variable.rb +68 -50
  50. data/lib/liquid/variable_lookup.rb +78 -0
  51. data/lib/liquid/version.rb +1 -1
  52. data/test/fixtures/en_locale.yml +9 -0
  53. data/test/integration/assign_test.rb +48 -0
  54. data/test/integration/blank_test.rb +106 -0
  55. data/test/integration/capture_test.rb +50 -0
  56. data/test/integration/context_test.rb +32 -0
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +271 -0
  59. data/test/integration/error_handling_test.rb +207 -0
  60. data/test/integration/filter_test.rb +125 -0
  61. data/test/integration/hash_ordering_test.rb +23 -0
  62. data/test/integration/output_test.rb +116 -0
  63. data/test/integration/parsing_quirks_test.rb +119 -0
  64. data/test/integration/render_profiling_test.rb +154 -0
  65. data/test/integration/security_test.rb +64 -0
  66. data/test/integration/standard_filter_test.rb +379 -0
  67. data/test/integration/tags/break_tag_test.rb +16 -0
  68. data/test/integration/tags/continue_tag_test.rb +16 -0
  69. data/test/integration/tags/extends_tag_test.rb +104 -0
  70. data/test/integration/tags/for_tag_test.rb +375 -0
  71. data/test/integration/tags/if_else_tag_test.rb +169 -0
  72. data/test/integration/tags/include_tag_test.rb +234 -0
  73. data/test/integration/tags/increment_tag_test.rb +24 -0
  74. data/test/integration/tags/raw_tag_test.rb +25 -0
  75. data/test/integration/tags/standard_tag_test.rb +297 -0
  76. data/test/integration/tags/statements_test.rb +113 -0
  77. data/test/integration/tags/table_row_test.rb +63 -0
  78. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  79. data/test/integration/template_test.rb +216 -0
  80. data/test/integration/variable_test.rb +82 -0
  81. data/test/test_helper.rb +83 -0
  82. data/test/unit/block_unit_test.rb +55 -0
  83. data/test/unit/condition_unit_test.rb +149 -0
  84. data/test/unit/context_unit_test.rb +482 -0
  85. data/test/unit/file_system_unit_test.rb +35 -0
  86. data/test/unit/i18n_unit_test.rb +37 -0
  87. data/test/unit/lexer_unit_test.rb +51 -0
  88. data/test/unit/module_ex_unit_test.rb +87 -0
  89. data/test/unit/parser_unit_test.rb +82 -0
  90. data/test/unit/regexp_unit_test.rb +44 -0
  91. data/test/unit/strainer_unit_test.rb +71 -0
  92. data/test/unit/tag_unit_test.rb +16 -0
  93. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  94. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  95. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  96. data/test/unit/template_unit_test.rb +70 -0
  97. data/test/unit/tokenizer_unit_test.rb +38 -0
  98. data/test/unit/variable_unit_test.rb +150 -0
  99. metadata +144 -15
  100. data/lib/extras/liquid_view.rb +0 -51
  101. data/lib/liquid/htmltags.rb +0 -74
  102. data/lib/liquid/tags/default_content.rb +0 -21
  103. data/lib/locomotivecms-liquid.rb +0 -1
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class BlockBody
3
+ def render_token_with_profiling(token, context)
4
+ Profiler.profile_token_render(token) do
5
+ render_token_without_profiling(token, context)
6
+ end
7
+ end
8
+
9
+ alias_method :render_token_without_profiling, :render_token
10
+ alias_method :render_token, :render_token_with_profiling
11
+ end
12
+
13
+ class Include < Tag
14
+ def render_with_profiling(context)
15
+ Profiler.profile_children(context.evaluate(@template_name).to_s) do
16
+ render_without_profiling(context)
17
+ end
18
+ end
19
+
20
+ alias_method :render_without_profiling, :render
21
+ alias_method :render, :render_with_profiling
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Liquid
2
+ class RangeLookup
3
+ def self.parse(start_markup, end_markup)
4
+ start_obj = Expression.parse(start_markup)
5
+ end_obj = Expression.parse(end_markup)
6
+ if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
7
+ new(start_obj, end_obj)
8
+ else
9
+ start_obj.to_i..end_obj.to_i
10
+ end
11
+ end
12
+
13
+ def initialize(start_obj, end_obj)
14
+ @start_obj = start_obj
15
+ @end_obj = end_obj
16
+ end
17
+
18
+ def evaluate(context)
19
+ context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class ResourceLimits
3
+ attr_accessor :render_length, :render_score, :assign_score,
4
+ :render_length_limit, :render_score_limit, :assign_score_limit
5
+
6
+ def initialize(limits)
7
+ @render_length_limit = limits[:render_length_limit]
8
+ @render_score_limit = limits[:render_score_limit]
9
+ @assign_score_limit = limits[:assign_score_limit]
10
+ reset
11
+ end
12
+
13
+ def reached?
14
+ (@render_length_limit && @render_length > @render_length_limit) ||
15
+ (@render_score_limit && @render_score > @render_score_limit ) ||
16
+ (@assign_score_limit && @assign_score > @assign_score_limit )
17
+ end
18
+
19
+ def reset
20
+ @render_length = @render_score = @assign_score = 0
21
+ end
22
+ end
23
+ end
@@ -4,6 +4,14 @@ require 'bigdecimal'
4
4
  module Liquid
5
5
 
6
6
  module StandardFilters
7
+ HTML_ESCAPE = {
8
+ '&'.freeze => '&amp;'.freeze,
9
+ '>'.freeze => '&gt;'.freeze,
10
+ '<'.freeze => '&lt;'.freeze,
11
+ '"'.freeze => '&quot;'.freeze,
12
+ "'".freeze => '&#39;'.freeze
13
+ }
14
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
7
15
 
8
16
  # Return the size of an array or of an string
9
17
  def size(input)
@@ -26,32 +34,43 @@ module Liquid
26
34
  end
27
35
 
28
36
  def escape(input)
29
- CGI.escapeHTML(input) rescue input
37
+ CGI.escapeHTML(input).untaint rescue input
30
38
  end
39
+ alias_method :h, :escape
31
40
 
32
41
  def escape_once(input)
33
- ActionView::Helpers::TagHelper.escape_once(input)
34
- rescue NameError
35
- input
42
+ input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
36
43
  end
37
44
 
38
- alias_method :h, :escape
45
+ def url_encode(input)
46
+ CGI.escape(input) rescue input
47
+ end
48
+
49
+ def slice(input, offset, length=nil)
50
+ offset = Integer(offset)
51
+ length = length ? Integer(length) : 1
52
+
53
+ if input.is_a?(Array)
54
+ input.slice(offset, length) || []
55
+ else
56
+ input.to_s.slice(offset, length) || ''
57
+ end
58
+ end
39
59
 
40
60
  # Truncate a string down to x characters
41
- def truncate(input, length = 50, truncate_string = "...")
61
+ def truncate(input, length = 50, truncate_string = "...".freeze)
42
62
  if input.nil? then return end
43
63
  l = length.to_i - truncate_string.length
44
64
  l = 0 if l < 0
45
- truncated = RUBY_VERSION[0,3] == "1.8" ? input.scan(/./mu)[0...l].to_s : input[0...l]
46
- input.length > length.to_i ? truncated + truncate_string : input
65
+ input.length > length.to_i ? input[0...l] + truncate_string : input
47
66
  end
48
67
 
49
- def truncatewords(input, words = 15, truncate_string = "...")
68
+ def truncatewords(input, words = 15, truncate_string = "...".freeze)
50
69
  if input.nil? then return end
51
70
  wordlist = input.to_s.split
52
71
  l = words.to_i - 1
53
72
  l = 0 if l < 0
54
- wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
73
+ wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
55
74
  end
56
75
 
57
76
  # Split input string into an array of substrings separated by given pattern.
@@ -60,53 +79,72 @@ module Liquid
60
79
  # <div class="summary">{{ post | split '//' | first }}</div>
61
80
  #
62
81
  def split(input, pattern)
63
- input.split(pattern)
82
+ input.to_s.split(pattern)
83
+ end
84
+
85
+ def strip(input)
86
+ input.to_s.strip
87
+ end
88
+
89
+ def lstrip(input)
90
+ input.to_s.lstrip
91
+ end
92
+
93
+ def rstrip(input)
94
+ input.to_s.rstrip
64
95
  end
65
96
 
66
97
  def strip_html(input)
67
- input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
98
+ empty = ''.freeze
99
+ input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
68
100
  end
69
101
 
70
102
  # Remove all newlines from the string
71
103
  def strip_newlines(input)
72
- input.to_s.gsub(/\r?\n/, '')
104
+ input.to_s.gsub(/\r?\n/, ''.freeze)
73
105
  end
74
106
 
75
107
  # Join elements of the array with certain character between them
76
- def join(input, array_glue = ' ', hash_glue = nil)
77
- hash_glue ||= array_glue
78
-
79
- # translate from hash to array if needed
80
- input = input.map{|k,v| "#{k}#{hash_glue}#{v}" } if input.is_a?(Hash)
81
-
82
- [input].flatten.join(array_glue)
108
+ def join(input, glue = ' '.freeze)
109
+ InputIterator.new(input).join(glue)
83
110
  end
84
111
 
85
112
  # Sort elements of the array
86
113
  # provide optional property with which to sort an array of hashes or drops
87
114
  def sort(input, property = nil)
88
- ary = flatten_if_necessary(input)
115
+ ary = InputIterator.new(input)
89
116
  if property.nil?
90
117
  ary.sort
91
- elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
118
+ elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
92
119
  ary.sort {|a,b| a[property] <=> b[property] }
93
120
  elsif ary.first.respond_to?(property)
94
121
  ary.sort {|a,b| a.send(property) <=> b.send(property) }
95
122
  end
96
123
  end
97
124
 
125
+ # Remove duplicate elements from an array
126
+ # provide optional property with which to determine uniqueness
127
+ def uniq(input, property = nil)
128
+ ary = InputIterator.new(input)
129
+ if property.nil?
130
+ input.uniq
131
+ elsif input.first.respond_to?(:[])
132
+ input.uniq{ |a| a[property] }
133
+ end
134
+ end
135
+
98
136
  # Reverse the elements of an array
99
137
  def reverse(input)
100
- ary = [input].flatten
138
+ ary = InputIterator.new(input)
101
139
  ary.reverse
102
140
  end
103
141
 
104
142
  # map/collect on a given property
105
143
  def map(input, property)
106
- flatten_if_necessary(input).map do |e|
144
+ InputIterator.new(input).map do |e|
107
145
  e = e.call if e.is_a?(Proc)
108
146
 
109
- if property == "to_liquid"
147
+ if property == "to_liquid".freeze
110
148
  e
111
149
  elsif e.respond_to?(:[])
112
150
  e[property]
@@ -115,23 +153,23 @@ module Liquid
115
153
  end
116
154
 
117
155
  # Replace occurrences of a string with another
118
- def replace(input, string, replacement = '')
156
+ def replace(input, string, replacement = ''.freeze)
119
157
  input.to_s.gsub(string, replacement.to_s)
120
158
  end
121
159
 
122
160
  # Replace the first occurrences of a string with another
123
- def replace_first(input, string, replacement = '')
161
+ def replace_first(input, string, replacement = ''.freeze)
124
162
  input.to_s.sub(string, replacement.to_s)
125
163
  end
126
164
 
127
165
  # remove a substring
128
166
  def remove(input, string)
129
- input.to_s.gsub(string, '')
167
+ input.to_s.gsub(string, ''.freeze)
130
168
  end
131
169
 
132
170
  # remove the first occurrences of a substring
133
171
  def remove_first(input, string)
134
- input.to_s.sub(string, '')
172
+ input.to_s.sub(string, ''.freeze)
135
173
  end
136
174
 
137
175
  # add one string to another
@@ -146,10 +184,10 @@ module Liquid
146
184
 
147
185
  # Add <br /> tags in front of all newlines in input string
148
186
  def newline_to_br(input)
149
- input.to_s.gsub(/\n/, "<br />\n")
187
+ input.to_s.gsub(/\n/, "<br />\n".freeze)
150
188
  end
151
189
 
152
- # Reformat a date
190
+ # Reformat a date using Ruby's core Time#strftime( string ) -> string
153
191
  #
154
192
  # %a - The abbreviated weekday name (``Sun'')
155
193
  # %A - The full weekday name (``Sunday'')
@@ -163,6 +201,7 @@ module Liquid
163
201
  # %m - Month of the year (01..12)
164
202
  # %M - Minute of the hour (00..59)
165
203
  # %p - Meridian indicator (``AM'' or ``PM'')
204
+ # %s - Number of seconds since 1970-01-01 00:00:00 UTC.
166
205
  # %S - Second of the minute (00..60)
167
206
  # %U - Week number of the current year,
168
207
  # starting with the first Sunday as the first
@@ -177,34 +216,14 @@ module Liquid
177
216
  # %Y - Year with century
178
217
  # %Z - Time zone name
179
218
  # %% - Literal ``%'' character
219
+ #
220
+ # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
180
221
  def date(input, format)
222
+ return input if format.to_s.empty?
181
223
 
182
- if format.to_s.empty?
183
- return input.to_s
184
- end
185
-
186
- if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
187
- input = Time.at(input.to_i)
188
- end
224
+ return input unless date = to_date(input)
189
225
 
190
- date = if input.is_a?(String)
191
- case input.downcase
192
- when 'now', 'today'
193
- Time.now
194
- else
195
- Time.parse(input)
196
- end
197
- else
198
- input
199
- end
200
-
201
- if date.respond_to?(:strftime)
202
- date.strftime(format.to_s)
203
- else
204
- input
205
- end
206
- rescue
207
- input
226
+ date.strftime(format.to_s)
208
227
  end
209
228
 
210
229
  # Get the first element of the passed in array
@@ -249,19 +268,28 @@ module Liquid
249
268
  apply_operation(input, operand, :%)
250
269
  end
251
270
 
252
- private
271
+ def round(input, n = 0)
272
+ result = to_number(input).round(to_number(n))
273
+ result = result.to_f if result.is_a?(BigDecimal)
274
+ result = result.to_i if n == 0
275
+ result
276
+ end
253
277
 
254
- def flatten_if_necessary(input)
255
- ary = if input.is_a?(Array)
256
- input.flatten
257
- elsif input.kind_of?(Enumerable)
258
- input
259
- else
260
- [input].flatten
261
- end
262
- ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
278
+ def ceil(input)
279
+ to_number(input).ceil.to_i
280
+ end
281
+
282
+ def floor(input)
283
+ to_number(input).floor.to_i
284
+ end
285
+
286
+ def default(input, default_value = "".freeze)
287
+ is_blank = input.respond_to?(:empty?) ? input.empty? : !input
288
+ is_blank ? default_value : input
263
289
  end
264
290
 
291
+ private
292
+
265
293
  def to_number(obj)
266
294
  case obj
267
295
  when Float
@@ -269,16 +297,63 @@ module Liquid
269
297
  when Numeric
270
298
  obj
271
299
  when String
272
- (obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i
300
+ (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
273
301
  else
274
302
  0
275
303
  end
276
304
  end
277
305
 
306
+ def to_date(obj)
307
+ return obj if obj.respond_to?(:strftime)
308
+
309
+ case obj
310
+ when 'now'.freeze, 'today'.freeze
311
+ Time.now
312
+ when /\A\d+\z/, Integer
313
+ Time.at(obj.to_i)
314
+ when String
315
+ Time.parse(obj)
316
+ else
317
+ nil
318
+ end
319
+ rescue ArgumentError
320
+ nil
321
+ end
322
+
278
323
  def apply_operation(input, operand, operation)
279
324
  result = to_number(input).send(operation, to_number(operand))
280
325
  result.is_a?(BigDecimal) ? result.to_f : result
281
326
  end
327
+
328
+ class InputIterator
329
+ include Enumerable
330
+
331
+ def initialize(input)
332
+ @input = if input.is_a?(Array)
333
+ input.flatten
334
+ elsif input.is_a?(Hash)
335
+ [input]
336
+ elsif input.is_a?(Enumerable)
337
+ input
338
+ else
339
+ Array(input)
340
+ end
341
+ end
342
+
343
+ def join(glue)
344
+ to_a.join(glue)
345
+ end
346
+
347
+ def reverse
348
+ reverse_each.to_a
349
+ end
350
+
351
+ def each
352
+ @input.each do |e|
353
+ yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
354
+ end
355
+ end
356
+ end
282
357
  end
283
358
 
284
359
  Template.register_filter(StandardFilters)
@@ -11,6 +11,11 @@ module Liquid
11
11
  @@filters = []
12
12
  @@known_filters = Set.new
13
13
  @@known_methods = Set.new
14
+ @@strainer_class_cache = Hash.new do |hash, filters|
15
+ hash[filters] = Class.new(Strainer) do
16
+ filters.each { |f| include f }
17
+ end
18
+ end
14
19
 
15
20
  def initialize(context)
16
21
  @context = context
@@ -32,10 +37,13 @@ module Liquid
32
37
  end
33
38
  end
34
39
 
35
- def self.create(context)
36
- strainer = Strainer.new(context)
37
- @@filters.each { |m| strainer.extend(m) }
38
- strainer
40
+ def self.strainer_class_cache
41
+ @@strainer_class_cache
42
+ end
43
+
44
+ def self.create(context, filters = [])
45
+ filters = @@filters + filters
46
+ strainer_class_cache[filters].new(context)
39
47
  end
40
48
 
41
49
  def invoke(method, *args)
@@ -44,6 +52,8 @@ module Liquid
44
52
  else
45
53
  args.first
46
54
  end
55
+ rescue ::ArgumentError => e
56
+ raise Liquid::ArgumentError.new(e.message)
47
57
  end
48
58
 
49
59
  def invokable?(method)