spinto-liquid 2.3.0.1

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 (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