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
@@ -1,22 +1,30 @@
1
1
  module Liquid
2
2
  class Raw < Block
3
- FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
3
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
4
 
5
5
  def parse(tokens)
6
- @nodelist ||= []
7
- @nodelist.clear
6
+ @body = ''
8
7
  while token = tokens.shift
9
8
  if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != ""
11
- if block_delimiter == $2
12
- end_tag
13
- return
14
- end
9
+ @body << $1 if $1 != "".freeze
10
+ return if block_delimiter == $2
15
11
  end
16
- @nodelist << token if not token.empty?
12
+ @body << token if not token.empty?
17
13
  end
18
14
  end
15
+
16
+ def render(context)
17
+ @body
18
+ end
19
+
20
+ def nodelist
21
+ [@body]
22
+ end
23
+
24
+ def blank?
25
+ @body.empty?
26
+ end
19
27
  end
20
28
 
21
- Template.register_tag('raw', Raw)
29
+ Template.register_tag('raw'.freeze, Raw)
22
30
  end
@@ -0,0 +1,72 @@
1
+ module Liquid
2
+ class TableRow < Block
3
+ Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
+
5
+ def initialize(tag_name, markup, options)
6
+ super
7
+ if markup =~ Syntax
8
+ @variable_name = $1
9
+ @collection_name = Expression.parse($2)
10
+ @attributes = {}
11
+ markup.scan(TagAttributes) do |key, value|
12
+ @attributes[key] = Expression.parse(value)
13
+ end
14
+ else
15
+ raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
16
+ end
17
+ end
18
+
19
+ def render(context)
20
+ collection = context.evaluate(@collection_name) or return ''.freeze
21
+
22
+ from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
23
+ to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
24
+
25
+ collection = Utils.slice_collection(collection, from, to)
26
+
27
+ length = collection.length
28
+
29
+ cols = context.evaluate(@attributes['cols'.freeze]).to_i
30
+
31
+ row = 1
32
+ col = 0
33
+
34
+ result = "<tr class=\"row1\">\n"
35
+ context.stack do
36
+
37
+ collection.each_with_index do |item, index|
38
+ context[@variable_name] = item
39
+ context['tablerowloop'.freeze] = {
40
+ 'length'.freeze => length,
41
+ 'index'.freeze => index + 1,
42
+ 'index0'.freeze => index,
43
+ 'col'.freeze => col + 1,
44
+ 'col0'.freeze => col,
45
+ 'rindex'.freeze => length - index,
46
+ 'rindex0'.freeze => length - index - 1,
47
+ 'first'.freeze => (index == 0),
48
+ 'last'.freeze => (index == length - 1),
49
+ 'col_first'.freeze => (col == 0),
50
+ 'col_last'.freeze => (col == cols - 1)
51
+ }
52
+
53
+
54
+ col += 1
55
+
56
+ result << "<td class=\"col#{col}\">" << super << '</td>'
57
+
58
+ if col == cols and (index != length - 1)
59
+ col = 0
60
+ row += 1
61
+ result << "</tr>\n<tr class=\"row#{row}\">"
62
+ end
63
+
64
+ end
65
+ end
66
+ result << "</tr>\n"
67
+ result
68
+ end
69
+ end
70
+
71
+ Template.register_tag('tablerow'.freeze, TableRow)
72
+ end
@@ -1,10 +1,9 @@
1
1
  require File.dirname(__FILE__) + '/if'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Unless is a conditional just like 'if' but works on the inverse logic.
6
5
  #
7
- # {% unless x < 0 %} x is greater than zero {% end %}
6
+ # {% unless x < 0 %} x is greater than zero {% endunless %}
8
7
  #
9
8
  class Unless < If
10
9
  def render(context)
@@ -13,21 +12,20 @@ module Liquid
13
12
  # First condition is interpreted backwards ( if not )
14
13
  first_block = @blocks.first
15
14
  unless first_block.evaluate(context)
16
- return render_all(first_block.attachment, context)
15
+ return first_block.attachment.render(context)
17
16
  end
18
17
 
19
18
  # After the first condition unless works just like if
20
19
  @blocks[1..-1].each do |block|
21
20
  if block.evaluate(context)
22
- return render_all(block.attachment, context)
21
+ return block.attachment.render(context)
23
22
  end
24
23
  end
25
24
 
26
- ''
25
+ ''.freeze
27
26
  end
28
27
  end
29
28
  end
30
29
 
31
-
32
- Template.register_tag('unless', Unless)
30
+ Template.register_tag('unless'.freeze, Unless)
33
31
  end
@@ -18,71 +18,110 @@ module Liquid
18
18
  :locale => I18n.new
19
19
  }
20
20
 
21
- attr_accessor :root, :resource_limits
21
+ attr_accessor :root
22
+ attr_reader :resource_limits
23
+
22
24
  @@file_system = BlankFileSystem.new
23
25
 
24
- class << self
25
- def file_system
26
- @@file_system
26
+ class TagRegistry
27
+ def initialize
28
+ @tags = {}
29
+ @cache = {}
27
30
  end
28
31
 
29
- def file_system=(obj)
30
- @@file_system = obj
31
- end
32
+ def [](tag_name)
33
+ return nil unless @tags.has_key?(tag_name)
34
+ return @cache[tag_name] if Liquid.cache_classes
32
35
 
33
- def register_tag(name, klass)
34
- tags[name.to_s] = klass
36
+ lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
35
37
  end
36
38
 
37
- def tags
38
- @tags ||= {}
39
+ def []=(tag_name, klass)
40
+ @tags[tag_name] = klass.name
41
+ @cache[tag_name] = klass
39
42
  end
40
43
 
41
- # Disabled (false) for better performance
42
- def count_lines=(flag)
43
- @count_lines = flag
44
+ def delete(tag_name)
45
+ @tags.delete(tag_name)
46
+ @cache.delete(tag_name)
44
47
  end
45
48
 
46
- def count_lines
47
- @count_lines || false
49
+ private
50
+
51
+ def lookup_class(name)
52
+ name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
48
53
  end
54
+ end
49
55
 
56
+ attr_reader :profiler
57
+
58
+ class << self
50
59
  # Sets how strict the parser should be.
51
60
  # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
52
61
  # :warn is the default and will give deprecation warnings when invalid syntax is used.
53
62
  # :strict will enforce correct syntax.
54
- def error_mode=(mode)
55
- @error_mode = mode
63
+ attr_writer :error_mode
64
+
65
+ # Sets how strict the taint checker should be.
66
+ # :lax is the default, and ignores the taint flag completely
67
+ # :warn adds a warning, but does not interrupt the rendering
68
+ # :error raises an error when tainted output is used
69
+ attr_writer :taint_mode
70
+
71
+ def file_system
72
+ @@file_system
73
+ end
74
+
75
+ def file_system=(obj)
76
+ @@file_system = obj
77
+ end
78
+
79
+ def register_tag(name, klass)
80
+ tags[name.to_s] = klass
81
+ end
82
+
83
+ def tags
84
+ @tags ||= TagRegistry.new
56
85
  end
57
86
 
58
87
  def error_mode
59
88
  @error_mode || :lax
60
89
  end
61
90
 
91
+ def taint_mode
92
+ @taint_mode || :lax
93
+ end
94
+
62
95
  # Pass a module with filter methods which should be available
63
96
  # to all liquid views. Good for registering the standard library
64
97
  def register_filter(mod)
65
98
  Strainer.global_filter(mod)
66
99
  end
67
100
 
101
+ def default_resource_limits
102
+ @default_resource_limits ||= {}
103
+ end
104
+
68
105
  # creates a new <tt>Template</tt> object from liquid source code
106
+ # To enable profiling, pass in <tt>profile: true</tt> as an option.
107
+ # See Liquid::Profiler for more information
69
108
  def parse(source, options = {})
70
109
  template = Template.new
71
110
  template.parse(source, options)
72
- template
73
111
  end
74
112
  end
75
113
 
76
- # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
77
114
  def initialize
78
- @resource_limits = {}
115
+ @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
79
116
  end
80
117
 
81
118
  # Parse source code.
82
119
  # Returns self for easy chaining
83
120
  def parse(source, options = {})
84
- _options = { template: self }.merge(DEFAULT_OPTIONS).merge(options)
85
- @root = Document.new(tokenize(source), _options)
121
+ @options = options
122
+ @profiling = options[:profile]
123
+ @line_numbers = options[:line_numbers] || @profiling
124
+ @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
86
125
  @warnings = nil
87
126
  self
88
127
  end
@@ -113,6 +152,9 @@ module Liquid
113
152
  # if you use the same filters over and over again consider registering them globally
114
153
  # with <tt>Template.register_filter</tt>
115
154
  #
155
+ # if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
156
+ # will be available via <tt>Template#profiler</tt>
157
+ #
116
158
  # Following options can be passed:
117
159
  #
118
160
  # * <tt>filters</tt> : array with local filters
@@ -120,11 +162,17 @@ module Liquid
120
162
  # filters and tags and might be useful to integrate liquid more with its host application
121
163
  #
122
164
  def render(*args)
123
- return '' if @root.nil?
165
+ return ''.freeze if @root.nil?
124
166
 
125
167
  context = case args.first
126
168
  when Liquid::Context
127
- args.shift
169
+ c = args.shift
170
+
171
+ if @rethrow_errors
172
+ c.exception_handler = ->(e) { true }
173
+ end
174
+
175
+ c
128
176
  when Liquid::Drop
129
177
  drop = args.shift
130
178
  drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -148,16 +196,24 @@ module Liquid
148
196
  context.add_filters(options[:filters])
149
197
  end
150
198
 
199
+ if options[:exception_handler]
200
+ context.exception_handler = options[:exception_handler]
201
+ end
151
202
  when Module
152
203
  context.add_filters(args.pop)
153
204
  when Array
154
205
  context.add_filters(args.pop)
155
206
  end
156
207
 
208
+ # Retrying a render resets resource usage
209
+ context.resource_limits.reset
210
+
157
211
  begin
158
212
  # render the nodelist.
159
213
  # for performance reasons we get an array back here. join will make a string out of it.
160
- result = @root.render(context)
214
+ result = with_profiling do
215
+ @root.render(context)
216
+ end
161
217
  result.respond_to?(:join) ? result.join : result
162
218
  rescue Liquid::MemoryError => e
163
219
  context.handle_error(e)
@@ -167,32 +223,8 @@ module Liquid
167
223
  end
168
224
 
169
225
  def render!(*args)
170
- @rethrow_errors = true; render(*args)
171
- end
172
-
173
- def walk(memo = {}, &block)
174
- # puts @root.nodelist.inspect
175
- self._walk(@root.nodelist, memo, &block)
176
- end
177
-
178
- def _walk(list, memo = {}, &block)
179
- list.each do |node|
180
- saved_memo = memo.clone
181
-
182
- # puts "fetch ! #{node.respond_to?(:name) ? node.name : 'String'} / #{node.respond_to?(:nodelist)}"
183
- if block_given?
184
- # puts "youpi ! #{node.name}"
185
- _memo = yield(node, memo) || {}
186
- memo.merge!(_memo)
187
- end
188
-
189
- if node.respond_to?(:nodelist) && !node.nodelist.blank?
190
- self._walk(node.nodelist, memo, &block)
191
- end
192
-
193
- memo = saved_memo
194
- end
195
- memo
226
+ @rethrow_errors = true
227
+ render(*args)
196
228
  end
197
229
 
198
230
  private
@@ -201,7 +233,8 @@ module Liquid
201
233
  def tokenize(source)
202
234
  source = source.source if source.respond_to?(:source)
203
235
  return [] if source.to_s.empty?
204
- tokens = source.split(TemplateParser)
236
+
237
+ tokens = calculate_line_numbers(source.split(TemplateParser))
205
238
 
206
239
  # removes the rogue empty element at the beginning of the array
207
240
  tokens.shift if tokens[0] and tokens[0].empty?
@@ -209,5 +242,32 @@ module Liquid
209
242
  tokens
210
243
  end
211
244
 
245
+ def calculate_line_numbers(raw_tokens)
246
+ return raw_tokens unless @line_numbers
247
+
248
+ current_line = 1
249
+ raw_tokens.map do |token|
250
+ Token.new(token, current_line).tap do
251
+ current_line += token.count("\n")
252
+ end
253
+ end
254
+ end
255
+
256
+ def with_profiling
257
+ if @profiling && !@options[:included]
258
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
259
+
260
+ @profiler = Profiler.new
261
+ @profiler.start
262
+
263
+ begin
264
+ yield
265
+ ensure
266
+ @profiler.stop
267
+ end
268
+ else
269
+ yield
270
+ end
271
+ end
212
272
  end
213
273
  end
@@ -0,0 +1,18 @@
1
+ module Liquid
2
+ class Token < String
3
+ attr_reader :line_number
4
+
5
+ def initialize(content, line_number)
6
+ super(content)
7
+ @line_number = line_number
8
+ end
9
+
10
+ def raw
11
+ "<raw>"
12
+ end
13
+
14
+ def child(string)
15
+ Token.new(string, @line_number)
16
+ end
17
+ end
18
+ end
data/lib/liquid/utils.rb CHANGED
@@ -1,5 +1,18 @@
1
1
  module Liquid
2
2
  module Utils
3
+
4
+ def self.slice_collection(collection, from, to)
5
+ if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
6
+ collection.load_slice(from, to)
7
+ else
8
+ slice_collection_using_each(collection, from, to)
9
+ end
10
+ end
11
+
12
+ def self.non_blank_string?(collection)
13
+ collection.is_a?(String) && collection != ''.freeze
14
+ end
15
+
3
16
  def self.slice_collection_using_each(collection, from, to)
4
17
  segments = []
5
18
  index = 0
@@ -22,9 +35,5 @@ module Liquid
22
35
 
23
36
  segments
24
37
  end
25
-
26
- def self.non_blank_string?(collection)
27
- collection.is_a?(String) && collection != ''
28
- end
29
38
  end
30
39
  end