liquid 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGELOG +38 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest.txt +60 -0
  4. data/README +38 -0
  5. data/Rakefile +24 -0
  6. data/example/server/example_servlet.rb +37 -0
  7. data/example/server/liquid_servlet.rb +28 -0
  8. data/example/server/server.rb +12 -0
  9. data/example/server/templates/index.liquid +6 -0
  10. data/example/server/templates/products.liquid +45 -0
  11. data/init.rb +6 -0
  12. data/lib/extras/liquid_view.rb +27 -0
  13. data/lib/liquid.rb +66 -0
  14. data/lib/liquid/block.rb +101 -0
  15. data/lib/liquid/condition.rb +91 -0
  16. data/lib/liquid/context.rb +216 -0
  17. data/lib/liquid/document.rb +17 -0
  18. data/lib/liquid/drop.rb +48 -0
  19. data/lib/liquid/errors.rb +7 -0
  20. data/lib/liquid/extensions.rb +56 -0
  21. data/lib/liquid/file_system.rb +62 -0
  22. data/lib/liquid/htmltags.rb +64 -0
  23. data/lib/liquid/standardfilters.rb +125 -0
  24. data/lib/liquid/strainer.rb +43 -0
  25. data/lib/liquid/tag.rb +25 -0
  26. data/lib/liquid/tags/assign.rb +22 -0
  27. data/lib/liquid/tags/capture.rb +22 -0
  28. data/lib/liquid/tags/case.rb +68 -0
  29. data/lib/liquid/tags/comment.rb +9 -0
  30. data/lib/liquid/tags/cycle.rb +46 -0
  31. data/lib/liquid/tags/for.rb +81 -0
  32. data/lib/liquid/tags/if.rb +51 -0
  33. data/lib/liquid/tags/ifchanged.rb +20 -0
  34. data/lib/liquid/tags/include.rb +56 -0
  35. data/lib/liquid/tags/unless.rb +29 -0
  36. data/lib/liquid/template.rb +150 -0
  37. data/lib/liquid/variable.rb +39 -0
  38. data/test/block_test.rb +50 -0
  39. data/test/context_test.rb +340 -0
  40. data/test/drop_test.rb +139 -0
  41. data/test/error_handling_test.rb +65 -0
  42. data/test/extra/breakpoint.rb +547 -0
  43. data/test/extra/caller.rb +80 -0
  44. data/test/file_system_test.rb +30 -0
  45. data/test/filter_test.rb +98 -0
  46. data/test/helper.rb +20 -0
  47. data/test/html_tag_test.rb +24 -0
  48. data/test/if_else_test.rb +95 -0
  49. data/test/include_tag_test.rb +91 -0
  50. data/test/output_test.rb +121 -0
  51. data/test/parsing_quirks_test.rb +14 -0
  52. data/test/regexp_test.rb +39 -0
  53. data/test/security_test.rb +41 -0
  54. data/test/standard_filter_test.rb +101 -0
  55. data/test/standard_tag_test.rb +336 -0
  56. data/test/statements_test.rb +137 -0
  57. data/test/strainer_test.rb +16 -0
  58. data/test/template_test.rb +26 -0
  59. data/test/unless_else_test.rb +19 -0
  60. data/test/variable_test.rb +135 -0
  61. metadata +114 -0
@@ -0,0 +1,91 @@
1
+ module Liquid
2
+ # Container for liquid nodes which conveniently wrapps decision making logic
3
+ #
4
+ # Example:
5
+ #
6
+ # c = Condition.new('1', '==', '1')
7
+ # c.evaluate #=> true
8
+ #
9
+ class Condition
10
+ attr_reader :attachment
11
+ attr_accessor :left, :operator, :right
12
+
13
+ def initialize(left = nil, operator = nil, right = nil)
14
+ @left, @operator, @right = left, operator, right
15
+ end
16
+
17
+ def evaluate(context = Context.new)
18
+ interpret_condition(left, right, operator, context)
19
+ end
20
+
21
+ def attach(attachment)
22
+ @attachment = attachment
23
+ end
24
+
25
+ def else?
26
+ false
27
+ end
28
+
29
+ private
30
+
31
+ def equal_variables(left, right)
32
+ if left.is_a?(Symbol)
33
+ if right.respond_to?(left)
34
+ return right.send(left.to_s)
35
+ else
36
+ return nil
37
+ end
38
+ end
39
+
40
+ if right.is_a?(Symbol)
41
+ if left.respond_to?(right)
42
+ return left.send(right.to_s)
43
+ else
44
+ return nil
45
+ end
46
+ end
47
+
48
+ left == right
49
+ end
50
+
51
+ def interpret_condition(left, right, op, context)
52
+
53
+ # If the operator is empty this means that the decision statement is just
54
+ # a single variable. We can just poll this variable from the context and
55
+ # return this as the result.
56
+ return context[left] if op == nil
57
+
58
+ left, right = context[left], context[right]
59
+
60
+ operation = case op
61
+ when '==' then return equal_variables(left, right)
62
+ when '!=' then return !equal_variables(left, right)
63
+ when '<>' then return !equal_variables(left, right)
64
+ when '>' then :>
65
+ when '<' then :<
66
+ when '>=' then :>=
67
+ when '<=' then :<=
68
+ else
69
+ raise ArgumentError.new("Error in tag '#{name}' - Unknown operator #{op}")
70
+ end
71
+
72
+ if left.respond_to?(operation) and right.respond_to?(operation)
73
+ left.send(operation, right)
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ end
79
+
80
+ class ElseCondition < Condition
81
+
82
+ def else?
83
+ true
84
+ end
85
+
86
+ def evaluate(context)
87
+ true
88
+ end
89
+ end
90
+
91
+ end
@@ -0,0 +1,216 @@
1
+ module Liquid
2
+
3
+ class ContextError < StandardError
4
+ end
5
+
6
+ # Context keeps the variable stack and resolves variables, as well as keywords
7
+ #
8
+ # context['variable'] = 'testing'
9
+ # context['variable'] #=> 'testing'
10
+ # context['true'] #=> true
11
+ # context['10.2232'] #=> 10.2232
12
+ #
13
+ # context.stack do
14
+ # context['bob'] = 'bobsen'
15
+ # end
16
+ #
17
+ # context['bob'] #=> nil class Context
18
+ class Context
19
+ attr_reader :scopes
20
+ attr_reader :template
21
+
22
+ def initialize(template)
23
+ @template = template
24
+ @scopes = [template.assigns]
25
+ end
26
+
27
+ def strainer
28
+ @strainer ||= Strainer.create(self)
29
+ end
30
+
31
+ def registers
32
+ @template.registers
33
+ end
34
+
35
+ # adds filters to this context.
36
+ # this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
37
+ # for that
38
+ def add_filters(filters)
39
+ filters = [filters].flatten.compact
40
+
41
+ raise ArgumentError, "Expected module but got: #{filter_module.class}" unless filters.all? { |f| f.is_a?(Module)}
42
+
43
+ filters.each do |f|
44
+ strainer.extend(f)
45
+ end
46
+ end
47
+
48
+ def invoke(method, *args)
49
+ if strainer.respond_to?(method)
50
+ strainer.__send__(method, *args)
51
+ else
52
+ args.first
53
+ end
54
+ end
55
+
56
+ # push new local scope on the stack. use <tt>Context#stack</tt> instead
57
+ def push
58
+ @scopes.unshift({})
59
+ end
60
+
61
+ # merge a hash of variables in the current local scope
62
+ def merge(new_scopes)
63
+ @scopes[0].merge!(new_scopes)
64
+ end
65
+
66
+ # pop from the stack. use <tt>Context#stack</tt> instead
67
+ def pop
68
+ raise ContextError if @scopes.size == 1
69
+ @scopes.shift
70
+ end
71
+
72
+ # pushes a new local scope on the stack, pops it at the end of the block
73
+ #
74
+ # Example:
75
+ #
76
+ # context.stack do
77
+ # context['var'] = 'hi'
78
+ # end
79
+ # context['var] #=> nil
80
+ #
81
+ def stack(&block)
82
+ result = nil
83
+ push
84
+ begin
85
+ result = yield
86
+ ensure
87
+ pop
88
+ end
89
+ result
90
+ end
91
+
92
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
93
+ def []=(key, value)
94
+ @scopes[0][key] = value
95
+ end
96
+
97
+ def [](key)
98
+ resolve(key)
99
+ end
100
+
101
+ def has_key?(key)
102
+ resolve(key) != nil
103
+ end
104
+
105
+ private
106
+
107
+ # Look up variable, either resolve directly after considering the name. We can directly handle
108
+ # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
109
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
110
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
111
+ #
112
+ # Example:
113
+ #
114
+ # products == empty #=> products.empty?
115
+ #
116
+ def resolve(key)
117
+ case key
118
+ when nil
119
+ nil
120
+ when 'true'
121
+ true
122
+ when 'false'
123
+ false
124
+ when 'empty'
125
+ :empty?
126
+ when 'nil', 'null'
127
+ nil
128
+ # Single quoted strings
129
+ when /^'(.*)'$/
130
+ $1.to_s
131
+ # Double quoted strings
132
+ when /^"(.*)"$/
133
+ $1.to_s
134
+ # Integer and floats
135
+ when /^(\d+)$/
136
+ $1.to_i
137
+ when /^(\d[\d\.]+)$/
138
+ $1.to_f
139
+ else
140
+ variable(key)
141
+ end
142
+ end
143
+
144
+ # fetches an object starting at the local scope and then moving up
145
+ # the hierachy
146
+ def find_variable(key)
147
+ @scopes.each do |scope|
148
+ if scope.has_key?(key)
149
+ variable = scope[key]
150
+ variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
151
+ variable.context = self if variable.respond_to?(:context=)
152
+ return variable
153
+ end
154
+ end
155
+ nil
156
+ end
157
+
158
+ # resolves namespaced queries gracefully.
159
+ #
160
+ # Example
161
+ #
162
+ # @context['hash'] = {"name" => 'tobi'}
163
+ # assert_equal 'tobi', @context['hash.name']
164
+ # assert_equal 'tobi', @context['hash[name]']
165
+ #
166
+ def variable(markup)
167
+ parts = markup.scan(VariableParser)
168
+
169
+ if object = find_variable(parts.shift).to_liquid
170
+
171
+ parts.each do |part|
172
+
173
+ # If object is a hash we look for the presence of the key and if its available
174
+ # we return it
175
+
176
+ # Hash
177
+ if object.respond_to?(:has_key?) and object.has_key?(part)
178
+
179
+ # if its a proc we will replace the entry in the hash table with the proc
180
+ object[part] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=)
181
+ object = object[part].to_liquid
182
+
183
+ # Array
184
+ elsif object.respond_to?(:fetch) and part =~ /^\d+$/
185
+ pos = part.to_i
186
+
187
+ object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=)
188
+ object = object[pos].to_liquid
189
+
190
+ # Some special cases. If no key with the same name was found we interpret following calls
191
+ # as commands and call them on the current object
192
+ elsif object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
193
+
194
+ object = object.send(part.intern).to_liquid
195
+
196
+ # No key was present with the desired value and it wasn't one of the directly supported
197
+ # keywords either. The only thing we got left is to return nil
198
+ else
199
+ return nil
200
+ end
201
+
202
+ # If we are dealing with a drop here we have to
203
+ object.context = self if object.respond_to?(:context=)
204
+ end
205
+ end
206
+
207
+ object
208
+ end
209
+
210
+ private
211
+
212
+ def execute_proc(proc)
213
+ proc.call(self)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,17 @@
1
+ module Liquid
2
+ class Document < Block
3
+ # we don't need markup to open this block
4
+ def initialize(tokens)
5
+ parse(tokens)
6
+ end
7
+
8
+ # There isn't a real delimter
9
+ def block_delimiter
10
+ []
11
+ end
12
+
13
+ # Document blocks don't need to be terminated since they are not actually opened
14
+ def assert_missing_delimitation!
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,48 @@
1
+ module Liquid
2
+
3
+ # A drop in liquid is a class which allows you to to export DOM like things to liquid
4
+ # Methods of drops are callable.
5
+ # The main use for liquid drops is the implement lazy loaded objects.
6
+ # If you would like to make data available to the web designers which you don't want loaded unless needed then
7
+ # a drop is a great way to do that
8
+ #
9
+ # Example:
10
+ #
11
+ # class ProductDrop < Liquid::Drop
12
+ # def top_sales
13
+ # Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
14
+ # end
15
+ # end
16
+ #
17
+ # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
18
+ # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
19
+ #
20
+ # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
21
+ # catch all
22
+ class Drop
23
+ attr_writer :context
24
+
25
+ # Catch all for the method
26
+ def before_method(method)
27
+ nil
28
+ end
29
+
30
+ # called by liquid to invoke a drop
31
+ def invoke_drop(method)
32
+ result = before_method(method)
33
+ result ||= send(method.to_sym) if self.class.public_instance_methods.include?(method.to_s)
34
+ result
35
+ end
36
+
37
+ def has_key?(name)
38
+ true
39
+ end
40
+
41
+ def to_liquid
42
+ self
43
+ end
44
+
45
+ alias :[] :invoke_drop
46
+ end
47
+
48
+ end
@@ -0,0 +1,7 @@
1
+ module Liquid
2
+ class FilterNotFound < StandardError
3
+ end
4
+
5
+ class FileSystemError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,56 @@
1
+ require 'time'
2
+ require 'date'
3
+
4
+ class String # :nodoc:
5
+ def to_liquid
6
+ self
7
+ end
8
+ end
9
+
10
+ class Array # :nodoc:
11
+ def to_liquid
12
+ self
13
+ end
14
+ end
15
+
16
+ class Hash # :nodoc:
17
+ def to_liquid
18
+ self
19
+ end
20
+ end
21
+
22
+ class Numeric # :nodoc:
23
+ def to_liquid
24
+ self
25
+ end
26
+ end
27
+
28
+ class Time # :nodoc:
29
+ def to_liquid
30
+ self
31
+ end
32
+ end
33
+
34
+ class DateTime < Date # :nodoc:
35
+ def to_liquid
36
+ self
37
+ end
38
+ end
39
+
40
+ class Date # :nodoc:
41
+ def to_liquid
42
+ self
43
+ end
44
+ end
45
+
46
+ def true.to_liquid # :nodoc:
47
+ self
48
+ end
49
+
50
+ def false.to_liquid # :nodoc:
51
+ self
52
+ end
53
+
54
+ def nil.to_liquid # :nodoc:
55
+ self
56
+ end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ # A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
3
+ #
4
+ # You can implement subclasses that retrieve templates from the database, from the file system using a different
5
+ # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
6
+ #
7
+ # You can add additional instance variables, arguments, or methods as needed.
8
+ #
9
+ # Example:
10
+ #
11
+ # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
12
+ # liquid = Liquid::Template.parse(template)
13
+ #
14
+ # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
15
+ class BlankFileSystem
16
+ # Called by Liquid to retrieve a template file
17
+ def read_template_file(template_path)
18
+ raise FileSystemError, "This liquid context does not allow includes."
19
+ end
20
+ end
21
+
22
+ # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
23
+ # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
24
+ #
25
+ # For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
26
+ #
27
+ # Example:
28
+ #
29
+ # file_system = Liquid::LocalFileSystem.new("/some/path")
30
+ #
31
+ # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
+ # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
+ #
34
+ class LocalFileSystem
35
+ attr_accessor :root
36
+
37
+ def initialize(root)
38
+ @root = root
39
+ end
40
+
41
+ def read_template_file(template_path)
42
+ full_path = full_path(template_path)
43
+ raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
44
+
45
+ File.read(full_path)
46
+ end
47
+
48
+ def full_path(template_path)
49
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
50
+
51
+ full_path = if template_path.include?('/')
52
+ File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
53
+ else
54
+ File.join(root, "_#{template_path}.liquid")
55
+ end
56
+
57
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
58
+
59
+ full_path
60
+ end
61
+ end
62
+ end