liquid 3.0.0.rc1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +4 -0
  3. data/README.md +2 -2
  4. data/lib/liquid.rb +8 -0
  5. data/lib/liquid/block.rb +50 -46
  6. data/lib/liquid/block_body.rb +123 -0
  7. data/lib/liquid/condition.rb +12 -5
  8. data/lib/liquid/context.rb +75 -148
  9. data/lib/liquid/errors.rb +50 -2
  10. data/lib/liquid/expression.rb +33 -0
  11. data/lib/liquid/parser_switching.rb +31 -0
  12. data/lib/liquid/profiler.rb +159 -0
  13. data/lib/liquid/profiler/hooks.rb +23 -0
  14. data/lib/liquid/range_lookup.rb +22 -0
  15. data/lib/liquid/standardfilters.rb +29 -4
  16. data/lib/liquid/tag.rb +6 -25
  17. data/lib/liquid/tags/assign.rb +2 -1
  18. data/lib/liquid/tags/case.rb +1 -1
  19. data/lib/liquid/tags/if.rb +5 -5
  20. data/lib/liquid/tags/ifchanged.rb +1 -1
  21. data/lib/liquid/tags/include.rb +11 -1
  22. data/lib/liquid/tags/raw.rb +1 -4
  23. data/lib/liquid/tags/table_row.rb +1 -1
  24. data/lib/liquid/template.rb +55 -4
  25. data/lib/liquid/token.rb +18 -0
  26. data/lib/liquid/variable.rb +68 -41
  27. data/lib/liquid/variable_lookup.rb +78 -0
  28. data/lib/liquid/version.rb +1 -1
  29. data/test/integration/assign_test.rb +12 -1
  30. data/test/integration/blank_test.rb +1 -1
  31. data/test/integration/capture_test.rb +1 -1
  32. data/test/integration/context_test.rb +10 -11
  33. data/test/integration/drop_test.rb +29 -3
  34. data/test/integration/error_handling_test.rb +138 -41
  35. data/test/integration/filter_test.rb +7 -7
  36. data/test/integration/hash_ordering_test.rb +6 -8
  37. data/test/integration/output_test.rb +1 -1
  38. data/test/integration/parsing_quirks_test.rb +40 -18
  39. data/test/integration/render_profiling_test.rb +154 -0
  40. data/test/integration/security_test.rb +1 -1
  41. data/test/integration/standard_filter_test.rb +47 -1
  42. data/test/integration/tags/break_tag_test.rb +1 -1
  43. data/test/integration/tags/continue_tag_test.rb +1 -1
  44. data/test/integration/tags/for_tag_test.rb +2 -2
  45. data/test/integration/tags/if_else_tag_test.rb +23 -20
  46. data/test/integration/tags/include_tag_test.rb +24 -2
  47. data/test/integration/tags/increment_tag_test.rb +1 -1
  48. data/test/integration/tags/raw_tag_test.rb +1 -1
  49. data/test/integration/tags/standard_tag_test.rb +4 -4
  50. data/test/integration/tags/statements_test.rb +1 -1
  51. data/test/integration/tags/table_row_test.rb +1 -1
  52. data/test/integration/tags/unless_else_tag_test.rb +1 -1
  53. data/test/integration/template_test.rb +16 -4
  54. data/test/integration/variable_test.rb +11 -1
  55. data/test/test_helper.rb +59 -31
  56. data/test/unit/block_unit_test.rb +2 -5
  57. data/test/unit/condition_unit_test.rb +5 -1
  58. data/test/unit/context_unit_test.rb +13 -7
  59. data/test/unit/file_system_unit_test.rb +5 -5
  60. data/test/unit/i18n_unit_test.rb +3 -3
  61. data/test/unit/lexer_unit_test.rb +1 -1
  62. data/test/unit/module_ex_unit_test.rb +1 -1
  63. data/test/unit/parser_unit_test.rb +1 -1
  64. data/test/unit/regexp_unit_test.rb +1 -1
  65. data/test/unit/strainer_unit_test.rb +3 -2
  66. data/test/unit/tag_unit_test.rb +6 -1
  67. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  68. data/test/unit/tags/for_tag_unit_test.rb +1 -1
  69. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  70. data/test/unit/template_unit_test.rb +1 -1
  71. data/test/unit/tokenizer_unit_test.rb +10 -1
  72. data/test/unit/variable_unit_test.rb +49 -46
  73. metadata +71 -47
@@ -1,12 +1,60 @@
1
1
  module Liquid
2
- class Error < ::StandardError; end
2
+ class Error < ::StandardError
3
+ attr_accessor :line_number
4
+ attr_accessor :markup_context
5
+
6
+ def to_s(with_prefix=true)
7
+ str = ""
8
+ str << message_prefix if with_prefix
9
+ str << super()
10
+
11
+ if markup_context
12
+ str << " "
13
+ str << markup_context
14
+ end
15
+
16
+ str
17
+ end
18
+
19
+ def set_line_number_from_token(token)
20
+ return unless token.respond_to?(:line_number)
21
+ return if self.line_number
22
+ self.line_number = token.line_number
23
+ end
24
+
25
+ def self.render(e)
26
+ if e.is_a?(Liquid::Error)
27
+ e.to_s
28
+ else
29
+ "Liquid error: #{e.to_s}"
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def message_prefix
36
+ str = ""
37
+ if is_a?(SyntaxError)
38
+ str << "Liquid syntax error"
39
+ else
40
+ str << "Liquid error"
41
+ end
42
+
43
+ if line_number
44
+ str << " (line #{line_number})"
45
+ end
46
+
47
+ str << ": "
48
+ str
49
+ end
50
+ end
3
51
 
4
52
  class ArgumentError < Error; end
5
53
  class ContextError < Error; end
6
- class FilterNotFound < Error; end
7
54
  class FileSystemError < Error; end
8
55
  class StandardError < Error; end
9
56
  class SyntaxError < Error; end
10
57
  class StackLevelError < Error; end
58
+ class TaintedError < Error; end
11
59
  class MemoryError < Error; end
12
60
  end
@@ -0,0 +1,33 @@
1
+ module Liquid
2
+ class Expression
3
+ LITERALS = {
4
+ nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
5
+ 'true'.freeze => true,
6
+ 'false'.freeze => false,
7
+ 'blank'.freeze => :blank?,
8
+ 'empty'.freeze => :empty?
9
+ }
10
+
11
+ def self.parse(markup)
12
+ if LITERALS.key?(markup)
13
+ LITERALS[markup]
14
+ else
15
+ case markup
16
+ when /\A'(.*)'\z/m # Single quoted strings
17
+ $1
18
+ when /\A"(.*)"\z/m # Double quoted strings
19
+ $1
20
+ when /\A(-?\d+)\z/ # Integer and floats
21
+ $1.to_i
22
+ when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
23
+ RangeLookup.parse($1, $2)
24
+ when /\A(-?\d[\d\.]+)\z/ # Floats
25
+ $1.to_f
26
+ else
27
+ VariableLookup.parse(markup)
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ module ParserSwitching
3
+ def parse_with_selected_parser(markup)
4
+ case @options[:error_mode] || Template.error_mode
5
+ when :strict then strict_parse_with_error_context(markup)
6
+ when :lax then lax_parse(markup)
7
+ when :warn
8
+ begin
9
+ return strict_parse_with_error_context(markup)
10
+ rescue SyntaxError => e
11
+ e.set_line_number_from_token(markup)
12
+ @warnings ||= []
13
+ @warnings << e
14
+ return lax_parse(markup)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+ def strict_parse_with_error_context(markup)
21
+ strict_parse(markup)
22
+ rescue SyntaxError => e
23
+ e.markup_context = markup_context(markup)
24
+ raise e
25
+ end
26
+
27
+ def markup_context(markup)
28
+ "in \"#{markup.strip}\""
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,159 @@
1
+ module Liquid
2
+
3
+ # Profiler enables support for profiling template rendering to help track down performance issues.
4
+ #
5
+ # To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
6
+ # <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
7
+ # class via the <tt>Liquid::Template#profiler</tt> method.
8
+ #
9
+ # template = Liquid::Template.parse(template_content, profile: true)
10
+ # output = template.render
11
+ # profile = template.profiler
12
+ #
13
+ # This object contains all profiling information, containing information on what tags were rendered,
14
+ # where in the templates these tags live, and how long each tag took to render.
15
+ #
16
+ # This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
17
+ # inside of <tt>{% include %}</tt> tags.
18
+ #
19
+ # profile.each do |node|
20
+ # # Access to the token itself
21
+ # node.code
22
+ #
23
+ # # Which template and line number of this node.
24
+ # # If top level, this will be "<root>".
25
+ # node.partial
26
+ # node.line_number
27
+ #
28
+ # # Render time in seconds of this node
29
+ # node.render_time
30
+ #
31
+ # # If the template used {% include %}, this node will also have children.
32
+ # node.children.each do |child2|
33
+ # # ...
34
+ # end
35
+ # end
36
+ #
37
+ # Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
38
+ #
39
+ # All render times are in seconds. There is a small performance hit when profiling is enabled.
40
+ #
41
+ class Profiler
42
+ include Enumerable
43
+
44
+ class Timing
45
+ attr_reader :code, :partial, :line_number, :children
46
+
47
+ def initialize(token, partial)
48
+ @code = token.respond_to?(:raw) ? token.raw : token
49
+ @partial = partial
50
+ @line_number = token.respond_to?(:line_number) ? token.line_number : nil
51
+ @children = []
52
+ end
53
+
54
+ def self.start(token, partial)
55
+ new(token, partial).tap do |t|
56
+ t.start
57
+ end
58
+ end
59
+
60
+ def start
61
+ @start_time = Time.now
62
+ end
63
+
64
+ def finish
65
+ @end_time = Time.now
66
+ end
67
+
68
+ def render_time
69
+ @end_time - @start_time
70
+ end
71
+ end
72
+
73
+ def self.profile_token_render(token)
74
+ if Profiler.current_profile && token.respond_to?(:render)
75
+ Profiler.current_profile.start_token(token)
76
+ output = yield
77
+ Profiler.current_profile.end_token(token)
78
+ output
79
+ else
80
+ yield
81
+ end
82
+ end
83
+
84
+ def self.profile_children(template_name)
85
+ if Profiler.current_profile
86
+ Profiler.current_profile.push_partial(template_name)
87
+ output = yield
88
+ Profiler.current_profile.pop_partial
89
+ output
90
+ else
91
+ yield
92
+ end
93
+ end
94
+
95
+ def self.current_profile
96
+ Thread.current[:liquid_profiler]
97
+ end
98
+
99
+ def initialize
100
+ @partial_stack = ["<root>"]
101
+
102
+ @root_timing = Timing.new("", current_partial)
103
+ @timing_stack = [@root_timing]
104
+
105
+ @render_start_at = Time.now
106
+ @render_end_at = @render_start_at
107
+ end
108
+
109
+ def start
110
+ Thread.current[:liquid_profiler] = self
111
+ @render_start_at = Time.now
112
+ end
113
+
114
+ def stop
115
+ Thread.current[:liquid_profiler] = nil
116
+ @render_end_at = Time.now
117
+ end
118
+
119
+ def total_render_time
120
+ @render_end_at - @render_start_at
121
+ end
122
+
123
+ def each(&block)
124
+ @root_timing.children.each(&block)
125
+ end
126
+
127
+ def [](idx)
128
+ @root_timing.children[idx]
129
+ end
130
+
131
+ def length
132
+ @root_timing.children.length
133
+ end
134
+
135
+ def start_token(token)
136
+ @timing_stack.push(Timing.start(token, current_partial))
137
+ end
138
+
139
+ def end_token(token)
140
+ timing = @timing_stack.pop
141
+ timing.finish
142
+
143
+ @timing_stack.last.children << timing
144
+ end
145
+
146
+ def current_partial
147
+ @partial_stack.last
148
+ end
149
+
150
+ def push_partial(partial_name)
151
+ @partial_stack.push(partial_name)
152
+ end
153
+
154
+ def pop_partial
155
+ @partial_stack.pop
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,23 @@
1
+ module Liquid
2
+ class Block < Tag
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(@template_name) 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
@@ -34,14 +34,28 @@ module Liquid
34
34
  end
35
35
 
36
36
  def escape(input)
37
- CGI.escapeHTML(input) rescue input
37
+ CGI.escapeHTML(input).untaint rescue input
38
38
  end
39
+ alias_method :h, :escape
39
40
 
40
41
  def escape_once(input)
41
42
  input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
42
43
  end
43
44
 
44
- 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
45
59
 
46
60
  # Truncate a string down to x characters
47
61
  def truncate(input, length = 50, truncate_string = "...".freeze)
@@ -65,7 +79,7 @@ module Liquid
65
79
  # <div class="summary">{{ post | split '//' | first }}</div>
66
80
  #
67
81
  def split(input, pattern)
68
- input.split(pattern)
82
+ input.to_s.split(pattern)
69
83
  end
70
84
 
71
85
  def strip(input)
@@ -101,13 +115,24 @@ module Liquid
101
115
  ary = InputIterator.new(input)
102
116
  if property.nil?
103
117
  ary.sort
104
- elsif ary.first.respond_to?('[]'.freeze) && !ary.first[property].nil?
118
+ elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
105
119
  ary.sort {|a,b| a[property] <=> b[property] }
106
120
  elsif ary.first.respond_to?(property)
107
121
  ary.sort {|a,b| a.send(property) <=> b.send(property) }
108
122
  end
109
123
  end
110
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
+
111
136
  # Reverse the elements of an array
112
137
  def reverse(input)
113
138
  ary = InputIterator.new(input)
@@ -1,7 +1,8 @@
1
1
  module Liquid
2
2
  class Tag
3
- attr_accessor :options
3
+ attr_accessor :options, :line_number
4
4
  attr_reader :nodelist, :warnings
5
+ include ParserSwitching
5
6
 
6
7
  class << self
7
8
  def parse(tag_name, markup, tokens, options)
@@ -22,6 +23,10 @@ module Liquid
22
23
  def parse(tokens)
23
24
  end
24
25
 
26
+ def raw
27
+ "#{@tag_name} #{@markup}"
28
+ end
29
+
25
30
  def name
26
31
  self.class.name.downcase
27
32
  end
@@ -33,29 +38,5 @@ module Liquid
33
38
  def blank?
34
39
  false
35
40
  end
36
-
37
- def parse_with_selected_parser(markup)
38
- case @options[:error_mode] || Template.error_mode
39
- when :strict then strict_parse_with_error_context(markup)
40
- when :lax then lax_parse(markup)
41
- when :warn
42
- begin
43
- return strict_parse_with_error_context(markup)
44
- rescue SyntaxError => e
45
- @warnings ||= []
46
- @warnings << e
47
- return lax_parse(markup)
48
- end
49
- end
50
- end
51
-
52
- private
53
-
54
- def strict_parse_with_error_context(markup)
55
- strict_parse(markup)
56
- rescue SyntaxError => e
57
- e.message << " in \"#{markup.strip}\""
58
- raise e
59
- end
60
41
  end
61
42
  end