hotcell 0.0.1 → 0.1.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 (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