liquid 1.7.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 (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,51 @@
1
+ module Liquid
2
+ class If < Block
3
+ Syntax = /(#{QuotedFragment})\s*([=!<>]+)?\s*(#{QuotedFragment})?/
4
+
5
+ def initialize(markup, tokens)
6
+ @blocks = []
7
+
8
+ push_block('if', markup)
9
+
10
+ super
11
+ end
12
+
13
+ def unknown_tag(tag, markup, tokens)
14
+ if ['elsif', 'else'].include?(tag)
15
+ push_block(tag, markup)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def render(context)
22
+ context.stack do
23
+ @blocks.each do |block|
24
+ if block.evaluate(context)
25
+ return render_all(block.attachment, context)
26
+ end
27
+ end
28
+ ''
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def push_block(tag, markup)
35
+
36
+ block = if tag == 'else'
37
+ ElseCondition.new
38
+ elsif markup =~ Syntax
39
+ Condition.new($1, $2, $3)
40
+ else
41
+ raise SyntaxError.new("Syntax Error in tag '#{tag}' - Valid syntax: #{tag} [condition]")
42
+ end
43
+
44
+ @blocks.push(block)
45
+ @nodelist = block.attach(Array.new)
46
+ end
47
+
48
+ end
49
+
50
+ Template.register_tag('if', If)
51
+ end
@@ -0,0 +1,20 @@
1
+ module Liquid
2
+ class Ifchanged < Block
3
+
4
+ def render(context)
5
+ context.stack do
6
+
7
+ output = render_all(@nodelist, context)
8
+
9
+ if output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = output
11
+ output
12
+ else
13
+ ''
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ Template.register_tag('ifchanged', Ifchanged)
20
+ end
@@ -0,0 +1,56 @@
1
+ module Liquid
2
+ class Include < Tag
3
+ Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
4
+
5
+ def initialize(markup, tokens)
6
+ if markup =~ Syntax
7
+
8
+ @template_name = $1
9
+ @variable_name = $3
10
+ @attributes = {}
11
+
12
+ markup.scan(TagAttributes) do |key, value|
13
+ @attributes[key] = value
14
+ end
15
+
16
+ else
17
+ raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
18
+ end
19
+
20
+ super
21
+ end
22
+
23
+ def parse(tokens)
24
+ end
25
+
26
+ def render(context)
27
+ source = Liquid::Template.file_system.read_template_file(context[@template_name])
28
+ partial = Liquid::Template.parse(source)
29
+
30
+
31
+ variable = context[@variable_name]
32
+
33
+ context.stack do
34
+ @attributes.each do |key, value|
35
+ context[key] = context[value]
36
+ end
37
+
38
+ if variable.is_a?(Array)
39
+
40
+ variable.collect do |variable|
41
+ context[@template_name[1..-2]] = variable
42
+ partial.render(context)
43
+ end
44
+
45
+ else
46
+
47
+ context[@template_name[1..-2]] = variable
48
+ partial.render(context)
49
+
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ Template.register_tag('include', Include)
56
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/if'
2
+
3
+ module Liquid
4
+
5
+ class Unless < If
6
+ def render(context)
7
+ context.stack do
8
+
9
+ # First condition is interpreted backwards ( if not )
10
+ block = @blocks.shift
11
+ unless block.evaluate(context)
12
+ return render_all(block.attachment, context)
13
+ end
14
+
15
+ # After the first condition unless works just like if
16
+ @blocks.each do |block|
17
+ if block.evaluate(context)
18
+ return render_all(block.attachment, context)
19
+ end
20
+ end
21
+
22
+ ''
23
+ end
24
+ end
25
+ end
26
+
27
+
28
+ Template.register_tag('unless', Unless)
29
+ end
@@ -0,0 +1,150 @@
1
+ module Liquid
2
+
3
+ # Templates are central to liquid.
4
+ # Interpretating templates is a two step process. First you compile the
5
+ # source code you got. During compile time some extensive error checking is performed.
6
+ # your code should expect to get some SyntaxErrors.
7
+ #
8
+ # After you have a compiled template you can then <tt>render</tt> it.
9
+ # You can use a compiled template over and over again and keep it cached.
10
+ #
11
+ # Example:
12
+ #
13
+ # template = Liquid::Template.parse(source)
14
+ # template.render('user_name' => 'bob')
15
+ #
16
+ class Template
17
+ attr_accessor :root
18
+ @@file_system = BlankFileSystem.new
19
+
20
+ class <<self
21
+ def file_system
22
+ @@file_system
23
+ end
24
+
25
+ def file_system=(obj)
26
+ @@file_system = obj
27
+ end
28
+
29
+ def register_tag(name, klass)
30
+ tags[name.to_s] = klass
31
+ end
32
+
33
+ def tags
34
+ @tags ||= {}
35
+ end
36
+
37
+ # Pass a module with filter methods which should be available
38
+ # to all liquid views. Good for registering the standard library
39
+ def register_filter(mod)
40
+ Strainer.global_filter(mod)
41
+ end
42
+
43
+ # creates a new <tt>Template</tt> object from liquid source code
44
+ def parse(source)
45
+ template = Template.new
46
+ template.parse(source)
47
+ template
48
+ end
49
+ end
50
+
51
+ # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
52
+ def initialize
53
+ end
54
+
55
+ # Parse source code.
56
+ # Returns self for easy chaining
57
+ def parse(source)
58
+ @root = Document.new(tokenize(source))
59
+ self
60
+ end
61
+
62
+ def registers
63
+ @registers ||= {}
64
+ end
65
+
66
+ def assigns
67
+ @assigns ||= {}
68
+ end
69
+
70
+ def errors
71
+ @errors ||= []
72
+ end
73
+
74
+ def handle_error(e)
75
+ errors.push(e)
76
+ raise if @rethrow_errors
77
+
78
+ case e
79
+ when SyntaxError
80
+ "Liquid syntax error: #{e.message}"
81
+ else
82
+ "Liquid error: #{e.message}"
83
+ end
84
+ end
85
+
86
+ # Render takes a hash with local variables.
87
+ #
88
+ # if you use the same filters over and over again consider registering them globally
89
+ # with <tt>Template.register_filter</tt>
90
+ #
91
+ # Following options can be passed:
92
+ #
93
+ # * <tt>filters</tt> : array with local filters
94
+ # * <tt>registers</tt> : hash with register variables. Those can be accessed from
95
+ # filters and tags and might be useful to integrate liquid more with its host application
96
+ #
97
+ def render(*args)
98
+ return '' if @root.nil?
99
+
100
+ context = case args.first
101
+ when Liquid::Context
102
+ args.shift
103
+ when Hash
104
+ self.assigns.merge!(args.shift)
105
+ Context.new(self)
106
+ when nil
107
+ Context.new(self)
108
+ end
109
+
110
+ case args.last
111
+ when Hash
112
+ options = args.pop
113
+
114
+ if options[:registers].is_a?(Hash)
115
+ self.registers.merge!(options[:registers])
116
+ end
117
+
118
+ if options[:filters]
119
+ context.add_filters(options[:filters])
120
+ end
121
+ when Module
122
+ context.add_filters(args.pop)
123
+ when Array
124
+ context.add_filters(args.pop)
125
+ end
126
+
127
+ # render the nodelist.
128
+ # for performance reasons we get a array back here. to_s will make a string out of it
129
+ @root.render(context).to_s
130
+ end
131
+
132
+ def render!(*args)
133
+ @rethrow_errors = true; render(*args)
134
+ end
135
+
136
+ private
137
+
138
+ # Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
139
+ def tokenize(source)
140
+ return [] if source.to_s.empty?
141
+ tokens = source.split(TemplateParser)
142
+
143
+ # removes the rogue empty element at the beginning of the array
144
+ tokens.shift if tokens[0] and tokens[0].empty?
145
+
146
+ tokens
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,39 @@
1
+ module Liquid
2
+
3
+ # Hols variables. Variables are only loaded "just in time"
4
+ # they are not evaluated as part of the render stage
5
+ class Variable
6
+ attr_accessor :filters, :name
7
+
8
+ def initialize(markup)
9
+ @markup = markup
10
+ @name = markup.match(/\s*(#{QuotedFragment})/)[1]
11
+ if markup.match(/#{FilterSperator}\s*(.*)/)
12
+ filters = Regexp.last_match(1).split(/#{FilterSperator}/)
13
+
14
+ @filters = filters.collect do |f|
15
+ filtername = f.match(/\s*(\w+)/)[1]
16
+ filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
17
+ [filtername.to_sym, filterargs]
18
+ end
19
+ else
20
+ @filters = []
21
+ end
22
+ end
23
+
24
+ def render(context)
25
+ output = context[@name]
26
+ @filters.inject(output) do |output, filter|
27
+ filterargs = filter[1].to_a.collect do |a|
28
+ context[a]
29
+ end
30
+ begin
31
+ output = context.invoke(filter[0], output, *filterargs)
32
+ rescue FilterNotFound
33
+ raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
34
+ end
35
+ end
36
+ output
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class VariableTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_blankspace
7
+ template = Liquid::Template.parse(" ")
8
+ assert_equal [" "], template.root.nodelist
9
+ end
10
+
11
+ def test_variable_beginning
12
+ template = Liquid::Template.parse("{{funk}} ")
13
+ assert_equal 2, template.root.nodelist.size
14
+ assert_equal Variable, template.root.nodelist[0].class
15
+ assert_equal String, template.root.nodelist[1].class
16
+ end
17
+
18
+ def test_variable_end
19
+ template = Liquid::Template.parse(" {{funk}}")
20
+ assert_equal 2, template.root.nodelist.size
21
+ assert_equal String, template.root.nodelist[0].class
22
+ assert_equal Variable, template.root.nodelist[1].class
23
+ end
24
+
25
+ def test_variable_middle
26
+ template = Liquid::Template.parse(" {{funk}} ")
27
+ assert_equal 3, template.root.nodelist.size
28
+ assert_equal String, template.root.nodelist[0].class
29
+ assert_equal Variable, template.root.nodelist[1].class
30
+ assert_equal String, template.root.nodelist[2].class
31
+ end
32
+
33
+ def test_variable_many_embedded_fragments
34
+ template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
35
+ assert_equal 7, template.root.nodelist.size
36
+ assert_equal [String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist)
37
+ end
38
+
39
+ def test_with_block
40
+ template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
41
+ assert_equal [String, Comment, String], block_types(template.root.nodelist)
42
+ assert_equal 3, template.root.nodelist.size
43
+ end
44
+
45
+ private
46
+
47
+ def block_types(nodelist)
48
+ nodelist.collect { |node| node.class }
49
+ end
50
+ end
@@ -0,0 +1,340 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+ class HundredCentes
3
+ def to_liquid
4
+ 100
5
+ end
6
+ end
7
+
8
+ class CentsDrop < Liquid::Drop
9
+ def amount
10
+ HundredCentes.new
11
+ end
12
+ end
13
+
14
+ class ContextSensitiveDrop < Liquid::Drop
15
+ def test
16
+ @context['test']
17
+ end
18
+ end
19
+
20
+
21
+ class ContextTest < Test::Unit::TestCase
22
+ include Liquid
23
+
24
+ def setup
25
+ @template = Liquid::Template.new
26
+ @context = Liquid::Context.new(@template)
27
+ end
28
+
29
+ def test_variables
30
+ @context['string'] = 'string'
31
+ assert_equal 'string', @context['string']
32
+
33
+ @context['num'] = 5
34
+ assert_equal 5, @context['num']
35
+
36
+ @context['time'] = Time.parse('2006-06-06 12:00:00')
37
+ assert_equal Time.parse('2006-06-06 12:00:00'), @context['time']
38
+
39
+ @context['date'] = Date.today
40
+ assert_equal Date.today, @context['date']
41
+
42
+ now = DateTime.now
43
+ @context['datetime'] = now
44
+ assert_equal now, @context['datetime']
45
+
46
+ @context['bool'] = true
47
+ assert_equal true, @context['bool']
48
+
49
+ @context['bool'] = false
50
+ assert_equal false, @context['bool']
51
+
52
+ @context['nil'] = nil
53
+ assert_equal nil, @context['nil']
54
+ assert_equal nil, @context['nil']
55
+ end
56
+
57
+ def test_variables_not_existing
58
+ assert_equal nil, @context['does_not_exist']
59
+ end
60
+
61
+ def test_scoping
62
+ assert_nothing_raised do
63
+ @context.push
64
+ @context.pop
65
+ end
66
+
67
+ assert_raise(Liquid::ContextError) do
68
+ @context.pop
69
+ end
70
+
71
+ assert_raise(Liquid::ContextError) do
72
+ @context.push
73
+ @context.pop
74
+ @context.pop
75
+ end
76
+ end
77
+
78
+ def test_length_query
79
+
80
+ @context['numbers'] = [1,2,3,4]
81
+
82
+ assert_equal 4, @context['numbers.size']
83
+
84
+ @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4}
85
+
86
+ assert_equal 4, @context['numbers.size']
87
+
88
+ @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000}
89
+
90
+ assert_equal 1000, @context['numbers.size']
91
+
92
+ end
93
+
94
+ def test_add_filter
95
+
96
+ filter = Module.new do
97
+ def hi(output)
98
+ output + ' hi!'
99
+ end
100
+ end
101
+
102
+ context = Context.new(@template)
103
+ context.add_filters(filter)
104
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
105
+
106
+ context = Context.new(@template)
107
+ assert_equal 'hi?', context.invoke(:hi, 'hi?')
108
+
109
+ context.add_filters(filter)
110
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
111
+
112
+ end
113
+
114
+ def test_override_global_filter
115
+ global = Module.new do
116
+ def notice(output)
117
+ "Global #{output}"
118
+ end
119
+ end
120
+
121
+ local = Module.new do
122
+ def notice(output)
123
+ "Local #{output}"
124
+ end
125
+ end
126
+
127
+ Template.register_filter(global)
128
+ assert_equal 'Global test', Template.parse("{{'test' | notice }}").render
129
+ assert_equal 'Local test', Template.parse("{{'test' | notice }}").render({}, :filters => [local])
130
+ end
131
+
132
+ def test_only_intended_filters_make_it_there
133
+
134
+ filter = Module.new do
135
+ def hi(output)
136
+ output + ' hi!'
137
+ end
138
+ end
139
+
140
+ context = Context.new(@template)
141
+ methods = context.strainer.methods
142
+ context.add_filters(filter)
143
+ assert_equal (methods + ['hi']).sort, context.strainer.methods.sort
144
+ end
145
+
146
+ def test_add_item_in_outer_scope
147
+ @context['test'] = 'test'
148
+ @context.push
149
+ assert_equal 'test', @context['test']
150
+ @context.pop
151
+ assert_equal 'test', @context['test']
152
+ end
153
+
154
+ def test_add_item_in_inner_scope
155
+ @context.push
156
+ @context['test'] = 'test'
157
+ assert_equal 'test', @context['test']
158
+ @context.pop
159
+ assert_equal nil, @context['test']
160
+ end
161
+
162
+ def test_hierachical_data
163
+ @context['hash'] = {"name" => 'tobi'}
164
+ assert_equal 'tobi', @context['hash.name']
165
+ end
166
+
167
+ def test_keywords
168
+ assert_equal true, @context['true']
169
+ assert_equal false, @context['false']
170
+ end
171
+
172
+ def test_digits
173
+ assert_equal 100, @context['100']
174
+ assert_equal 100.00, @context['100.00']
175
+ end
176
+
177
+ def test_strings
178
+ assert_equal "hello!", @context['"hello!"']
179
+ assert_equal "hello!", @context["'hello!'"]
180
+ end
181
+
182
+ def test_merge
183
+ @context.merge({ "test" => "test" })
184
+ assert_equal 'test', @context['test']
185
+ @context.merge({ "test" => "newvalue", "foo" => "bar" })
186
+ assert_equal 'newvalue', @context['test']
187
+ assert_equal 'bar', @context['foo']
188
+ end
189
+
190
+ def test_array_notation
191
+ @context['test'] = [1,2,3,4,5]
192
+
193
+ assert_equal 1, @context['test[0]']
194
+ assert_equal 2, @context['test[1]']
195
+ assert_equal 3, @context['test[2]']
196
+ assert_equal 4, @context['test[3]']
197
+ assert_equal 5, @context['test[4]']
198
+ end
199
+
200
+ def test_recoursive_array_notation
201
+ @context['test'] = {'test' => [1,2,3,4,5]}
202
+
203
+ assert_equal 1, @context['test.test[0]']
204
+
205
+ @context['test'] = [{'test' => 'worked'}]
206
+
207
+ assert_equal 'worked', @context['test[0].test']
208
+ end
209
+
210
+ def test_try_first
211
+ @context['test'] = [1,2,3,4,5]
212
+
213
+ assert_equal 1, @context['test.first']
214
+ assert_equal 5, @context['test.last']
215
+
216
+ @context['test'] = {'test' => [1,2,3,4,5]}
217
+
218
+ assert_equal 1, @context['test.test.first']
219
+ assert_equal 5, @context['test.test.last']
220
+
221
+ @context['test'] = [1]
222
+ assert_equal 1, @context['test.first']
223
+ assert_equal 1, @context['test.last']
224
+ end
225
+
226
+ def test_access_hashes_with_hash_notation
227
+
228
+ @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
229
+ @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]}
230
+
231
+
232
+ assert_equal 5, @context['products[count]']
233
+ assert_equal 'deepsnow', @context['products[tags][0]']
234
+ assert_equal 'deepsnow', @context['products[tags].first']
235
+ assert_equal 'draft151cm', @context['product[variants][0][title]']
236
+ assert_equal 'element151cm', @context['product[variants][1][title]']
237
+ assert_equal 'draft151cm', @context['product[variants][0][title]']
238
+ assert_equal 'element151cm', @context['product[variants][last][title]']
239
+ end
240
+
241
+
242
+ def test_first_can_appear_in_middle_of_callchain
243
+
244
+ @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]}
245
+
246
+ assert_equal 'draft151cm', @context['product.variants[0].title']
247
+ assert_equal 'element151cm', @context['product.variants[1].title']
248
+ assert_equal 'draft151cm', @context['product.variants.first.title']
249
+ assert_equal 'element151cm', @context['product.variants.last.title']
250
+
251
+ end
252
+
253
+ def test_cents
254
+ @context.merge( "cents" => HundredCentes.new )
255
+ assert_equal 100, @context['cents']
256
+ end
257
+
258
+ def test_nested_cents
259
+ @context.merge( "cents" => { 'amount' => HundredCentes.new} )
260
+ assert_equal 100, @context['cents.amount']
261
+
262
+ @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } )
263
+ assert_equal 100, @context['cents.cents.amount']
264
+ end
265
+
266
+ def test_cents_through_drop
267
+ @context.merge( "cents" => CentsDrop.new )
268
+ assert_equal 100, @context['cents.amount']
269
+ end
270
+
271
+ def test_nested_cents_through_drop
272
+ @context.merge( "vars" => {"cents" => CentsDrop.new} )
273
+ assert_equal 100, @context['vars.cents.amount']
274
+ end
275
+
276
+ def test_context_from_within_drop
277
+ @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new )
278
+ assert_equal '123', @context['vars.test']
279
+ end
280
+
281
+ def test_nested_context_from_within_drop
282
+ @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } )
283
+ assert_equal '123', @context['vars.local.test']
284
+ end
285
+
286
+ def test_cents_through_drop_nestedly
287
+ @context.merge( "cents" => {"cents" => CentsDrop.new} )
288
+ assert_equal 100, @context['cents.cents.amount']
289
+
290
+ @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} )
291
+ assert_equal 100, @context['cents.cents.cents.amount']
292
+ end
293
+
294
+ def test_proc_as_variable
295
+ @context['dynamic'] = Proc.new { 'Hello' }
296
+
297
+ assert_equal 'Hello', @context['dynamic']
298
+ end
299
+
300
+ def test_lambda_as_variable
301
+ @context['dynamic'] = lambda { 'Hello' }
302
+
303
+ assert_equal 'Hello', @context['dynamic']
304
+ end
305
+
306
+ def test_nested_lambda_as_variable
307
+ @context['dynamic'] = { "lambda" => lambda { 'Hello' } }
308
+
309
+ assert_equal 'Hello', @context['dynamic.lambda']
310
+ end
311
+
312
+ def test_lambda_is_called_once
313
+ @context['callcount'] = lambda { @global ||= 0; @global += 1; @global.to_s }
314
+
315
+ assert_equal '1', @context['callcount']
316
+ assert_equal '1', @context['callcount']
317
+ assert_equal '1', @context['callcount']
318
+
319
+ @global = nil
320
+ end
321
+
322
+ def test_nested_lambda_is_called_once
323
+ @context['callcount'] = { "lambda" => lambda { @global ||= 0; @global += 1; @global.to_s } }
324
+
325
+ assert_equal '1', @context['callcount.lambda']
326
+ assert_equal '1', @context['callcount.lambda']
327
+ assert_equal '1', @context['callcount.lambda']
328
+
329
+ @global = nil
330
+ end
331
+
332
+ def test_access_to_context_from_proc
333
+ @context.registers[:magic] = 345392
334
+
335
+ @context['magic'] = lambda { @context.registers[:magic] }
336
+
337
+ assert_equal 345392, @context['magic']
338
+ end
339
+
340
+ end