hotcell 0.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 (50) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +15 -0
  5. data/Guardfile +24 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +17 -0
  9. data/hotcell.gemspec +22 -0
  10. data/lib/hotcell/.DS_Store +0 -0
  11. data/lib/hotcell/config.rb +31 -0
  12. data/lib/hotcell/context.rb +36 -0
  13. data/lib/hotcell/errors.rb +43 -0
  14. data/lib/hotcell/extensions.rb +42 -0
  15. data/lib/hotcell/lexer.rb +783 -0
  16. data/lib/hotcell/lexer.rl +299 -0
  17. data/lib/hotcell/manipulator.rb +31 -0
  18. data/lib/hotcell/node/arrayer.rb +7 -0
  19. data/lib/hotcell/node/assigner.rb +11 -0
  20. data/lib/hotcell/node/block.rb +58 -0
  21. data/lib/hotcell/node/calculator.rb +35 -0
  22. data/lib/hotcell/node/command.rb +41 -0
  23. data/lib/hotcell/node/hasher.rb +7 -0
  24. data/lib/hotcell/node/joiner.rb +7 -0
  25. data/lib/hotcell/node/sequencer.rb +7 -0
  26. data/lib/hotcell/node/summoner.rb +11 -0
  27. data/lib/hotcell/node/tag.rb +26 -0
  28. data/lib/hotcell/node.rb +55 -0
  29. data/lib/hotcell/parser.rb +1186 -0
  30. data/lib/hotcell/parser.y +231 -0
  31. data/lib/hotcell/scope.rb +57 -0
  32. data/lib/hotcell/template.rb +29 -0
  33. data/lib/hotcell/version.rb +3 -0
  34. data/lib/hotcell.rb +19 -0
  35. data/misc/rage.rl +1999 -0
  36. data/misc/unicode2ragel.rb +305 -0
  37. data/spec/data/dstrings +8 -0
  38. data/spec/data/sstrings +6 -0
  39. data/spec/lib/hotcell/config_spec.rb +57 -0
  40. data/spec/lib/hotcell/context_spec.rb +53 -0
  41. data/spec/lib/hotcell/lexer_spec.rb +340 -0
  42. data/spec/lib/hotcell/manipulator_spec.rb +64 -0
  43. data/spec/lib/hotcell/node/block_spec.rb +188 -0
  44. data/spec/lib/hotcell/node/command_spec.rb +71 -0
  45. data/spec/lib/hotcell/parser_spec.rb +382 -0
  46. data/spec/lib/hotcell/scope_spec.rb +160 -0
  47. data/spec/lib/hotcell/template_spec.rb +41 -0
  48. data/spec/lib/hotcell_spec.rb +8 -0
  49. data/spec/spec_helper.rb +44 -0
  50. metadata +139 -0
@@ -0,0 +1,340 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ describe Hotcell::Lexer do
5
+ def scan template
6
+ described_class.new(template).tokens
7
+ end
8
+
9
+ def expression template
10
+ scan("{{ #{template} }}")[1..-2]
11
+ end
12
+
13
+ specify { scan('').should == [] }
14
+ specify { scan('{{ }}').should == [[:TOPEN, "{{"], [:TCLOSE, "}}"]] }
15
+ specify { expression('').should == [] }
16
+
17
+ context 'arithmetic' do
18
+ specify { expression('+').should == [[:PLUS, '+']] }
19
+ specify { expression('-').should == [[:MINUS, '-']] }
20
+ specify { expression('*').should == [[:MULTIPLY, '*']] }
21
+ specify { expression('* *').should == [[:MULTIPLY, '*'], [:MULTIPLY, '*']] }
22
+ specify { expression('**').should == [[:POWER, '**']] }
23
+ specify { expression('/').should == [[:DIVIDE, '/']] }
24
+ specify { expression('%').should == [[:MODULO, '%']] }
25
+ end
26
+
27
+ context 'logic' do
28
+ specify { expression('&&').should == [[:AND, '&&']] }
29
+ specify { expression('||').should == [[:OR, '||']] }
30
+ specify { expression('!').should == [[:NOT, '!']] }
31
+ specify { expression('!!').should == [[:NOT, '!'], [:NOT, '!']] }
32
+ specify { expression('==').should == [[:EQUAL, '==']] }
33
+ specify { expression('!=').should == [[:INEQUAL, '!=']] }
34
+ specify { expression('! =').should == [[:NOT, '!'], [:ASSIGN, '=']] }
35
+ specify { expression('>').should == [[:GT, '>']] }
36
+ specify { expression('>=').should == [[:GTE, '>=']] }
37
+ specify { expression('> =').should == [[:GT, '>'], [:ASSIGN, '=']] }
38
+ specify { expression('<').should == [[:LT, '<']] }
39
+ specify { expression('<=').should == [[:LTE, '<=']] }
40
+ specify { expression('< =').should == [[:LT, '<'], [:ASSIGN, '=']] }
41
+ end
42
+
43
+ context 'flow' do
44
+ specify { expression('=').should == [[:ASSIGN, '=']] }
45
+ specify { expression('= =').should == [[:ASSIGN, '='], [:ASSIGN, '=']] }
46
+ specify { expression(',').should == [[:COMMA, ',']] }
47
+ specify { expression('.').should == [[:PERIOD, '.']] }
48
+ specify { expression(':').should == [[:COLON, ':']] }
49
+ specify { expression('?').should == [[:QUESTION, '?']] }
50
+ specify { expression('hello?').should == [[:IDENTIFER, 'hello?']] }
51
+ specify { expression('hello ?').should == [[:IDENTIFER, 'hello'], [:QUESTION, '?']] }
52
+ specify { expression(';').should == [[:SEMICOLON, ';']] }
53
+ specify { expression("\n").should == [[:NEWLINE, "\n"]] }
54
+ end
55
+
56
+ context 'structure' do
57
+ specify { expression('[').should == [[:AOPEN, '[']] }
58
+ specify { expression(']').should == [[:ACLOSE, ']']] }
59
+ specify { expression('{').should == [[:HOPEN, '{']] }
60
+ specify { expression('}').should == [[:HCLOSE, '}']] }
61
+ specify { expression('(').should == [[:POPEN, '(']] }
62
+ specify { expression(')').should == [[:PCLOSE, ')']] }
63
+ end
64
+
65
+ context 'numeric' do
66
+ context 'integer' do
67
+ specify { expression('42').should == [[:INTEGER, 42]] }
68
+ specify { expression('-42').should == [[:MINUS, '-'], [:INTEGER, 42]] }
69
+ specify { expression('!42').should == [[:NOT, '!'], [:INTEGER, 42]] }
70
+ specify { expression('42.').should == [[:INTEGER, 42], [:PERIOD, '.']] }
71
+ specify { expression('42.foo').should == [[:INTEGER, 42], [:PERIOD, '.'], [:IDENTIFER, 'foo']] }
72
+ specify { expression('42foo').should == [[:INTEGER, 42], [:IDENTIFER, 'foo']] }
73
+ end
74
+
75
+ context 'float' do
76
+ specify { expression('42.42').should == [[:FLOAT, 42.42]] }
77
+ specify { expression('-42.42').should == [[:MINUS, '-'], [:FLOAT, 42.42]] }
78
+ specify { expression('!42.42').should == [[:NOT, '!'], [:FLOAT, 42.42]] }
79
+ specify { expression('0.42').should == [[:FLOAT, 0.42]] }
80
+ specify { expression('.42').should == [[:FLOAT, 0.42]] }
81
+ specify { expression('.42.').should == [[:FLOAT, 0.42], [:PERIOD, '.']] }
82
+ specify { expression('.42.foo').should == [[:FLOAT, 0.42], [:PERIOD, '.'], [:IDENTIFER, 'foo']] }
83
+ specify { expression('.42foo').should == [[:FLOAT, 0.42], [:IDENTIFER, 'foo']] }
84
+ specify { expression('..42').should == [[:PERIOD, '.'], [:FLOAT, 0.42]] }
85
+ end
86
+ end
87
+
88
+ context 'identifer' do
89
+ specify { expression('foo').should == [[:IDENTIFER, 'foo']] }
90
+ specify { expression('foo42').should == [[:IDENTIFER, 'foo42']] }
91
+ specify { expression('f42').should == [[:IDENTIFER, 'f42']] }
92
+ specify { expression('foo42bar').should == [[:IDENTIFER, 'foo42bar']] }
93
+ specify { expression('42foo42').should == [[:INTEGER, 42], [:IDENTIFER, 'foo42']] }
94
+ specify { expression('foo?').should == [[:IDENTIFER, 'foo?']] }
95
+ specify { expression('foo?bar').should == [[:IDENTIFER, 'foo?'], [:IDENTIFER, 'bar']] }
96
+ specify { expression('foo!').should == [[:IDENTIFER, 'foo!']] }
97
+ specify { expression('foo!bar').should == [[:IDENTIFER, 'foo!'], [:IDENTIFER, 'bar']] }
98
+ specify { expression('foo.bar').should == [[:IDENTIFER, 'foo'], [:PERIOD, '.'], [:IDENTIFER, 'bar']] }
99
+ specify { expression('!foo').should == [[:NOT, '!'], [:IDENTIFER, 'foo']] }
100
+ specify { expression('-foo').should == [[:MINUS, '-'], [:IDENTIFER, 'foo']] }
101
+
102
+ context 'constants' do
103
+ specify { expression('nil').should == [[:NIL, nil]] }
104
+ specify { expression('"nil"').should == [[:STRING, 'nil']] }
105
+ specify { expression('null').should == [[:NIL, nil]] }
106
+ specify { expression('"null"').should == [[:STRING, 'null']] }
107
+ specify { expression('false').should == [[:FALSE, false]] }
108
+ specify { expression('"false"').should == [[:STRING, 'false']] }
109
+ specify { expression('true').should == [[:TRUE, true]] }
110
+ specify { expression('"true"').should == [[:STRING, 'true']] }
111
+ end
112
+ end
113
+
114
+ context 'string' do
115
+ context 'single quoted' do
116
+ specify { expression(%q{''}).should == [[:STRING, '']] }
117
+ specify { expression(%q{'foo'}).should == [[:STRING, 'foo']] }
118
+ specify { expression(%q{'fo"o'}).should == [[:STRING, 'fo"o']] }
119
+ specify { expression(%q{'fo\o'}).should == [[:STRING, 'fo\o']] }
120
+ specify { expression(%q{'fo\'o'}).should == [[:STRING, 'fo\'o']] }
121
+ specify { expression(%q{'fo\"o'}).should == [[:STRING, 'fo\"o']] }
122
+ specify { expression(%q{'fo\no'}).should == [[:STRING, 'fo\no']] }
123
+ specify { expression(%q{'fo\mo'}).should == [[:STRING, 'fo\mo']] }
124
+ specify { expression(%q{'foo42'}).should == [[:STRING, 'foo42']] }
125
+ specify { expression(%q{'привет'}).should == [[:STRING, 'привет']] }
126
+ specify { expression(%q{'при\вет'}).should == [[:STRING, 'при\вет']] }
127
+
128
+ context do
129
+ let(:strings) { data 'sstrings' }
130
+
131
+ specify { expression(strings).delete_if { |token| token.first == :NEWLINE }.should == [
132
+ [:STRING, 'fo\'o'], [:STRING, 'fo\o'], [:STRING, 'fo\\o'],
133
+ [:STRING, 'fo\no'], [:STRING, "foo\nbar"]
134
+ ] }
135
+ end
136
+ end
137
+
138
+ context 'double quoted' do
139
+ specify { expression(%q{""}).should == [[:STRING, ""]] }
140
+ specify { expression(%q{"foo"}).should == [[:STRING, "foo"]] }
141
+ specify { expression(%q{"fo'o"}).should == [[:STRING, "fo'o"]] }
142
+ specify { expression(%q{"fo\o"}).should == [[:STRING, "fo\o"]] }
143
+ specify { expression(%q{"fo\"o"}).should == [[:STRING, "fo\"o"]] }
144
+ specify { expression(%q{"fo\'o"}).should == [[:STRING, "fo\'o"]] }
145
+ specify { expression(%q{"fo\no"}).should == [[:STRING, "fo\no"]] }
146
+ specify { expression(%q{"fo\mo"}).should == [[:STRING, "fo\mo"]] }
147
+ specify { expression(%q{"foo42"}).should == [[:STRING, "foo42"]] }
148
+ specify { expression(%q{"привет"}).should == [[:STRING, "привет"]] }
149
+ specify { expression(%q{"при\вет"}).should == [[:STRING, "при\вет"]] }
150
+
151
+ context do
152
+ let(:strings) { data 'dstrings' }
153
+
154
+ specify { expression(strings).delete_if { |token| token.first == :NEWLINE }.should == [
155
+ [:STRING, "fo\"o"], [:STRING, "fo\o"], [:STRING, "fo\\o"],
156
+ [:STRING, "fo\no"], [:STRING, "fo\mo"], [:STRING, "fo\to"],
157
+ [:STRING, "foo\nbar"]
158
+ ] }
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'regexp' do
164
+ specify { expression('//').should == [[:REGEXP, //]] }
165
+ specify { expression('/regexp/').should == [[:REGEXP, /regexp/]] }
166
+ specify { expression('/regexp/i').should == [[:REGEXP, /regexp/i]] }
167
+ specify { expression('/regexp/m').should == [[:REGEXP, /regexp/m]] }
168
+ specify { expression('/regexp/x').should == [[:REGEXP, /regexp/x]] }
169
+ specify { expression('/regexp/sdmfri').should == [[:REGEXP, /regexp/im]] }
170
+ specify { expression('/\.*/').should == [[:REGEXP, /\.*/]] }
171
+ specify { expression('/\//').should == [[:REGEXP, /\//]] }
172
+ specify { expression('/\//ix').should == [[:REGEXP, /\//ix]] }
173
+ specify { expression('/регексп/').should == [[:REGEXP, /регексп/]] }
174
+ specify { expression('/регексп/m').should == [[:REGEXP, /регексп/m]] }
175
+
176
+ context 'ambiguity' do
177
+ specify { expression('hello /regexp/').should == [
178
+ [:IDENTIFER, "hello"], [:DIVIDE, "/"], [:IDENTIFER, "regexp"], [:DIVIDE, "/"]
179
+ ] }
180
+ specify { expression('hello(/regexp/)').should == [
181
+ [:IDENTIFER, "hello"], [:POPEN, "("], [:REGEXP, /regexp/], [:PCLOSE, ")"]
182
+ ] }
183
+ specify { expression('[/regexp/]').should == [
184
+ [:AOPEN, "["], [:REGEXP, /regexp/], [:ACLOSE, "]"]
185
+ ] }
186
+ specify { expression('[42, /regexp/]').should == [
187
+ [:AOPEN, "["], [:INTEGER, 42], [:COMMA, ","], [:REGEXP, /regexp/], [:ACLOSE, "]"]
188
+ ] }
189
+ specify { expression('{/regexp/: 42}').should == [
190
+ [:HOPEN, "{"], [:REGEXP, /regexp/], [:COLON, ":"], [:INTEGER, 42], [:HCLOSE, "}"]
191
+ ] }
192
+ specify { expression('42 : /regexp/').should == [
193
+ [:INTEGER, 42], [:COLON, ":"], [:REGEXP, /regexp/]
194
+ ] }
195
+ specify { expression('42; /regexp/').should == [
196
+ [:INTEGER, 42], [:SEMICOLON, ";"], [:REGEXP, /regexp/]
197
+ ] }
198
+ specify { expression('"hello" /regexp/').should == [
199
+ [:STRING, "hello"], [:DIVIDE, "/"], [:IDENTIFER, "regexp"], [:DIVIDE, "/"]
200
+ ] }
201
+ end
202
+ end
203
+
204
+ context 'expression comments' do
205
+ specify { scan('{{ # }}').should == [[:TOPEN, '{{'], [:COMMENT, '# '], [:TCLOSE, '}}']] }
206
+ specify { scan('{{ #}}').should == [[:TOPEN, '{{'], [:COMMENT, '#'], [:TCLOSE, '}}']] }
207
+ specify { scan('{{ #hello }}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello '], [:TCLOSE, '}}']] }
208
+ specify { scan('{{ #hello}}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello'], [:TCLOSE, '}}']] }
209
+ specify { scan('{{ #hello}}}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello'], [:TCLOSE, '}}'], [:TEMPLATE, '}']] }
210
+ specify { scan('{{ #hello} }}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello} '], [:TCLOSE, '}}']] }
211
+ specify { scan('{{ #hello# }}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello# '], [:TCLOSE, '}}']] }
212
+ specify { scan('{{ #hello#}}').should == [[:TOPEN, '{{'], [:COMMENT, '#hello#'], [:TCLOSE, '}}']] }
213
+ specify { scan('{{ #hel}lo#}}').should == [[:TOPEN, '{{'], [:COMMENT, '#hel}lo#'], [:TCLOSE, '}}']] }
214
+ specify { scan('{{ #hel{lo#}}').should == [[:TOPEN, '{{'], [:COMMENT, '#hel{lo#'], [:TCLOSE, '}}']] }
215
+ specify { scan("{{ #hel{\nlo#}}").should == [[:TOPEN, "{{"], [:COMMENT, "#hel{"], [:NEWLINE, "\n"],
216
+ [:IDENTIFER, "lo"], [:COMMENT, "#"], [:TCLOSE, "}}"]] }
217
+ specify { scan("{{ #hel{#\n#lo#}}").should == [[:TOPEN, "{{"], [:COMMENT, "#hel{#"], [:NEWLINE, "\n"],
218
+ [:COMMENT, "#lo#"], [:TCLOSE, "}}"]] }
219
+ end
220
+
221
+ context 'errors' do
222
+ describe Hotcell::Errors::UnexpectedSymbol do
223
+ let(:error) { Hotcell::Errors::UnexpectedSymbol }
224
+
225
+ specify { expect { expression("hello @world") }.to raise_error(error, /1:10/) }
226
+ specify { expect { expression("@hello world") }.to raise_error(error, /1:4/) }
227
+ specify { expect { expression("hello world@") }.to raise_error(error, /1:15/) }
228
+ specify { expect { expression("hello\n@ world") }.to raise_error(error, /2:1/) }
229
+ specify { expect { expression("hello\n @world") }.to raise_error(error, /2:2/) }
230
+ specify { expect { expression("hello\n world@") }.to raise_error(error, /2:7/) }
231
+ specify { expect { expression("hello@\n world") }.to raise_error(error, /1:9/) }
232
+ specify { expect { expression("@hello\n world") }.to raise_error(error, /1:4/) }
233
+ specify { expect { expression("'привет' @ 'мир'") }.to raise_error(error, /1:13/) }
234
+ end
235
+
236
+ describe Hotcell::Errors::UnterminatedString do
237
+ let(:error) { Hotcell::Errors::UnterminatedString }
238
+
239
+ specify { expect { expression("hello 'world") }.to raise_error(error, /1:10/) }
240
+ specify { expect { expression("hello\nwor'ld") }.to raise_error(error, /2:4/) }
241
+ specify { expect { expression("hello 'world\\'") }.to raise_error(error, /1:10/) }
242
+ specify { expect { expression("hello 'wor\\'ld") }.to raise_error(error, /1:10/) }
243
+ specify { expect { expression("\"hello world") }.to raise_error(error, /1:4/) }
244
+ specify { expect { expression("he\"llo\\\" world") }.to raise_error(error, /1:6/) }
245
+ specify { expect { expression("he\"llo\\\" \nworld") }.to raise_error(error, /1:6/) }
246
+ specify { expect { expression("\"hello\\\"\n world") }.to raise_error(error, /1:4/) }
247
+ specify { expect { expression("'привет' 'мир") }.to raise_error(error, /1:13/) }
248
+ end
249
+ end
250
+
251
+ context 'complex expressions' do
252
+ specify { expression('3+2 * 9').should == [
253
+ [:INTEGER, 3], [:PLUS, "+"], [:INTEGER, 2],
254
+ [:MULTIPLY, "*"], [:INTEGER, 9]
255
+ ] }
256
+ specify { expression('3 / 2 / 9').should == [
257
+ [:INTEGER, 3], [:DIVIDE, "/"], [:INTEGER, 2],
258
+ [:DIVIDE, "/"], [:INTEGER, 9]
259
+ ] }
260
+ specify { expression("foo.bar.baz('hello', 16)").should == [
261
+ [:IDENTIFER, "foo"], [:PERIOD, "."], [:IDENTIFER, "bar"],
262
+ [:PERIOD, "."], [:IDENTIFER, "baz"], [:POPEN, "("],
263
+ [:STRING, "hello"], [:COMMA, ","], [:INTEGER, 16],
264
+ [:PCLOSE, ")"]
265
+ ] }
266
+ specify { expression("foo(36.6);\n a = \"привет\"").should == [
267
+ [:IDENTIFER, "foo"], [:POPEN, "("], [:FLOAT, 36.6],
268
+ [:PCLOSE, ")"], [:SEMICOLON, ";"], [:NEWLINE, "\n"],
269
+ [:IDENTIFER, "a"], [:ASSIGN, "="], [:STRING, "привет"]
270
+ ] }
271
+ specify { expression("'foo'.match(\"^foo$\")").should == [
272
+ [:STRING, "foo"], [:PERIOD, "."], [:IDENTIFER, "match"],
273
+ [:POPEN, "("], [:STRING, "^foo$"], [:PCLOSE, ")"]
274
+ ] }
275
+ end
276
+
277
+ context 'templates' do
278
+ specify { scan(' ').should == [[:TEMPLATE, " "]] }
279
+ specify { scan('{').should == [[:TEMPLATE, "{"]] }
280
+ specify { scan('}').should == [[:TEMPLATE, "}"]] }
281
+ specify { scan('{{').should == [[:TOPEN, "{{"]] }
282
+ specify { scan('}}').should == [[:TEMPLATE, "}}"]] }
283
+ specify { scan('hello').should == [[:TEMPLATE, "hello"]] }
284
+ specify { scan('hel{lo').should == [[:TEMPLATE, "hel{lo"]] }
285
+ specify { scan('hel{{lo').should == [
286
+ [:TEMPLATE, "hel"], [:TOPEN, "{{"], [:IDENTIFER, "lo"]
287
+ ] }
288
+ specify { scan('hel}lo').should == [[:TEMPLATE, "hel}lo"]] }
289
+ specify { scan('hel}}lo').should == [[:TEMPLATE, "hel}}lo"]] }
290
+ specify { scan('}}hello').should == [[:TEMPLATE, "}}hello"]] }
291
+ specify { scan('hello{{').should == [[:TEMPLATE, "hello"], [:TOPEN, "{{"]] }
292
+ specify { scan('2 + 3').should == [[:TEMPLATE, "2 + 3"]] }
293
+ specify { scan('hel}}lo{{}}').should == [
294
+ [:TEMPLATE, "hel}}lo"], [:TOPEN, "{{"], [:TCLOSE, "}}"]
295
+ ] }
296
+ specify { scan('hel}}lo{{').should == [[:TEMPLATE, "hel}}lo"], [:TOPEN, "{{"]] }
297
+ specify { scan('hel}}lo{{ !empty').should == [
298
+ [:TEMPLATE, "hel}}lo"], [:TOPEN, "{{"], [:NOT, "!"], [:IDENTIFER, "empty"]
299
+ ] }
300
+ specify { scan(' {{ }} ').should == [
301
+ [:TEMPLATE, " "], [:TOPEN, "{{"], [:TCLOSE, "}}"], [:TEMPLATE, " "]
302
+ ] }
303
+ specify { scan('2 + 3 {{ 2 || 3 }} hello').should == [
304
+ [:TEMPLATE, "2 + 3 "], [:TOPEN, "{{"], [:INTEGER, 2],
305
+ [:OR, "||"], [:INTEGER, 3], [:TCLOSE, "}}"], [:TEMPLATE, " hello"]
306
+ ] }
307
+
308
+ context 'tag types' do
309
+ specify { scan('{{hello}}').should == [
310
+ [:TOPEN, "{{"], [:IDENTIFER, "hello"], [:TCLOSE, "}}"]
311
+ ] }
312
+ specify { scan('{{! hello}}').should == [
313
+ [:TOPEN, "{{!"], [:IDENTIFER, "hello"], [:TCLOSE, "}}"]
314
+ ] }
315
+ specify { scan('{{ !hello}}').should == [
316
+ [:TOPEN, "{{"], [:NOT, "!"], [:IDENTIFER, "hello"], [:TCLOSE, "}}"]
317
+ ] }
318
+ specify { scan('{{/ hello}}').should == [
319
+ [:TOPEN, "{{"], [:DIVIDE, "/"], [:IDENTIFER, "hello"], [:TCLOSE, "}}"]
320
+ ] }
321
+ specify { scan('{{ /hello}}').should == [
322
+ [:TOPEN, "{{"], [:DIVIDE, "/"], [:IDENTIFER, "hello"], [:TCLOSE, "}}"]
323
+ ] }
324
+ end
325
+ end
326
+
327
+ context 'template comments' do
328
+ specify { scan('{{#').should == [[:COMMENT, "{{#"]] }
329
+ specify { scan('{{# }}').should == [[:COMMENT, "{{# }}"]] }
330
+ specify { scan('{{##}}').should == [[:COMMENT, "{{##}}"]] }
331
+ specify { scan('{{###}}').should == [[:COMMENT, "{{###}}"]] }
332
+ specify { scan('{{# {{# blabla').should == [[:COMMENT, "{{# {{# blabla"]] }
333
+ specify { scan('{{# {{# }} blabla').should == [[:COMMENT, "{{# {{# }} blabla"]] }
334
+ specify { scan('{{# {{ #}} blabla').should == [[:COMMENT, "{{# {{ #}}"], [:TEMPLATE, " blabla"]] }
335
+ specify { scan('{# {{}}{{# {{# blabla').should == [[:TEMPLATE, "{# "], [:TOPEN, "{{"], [:TCLOSE, "}}"],
336
+ [:COMMENT, "{{# {{# blabla"]] }
337
+ specify { scan('{{ } {{# blabla').should == [[:TOPEN, "{{"], [:HCLOSE, "}"], [:HOPEN, "{"],
338
+ [:HOPEN, "{"], [:COMMENT, "# blabla"]] }
339
+ end
340
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Manipulator do
4
+ context 'mixed in' do
5
+ context do
6
+ let(:klass) do
7
+ Class.new(Numeric) do
8
+ include Hotcell::Manipulator::Mixin
9
+ end
10
+ end
11
+ subject { klass.new }
12
+
13
+ its(:manipulator_methods) { should be_a Set }
14
+ its(:manipulator_methods) { should be_empty }
15
+ its(:to_manipulator) { should === subject }
16
+ specify { subject.manipulator_invoke('round').should be_nil }
17
+ specify { subject.manipulator_invoke(:round).should be_nil }
18
+ specify { subject.manipulator_invoke('round', 42, :arg).should be_nil }
19
+ end
20
+
21
+ context do
22
+ let(:klass) do
23
+ Class.new(String) do
24
+ include Hotcell::Manipulator::Mixin
25
+
26
+ manipulator :split, :size
27
+ end
28
+ end
29
+ subject { klass.new('hello world') }
30
+
31
+ its('manipulator_methods.to_a') { should =~ %w(split size) }
32
+ its(:to_manipulator) { should === subject }
33
+ specify { subject.manipulator_invoke('length').should be_nil }
34
+ specify { subject.manipulator_invoke('length', 42, :arg).should be_nil }
35
+ specify { subject.manipulator_invoke(:split).should be_nil }
36
+ specify { subject.manipulator_invoke('split').should == %w(hello world) }
37
+ specify { subject.manipulator_invoke('size').should == 11 }
38
+ specify { expect { subject.manipulator_invoke('size', 42) }.to raise_error ArgumentError }
39
+ end
40
+ end
41
+
42
+ context 'inherited' do
43
+ let(:klass) do
44
+ Class.new(described_class) do
45
+ def foo
46
+ 'foo'
47
+ end
48
+
49
+ def bar arg1, arg2
50
+ arg1 + arg2
51
+ end
52
+ end
53
+ end
54
+ subject { klass.new }
55
+
56
+ its('manipulator_methods.to_a') { should =~ %w(foo bar) }
57
+ its(:to_manipulator) { should === subject }
58
+ specify { subject.manipulator_invoke('send').should be_nil }
59
+ specify { subject.manipulator_invoke(:bar).should be_nil }
60
+ specify { subject.manipulator_invoke('foo').should == 'foo' }
61
+ specify { subject.manipulator_invoke('bar', 5, 8).should == 13 }
62
+ specify { expect { subject.manipulator_invoke('bar') }.to raise_error ArgumentError }
63
+ end
64
+ end
@@ -0,0 +1,188 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Block do
4
+ def method_missing method, *args, &block
5
+ klass = Hotcell::Calculator::HANDLERS[method] ?
6
+ Hotcell::Calculator : Hotcell::Node
7
+
8
+ instance = Hotcell::Assigner.new *args if method == :ASSIGN
9
+ instance = Hotcell::Summoner.new *args if method == :METHOD
10
+ klass = Hotcell::Arrayer if [:ARRAY, :PAIR].include?(method)
11
+ klass = Hotcell::Hasher if method == :HASH
12
+ klass = Hotcell::Sequencer if method == :SEQUENCE
13
+
14
+ klass = Hotcell::Tag if method == :TAG
15
+ instance = Hotcell::Command.new *args if method == :COMMAND
16
+ instance = Hotcell::Block.new *args if method == :BLOCK
17
+ klass = Hotcell::Joiner if method == :JOINER
18
+
19
+ instance || klass.new(method, *args)
20
+ end
21
+
22
+ let(:context) { Hotcell::Context.new }
23
+
24
+ describe 'complex parsing and rendering' do
25
+ def parse source
26
+ Hotcell::Template.parse(source)
27
+ end
28
+
29
+ let(:if_tag) do
30
+ Class.new(described_class) do
31
+ subcommands :else, :elsif
32
+
33
+ def validate!
34
+ names = subcommands.map { |subcommand| subcommand[:name] }
35
+ valid = names.empty? || (
36
+ names.any? && names.last.in?('elsif', 'else') &&
37
+ names[0..-2].uniq.in?(['elsif'], [])
38
+ )
39
+ raise Hotcell::Errors::BlockError.new 'Invalid if syntax' unless valid
40
+ end
41
+
42
+ def process context, subnodes, condition
43
+ conditions = [[condition]]
44
+ subnodes.each do |subnode|
45
+ if subnode.is_a?(Hash)
46
+ conditions.last[1] = '' if conditions.last[1] == nil
47
+ conditions << (subnode[:name] == 'elsif' ? [subnode[:args].first] : [true])
48
+ else
49
+ conditions.last[1] = subnode
50
+ end
51
+ end
52
+ condition = conditions.detect { |condition| !!condition[0] }
53
+ condition ? condition[1] : nil
54
+ end
55
+ end
56
+ end
57
+
58
+ before { Hotcell.stub(:commands) { {} } }
59
+ before { Hotcell.stub(:blocks) { { 'if' => if_tag } } }
60
+ before { Hotcell.stub(:subcommands) { { 'elsif' => if_tag, 'else' => if_tag } } }
61
+
62
+ context do
63
+ subject(:template) { parse(<<-SOURCE
64
+ {{ if value == 'hello' }}
65
+ hello
66
+ {{ elsif value == 'world' }}
67
+ world
68
+ {{ else }}
69
+ foobar
70
+ {{ end if }}
71
+ SOURCE
72
+ ) }
73
+ specify { subject.render(value: 'hello').strip.should == 'hello' }
74
+ specify { subject.render(value: 'world').strip.should == 'world' }
75
+ specify { subject.render(value: 'foo').strip.should == 'foobar' }
76
+ end
77
+
78
+ context do
79
+ subject(:template) { parse("{{ if value == 'hello' }}{{ elsif value == 'world' }}world{{ end if }}") }
80
+ specify { subject.render(value: 'hello').should == '' }
81
+ specify { subject.render(value: 'world').should == 'world' }
82
+ specify { subject.render(value: 'foo').should == '' }
83
+ end
84
+
85
+ context do
86
+ subject(:template) { parse("{{ if value == 'hello' }}{{ else }}world{{ end if }}") }
87
+ specify { subject.render(value: 'hello').should == '' }
88
+ specify { subject.render(value: 'foo').should == 'world' }
89
+ end
90
+
91
+ context do
92
+ specify { expect {
93
+ parse("{{ if value == 'hello' }}{{ else }}world{{ elsif }}{{ end if }}").syntax
94
+ }.to raise_error Hotcell::Errors::BlockError }
95
+ end
96
+ end
97
+
98
+ describe '.subcommands' do
99
+ subject { Class.new(described_class) }
100
+
101
+ before { subject.subcommands :elsif, :else }
102
+ its(:subcommands) { should == %w(elsif else) }
103
+
104
+ context do
105
+ before { subject.subcommands :when }
106
+ its(:subcommands) { should == %w(elsif else when) }
107
+ end
108
+ end
109
+
110
+ describe '#subcommands' do
111
+ specify { described_class.new('if',
112
+ subnodes: [{name: 'elsif'}, JOINER(), {name: 'else'}, JOINER()]).subcommands.should == [
113
+ {name: 'elsif'}, {name: 'else'}] }
114
+ end
115
+
116
+ describe '#render_subnodes' do
117
+ specify { described_class.new('if',
118
+ subnodes: [{name: 'elsif', args: ARRAY(3, 5)}, JOINER(42), {name: 'else'}, JOINER(43)]
119
+ ).render_subnodes(context).should == [
120
+ {name: 'elsif', args: [3, 5]}, '42', {name: 'else'}, '43'
121
+ ] }
122
+ end
123
+
124
+ describe '#render' do
125
+ let(:block) do
126
+ Class.new(described_class) do
127
+ def process context, subnodes, condition
128
+ "condition #{condition}"
129
+ end
130
+ end
131
+ end
132
+
133
+ specify { block.new('if').render(context).should =~ /ArgumentError/ }
134
+ specify { block.new('if', mode: :silence).render(context).should =~ /ArgumentError/ }
135
+ specify { block.new('if', true).render(context).should == "condition true" }
136
+ specify { block.new('if', true, mode: :silence).render(context).should == '' }
137
+
138
+ context 'assigning' do
139
+ before { subject.render(context) }
140
+
141
+ context do
142
+ subject { block.new('if', assign: 'result') }
143
+ specify { context.key?('result').should be_false }
144
+ end
145
+
146
+ context do
147
+ subject { block.new('if', mode: :silence, assign: 'result') }
148
+ specify { context.key?('result').should be_false }
149
+ end
150
+
151
+ context do
152
+ subject { block.new('if', true, assign: 'result') }
153
+ specify { context['result'].should == "condition true" }
154
+ end
155
+
156
+ context do
157
+ subject { block.new('if', 42, mode: :silence, assign: 'result') }
158
+ specify { context['result'].should == "condition 42" }
159
+ end
160
+ end
161
+ end
162
+
163
+ context '#validate!' do
164
+ let(:block) do
165
+ Class.new(described_class) do
166
+ subcommands :else
167
+ def process context, subnodes, condition
168
+ "condition #{condition}"
169
+ end
170
+ end
171
+ end
172
+
173
+ context do
174
+ subject { block.new('if', true, subnodes: [TEMPLATE('hello')] ) }
175
+ specify { expect { subject.validate! }.not_to raise_error }
176
+ end
177
+
178
+ context do
179
+ subject { block.new('if', true, subnodes: [{ name: 'else' }] ) }
180
+ specify { expect { subject.validate! }.not_to raise_error }
181
+ end
182
+
183
+ context do
184
+ subject { block.new('if', true, subnodes: [{ name: 'case' }] ) }
185
+ specify { expect { subject.validate! }.to raise_error }
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Command do
4
+ let(:context) { Hotcell::Context.new }
5
+
6
+ describe 'complex parsing and rendering' do
7
+ def parse source
8
+ Hotcell::Template.parse(source)
9
+ end
10
+
11
+ let(:include_tag) do
12
+ Class.new(described_class) do
13
+ def validate!
14
+ raise Hotcell::Errors::ArgumentError.new('Template path is required') if children.count != 1
15
+ end
16
+
17
+ def process context, path
18
+ "included #{path}"
19
+ end
20
+ end
21
+ end
22
+
23
+ before { Hotcell.stub(:commands) { { 'include' => include_tag } } }
24
+ before { Hotcell.stub(:blocks) { {} } }
25
+ before { Hotcell.stub(:subcommands) { {} } }
26
+
27
+ specify { parse("{{ include 'template/path' }}").render(context).should == 'included template/path' }
28
+ specify { expect {
29
+ parse("{{ include }}").syntax
30
+ }.to raise_error Hotcell::Errors::ArgumentError }
31
+ end
32
+
33
+ describe '#render' do
34
+ let(:command) do
35
+ Class.new(described_class) do
36
+ def process context, path
37
+ "rendered #{path}"
38
+ end
39
+ end
40
+ end
41
+
42
+ specify { command.new('include').render(context).should =~ /ArgumentError/ }
43
+ specify { command.new('include', mode: :silence).render(context).should =~ /ArgumentError/ }
44
+ specify { command.new('include', 'path').render(context).should == "rendered path" }
45
+ specify { command.new('include', 'path', mode: :silence).render(context).should == '' }
46
+
47
+ context 'assigning' do
48
+ before { subject.render(context) }
49
+
50
+ context do
51
+ subject { command.new('include', assign: 'inclusion') }
52
+ specify { context.key?('inclusion').should be_false }
53
+ end
54
+
55
+ context do
56
+ subject { command.new('include', mode: :silence, assign: 'inclusion') }
57
+ specify { context.key?('inclusion').should be_false }
58
+ end
59
+
60
+ context do
61
+ subject { command.new('include', 'path', assign: 'inclusion') }
62
+ specify { context['inclusion'].should == "rendered path" }
63
+ end
64
+
65
+ context do
66
+ subject { command.new('include', 'path', mode: :silence, assign: 'inclusion') }
67
+ specify { context['inclusion'].should == "rendered path" }
68
+ end
69
+ end
70
+ end
71
+ end