liquid 2.6.3 → 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 +46 -13
- data/README.md +27 -2
- data/lib/liquid/block.rb +85 -51
- data/lib/liquid/block_body.rb +123 -0
- data/lib/liquid/condition.rb +26 -15
- data/lib/liquid/context.rb +106 -140
- data/lib/liquid/document.rb +3 -3
- data/lib/liquid/drop.rb +17 -1
- data/lib/liquid/errors.rb +50 -2
- data/lib/liquid/expression.rb +33 -0
- data/lib/liquid/file_system.rb +17 -6
- data/lib/liquid/i18n.rb +39 -0
- data/lib/liquid/interrupts.rb +1 -1
- data/lib/liquid/lexer.rb +51 -0
- data/lib/liquid/locales/en.yml +22 -0
- data/lib/liquid/parser.rb +90 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/profiler.rb +159 -0
- data/lib/liquid/range_lookup.rb +22 -0
- data/lib/liquid/standardfilters.rb +143 -55
- data/lib/liquid/strainer.rb +14 -4
- data/lib/liquid/tag.rb +25 -9
- data/lib/liquid/tags/assign.rb +12 -9
- data/lib/liquid/tags/break.rb +1 -1
- data/lib/liquid/tags/capture.rb +10 -8
- data/lib/liquid/tags/case.rb +13 -13
- data/lib/liquid/tags/comment.rb +9 -2
- data/lib/liquid/tags/continue.rb +1 -4
- data/lib/liquid/tags/cycle.rb +5 -7
- data/lib/liquid/tags/decrement.rb +3 -4
- data/lib/liquid/tags/for.rb +69 -36
- data/lib/liquid/tags/if.rb +52 -25
- data/lib/liquid/tags/ifchanged.rb +3 -3
- data/lib/liquid/tags/include.rb +19 -8
- data/lib/liquid/tags/increment.rb +4 -8
- data/lib/liquid/tags/raw.rb +4 -7
- data/lib/liquid/tags/table_row.rb +73 -0
- data/lib/liquid/tags/unless.rb +2 -4
- data/lib/liquid/template.rb +124 -14
- data/lib/liquid/token.rb +18 -0
- data/lib/liquid/utils.rb +13 -4
- data/lib/liquid/variable.rb +103 -25
- data/lib/liquid/variable_lookup.rb +78 -0
- data/lib/liquid/version.rb +1 -1
- data/lib/liquid.rb +19 -11
- data/test/fixtures/en_locale.yml +9 -0
- data/test/{liquid → integration}/assign_test.rb +18 -1
- data/test/integration/blank_test.rb +106 -0
- data/test/{liquid → integration}/capture_test.rb +3 -3
- data/test/integration/context_test.rb +32 -0
- data/test/integration/drop_test.rb +271 -0
- data/test/integration/error_handling_test.rb +207 -0
- data/test/{liquid → integration}/filter_test.rb +11 -11
- data/test/integration/hash_ordering_test.rb +23 -0
- data/test/{liquid → integration}/output_test.rb +13 -13
- data/test/integration/parsing_quirks_test.rb +116 -0
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/{liquid → integration}/security_test.rb +10 -10
- data/test/{liquid → integration}/standard_filter_test.rb +148 -32
- data/test/{liquid → integration}/tags/break_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/for_tag_test.rb +80 -2
- data/test/{liquid → integration}/tags/if_else_tag_test.rb +24 -21
- data/test/integration/tags/include_tag_test.rb +234 -0
- data/test/{liquid → integration}/tags/increment_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/raw_tag_test.rb +2 -1
- data/test/{liquid → integration}/tags/standard_tag_test.rb +28 -26
- data/test/integration/tags/statements_test.rb +113 -0
- data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
- data/test/{liquid → integration}/tags/unless_else_tag_test.rb +1 -1
- data/test/{liquid → integration}/template_test.rb +81 -45
- data/test/integration/variable_test.rb +82 -0
- data/test/test_helper.rb +73 -20
- data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +2 -5
- data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +23 -1
- data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +39 -25
- data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +11 -5
- data/test/unit/i18n_unit_test.rb +37 -0
- data/test/unit/lexer_unit_test.rb +48 -0
- data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
- data/test/unit/parser_unit_test.rb +82 -0
- data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
- data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +20 -1
- data/test/unit/tag_unit_test.rb +16 -0
- data/test/unit/tags/case_tag_unit_test.rb +10 -0
- data/test/unit/tags/for_tag_unit_test.rb +13 -0
- data/test/unit/tags/if_tag_unit_test.rb +8 -0
- data/test/unit/template_unit_test.rb +69 -0
- data/test/unit/tokenizer_unit_test.rb +38 -0
- data/test/unit/variable_unit_test.rb +139 -0
- metadata +135 -67
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/liquid/htmltags.rb +0 -73
- data/test/liquid/drop_test.rb +0 -180
- data/test/liquid/error_handling_test.rb +0 -81
- data/test/liquid/hash_ordering_test.rb +0 -25
- data/test/liquid/parsing_quirks_test.rb +0 -52
- data/test/liquid/tags/include_tag_test.rb +0 -166
- data/test/liquid/tags/statements_test.rb +0 -134
- data/test/liquid/variable_test.rb +0 -186
data/lib/liquid/context.rb
CHANGED
@@ -14,17 +14,35 @@ module Liquid
|
|
14
14
|
# context['bob'] #=> nil class Context
|
15
15
|
class Context
|
16
16
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@resource_limits
|
17
|
+
attr_accessor :exception_handler
|
18
|
+
|
19
|
+
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
20
|
+
@environments = [environments].flatten
|
21
|
+
@scopes = [(outer_scope || {})]
|
22
|
+
@registers = registers
|
23
|
+
@errors = []
|
24
|
+
@resource_limits = resource_limits || Template.default_resource_limits.dup
|
25
|
+
@resource_limits[:render_score_current] = 0
|
26
|
+
@resource_limits[:assign_score_current] = 0
|
27
|
+
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
|
25
28
|
squash_instance_assigns_with_environments
|
26
29
|
|
30
|
+
@this_stack_used = false
|
31
|
+
|
32
|
+
if rethrow_errors
|
33
|
+
self.exception_handler = ->(e) { true }
|
34
|
+
end
|
35
|
+
|
27
36
|
@interrupts = []
|
37
|
+
@filters = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def increment_used_resources(key, obj)
|
41
|
+
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
|
42
|
+
obj.length
|
43
|
+
else
|
44
|
+
1
|
45
|
+
end
|
28
46
|
end
|
29
47
|
|
30
48
|
def resource_limits_reached?
|
@@ -34,7 +52,7 @@ module Liquid
|
|
34
52
|
end
|
35
53
|
|
36
54
|
def strainer
|
37
|
-
@strainer ||= Strainer.create(self)
|
55
|
+
@strainer ||= Strainer.create(self, @filters)
|
38
56
|
end
|
39
57
|
|
40
58
|
# Adds filters to this context.
|
@@ -43,17 +61,26 @@ module Liquid
|
|
43
61
|
# for that
|
44
62
|
def add_filters(filters)
|
45
63
|
filters = [filters].flatten.compact
|
46
|
-
|
47
64
|
filters.each do |f|
|
48
65
|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
49
66
|
Strainer.add_known_filter(f)
|
50
|
-
|
67
|
+
end
|
68
|
+
|
69
|
+
# If strainer is already setup then there's no choice but to use a runtime
|
70
|
+
# extend call. If strainer is not yet created, we can utilize strainers
|
71
|
+
# cached class based API, which avoids busting the method cache.
|
72
|
+
if @strainer
|
73
|
+
filters.each do |f|
|
74
|
+
strainer.extend(f)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
@filters.concat filters
|
51
78
|
end
|
52
79
|
end
|
53
80
|
|
54
81
|
# are there any not handled interrupts?
|
55
82
|
def has_interrupt?
|
56
|
-
|
83
|
+
!@interrupts.empty?
|
57
84
|
end
|
58
85
|
|
59
86
|
# push an interrupt to the stack. this interrupt is considered not handled.
|
@@ -66,26 +93,25 @@ module Liquid
|
|
66
93
|
@interrupts.pop
|
67
94
|
end
|
68
95
|
|
69
|
-
def handle_error(e)
|
70
|
-
errors.push(e)
|
71
|
-
raise if @rethrow_errors
|
72
96
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
else
|
77
|
-
"Liquid error: #{e.message}"
|
97
|
+
def handle_error(e, token=nil)
|
98
|
+
if e.is_a?(Liquid::Error)
|
99
|
+
e.set_line_number_from_token(token)
|
78
100
|
end
|
101
|
+
|
102
|
+
errors.push(e)
|
103
|
+
raise if exception_handler && exception_handler.call(e)
|
104
|
+
Liquid::Error.render(e)
|
79
105
|
end
|
80
106
|
|
81
107
|
def invoke(method, *args)
|
82
|
-
strainer.invoke(method, *args)
|
108
|
+
strainer.invoke(method, *args).to_liquid
|
83
109
|
end
|
84
110
|
|
85
111
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
86
112
|
def push(new_scope={})
|
87
113
|
@scopes.unshift(new_scope)
|
88
|
-
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
|
114
|
+
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
|
89
115
|
end
|
90
116
|
|
91
117
|
# Merge a hash of variables in the current local scope
|
@@ -107,11 +133,19 @@ module Liquid
|
|
107
133
|
# end
|
108
134
|
#
|
109
135
|
# context['var] #=> nil
|
110
|
-
def stack(new_scope=
|
111
|
-
|
136
|
+
def stack(new_scope=nil)
|
137
|
+
old_stack_used = @this_stack_used
|
138
|
+
if new_scope
|
139
|
+
push(new_scope)
|
140
|
+
@this_stack_used = true
|
141
|
+
else
|
142
|
+
@this_stack_used = false
|
143
|
+
end
|
144
|
+
|
112
145
|
yield
|
113
146
|
ensure
|
114
|
-
pop
|
147
|
+
pop if @this_stack_used
|
148
|
+
@this_stack_used = old_stack_used
|
115
149
|
end
|
116
150
|
|
117
151
|
def clear_instance_assigns
|
@@ -120,138 +154,71 @@ module Liquid
|
|
120
154
|
|
121
155
|
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
122
156
|
def []=(key, value)
|
157
|
+
unless @this_stack_used
|
158
|
+
@this_stack_used = true
|
159
|
+
push({})
|
160
|
+
end
|
123
161
|
@scopes[0][key] = value
|
124
162
|
end
|
125
163
|
|
126
|
-
|
127
|
-
|
164
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
165
|
+
# Strings, digits, floats and booleans (true,false).
|
166
|
+
# If no match is made we lookup the variable in the current scope and
|
167
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
168
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
169
|
+
#
|
170
|
+
# Example:
|
171
|
+
# products == empty #=> products.empty?
|
172
|
+
def [](expression)
|
173
|
+
evaluate(@parsed_expression[expression])
|
128
174
|
end
|
129
175
|
|
130
176
|
def has_key?(key)
|
131
|
-
|
177
|
+
self[key] != nil
|
132
178
|
end
|
133
179
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
'true' => true,
|
138
|
-
'false' => false,
|
139
|
-
'blank' => :blank?,
|
140
|
-
'empty' => :empty?
|
141
|
-
}
|
142
|
-
|
143
|
-
# Look up variable, either resolve directly after considering the name. We can directly handle
|
144
|
-
# Strings, digits, floats and booleans (true,false).
|
145
|
-
# If no match is made we lookup the variable in the current scope and
|
146
|
-
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
147
|
-
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
148
|
-
#
|
149
|
-
# Example:
|
150
|
-
# products == empty #=> products.empty?
|
151
|
-
def resolve(key)
|
152
|
-
if LITERALS.key?(key)
|
153
|
-
LITERALS[key]
|
154
|
-
else
|
155
|
-
case key
|
156
|
-
when /^'(.*)'$/ # Single quoted strings
|
157
|
-
$1
|
158
|
-
when /^"(.*)"$/ # Double quoted strings
|
159
|
-
$1
|
160
|
-
when /^(-?\d+)$/ # Integer and floats
|
161
|
-
$1.to_i
|
162
|
-
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
|
163
|
-
(resolve($1).to_i..resolve($2).to_i)
|
164
|
-
when /^(-?\d[\d\.]+)$/ # Floats
|
165
|
-
$1.to_f
|
166
|
-
else
|
167
|
-
variable(key)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Fetches an object starting at the local scope and then moving up the hierachy
|
173
|
-
def find_variable(key)
|
174
|
-
scope = @scopes.find { |s| s.has_key?(key) }
|
175
|
-
variable = nil
|
176
|
-
|
177
|
-
if scope.nil?
|
178
|
-
@environments.each do |e|
|
179
|
-
if variable = lookup_and_evaluate(e, key)
|
180
|
-
scope = e
|
181
|
-
break
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
scope ||= @environments.last || @scopes.last
|
187
|
-
variable ||= lookup_and_evaluate(scope, key)
|
188
|
-
|
189
|
-
variable = variable.to_liquid
|
190
|
-
variable.context = self if variable.respond_to?(:context=)
|
180
|
+
def evaluate(object)
|
181
|
+
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
182
|
+
end
|
191
183
|
|
192
|
-
|
193
|
-
|
184
|
+
# Fetches an object starting at the local scope and then moving up the hierachy
|
185
|
+
def find_variable(key)
|
194
186
|
|
195
|
-
#
|
196
|
-
#
|
197
|
-
|
198
|
-
|
199
|
-
# assert_equal 'tobi', @context['hash.name']
|
200
|
-
# assert_equal 'tobi', @context['hash["name"]']
|
201
|
-
def variable(markup)
|
202
|
-
parts = markup.scan(VariableParser)
|
203
|
-
square_bracketed = /^\[(.*)\]$/
|
187
|
+
# This was changed from find() to find_index() because this is a very hot
|
188
|
+
# path and find_index() is optimized in MRI to reduce object allocation
|
189
|
+
index = @scopes.find_index { |s| s.has_key?(key) }
|
190
|
+
scope = @scopes[index] if index
|
204
191
|
|
205
|
-
|
192
|
+
variable = nil
|
206
193
|
|
207
|
-
|
208
|
-
|
194
|
+
if scope.nil?
|
195
|
+
@environments.each do |e|
|
196
|
+
variable = lookup_and_evaluate(e, key)
|
197
|
+
unless variable.nil?
|
198
|
+
scope = e
|
199
|
+
break
|
200
|
+
end
|
209
201
|
end
|
202
|
+
end
|
210
203
|
|
211
|
-
|
212
|
-
|
213
|
-
parts.each do |part|
|
214
|
-
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
215
|
-
|
216
|
-
# If object is a hash- or array-like object we look for the
|
217
|
-
# presence of the key and if its available we return it
|
218
|
-
if object.respond_to?(:[]) and
|
219
|
-
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
220
|
-
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
221
|
-
|
222
|
-
# if its a proc we will replace the entry with the proc
|
223
|
-
res = lookup_and_evaluate(object, part)
|
224
|
-
object = res.to_liquid
|
225
|
-
|
226
|
-
# Some special cases. If the part wasn't in square brackets and
|
227
|
-
# no key with the same name was found we interpret following calls
|
228
|
-
# as commands and call them on the current object
|
229
|
-
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
|
230
|
-
|
231
|
-
object = object.send(part.intern).to_liquid
|
204
|
+
scope ||= @environments.last || @scopes.last
|
205
|
+
variable ||= lookup_and_evaluate(scope, key)
|
232
206
|
|
233
|
-
|
234
|
-
|
235
|
-
else
|
236
|
-
return nil
|
237
|
-
end
|
207
|
+
variable = variable.to_liquid
|
208
|
+
variable.context = self if variable.respond_to?(:context=)
|
238
209
|
|
239
|
-
|
240
|
-
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
object
|
245
|
-
end # variable
|
210
|
+
return variable
|
211
|
+
end
|
246
212
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
213
|
+
def lookup_and_evaluate(obj, key)
|
214
|
+
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
215
|
+
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
216
|
+
else
|
217
|
+
value
|
218
|
+
end
|
219
|
+
end
|
254
220
|
|
221
|
+
private
|
255
222
|
def squash_instance_assigns_with_environments
|
256
223
|
@scopes.last.each_key do |k|
|
257
224
|
@environments.each do |env|
|
@@ -263,5 +230,4 @@ module Liquid
|
|
263
230
|
end
|
264
231
|
end # squash_instance_assigns_with_environments
|
265
232
|
end # Context
|
266
|
-
|
267
233
|
end # Liquid
|
data/lib/liquid/document.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Liquid
|
2
2
|
class Document < Block
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
def self.parse(tokens, options={})
|
4
|
+
# we don't need markup to open this block
|
5
|
+
super(nil, nil, tokens, options)
|
6
6
|
end
|
7
7
|
|
8
8
|
# There isn't a real delimiter
|
data/lib/liquid/drop.rb
CHANGED
@@ -44,17 +44,33 @@ module Liquid
|
|
44
44
|
true
|
45
45
|
end
|
46
46
|
|
47
|
+
def inspect
|
48
|
+
self.class.to_s
|
49
|
+
end
|
50
|
+
|
47
51
|
def to_liquid
|
48
52
|
self
|
49
53
|
end
|
50
54
|
|
55
|
+
def to_s
|
56
|
+
self.class.name
|
57
|
+
end
|
58
|
+
|
51
59
|
alias :[] :invoke_drop
|
52
60
|
|
53
61
|
private
|
54
62
|
|
55
63
|
# Check for method existence without invoking respond_to?, which creates symbols
|
56
64
|
def self.invokable?(method_name)
|
57
|
-
@invokable_methods
|
65
|
+
unless @invokable_methods
|
66
|
+
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
67
|
+
if include?(Enumerable)
|
68
|
+
blacklist += Enumerable.public_instance_methods
|
69
|
+
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
70
|
+
end
|
71
|
+
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
72
|
+
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
73
|
+
end
|
58
74
|
@invokable_methods.include?(method_name.to_s)
|
59
75
|
end
|
60
76
|
end
|
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
|
data/lib/liquid/file_system.rb
CHANGED
@@ -31,11 +31,22 @@ module Liquid
|
|
31
31
|
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
32
32
|
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
33
33
|
#
|
34
|
+
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
35
|
+
# The Kernel::sprintf format specification is used.
|
36
|
+
# Default pattern is "_%s.liquid".
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
#
|
40
|
+
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
41
|
+
#
|
42
|
+
# file_system.full_path("index") # => "/some/path/index.html"
|
43
|
+
#
|
34
44
|
class LocalFileSystem
|
35
45
|
attr_accessor :root
|
36
46
|
|
37
|
-
def initialize(root)
|
47
|
+
def initialize(root, pattern = "_%s.liquid".freeze)
|
38
48
|
@root = root
|
49
|
+
@pattern = pattern
|
39
50
|
end
|
40
51
|
|
41
52
|
def read_template_file(template_path, context)
|
@@ -46,15 +57,15 @@ module Liquid
|
|
46
57
|
end
|
47
58
|
|
48
59
|
def full_path(template_path)
|
49
|
-
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~
|
60
|
+
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
|
50
61
|
|
51
|
-
full_path = if template_path.include?('/')
|
52
|
-
File.join(root, File.dirname(template_path),
|
62
|
+
full_path = if template_path.include?('/'.freeze)
|
63
|
+
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
53
64
|
else
|
54
|
-
File.join(root,
|
65
|
+
File.join(root, @pattern % template_path)
|
55
66
|
end
|
56
67
|
|
57
|
-
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~
|
68
|
+
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
|
58
69
|
|
59
70
|
full_path
|
60
71
|
end
|
data/lib/liquid/i18n.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class I18n
|
5
|
+
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
|
6
|
+
|
7
|
+
class TranslationError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
def initialize(path = DEFAULT_LOCALE)
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
|
16
|
+
def translate(name, vars = {})
|
17
|
+
interpolate(deep_fetch_translation(name), vars)
|
18
|
+
end
|
19
|
+
alias_method :t, :translate
|
20
|
+
|
21
|
+
def locale
|
22
|
+
@locale ||= YAML.load_file(@path)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def interpolate(name, vars)
|
27
|
+
name.gsub(/%\{(\w+)\}/) {
|
28
|
+
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
29
|
+
"#{vars[$1.to_sym]}"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def deep_fetch_translation(name)
|
34
|
+
name.split('.'.freeze).reduce(locale) do |level, cur|
|
35
|
+
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/liquid/interrupts.rb
CHANGED
data/lib/liquid/lexer.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "strscan"
|
2
|
+
module Liquid
|
3
|
+
class Lexer
|
4
|
+
SPECIALS = {
|
5
|
+
'|'.freeze => :pipe,
|
6
|
+
'.'.freeze => :dot,
|
7
|
+
':'.freeze => :colon,
|
8
|
+
','.freeze => :comma,
|
9
|
+
'['.freeze => :open_square,
|
10
|
+
']'.freeze => :close_square,
|
11
|
+
'('.freeze => :open_round,
|
12
|
+
')'.freeze => :close_round
|
13
|
+
}
|
14
|
+
IDENTIFIER = /[\w\-?!]+/
|
15
|
+
SINGLE_STRING_LITERAL = /'[^\']*'/
|
16
|
+
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
17
|
+
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
18
|
+
DOTDOT = /\.\./
|
19
|
+
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
20
|
+
|
21
|
+
def initialize(input)
|
22
|
+
@ss = StringScanner.new(input.rstrip)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tokenize
|
26
|
+
@output = []
|
27
|
+
|
28
|
+
while !@ss.eos?
|
29
|
+
@ss.skip(/\s*/)
|
30
|
+
tok = case
|
31
|
+
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
32
|
+
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
33
|
+
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
34
|
+
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
35
|
+
when t = @ss.scan(IDENTIFIER) then [:id, t]
|
36
|
+
when t = @ss.scan(DOTDOT) then [:dotdot, t]
|
37
|
+
else
|
38
|
+
c = @ss.getch
|
39
|
+
if s = SPECIALS[c]
|
40
|
+
[s,c]
|
41
|
+
else
|
42
|
+
raise SyntaxError, "Unexpected character #{c}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@output << tok
|
46
|
+
end
|
47
|
+
|
48
|
+
@output << [:end_of_string]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
---
|
2
|
+
errors:
|
3
|
+
syntax:
|
4
|
+
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
5
|
+
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
6
|
+
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
7
|
+
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
|
8
|
+
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
9
|
+
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
10
|
+
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
11
|
+
for_invalid_in: "For loops require an 'in' clause"
|
12
|
+
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
13
|
+
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
14
|
+
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
15
|
+
unknown_tag: "Unknown tag '%{tag}'"
|
16
|
+
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
17
|
+
unexpected_else: "%{block_name} tag does not expect else tag"
|
18
|
+
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
19
|
+
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
20
|
+
tag_never_closed: "'%{block_name}' tag was never closed"
|
21
|
+
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
22
|
+
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|