liquid 3.0.0.rc1 → 3.0.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.
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