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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +46 -13
  3. data/README.md +27 -2
  4. data/lib/liquid/block.rb +85 -51
  5. data/lib/liquid/block_body.rb +123 -0
  6. data/lib/liquid/condition.rb +26 -15
  7. data/lib/liquid/context.rb +106 -140
  8. data/lib/liquid/document.rb +3 -3
  9. data/lib/liquid/drop.rb +17 -1
  10. data/lib/liquid/errors.rb +50 -2
  11. data/lib/liquid/expression.rb +33 -0
  12. data/lib/liquid/file_system.rb +17 -6
  13. data/lib/liquid/i18n.rb +39 -0
  14. data/lib/liquid/interrupts.rb +1 -1
  15. data/lib/liquid/lexer.rb +51 -0
  16. data/lib/liquid/locales/en.yml +22 -0
  17. data/lib/liquid/parser.rb +90 -0
  18. data/lib/liquid/parser_switching.rb +31 -0
  19. data/lib/liquid/profiler/hooks.rb +23 -0
  20. data/lib/liquid/profiler.rb +159 -0
  21. data/lib/liquid/range_lookup.rb +22 -0
  22. data/lib/liquid/standardfilters.rb +143 -55
  23. data/lib/liquid/strainer.rb +14 -4
  24. data/lib/liquid/tag.rb +25 -9
  25. data/lib/liquid/tags/assign.rb +12 -9
  26. data/lib/liquid/tags/break.rb +1 -1
  27. data/lib/liquid/tags/capture.rb +10 -8
  28. data/lib/liquid/tags/case.rb +13 -13
  29. data/lib/liquid/tags/comment.rb +9 -2
  30. data/lib/liquid/tags/continue.rb +1 -4
  31. data/lib/liquid/tags/cycle.rb +5 -7
  32. data/lib/liquid/tags/decrement.rb +3 -4
  33. data/lib/liquid/tags/for.rb +69 -36
  34. data/lib/liquid/tags/if.rb +52 -25
  35. data/lib/liquid/tags/ifchanged.rb +3 -3
  36. data/lib/liquid/tags/include.rb +19 -8
  37. data/lib/liquid/tags/increment.rb +4 -8
  38. data/lib/liquid/tags/raw.rb +4 -7
  39. data/lib/liquid/tags/table_row.rb +73 -0
  40. data/lib/liquid/tags/unless.rb +2 -4
  41. data/lib/liquid/template.rb +124 -14
  42. data/lib/liquid/token.rb +18 -0
  43. data/lib/liquid/utils.rb +13 -4
  44. data/lib/liquid/variable.rb +103 -25
  45. data/lib/liquid/variable_lookup.rb +78 -0
  46. data/lib/liquid/version.rb +1 -1
  47. data/lib/liquid.rb +19 -11
  48. data/test/fixtures/en_locale.yml +9 -0
  49. data/test/{liquid → integration}/assign_test.rb +18 -1
  50. data/test/integration/blank_test.rb +106 -0
  51. data/test/{liquid → integration}/capture_test.rb +3 -3
  52. data/test/integration/context_test.rb +32 -0
  53. data/test/integration/drop_test.rb +271 -0
  54. data/test/integration/error_handling_test.rb +207 -0
  55. data/test/{liquid → integration}/filter_test.rb +11 -11
  56. data/test/integration/hash_ordering_test.rb +23 -0
  57. data/test/{liquid → integration}/output_test.rb +13 -13
  58. data/test/integration/parsing_quirks_test.rb +116 -0
  59. data/test/integration/render_profiling_test.rb +154 -0
  60. data/test/{liquid → integration}/security_test.rb +10 -10
  61. data/test/{liquid → integration}/standard_filter_test.rb +148 -32
  62. data/test/{liquid → integration}/tags/break_tag_test.rb +1 -1
  63. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -1
  64. data/test/{liquid → integration}/tags/for_tag_test.rb +80 -2
  65. data/test/{liquid → integration}/tags/if_else_tag_test.rb +24 -21
  66. data/test/integration/tags/include_tag_test.rb +234 -0
  67. data/test/{liquid → integration}/tags/increment_tag_test.rb +1 -1
  68. data/test/{liquid → integration}/tags/raw_tag_test.rb +2 -1
  69. data/test/{liquid → integration}/tags/standard_tag_test.rb +28 -26
  70. data/test/integration/tags/statements_test.rb +113 -0
  71. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
  72. data/test/{liquid → integration}/tags/unless_else_tag_test.rb +1 -1
  73. data/test/{liquid → integration}/template_test.rb +81 -45
  74. data/test/integration/variable_test.rb +82 -0
  75. data/test/test_helper.rb +73 -20
  76. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +2 -5
  77. data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +23 -1
  78. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +39 -25
  79. data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +11 -5
  80. data/test/unit/i18n_unit_test.rb +37 -0
  81. data/test/unit/lexer_unit_test.rb +48 -0
  82. data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
  83. data/test/unit/parser_unit_test.rb +82 -0
  84. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
  85. data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +20 -1
  86. data/test/unit/tag_unit_test.rb +16 -0
  87. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  88. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  89. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  90. data/test/unit/template_unit_test.rb +69 -0
  91. data/test/unit/tokenizer_unit_test.rb +38 -0
  92. data/test/unit/variable_unit_test.rb +139 -0
  93. metadata +135 -67
  94. data/lib/extras/liquid_view.rb +0 -51
  95. data/lib/liquid/htmltags.rb +0 -73
  96. data/test/liquid/drop_test.rb +0 -180
  97. data/test/liquid/error_handling_test.rb +0 -81
  98. data/test/liquid/hash_ordering_test.rb +0 -25
  99. data/test/liquid/parsing_quirks_test.rb +0 -52
  100. data/test/liquid/tags/include_tag_test.rb +0 -166
  101. data/test/liquid/tags/statements_test.rb +0 -134
  102. data/test/liquid/variable_test.rb +0 -186
@@ -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
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
19
- @environments = [environments].flatten
20
- @scopes = [(outer_scope || {})]
21
- @registers = registers
22
- @errors = []
23
- @rethrow_errors = rethrow_errors
24
- @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
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
- strainer.extend(f)
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
- @interrupts.any?
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
- case e
74
- when SyntaxError
75
- "Liquid syntax error: #{e.message}"
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
- push(new_scope)
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
- def [](key)
127
- resolve(key)
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
- resolve(key) != nil
177
+ self[key] != nil
132
178
  end
133
179
 
134
- private
135
- LITERALS = {
136
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
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
- return variable
193
- end
184
+ # Fetches an object starting at the local scope and then moving up the hierachy
185
+ def find_variable(key)
194
186
 
195
- # Resolves namespaced queries gracefully.
196
- #
197
- # Example
198
- # @context['hash'] = {"name" => 'tobi'}
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
- first_part = parts.shift
192
+ variable = nil
206
193
 
207
- if first_part =~ square_bracketed
208
- first_part = resolve($1)
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
- if object = find_variable(first_part)
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
- # No key was present with the desired value and it wasn't one of the directly supported
234
- # keywords either. The only thing we got left is to return nil
235
- else
236
- return nil
237
- end
207
+ variable = variable.to_liquid
208
+ variable.context = self if variable.respond_to?(:context=)
238
209
 
239
- # If we are dealing with a drop here we have to
240
- object.context = self if object.respond_to?(:context=)
241
- end
242
- end
243
-
244
- object
245
- end # variable
210
+ return variable
211
+ end
246
212
 
247
- def lookup_and_evaluate(obj, key)
248
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
249
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
250
- else
251
- value
252
- end
253
- end # lookup_and_evaluate
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
@@ -1,8 +1,8 @@
1
1
  module Liquid
2
2
  class Document < Block
3
- # we don't need markup to open this block
4
- def initialize(tokens)
5
- parse(tokens)
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 ||= Set.new(["to_liquid"] + (public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
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; 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
@@ -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 =~ /^[^.\/][a-zA-Z0-9_\/]+$/
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), "_#{File.basename(template_path)}.liquid")
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, "_#{template_path}.liquid")
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) =~ /^#{File.expand_path(root)}/
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
@@ -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
@@ -5,7 +5,7 @@ module Liquid
5
5
  attr_reader :message
6
6
 
7
7
  def initialize(message=nil)
8
- @message = message || "interrupt"
8
+ @message = message || "interrupt".freeze
9
9
  end
10
10
  end
11
11
 
@@ -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"