spinto-liquid 2.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/History.md +56 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +44 -0
  4. data/lib/extras/liquid_view.rb +51 -0
  5. data/lib/liquid/block.rb +101 -0
  6. data/lib/liquid/condition.rb +120 -0
  7. data/lib/liquid/context.rb +245 -0
  8. data/lib/liquid/document.rb +17 -0
  9. data/lib/liquid/drop.rb +49 -0
  10. data/lib/liquid/errors.rb +11 -0
  11. data/lib/liquid/extensions.rb +62 -0
  12. data/lib/liquid/file_system.rb +62 -0
  13. data/lib/liquid/htmltags.rb +75 -0
  14. data/lib/liquid/module_ex.rb +62 -0
  15. data/lib/liquid/standardfilters.rb +241 -0
  16. data/lib/liquid/strainer.rb +54 -0
  17. data/lib/liquid/tag.rb +26 -0
  18. data/lib/liquid/tags/assign.rb +33 -0
  19. data/lib/liquid/tags/capture.rb +35 -0
  20. data/lib/liquid/tags/case.rb +79 -0
  21. data/lib/liquid/tags/comment.rb +9 -0
  22. data/lib/liquid/tags/cycle.rb +59 -0
  23. data/lib/liquid/tags/decrement.rb +39 -0
  24. data/lib/liquid/tags/for.rb +190 -0
  25. data/lib/liquid/tags/if.rb +79 -0
  26. data/lib/liquid/tags/ifchanged.rb +20 -0
  27. data/lib/liquid/tags/include.rb +65 -0
  28. data/lib/liquid/tags/increment.rb +35 -0
  29. data/lib/liquid/tags/raw.rb +21 -0
  30. data/lib/liquid/tags/unless.rb +33 -0
  31. data/lib/liquid/template.rb +150 -0
  32. data/lib/liquid/variable.rb +50 -0
  33. data/lib/liquid.rb +66 -0
  34. data/test/liquid/assign_test.rb +21 -0
  35. data/test/liquid/block_test.rb +58 -0
  36. data/test/liquid/capture_test.rb +40 -0
  37. data/test/liquid/condition_test.rb +127 -0
  38. data/test/liquid/context_test.rb +478 -0
  39. data/test/liquid/drop_test.rb +162 -0
  40. data/test/liquid/error_handling_test.rb +81 -0
  41. data/test/liquid/file_system_test.rb +29 -0
  42. data/test/liquid/filter_test.rb +106 -0
  43. data/test/liquid/module_ex_test.rb +87 -0
  44. data/test/liquid/output_test.rb +116 -0
  45. data/test/liquid/parsing_quirks_test.rb +52 -0
  46. data/test/liquid/regexp_test.rb +44 -0
  47. data/test/liquid/security_test.rb +41 -0
  48. data/test/liquid/standard_filter_test.rb +195 -0
  49. data/test/liquid/strainer_test.rb +25 -0
  50. data/test/liquid/tags/for_tag_test.rb +215 -0
  51. data/test/liquid/tags/html_tag_test.rb +39 -0
  52. data/test/liquid/tags/if_else_tag_test.rb +160 -0
  53. data/test/liquid/tags/include_tag_test.rb +139 -0
  54. data/test/liquid/tags/increment_tag_test.rb +24 -0
  55. data/test/liquid/tags/raw_tag_test.rb +15 -0
  56. data/test/liquid/tags/standard_tag_test.rb +295 -0
  57. data/test/liquid/tags/statements_test.rb +134 -0
  58. data/test/liquid/tags/unless_else_tag_test.rb +26 -0
  59. data/test/liquid/template_test.rb +74 -0
  60. data/test/liquid/variable_test.rb +170 -0
  61. data/test/test_helper.rb +29 -0
  62. metadata +136 -0
@@ -0,0 +1,54 @@
1
+ require 'set'
2
+
3
+ module Liquid
4
+
5
+ parent_object = if defined? BlankObject
6
+ BlankObject
7
+ else
8
+ Object
9
+ end
10
+
11
+ # Strainer is the parent class for the filters system.
12
+ # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
13
+ #
14
+ # One of the strainer's responsibilities is to keep malicious method calls out
15
+ class Strainer < parent_object #:nodoc:
16
+ INTERNAL_METHOD = /^__/
17
+ @@required_methods = Set.new([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id])
18
+
19
+ # Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
20
+ @@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?
21
+
22
+ @@filters = {}
23
+
24
+ def initialize(context)
25
+ @context = context
26
+ end
27
+
28
+ def self.global_filter(filter)
29
+ raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
30
+ @@filters[filter.name] = filter
31
+ end
32
+
33
+ def self.create(context)
34
+ strainer = Strainer.new(context)
35
+ @@filters.each { |k,m| strainer.extend(m) }
36
+ strainer
37
+ end
38
+
39
+ def respond_to?(method, include_private = false)
40
+ method_name = method.to_s
41
+ return false if method_name =~ INTERNAL_METHOD
42
+ return false if @@required_methods.include?(method_name)
43
+ super
44
+ end
45
+
46
+ # remove all standard methods from the bucket so circumvent security
47
+ # problems
48
+ instance_methods.each do |m|
49
+ unless @@required_methods.include?(m.to_sym)
50
+ undef_method m
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/liquid/tag.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Liquid
2
+
3
+ class Tag
4
+
5
+ attr_accessor :nodelist
6
+
7
+ def initialize(tag_name, markup, tokens)
8
+ @tag_name = tag_name
9
+ @markup = markup
10
+ parse(tokens)
11
+ end
12
+
13
+ def parse(tokens)
14
+ end
15
+
16
+ def name
17
+ self.class.name.downcase
18
+ end
19
+
20
+ def render(context)
21
+ ''
22
+ end
23
+
24
+ end # Tag
25
+
26
+ end # Tag
@@ -0,0 +1,33 @@
1
+ module Liquid
2
+
3
+ # Assign sets a variable in your template.
4
+ #
5
+ # {% assign foo = 'monkey' %}
6
+ #
7
+ # You can then use the variable later in the page.
8
+ #
9
+ # {{ foo }}
10
+ #
11
+ class Assign < Tag
12
+ Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/
13
+
14
+ def initialize(tag_name, markup, tokens)
15
+ if markup =~ Syntax
16
+ @to = $1
17
+ @from = Variable.new($2)
18
+ else
19
+ raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
20
+ end
21
+
22
+ super
23
+ end
24
+
25
+ def render(context)
26
+ context.scopes.last[@to] = @from.render(context)
27
+ ''
28
+ end
29
+
30
+ end
31
+
32
+ Template.register_tag('assign', Assign)
33
+ end
@@ -0,0 +1,35 @@
1
+ module Liquid
2
+
3
+ # Capture stores the result of a block into a variable without rendering it inplace.
4
+ #
5
+ # {% capture heading %}
6
+ # Monkeys!
7
+ # {% endcapture %}
8
+ # ...
9
+ # <h1>{{ heading }}</h1>
10
+ #
11
+ # Capture is useful for saving content for use later in your template, such as
12
+ # in a sidebar or footer.
13
+ #
14
+ class Capture < Block
15
+ Syntax = /(\w+)/
16
+
17
+ def initialize(tag_name, markup, tokens)
18
+ if markup =~ Syntax
19
+ @to = $1
20
+ else
21
+ raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
22
+ end
23
+
24
+ super
25
+ end
26
+
27
+ def render(context)
28
+ output = super
29
+ context.scopes.last[@to] = output
30
+ ''
31
+ end
32
+ end
33
+
34
+ Template.register_tag('capture', Capture)
35
+ end
@@ -0,0 +1,79 @@
1
+ module Liquid
2
+ class Case < Block
3
+ Syntax = /(#{QuotedFragment})/
4
+ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
5
+
6
+ def initialize(tag_name, markup, tokens)
7
+ @blocks = []
8
+
9
+ if markup =~ Syntax
10
+ @left = $1
11
+ else
12
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ def unknown_tag(tag, markup, tokens)
19
+ @nodelist = []
20
+ case tag
21
+ when 'when'
22
+ record_when_condition(markup)
23
+ when 'else'
24
+ record_else_condition(markup)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def render(context)
31
+ context.stack do
32
+ execute_else_block = true
33
+
34
+ output = ''
35
+ @blocks.each do |block|
36
+ if block.else?
37
+ return render_all(block.attachment, context) if execute_else_block
38
+ elsif block.evaluate(context)
39
+ execute_else_block = false
40
+ output << render_all(block.attachment, context)
41
+ end
42
+ end
43
+ output
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def record_when_condition(markup)
50
+ while markup
51
+ # Create a new nodelist and assign it to the new block
52
+ if not markup =~ WhenSyntax
53
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
54
+ end
55
+
56
+ markup = $2
57
+
58
+ block = Condition.new(@left, '==', $1)
59
+ block.attach(@nodelist)
60
+ @blocks.push(block)
61
+ end
62
+ end
63
+
64
+ def record_else_condition(markup)
65
+
66
+ if not markup.strip.empty?
67
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
68
+ end
69
+
70
+ block = ElseCondition.new
71
+ block.attach(@nodelist)
72
+ @blocks << block
73
+ end
74
+
75
+
76
+ end
77
+
78
+ Template.register_tag('case', Case)
79
+ end
@@ -0,0 +1,9 @@
1
+ module Liquid
2
+ class Comment < Block
3
+ def render(context)
4
+ ''
5
+ end
6
+ end
7
+
8
+ Template.register_tag('comment', Comment)
9
+ end
@@ -0,0 +1,59 @@
1
+ module Liquid
2
+
3
+ # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
4
+ #
5
+ # {% for item in items %}
6
+ # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
7
+ # {% end %}
8
+ #
9
+ # <div class="red"> Item one </div>
10
+ # <div class="green"> Item two </div>
11
+ # <div class="blue"> Item three </div>
12
+ # <div class="red"> Item four </div>
13
+ # <div class="green"> Item five</div>
14
+ #
15
+ class Cycle < Tag
16
+ SimpleSyntax = /^#{QuotedFragment}+/
17
+ NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/
18
+
19
+ def initialize(tag_name, markup, tokens)
20
+ case markup
21
+ when NamedSyntax
22
+ @variables = variables_from_string($2)
23
+ @name = $1
24
+ when SimpleSyntax
25
+ @variables = variables_from_string(markup)
26
+ @name = "'#{@variables.to_s}'"
27
+ else
28
+ raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
29
+ end
30
+ super
31
+ end
32
+
33
+ def render(context)
34
+ context.registers[:cycle] ||= Hash.new(0)
35
+
36
+ context.stack do
37
+ key = context[@name]
38
+ iteration = context.registers[:cycle][key]
39
+ result = context[@variables[iteration]]
40
+ iteration += 1
41
+ iteration = 0 if iteration >= @variables.size
42
+ context.registers[:cycle][key] = iteration
43
+ result
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def variables_from_string(markup)
50
+ markup.split(',').collect do |var|
51
+ var =~ /\s*(#{QuotedFragment})\s*/
52
+ $1 ? $1 : nil
53
+ end.compact
54
+ end
55
+
56
+ end
57
+
58
+ Template.register_tag('cycle', Cycle)
59
+ end
@@ -0,0 +1,39 @@
1
+ module Liquid
2
+
3
+ # decrement is used in a place where one needs to insert a counter
4
+ # into a template, and needs the counter to survive across
5
+ # multiple instantiations of the template.
6
+ # NOTE: decrement is a pre-decrement, --i,
7
+ # while increment is post: i++.
8
+ #
9
+ # (To achieve the survival, the application must keep the context)
10
+ #
11
+ # if the variable does not exist, it is created with value 0.
12
+
13
+ # Hello: {% decrement variable %}
14
+ #
15
+ # gives you:
16
+ #
17
+ # Hello: -1
18
+ # Hello: -2
19
+ # Hello: -3
20
+ #
21
+ class Decrement < Tag
22
+ def initialize(tag_name, markup, tokens)
23
+ @variable = markup.strip
24
+
25
+ super
26
+ end
27
+
28
+ def render(context)
29
+ value = context.environments.first[@variable] ||= 0
30
+ value = value - 1
31
+ context.environments.first[@variable] = value
32
+ value.to_s
33
+ end
34
+
35
+ private
36
+ end
37
+
38
+ Template.register_tag('decrement', Decrement)
39
+ end
@@ -0,0 +1,190 @@
1
+ module Liquid
2
+
3
+ # "For" iterates over an array or collection.
4
+ # Several useful variables are available to you within the loop.
5
+ #
6
+ # == Basic usage:
7
+ # {% for item in collection %}
8
+ # {{ forloop.index }}: {{ item.name }}
9
+ # {% endfor %}
10
+ #
11
+ # == Advanced usage:
12
+ # {% for item in collection %}
13
+ # <div {% if forloop.first %}class="first"{% endif %}>
14
+ # Item {{ forloop.index }}: {{ item.name }}
15
+ # </div>
16
+ # {% else %}
17
+ # There is nothing in the collection.
18
+ # {% endfor %}
19
+ #
20
+ # You can also define a limit and offset much like SQL. Remember
21
+ # that offset starts at 0 for the first item.
22
+ #
23
+ # {% for item in collection limit:5 offset:10 %}
24
+ # {{ item.name }}
25
+ # {% end %}
26
+ #
27
+ # You can also specify an order for the collection items
28
+ #
29
+ # {% for item in collection order:ascending %}
30
+ # {{ item.name }}
31
+ # {% end %}
32
+ #
33
+ # You can also specify which attribute to sort by.
34
+ #
35
+ # {% for item in collection sort_by:name order:descending %}
36
+ # {{ item.name }}
37
+ # {% end %}
38
+ #
39
+ # To reverse the for loop simply use {% for item in collection reversed %}
40
+ #
41
+ # == Available variables:
42
+ #
43
+ # forloop.name:: 'item-collection'
44
+ # forloop.length:: Length of the loop
45
+ # forloop.index:: The current item's position in the collection;
46
+ # forloop.index starts at 1.
47
+ # This is helpful for non-programmers who start believe
48
+ # the first item in an array is 1, not 0.
49
+ # forloop.index0:: The current item's position in the collection
50
+ # where the first item is 0
51
+ # forloop.rindex:: Number of items remaining in the loop
52
+ # (length - index) where 1 is the last item.
53
+ # forloop.rindex0:: Number of items remaining in the loop
54
+ # where 0 is the last item.
55
+ # forloop.first:: Returns true if the item is the first item.
56
+ # forloop.last:: Returns true if the item is the last item.
57
+ #
58
+ class For < Block
59
+ Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/
60
+
61
+ def initialize(tag_name, markup, tokens)
62
+ if markup =~ Syntax
63
+ @variable_name = $1
64
+ @collection_name = $2
65
+ @name = "#{$1}-#{$2}"
66
+ @reversed = $3
67
+ @attributes = {}
68
+ markup.scan(TagAttributes) do |key, value|
69
+ @attributes[key] = value
70
+ end
71
+ @reversed = 'reversed' if @attributes['order'] == 'descending'
72
+ else
73
+ raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
74
+ end
75
+
76
+ @nodelist = @for_block = []
77
+ super
78
+ end
79
+
80
+ def unknown_tag(tag, markup, tokens)
81
+ return super unless tag == 'else'
82
+ @nodelist = @else_block = []
83
+ end
84
+
85
+ def render(context)
86
+ context.registers[:for] ||= Hash.new(0)
87
+
88
+ collection = context[@collection_name]
89
+ collection = collection.to_a if collection.is_a?(Range)
90
+
91
+ # Maintains Ruby 1.8.7 String#each behaviour on 1.9
92
+ return render_else(context) unless iterable?(collection)
93
+
94
+ sort_property = @attributes['sort_by']
95
+ order_property = @attributes['order']
96
+ if sort_property || order_property
97
+ collection = if sort_property.nil? && (@attributes['order'] == 'ascending' || @attributes['order'] == 'descending')
98
+ collection.sort
99
+ elsif collection.first.respond_to?('[]') and !collection.first[sort_property].nil?
100
+ collection.sort {|a,b| a[sort_property] <=> b[sort_property] }
101
+ elsif collection.first.respond_to?(sort_property)
102
+ collection.sort {|a,b| a.send(sort_property) <=> b.send(sort_property) }
103
+ else
104
+ collection
105
+ end
106
+ end
107
+
108
+ collection.reverse! if @reversed
109
+
110
+ from = if @attributes['offset'] == 'continue'
111
+ context.registers[:for][@name].to_i
112
+ else
113
+ context[@attributes['offset']].to_i
114
+ end
115
+
116
+ limit = context[@attributes['limit']]
117
+ to = limit ? limit.to_i + from : nil
118
+
119
+
120
+ segment = slice_collection_using_each(collection, from, to)
121
+
122
+ return render_else(context) if segment.empty?
123
+
124
+ result = ''
125
+
126
+ length = segment.length
127
+
128
+ # Store our progress through the collection for the continue flag
129
+ context.registers[:for][@name] = from + segment.length
130
+
131
+ context.stack do
132
+ segment.each_with_index do |item, index|
133
+ context[@variable_name] = item
134
+ context['forloop'] = {
135
+ 'name' => @name,
136
+ 'length' => length,
137
+ 'index' => index + 1,
138
+ 'index0' => index,
139
+ 'rindex' => length - index,
140
+ 'rindex0' => length - index -1,
141
+ 'first' => (index == 0),
142
+ 'last' => (index == length - 1) }
143
+
144
+ result << render_all(@for_block, context)
145
+ end
146
+ end
147
+ result
148
+ end
149
+
150
+ def slice_collection_using_each(collection, from, to)
151
+ segments = []
152
+ index = 0
153
+ yielded = 0
154
+
155
+ # Maintains Ruby 1.8.7 String#each behaviour on 1.9
156
+ return [collection] if non_blank_string?(collection)
157
+
158
+ collection.each do |item|
159
+
160
+ if to && to <= index
161
+ break
162
+ end
163
+
164
+ if from <= index
165
+ segments << item
166
+ end
167
+
168
+ index += 1
169
+ end
170
+
171
+ segments
172
+ end
173
+
174
+ private
175
+
176
+ def render_else(context)
177
+ return @else_block ? [render_all(@else_block, context)] : ''
178
+ end
179
+
180
+ def iterable?(collection)
181
+ collection.respond_to?(:each) || non_blank_string?(collection)
182
+ end
183
+
184
+ def non_blank_string?(collection)
185
+ collection.is_a?(String) && collection != ''
186
+ end
187
+ end
188
+
189
+ Template.register_tag('for', For)
190
+ end