hotcell 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -1
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -1
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -1
  7. data/README.md +361 -2
  8. data/Rakefile +28 -6
  9. data/ext/lexerc/extconf.rb +3 -0
  10. data/ext/lexerc/lexerc.c +618 -0
  11. data/ext/lexerc/lexerc.h +20 -0
  12. data/ext/lexerc/lexerc.rl +167 -0
  13. data/hotcell.gemspec +8 -7
  14. data/lib/hotcell/commands/case.rb +59 -0
  15. data/lib/hotcell/commands/cycle.rb +38 -0
  16. data/lib/hotcell/commands/for.rb +70 -0
  17. data/lib/hotcell/commands/if.rb +51 -0
  18. data/lib/hotcell/commands/include.rb +21 -0
  19. data/lib/hotcell/commands/scope.rb +13 -0
  20. data/lib/hotcell/commands/unless.rb +23 -0
  21. data/lib/hotcell/commands.rb +13 -0
  22. data/lib/hotcell/config.rb +33 -6
  23. data/lib/hotcell/context.rb +40 -7
  24. data/lib/hotcell/errors.rb +37 -28
  25. data/lib/hotcell/extensions.rb +4 -0
  26. data/lib/hotcell/lexer.rb +19 -635
  27. data/lib/hotcell/lexerr.rb +572 -0
  28. data/lib/hotcell/lexerr.rl +137 -0
  29. data/lib/hotcell/node/assigner.rb +1 -5
  30. data/lib/hotcell/node/block.rb +17 -40
  31. data/lib/hotcell/node/command.rb +29 -22
  32. data/lib/hotcell/node/hasher.rb +1 -1
  33. data/lib/hotcell/node/summoner.rb +2 -6
  34. data/lib/hotcell/node/tag.rb +10 -7
  35. data/lib/hotcell/node.rb +12 -1
  36. data/lib/hotcell/parser.rb +474 -408
  37. data/lib/hotcell/parser.y +175 -117
  38. data/lib/hotcell/resolver.rb +44 -0
  39. data/lib/hotcell/source.rb +35 -0
  40. data/lib/hotcell/template.rb +15 -6
  41. data/lib/hotcell/version.rb +1 -1
  42. data/lib/hotcell.rb +15 -10
  43. data/spec/data/templates/simple.hc +1 -0
  44. data/spec/lib/hotcell/commands/case_spec.rb +39 -0
  45. data/spec/lib/hotcell/commands/cycle_spec.rb +29 -0
  46. data/spec/lib/hotcell/commands/for_spec.rb +65 -0
  47. data/spec/lib/hotcell/commands/if_spec.rb +35 -0
  48. data/spec/lib/hotcell/commands/include_spec.rb +39 -0
  49. data/spec/lib/hotcell/commands/scope_spec.rb +16 -0
  50. data/spec/lib/hotcell/commands/unless_spec.rb +23 -0
  51. data/spec/lib/hotcell/config_spec.rb +35 -10
  52. data/spec/lib/hotcell/context_spec.rb +58 -18
  53. data/spec/lib/hotcell/lexer_spec.rb +37 -28
  54. data/spec/lib/hotcell/node/block_spec.rb +28 -56
  55. data/spec/lib/hotcell/node/command_spec.rb +7 -31
  56. data/spec/lib/hotcell/node/tag_spec.rb +16 -0
  57. data/spec/lib/hotcell/parser_spec.rb +152 -123
  58. data/spec/lib/hotcell/resolver_spec.rb +28 -0
  59. data/spec/lib/hotcell/source_spec.rb +41 -0
  60. data/spec/lib/hotcell/template_spec.rb +47 -4
  61. data/spec/lib/hotcell_spec.rb +2 -1
  62. data/spec/spec_helper.rb +6 -2
  63. metadata +54 -24
  64. data/lib/hotcell/.DS_Store +0 -0
  65. data/lib/hotcell/lexer.rl +0 -299
  66. data/misc/rage.rl +0 -1999
  67. data/misc/unicode2ragel.rb +0 -305
data/lib/hotcell/parser.y CHANGED
@@ -52,146 +52,194 @@ prechigh
52
52
  preclow
53
53
  start document
54
54
  rule
55
- document: document document_unit { val[0].children.push(val[1]) }
56
- | document_unit { result = Joiner.build :JOINER, val[0] }
57
- document_unit: template | command_tag | block_tag | tag
55
+ document: document document_unit { pospoppush(2); val[0].children.push(val[1]) }
56
+ | document_unit { result = build Joiner, :JOINER, val[0], position: pospoppush(1) }
57
+ document_unit: template | tag | block_tag | command_tag
58
58
 
59
59
  template: TEMPLATE { result = val[0] }
60
- tag: TOPEN TCLOSE { result = Tag.build :TAG, mode: TAG_MODES[val[0]] }
61
- | TOPEN sequence TCLOSE { result = Tag.build :TAG, *val[1].flatten, mode: TAG_MODES[val[0]] }
62
-
63
- command_tag: TOPEN assigned_command TCLOSE {
64
- command = val[1][:command]
65
- assign = val[1][:assign]
66
- command.options[:mode] = TAG_MODES[val[0]]
67
- command.options[:assign] = assign if assign
68
- command.validate!
69
- result = command
70
- }
71
- block_open: TOPEN assigned_block TCLOSE {
72
- block = val[1][:block]
73
- assign = val[1][:assign]
74
- block.options[:mode] = TAG_MODES[val[0]]
75
- block.options[:assign] = assign if assign
76
- result = block
77
- }
78
- block_close: TOPEN endblock TCLOSE
79
- block_body: block_body document_unit {
80
- val[0][-1].is_a?(Joiner) ?
81
- val[0][-1].children.push(val[1]) :
82
- val[0].push(Joiner.build(:JOINER, val[1]))
60
+ tag: TOPEN TCLOSE { result = build Tag, :TAG, mode: TAG_MODES[val[0]], position: pospoppush(2) }
61
+ | TOPEN sequence TCLOSE {
62
+ result = build Tag, :TAG, *Array.wrap(val[1]).flatten, mode: TAG_MODES[val[0]], position: pospoppush(3)
63
+ }
64
+
65
+ command_body: COMMAND { result = build @commands[val[0]] || Command, val[0], position: pospoppush(1) }
66
+ | COMMAND arguments {
67
+ result = build @commands[val[0]] || Command, val[0], *val[1], position: pospoppush(2)
68
+ }
69
+ command: command_body
70
+ | IDENTIFER ASSIGN command_body {
71
+ result = build Assigner, val[0], val[2], position: pospoppush(3)
72
+ }
73
+ command_tag: TOPEN command TCLOSE {
74
+ command = val[1].is_a?(Command) ? val[1] : val[1].children[0]
75
+ command.validate!
76
+ result = build Tag, :TAG, val[1], mode: TAG_MODES[val[0]], position: pospoppush(3)
77
+ }
78
+
79
+ subcommand: SUBCOMMAND { result = build @substack.last[val[0]], val[0], position: pospoppush(1) }
80
+ | SUBCOMMAND arguments {
81
+ result = build @substack.last[val[0]], val[0], *val[1], position: pospoppush(2)
82
+ }
83
+ subcommand_tag: TOPEN subcommand TCLOSE { pospoppush(3); result = val[1] }
84
+
85
+ block_body: BLOCK { result = build @blocks[val[0]] || Block, val[0], position: pospoppush(1) }
86
+ | BLOCK arguments {
87
+ result = build @blocks[val[0]] || Block, val[0], *val[1], position: pospoppush(2)
88
+ }
89
+ block_open: block_body
90
+ | IDENTIFER ASSIGN block_body {
91
+ result = build Assigner, val[0], val[2], position: pospoppush(3)
83
92
  }
84
- | block_body subcommand_tag { val[0].push(val[1]) }
85
- | document_unit { result = [Joiner.build(:JOINER, val[0])] }
86
- | subcommand_tag { result = [val[0]] }
87
- block_tag: block_open block_close
88
- | block_open block_body block_close { val[0].options[:subnodes] = val[1]; val[0].validate! }
89
- subcommand_tag: TOPEN subcommand TCLOSE { result = val[1] }
90
-
91
- command: COMMAND { result = Command.build val[0] }
92
- | COMMAND arguments { result = Command.build val[0], *val[1] }
93
- assigned_command: command { result = { command: val[0] } }
94
- | IDENTIFER ASSIGN command { result = { command: val[2], assign: val[0] } }
95
- block: BLOCK { result = Block.build val[0] }
96
- | BLOCK arguments { result = Block.build val[0], *val[1] }
97
- assigned_block: block { result = { block: val[0] } }
98
- | IDENTIFER ASSIGN block { result = { block: val[2], assign: val[0] } }
99
- endblock: ENDBLOCK
100
- | END BLOCK
101
- subcommand: SUBCOMMAND { result = { name: val[0] } }
102
- | SUBCOMMAND arguments { result = { name: val[0], args: Arrayer.build(:ARRAY, *val[1]) } }
103
-
104
- sequence: sequence SEMICOLON sequence { result = val[0].push(val[2]) }
105
- | sequence SEMICOLON
106
- | SEMICOLON sequence { result = val[1] }
93
+ block_close: ENDBLOCK
94
+ | END BLOCK { pospoppush(2) }
95
+ | END
96
+ block_open_tag: TOPEN block_open TCLOSE {
97
+ result = build Tag, :TAG, val[1], mode: TAG_MODES[val[0]], position: pospoppush(3)
98
+ }
99
+ block_close_tag: TOPEN block_close TCLOSE { pospoppush(3) }
100
+ block_subnodes: block_subnodes document_unit {
101
+ pospoppush(2)
102
+ val[0][-1].is_a?(Joiner) ?
103
+ val[0][-1].children.push(val[1]) :
104
+ val[0].push(build(Joiner, :JOINER, val[1]))
105
+ }
106
+ | block_subnodes subcommand_tag { pospoppush(2); val[0].push(val[1]) }
107
+ | document_unit { result = [build(Joiner, :JOINER, val[0], position: pospoppush(1))] }
108
+ | subcommand_tag { result = [val[0]] }
109
+ block_tag: block_open_tag block_close_tag {
110
+ pospoppush(2)
111
+ block = val[0].children[0].is_a?(Block) ?
112
+ val[0].children[0] : val[0].children[0].children[0]
113
+ block.validate!
114
+ }
115
+ | block_open_tag block_subnodes block_close_tag {
116
+ pospoppush(3)
117
+ block = val[0].children[0].is_a?(Block) ?
118
+ val[0].children[0] : val[0].children[0].children[0]
119
+ block.options[:subnodes] = val[1]
120
+ block.validate!
121
+ }
122
+
123
+ sequence: sequence SEMICOLON sequence { pospoppush(2); result = val[0].push(val[2]) }
124
+ | sequence SEMICOLON { pospoppush(2) }
125
+ | SEMICOLON sequence { pospoppush(2, 1); result = val[1] }
107
126
  | SEMICOLON { result = [] }
108
- | sequence NEWLINE sequence { result = val[0].push(val[2]) }
109
- | sequence NEWLINE
110
- | NEWLINE sequence { result = val[1] }
127
+ | sequence NEWLINE sequence { pospoppush(2); result = val[0].push(val[2]) }
128
+ | sequence NEWLINE { pospoppush(2) }
129
+ | NEWLINE sequence { pospoppush(2, 1); result = val[1] }
111
130
  | NEWLINE { result = [] }
112
131
  | expr { result = [val[0]] }
113
132
 
114
- expr: expr MULTIPLY expr { result = Calculator.build :MULTIPLY, val[0], val[2] }
115
- | expr POWER expr { result = Calculator.build :POWER, val[0], val[2] }
116
- | expr DIVIDE expr { result = Calculator.build :DIVIDE, val[0], val[2] }
117
- | expr PLUS expr { result = Calculator.build :PLUS, val[0], val[2] }
118
- | expr MINUS expr { result = Calculator.build :MINUS, val[0], val[2] }
119
- | expr MODULO expr { result = Calculator.build :MODULO, val[0], val[2] }
120
- | MINUS expr =UMINUS { result = Calculator.build :UMINUS, val[1] }
121
- | PLUS expr =UPLUS { result = Calculator.build :UPLUS, val[1] }
122
- | expr AND expr { result = Calculator.build :AND, val[0], val[2] }
123
- | expr OR expr { result = Calculator.build :OR, val[0], val[2] }
124
- | expr GT expr { result = Calculator.build :GT, val[0], val[2] }
125
- | expr GTE expr { result = Calculator.build :GTE, val[0], val[2] }
126
- | expr LT expr { result = Calculator.build :LT, val[0], val[2] }
127
- | expr LTE expr { result = Calculator.build :LTE, val[0], val[2] }
128
- | expr EQUAL expr { result = Calculator.build :EQUAL, val[0], val[2] }
129
- | expr INEQUAL expr { result = Calculator.build :INEQUAL, val[0], val[2] }
130
- | NOT expr { result = Calculator.build :NOT, val[1] }
131
- | IDENTIFER ASSIGN expr { result = Assigner.build val[0], val[2] }
132
- | expr PERIOD method { val[2].children[0] = val[0]; result = val[2] }
133
- | expr AOPEN arguments ACLOSE { result = Summoner.build val[0], '[]', *val[2] }
134
- | POPEN PCLOSE { result = nil }
133
+ expr: expr MULTIPLY expr { result = build Calculator, :MULTIPLY, val[0], val[2], position: pospoppush(3) }
134
+ | expr POWER expr { result = build Calculator, :POWER, val[0], val[2], position: pospoppush(3) }
135
+ | expr DIVIDE expr { result = build Calculator, :DIVIDE, val[0], val[2], position: pospoppush(3) }
136
+ | expr PLUS expr { result = build Calculator, :PLUS, val[0], val[2], position: pospoppush(3) }
137
+ | expr MINUS expr { result = build Calculator, :MINUS, val[0], val[2], position: pospoppush(3) }
138
+ | expr MODULO expr { result = build Calculator, :MODULO, val[0], val[2], position: pospoppush(3) }
139
+ | MINUS expr =UMINUS { result = build Calculator, :UMINUS, val[1], position: pospoppush(2) }
140
+ | PLUS expr =UPLUS { result = build Calculator, :UPLUS, val[1], position: pospoppush(2) }
141
+ | expr AND expr { result = build Calculator, :AND, val[0], val[2], position: pospoppush(3) }
142
+ | expr OR expr { result = build Calculator, :OR, val[0], val[2], position: pospoppush(3) }
143
+ | expr GT expr { result = build Calculator, :GT, val[0], val[2], position: pospoppush(3) }
144
+ | expr GTE expr { result = build Calculator, :GTE, val[0], val[2], position: pospoppush(3) }
145
+ | expr LT expr { result = build Calculator, :LT, val[0], val[2], position: pospoppush(3) }
146
+ | expr LTE expr { result = build Calculator, :LTE, val[0], val[2], position: pospoppush(3) }
147
+ | expr EQUAL expr { result = build Calculator, :EQUAL, val[0], val[2], position: pospoppush(3) }
148
+ | expr INEQUAL expr { result = build Calculator, :INEQUAL, val[0], val[2], position: pospoppush(3) }
149
+ | NOT expr { result = build Calculator, :NOT, val[1], position: pospoppush(2) }
150
+ | IDENTIFER ASSIGN expr { result = build Assigner, val[0], val[2], position: pospoppush(3) }
151
+ | expr PERIOD method { pospoppush(3); val[2].children[0] = val[0]; result = val[2] }
152
+ | expr AOPEN arguments ACLOSE {
153
+ result = build Summoner, 'manipulator_brackets', val[0], *val[2], position: pospoppush(4)
154
+ }
155
+ | POPEN PCLOSE { pospoppush(2); result = nil }
135
156
  | POPEN sequence PCLOSE {
136
- result = case val[1].size
137
- when 0
138
- nil
139
- when 1
140
- val[1][0]
141
- else
142
- Sequencer.build :SEQUENCE, *val[1].flatten
143
- end
144
- }
157
+ position = pospoppush(3)
158
+ result = case val[1].size
159
+ when 1
160
+ val[1][0]
161
+ else
162
+ build Sequencer, :SEQUENCE, *val[1].flatten, position: position
163
+ end
164
+ }
145
165
  | value
146
166
 
147
- value: const | number | string | array | hash | method# | ternary
167
+ value: const | number | string | array | hash | method
148
168
 
149
169
  const: NIL | TRUE | FALSE
150
170
  number: INTEGER | FLOAT
151
171
  string: STRING | REGEXP
152
172
 
153
- # ternary: expr QUESTION expr COLON expr =TERNARY { result = Node.build :TERNARY, val[0], val[2], val[4] }
154
-
155
- array: AOPEN ACLOSE { result = Arrayer.build :ARRAY }
156
- | AOPEN params ACLOSE { result = Arrayer.build :ARRAY, *val[1] }
157
- params: params COMMA expr { val[0].push(val[2]) }
173
+ array: AOPEN ACLOSE { result = build Arrayer, :ARRAY, position: pospoppush(2) }
174
+ | AOPEN params ACLOSE { result = build Arrayer, :ARRAY, *val[1], position: pospoppush(3) }
175
+ params: params COMMA expr { pospoppush(3); val[0].push(val[2]) }
158
176
  | expr { result = [val[0]] }
159
177
 
160
- hash: HOPEN HCLOSE { result = Hasher.build :HASH }
161
- | HOPEN pairs HCLOSE { result = Hasher.build :HASH, *val[1] }
162
- pairs: pairs COMMA pair { val[0].push(val[2]) }
178
+ hash: HOPEN HCLOSE { result = build Hasher, :HASH, position: pospoppush(2) }
179
+ | HOPEN pairs HCLOSE { result = build Hasher, :HASH, *val[1], position: pospoppush(3) }
180
+ pairs: pairs COMMA pair { pospoppush(3); val[0].push(val[2]) }
163
181
  | pair { result = [val[0]] }
164
- pair: IDENTIFER COLON expr { result = Arrayer.build :PAIR, val[0], val[2] }
182
+ pair: IDENTIFER COLON expr { result = build Arrayer, :PAIR, val[0], val[2], position: pospoppush(3) }
165
183
 
166
- arguments: params COMMA pairs { result = [*val[0], Hasher.build(:HASH, *val[2])] }
184
+ arguments: params COMMA pairs { result = [*val[0], build(Hasher, :HASH, *val[2], position: pospoppush(3))] }
167
185
  | params
168
- | pairs { result = Hasher.build(:HASH, *val[0]) }
169
- method: IDENTIFER { result = Summoner.build nil, val[0] }
170
- | IDENTIFER POPEN PCLOSE { result = Summoner.build nil, val[0] }
171
- | IDENTIFER POPEN arguments PCLOSE { result = Summoner.build nil, val[0], *val[2] }
186
+ | pairs { result = build Hasher, :HASH, *val[0], position: pospoppush(1) }
187
+ method: IDENTIFER { result = build Summoner, val[0], position: pospoppush(1) }
188
+ | IDENTIFER POPEN PCLOSE { result = build Summoner, val[0], position: pospoppush(3) }
189
+ | IDENTIFER POPEN arguments PCLOSE {
190
+ result = build Summoner, val[0], nil, *val[2], position: pospoppush(4)
191
+ }
172
192
 
173
- ---- header
174
- require 'hotcell/lexer'
175
193
  ---- inner
176
- NEWLINE_PRED = Set.new(Lexer::BOPEN.values + Lexer::OPERATIONS.values)
177
- NEWLINE_NEXT = Set.new(Lexer::BCLOSE.values + [:NEWLINE])
194
+ OPERATIONS = {
195
+ '+' => :PLUS, '-' => :MINUS, '*' => :MULTIPLY, '**' => :POWER, '/' => :DIVIDE, '%' => :MODULO,
196
+
197
+ '&&' => :AND, '||' => :OR, '!' => :NOT, '==' => :EQUAL, '!=' => :INEQUAL,
198
+ '>' => :GT, '>=' => :GTE, '<' => :LT, '<=' => :LTE,
199
+
200
+ '=' => :ASSIGN, ',' => :COMMA, '.' => :PERIOD, ':' => :COLON, '?' => :QUESTION,
201
+ ';' => :SEMICOLON
202
+ }
203
+
204
+ BOPEN = { '[' => :AOPEN, '{' => :HOPEN, '(' => :POPEN }
205
+ BCLOSE = { ']' => :ACLOSE, '}' => :HCLOSE, ')' => :PCLOSE }
206
+
207
+ NEWLINE_PRED = Set.new(BOPEN.values + OPERATIONS.values)
208
+ NEWLINE_NEXT = Set.new(BCLOSE.values + [:NEWLINE])
178
209
 
179
210
  TAG_MODES = { '{{' => :normal, '{{!' => :silence }
180
211
 
181
- def initialize string, options = {}
182
- @lexer = Lexer.new(string)
212
+ def initialize source, options = {}
213
+ @source = Source.wrap(source)
214
+ @lexer = Lexer.new(source)
183
215
  @tokens = @lexer.tokens
184
216
  @position = -1
185
217
 
186
- @commands = Set.new(Array.wrap(options[:commands]).map(&:to_s))
187
- @blocks = Set.new(Array.wrap(options[:blocks]).map(&:to_s))
188
- @endblocks = Set.new(Array.wrap(options[:blocks]).map { |identifer| "end#{identifer}" })
189
- @subcommands = Set.new(Array.wrap(options[:subcommands]).map(&:to_s))
218
+ @commands = options[:commands] || {}
219
+ @blocks = options[:blocks] || {}
220
+ @endblocks = Set.new(@blocks.keys.map { |identifer| "end#{identifer}" })
221
+
222
+ @substack = []
223
+ @posstack = []
224
+ end
225
+
226
+ def build klass, *args
227
+ options = args.extract_options!
228
+ options[:source] = @source
229
+ klass.build *args.push(options)
230
+ end
231
+
232
+ def pospoppush pop, push = 0
233
+ # because fuck the brains, that's why!
234
+ last = @posstack.pop
235
+ reduced = @posstack.push(@posstack.pop(pop)[push])[-1]
236
+ @posstack.push last
237
+ reduced
190
238
  end
191
239
 
192
240
  def parse
193
241
  if @tokens.size == 0
194
- Joiner.build :JOINER
242
+ build Joiner, :JOINER, position: 0
195
243
  else
196
244
  do_parse
197
245
  end
@@ -199,27 +247,32 @@ rule
199
247
 
200
248
  def next_token
201
249
  @position = @position + 1
202
-
203
250
  tcurr = @tokens[@position]
204
- tnext = @tokens[@position.next]
205
- tpred = @tokens[@position.pred]
206
251
 
207
252
  if tcurr && (tcurr[0] == :COMMENT || tcurr[0] == :NEWLINE && (
208
- (tpred && NEWLINE_PRED.include?(tpred[0])) ||
209
- (tnext && NEWLINE_NEXT.include?(tnext[0]))
253
+ ((tpred = @tokens[@position.pred]) && NEWLINE_PRED.include?(tpred[0])) ||
254
+ ((tnext = @tokens[@position.next]) && NEWLINE_NEXT.include?(tnext[0]))
210
255
  ))
211
256
  next_token
212
257
  else
258
+ if tcurr
259
+ @posstack << tcurr[1][1]
260
+ tcurr = [tcurr[0], tcurr[1][0]]
261
+ end
262
+
213
263
  if tcurr && tcurr[0] == :IDENTIFER
214
- if @commands.include?(tcurr[1])
264
+ if @commands.key?(tcurr[1])
215
265
  [:COMMAND, tcurr[1]]
216
- elsif @blocks.include?(tcurr[1])
266
+ elsif @blocks.key?(tcurr[1])
267
+ @substack.push(@blocks[tcurr[1]].subcommands)
217
268
  [:BLOCK, tcurr[1]]
269
+ elsif @substack.last && @substack.last.key?(tcurr[1])
270
+ [:SUBCOMMAND, tcurr[1]]
218
271
  elsif @endblocks.include?(tcurr[1])
272
+ @substack.pop
219
273
  [:ENDBLOCK, tcurr[1]]
220
- elsif @subcommands.include?(tcurr[1])
221
- [:SUBCOMMAND, tcurr[1]]
222
274
  elsif tcurr[1] == 'end'
275
+ @substack.pop
223
276
  [:END, tcurr[1]]
224
277
  else
225
278
  tcurr
@@ -229,3 +282,8 @@ rule
229
282
  end
230
283
  end
231
284
  end
285
+
286
+ def on_error(token, value, vstack)
287
+ raise Hotcell::UnexpectedLexem.new("#{token_to_str(token) || '?'} `#{value}`",
288
+ *@source.info(@posstack.last).values_at(:line, :column))
289
+ end
@@ -0,0 +1,44 @@
1
+ module Hotcell
2
+ # Base template resolving class for `include` command.
3
+ # Please inherit your own resolvers from it.
4
+ class Resolver
5
+ def template path, context = nil
6
+ cache(path, context) do
7
+ source = resolve path, context
8
+ Template.parse(source)
9
+ end
10
+ end
11
+
12
+ # Returns template source
13
+ # Not implemented by default
14
+ def resolve path, context = nil
15
+ raise NotImplementedError, 'Default resolver`s template resolve function is not implemented'
16
+ end
17
+
18
+ # Caches parsed template
19
+ # No cache by default
20
+ def cache path, context = nil, &block
21
+ @cache ||= {}
22
+ @cache[path] ||= block.call
23
+ end
24
+ end
25
+
26
+ # Basic file system resolver class.
27
+ # Ex:
28
+ # resolver = Hotcell::FileSystemResolver.new('/Users/username/work/project/app/views')
29
+ # resolver.template('articles/new') #=> returns Hotcell::Template instance
30
+ # # for `/Users/username/work/project/app/views/articles/new.hc`
31
+ class FileSystemResolver < Resolver
32
+ attr_reader :root
33
+
34
+ def initialize root
35
+ @root = root.to_s
36
+ end
37
+
38
+ def resolve path, context = nil
39
+ full_path = File.expand_path path, root
40
+ full_path = "#{full_path}.hc"
41
+ File.read(full_path)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Hotcell
2
+ class Source
3
+ PACK_MODE = 'c*'
4
+
5
+ attr_reader :source, :file
6
+
7
+ def self.wrap source, *args
8
+ source.is_a?(Hotcell::Source) ? source : Source.new(source, *args)
9
+ end
10
+
11
+ def initialize source, file = nil
12
+ @source, @file = source, file
13
+ end
14
+
15
+ def encoding
16
+ 'UTF-8'
17
+ end
18
+
19
+ def data
20
+ @data ||= source.unpack(PACK_MODE)
21
+ end
22
+
23
+ def info position
24
+ parsed = data[0..position]
25
+ line = parsed.count(10) + 1
26
+ lastnl = (parsed.rindex(10) || -1) + 1
27
+ column = parsed[lastnl..position].pack(PACK_MODE).force_encoding(encoding).size
28
+ { line: line, column: column }
29
+ end
30
+
31
+ def inspect
32
+ "Source: `#{source}`"
33
+ end
34
+ end
35
+ end
@@ -4,25 +4,34 @@ module Hotcell
4
4
 
5
5
  def self.parse source
6
6
  new source,
7
- commands: Hotcell.commands.keys,
8
- blocks: Hotcell.blocks.keys,
9
- subcommands: Hotcell.subcommands.keys
7
+ commands: Hotcell.commands,
8
+ blocks: Hotcell.blocks
10
9
  end
11
10
 
12
11
  def initialize source, options = {}
13
- @source = source
12
+ @source = Source.wrap(source, options.delete(:file))
14
13
  @options = options
15
14
  end
16
15
 
17
16
  def syntax
18
- @syntax ||= Parser.new(source, options.slice(:commands, :blocks, :subcommands)).parse
17
+ @syntax ||= Parser.new(source, options.slice(:commands, :blocks)).parse
19
18
  end
20
19
 
21
20
  def render context = {}
22
21
  if context.is_a?(Context)
23
22
  syntax.render(context)
24
23
  else
25
- syntax.render(Context.new(context))
24
+ default_context = { helpers: Hotcell.helpers }
25
+ syntax.render(Context.new(default_context.merge!(context)))
26
+ end
27
+ end
28
+
29
+ def render! context = {}
30
+ if context.is_a?(Context)
31
+ context.options[:reraise] = true
32
+ render context
33
+ else
34
+ render context.merge(reraise: true)
26
35
  end
27
36
  end
28
37
  end
@@ -1,3 +1,3 @@
1
1
  module Hotcell
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/hotcell.rb CHANGED
@@ -1,19 +1,24 @@
1
1
  require 'active_support/all'
2
2
  require 'hotcell/version'
3
+ require 'hotcell/resolver'
3
4
  require 'hotcell/config'
4
5
  require 'hotcell/errors'
6
+
7
+ module Hotcell
8
+ def self.config; Config.instance; end
9
+
10
+ singleton_class.delegate :commands, :blocks, :helpers, :register_command, :register_helpers,
11
+ :resolver, :resolver=, to: :config
12
+ end
13
+
14
+ require 'hotcell/manipulator'
15
+ require 'hotcell/extensions'
16
+ require 'hotcell/source'
5
17
  require 'hotcell/lexer'
18
+ require 'hotcell/lexerr'
19
+ require 'hotcell/lexerc'
6
20
  require 'hotcell/parser'
7
21
  require 'hotcell/node'
22
+ require 'hotcell/commands'
8
23
  require 'hotcell/context'
9
- require 'hotcell/manipulator'
10
- require 'hotcell/extensions'
11
24
  require 'hotcell/template'
12
-
13
- module Hotcell
14
- [:commands, :blocks, :subcommands, :register_command].each do |method|
15
- define_singleton_method method do
16
- Config.instance.send(method)
17
- end
18
- end
19
- end
@@ -0,0 +1 @@
1
+ Hello, {{ name }}
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Commands::If do
4
+ def parse source
5
+ Hotcell::Template.parse(source)
6
+ end
7
+
8
+ describe '#validate!' do
9
+ specify { expect { parse('{{ case }}{{ end case }}').syntax
10
+ }.to raise_error Hotcell::ArgumentError, 'Wrond number of arguments for `case` (0 for 1) at 1:4' }
11
+ specify { expect { parse('{{ case 42 }}{{ when }}{{ end case }}').syntax
12
+ }.to raise_error Hotcell::ArgumentError, 'Wrond number of arguments for `when` (0 for 1..Infinity) at 1:17' }
13
+ specify { expect { parse('{{ case 42 }}{{ when 42 }}{{ else 42 }}{{ end case }}').syntax
14
+ }.to raise_error Hotcell::ArgumentError, 'Wrond number of arguments for `else` (1 for 0) at 1:30' }
15
+ specify { expect { parse('{{ case 42 }}{{ end case }}').syntax
16
+ }.to raise_error Hotcell::BlockError, 'Expected `when` for `case` command at 1:4' }
17
+ specify { expect { parse('{{ case 42 }}foo{{ when 42 }}{{ end case }}').syntax
18
+ }.to raise_error Hotcell::BlockError, 'Expected `when` for `case` command at 1:12' }
19
+ specify { expect { parse('{{ case 42 }}{{ else }}{{ end case }}').syntax
20
+ }.to raise_error Hotcell::BlockError, 'Unexpected `else` for `case` command at 1:17' }
21
+ specify { expect { parse('{{ case 42 }}{{ when 42 }}{{ else }}{{ when 43 }}{{ end case }}').syntax
22
+ }.to raise_error Hotcell::BlockError, 'Unexpected `else` for `case` command at 1:30' }
23
+ specify { expect { parse('{{ case 42 }}{{ when 42 }}{{ else }}{{ else }}{{ end case }}').syntax
24
+ }.to raise_error Hotcell::BlockError, 'Unexpected `else` for `case` command at 1:30' }
25
+ end
26
+
27
+ describe '#render' do
28
+ specify { parse('{{ case 42 }}{{ when 42 }}Hello{{ end case }}').render.should == 'Hello' }
29
+ specify { parse('{{ case 42 }}{{ when 43 }}Hello{{ end case }}').render.should == '' }
30
+ specify { parse('{{ case 42 }}{{ when 42 }}Hello{{ when 43}}World{{ end case }}').render.should == 'Hello' }
31
+ specify { parse('{{ case 43 }}{{ when 42 }}Hello{{ when 43}}World{{ end case }}').render.should == 'World' }
32
+ specify { parse('{{ case 42 }}{{ when 42, 43 }}Hello{{ else }}World{{ end case }}').render.should == 'Hello' }
33
+ specify { parse('{{ case 43 }}{{ when 42, 43 }}Hello{{ else }}World{{ end case }}').render.should == 'Hello' }
34
+ specify { parse('{{ case 44 }}{{ when 42, 43 }}Hello{{ else }}World{{ end case }}').render.should == 'World' }
35
+ specify { parse("{{ case 'hello' }}{{ when 'hello', 'world' }}Hello{{ else }}World{{ end case }}").render.should == 'Hello' }
36
+ specify { parse("{{ case 'world' }}{{ when 'hello', 'world' }}Hello{{ else }}World{{ end case }}").render.should == 'Hello' }
37
+ specify { parse("{{ case 'foo' }}{{ when 'hello', 'world' }}Hello{{ else }}World{{ end case }}").render.should == 'World' }
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Commands::Cycle do
4
+ def parse source
5
+ Hotcell::Template.parse(source)
6
+ end
7
+
8
+ describe '#normalize_arguments' do
9
+ subject { described_class.new 'cycle' }
10
+ specify { subject.normalize_arguments([42]).should == [[42], subject.object_id.to_s] }
11
+ specify { subject.normalize_arguments([42, 43, 44]).should == [[42, 43, 44], subject.object_id.to_s] }
12
+ specify { subject.normalize_arguments(['hello' => 42]).should == [[42], 'hello'] }
13
+ specify { subject.normalize_arguments(['hello' => [42, 43, 44]]).should == [[42, 43, 44], 'hello'] }
14
+ specify { subject.normalize_arguments([42, { 'group' => 'hello' }]).should == [[42], 'hello'] }
15
+ specify { subject.normalize_arguments([42, 43, { 'group' => 'hello' }]).should == [[42, 43], 'hello'] }
16
+ end
17
+
18
+ describe '#render' do
19
+ specify { parse(
20
+ "{{ cycle 'one', 'two', 'three' }} {{ cycle 'one', 'two', 'three' }}"
21
+ ).render.should == 'one one' }
22
+ specify { parse(
23
+ "{{ cycle count: ['one', 'two', 'three'] }} {{ cycle 'one', 'two', 'three', group: 'count' }}"
24
+ ).render.should == 'one two' }
25
+ specify { parse(
26
+ "{{ for i, in: [1, 2, 3], loop: true }}{{ i }} {{ cycle ['one', 'two', 'three'] }}{{ unless loop.last? }}, {{ end unless }}{{ end for }}"
27
+ ).render.should == '1 one, 2 two, 3 three' }
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hotcell::Commands::For do
4
+ def parse source
5
+ Hotcell::Template.parse(source)
6
+ end
7
+
8
+ describe '#validate!' do
9
+ specify { expect { parse('{{ for }}{{ end for }}').syntax
10
+ }.to raise_error Hotcell::ArgumentError, 'Wrond number of arguments for `for` (0 for 2) at 1:4' }
11
+ specify { expect { parse('{{ for item }}{{ end for }}').syntax
12
+ }.to raise_error Hotcell::ArgumentError, 'Wrond number of arguments for `for` (1 for 2) at 1:4' }
13
+ specify { expect { parse("{{ for 'item', in: [] }}{{ end for }}").syntax
14
+ }.to raise_error Hotcell::SyntaxError, 'Expected IDENTIFER as first argument in `for` command at 1:4' }
15
+ end
16
+
17
+ describe '#render' do
18
+ specify { parse('{{ for item, in: [1, 2, 3] }}{{ end for }}').render.should == '' }
19
+ specify { parse('{{ for item, in: [1, 2, 3] }}{{ item }}{{ end for }}').render.should == '123' }
20
+ specify { parse(
21
+ '{{ for item, in: [1, 2, 3] }}{{ item }} * 3 = {{ item * 3 }}; {{ end for }}'
22
+ ).render(reraise: true).should == '1 * 3 = 3; 2 * 3 = 6; 3 * 3 = 9; ' }
23
+ specify { parse(
24
+ '{{ for item, in: [5, 6, 7], loop: true }}{{ loop.index }}{{ loop.last? }}{{ end for }}'
25
+ ).render.should == '0false1false2true' }
26
+ end
27
+ end
28
+
29
+ describe Hotcell::Commands::For::Forloop do
30
+ subject { described_class.new([5, 6, 7, 8], 2) }
31
+
32
+ its(:prev) { should == 6 }
33
+ its(:next) { should == 8 }
34
+ its(:length) { should == 4 }
35
+ its(:size) { should == 4 }
36
+ its(:count) { should == 4 }
37
+ its(:index) { should == 2 }
38
+ its(:rindex) { should == 1 }
39
+ its(:first) { should == false }
40
+ its(:first?) { should == false }
41
+ its(:last) { should == false }
42
+ its(:last?) { should == false }
43
+
44
+ context do
45
+ subject { described_class.new([5, 6, 7, 8], 0) }
46
+
47
+ its(:prev) { should be_nil }
48
+ its(:next) { should == 6 }
49
+ its(:first) { should be_true }
50
+ its(:first?) { should be_true }
51
+ its(:last) { should be_false }
52
+ its(:last?) { should be_false }
53
+ end
54
+
55
+ context do
56
+ subject { described_class.new([5, 6, 7, 8], 3) }
57
+
58
+ its(:prev) { should == 7 }
59
+ its(:next) { should be_nil }
60
+ its(:first) { should be_false }
61
+ its(:first?) { should be_false }
62
+ its(:last) { should be_true }
63
+ its(:last?) { should be_true }
64
+ end
65
+ end