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.
- checksums.yaml +4 -4
- data/History.md +4 -0
- data/README.md +2 -2
- data/lib/liquid.rb +8 -0
- data/lib/liquid/block.rb +50 -46
- data/lib/liquid/block_body.rb +123 -0
- data/lib/liquid/condition.rb +12 -5
- data/lib/liquid/context.rb +75 -148
- data/lib/liquid/errors.rb +50 -2
- data/lib/liquid/expression.rb +33 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler.rb +159 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/range_lookup.rb +22 -0
- data/lib/liquid/standardfilters.rb +29 -4
- data/lib/liquid/tag.rb +6 -25
- data/lib/liquid/tags/assign.rb +2 -1
- data/lib/liquid/tags/case.rb +1 -1
- data/lib/liquid/tags/if.rb +5 -5
- data/lib/liquid/tags/ifchanged.rb +1 -1
- data/lib/liquid/tags/include.rb +11 -1
- data/lib/liquid/tags/raw.rb +1 -4
- data/lib/liquid/tags/table_row.rb +1 -1
- data/lib/liquid/template.rb +55 -4
- data/lib/liquid/token.rb +18 -0
- data/lib/liquid/variable.rb +68 -41
- data/lib/liquid/variable_lookup.rb +78 -0
- data/lib/liquid/version.rb +1 -1
- data/test/integration/assign_test.rb +12 -1
- data/test/integration/blank_test.rb +1 -1
- data/test/integration/capture_test.rb +1 -1
- data/test/integration/context_test.rb +10 -11
- data/test/integration/drop_test.rb +29 -3
- data/test/integration/error_handling_test.rb +138 -41
- data/test/integration/filter_test.rb +7 -7
- data/test/integration/hash_ordering_test.rb +6 -8
- data/test/integration/output_test.rb +1 -1
- data/test/integration/parsing_quirks_test.rb +40 -18
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/integration/security_test.rb +1 -1
- data/test/integration/standard_filter_test.rb +47 -1
- data/test/integration/tags/break_tag_test.rb +1 -1
- data/test/integration/tags/continue_tag_test.rb +1 -1
- data/test/integration/tags/for_tag_test.rb +2 -2
- data/test/integration/tags/if_else_tag_test.rb +23 -20
- data/test/integration/tags/include_tag_test.rb +24 -2
- data/test/integration/tags/increment_tag_test.rb +1 -1
- data/test/integration/tags/raw_tag_test.rb +1 -1
- data/test/integration/tags/standard_tag_test.rb +4 -4
- data/test/integration/tags/statements_test.rb +1 -1
- data/test/integration/tags/table_row_test.rb +1 -1
- data/test/integration/tags/unless_else_tag_test.rb +1 -1
- data/test/integration/template_test.rb +16 -4
- data/test/integration/variable_test.rb +11 -1
- data/test/test_helper.rb +59 -31
- data/test/unit/block_unit_test.rb +2 -5
- data/test/unit/condition_unit_test.rb +5 -1
- data/test/unit/context_unit_test.rb +13 -7
- data/test/unit/file_system_unit_test.rb +5 -5
- data/test/unit/i18n_unit_test.rb +3 -3
- data/test/unit/lexer_unit_test.rb +1 -1
- data/test/unit/module_ex_unit_test.rb +1 -1
- data/test/unit/parser_unit_test.rb +1 -1
- data/test/unit/regexp_unit_test.rb +1 -1
- data/test/unit/strainer_unit_test.rb +3 -2
- data/test/unit/tag_unit_test.rb +6 -1
- data/test/unit/tags/case_tag_unit_test.rb +1 -1
- data/test/unit/tags/for_tag_unit_test.rb +1 -1
- data/test/unit/tags/if_tag_unit_test.rb +1 -1
- data/test/unit/template_unit_test.rb +1 -1
- data/test/unit/tokenizer_unit_test.rb +10 -1
- data/test/unit/variable_unit_test.rb +49 -46
- metadata +71 -47
data/lib/liquid/errors.rb
CHANGED
@@ -1,12 +1,60 @@
|
|
1
1
|
module Liquid
|
2
|
-
class Error < ::StandardError
|
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
|
-
|
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?(
|
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)
|
data/lib/liquid/tag.rb
CHANGED
@@ -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
|