hotcell 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -1
- data/.rspec +1 -0
- data/.rvmrc +1 -1
- data/.travis.yml +7 -0
- data/Gemfile +4 -1
- data/README.md +361 -2
- data/Rakefile +28 -6
- data/ext/lexerc/extconf.rb +3 -0
- data/ext/lexerc/lexerc.c +618 -0
- data/ext/lexerc/lexerc.h +20 -0
- data/ext/lexerc/lexerc.rl +167 -0
- data/hotcell.gemspec +8 -7
- data/lib/hotcell/commands/case.rb +59 -0
- data/lib/hotcell/commands/cycle.rb +38 -0
- data/lib/hotcell/commands/for.rb +70 -0
- data/lib/hotcell/commands/if.rb +51 -0
- data/lib/hotcell/commands/include.rb +21 -0
- data/lib/hotcell/commands/scope.rb +13 -0
- data/lib/hotcell/commands/unless.rb +23 -0
- data/lib/hotcell/commands.rb +13 -0
- data/lib/hotcell/config.rb +33 -6
- data/lib/hotcell/context.rb +40 -7
- data/lib/hotcell/errors.rb +37 -28
- data/lib/hotcell/extensions.rb +4 -0
- data/lib/hotcell/lexer.rb +19 -635
- data/lib/hotcell/lexerr.rb +572 -0
- data/lib/hotcell/lexerr.rl +137 -0
- data/lib/hotcell/node/assigner.rb +1 -5
- data/lib/hotcell/node/block.rb +17 -40
- data/lib/hotcell/node/command.rb +29 -22
- data/lib/hotcell/node/hasher.rb +1 -1
- data/lib/hotcell/node/summoner.rb +2 -6
- data/lib/hotcell/node/tag.rb +10 -7
- data/lib/hotcell/node.rb +12 -1
- data/lib/hotcell/parser.rb +474 -408
- data/lib/hotcell/parser.y +175 -117
- data/lib/hotcell/resolver.rb +44 -0
- data/lib/hotcell/source.rb +35 -0
- data/lib/hotcell/template.rb +15 -6
- data/lib/hotcell/version.rb +1 -1
- data/lib/hotcell.rb +15 -10
- data/spec/data/templates/simple.hc +1 -0
- data/spec/lib/hotcell/commands/case_spec.rb +39 -0
- data/spec/lib/hotcell/commands/cycle_spec.rb +29 -0
- data/spec/lib/hotcell/commands/for_spec.rb +65 -0
- data/spec/lib/hotcell/commands/if_spec.rb +35 -0
- data/spec/lib/hotcell/commands/include_spec.rb +39 -0
- data/spec/lib/hotcell/commands/scope_spec.rb +16 -0
- data/spec/lib/hotcell/commands/unless_spec.rb +23 -0
- data/spec/lib/hotcell/config_spec.rb +35 -10
- data/spec/lib/hotcell/context_spec.rb +58 -18
- data/spec/lib/hotcell/lexer_spec.rb +37 -28
- data/spec/lib/hotcell/node/block_spec.rb +28 -56
- data/spec/lib/hotcell/node/command_spec.rb +7 -31
- data/spec/lib/hotcell/node/tag_spec.rb +16 -0
- data/spec/lib/hotcell/parser_spec.rb +152 -123
- data/spec/lib/hotcell/resolver_spec.rb +28 -0
- data/spec/lib/hotcell/source_spec.rb +41 -0
- data/spec/lib/hotcell/template_spec.rb +47 -4
- data/spec/lib/hotcell_spec.rb +2 -1
- data/spec/spec_helper.rb +6 -2
- metadata +54 -24
- data/lib/hotcell/.DS_Store +0 -0
- data/lib/hotcell/lexer.rl +0 -299
- data/misc/rage.rl +0 -1999
- 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 =
|
57
|
-
document_unit: template |
|
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 =
|
61
|
-
| TOPEN sequence TCLOSE {
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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 =
|
115
|
-
| expr POWER expr { result =
|
116
|
-
| expr DIVIDE expr { result =
|
117
|
-
| expr PLUS expr { result =
|
118
|
-
| expr MINUS expr { result =
|
119
|
-
| expr MODULO expr { result =
|
120
|
-
| MINUS expr =UMINUS { result =
|
121
|
-
| PLUS expr =UPLUS { result =
|
122
|
-
| expr AND expr { result =
|
123
|
-
| expr OR expr { result =
|
124
|
-
| expr GT expr { result =
|
125
|
-
| expr GTE expr { result =
|
126
|
-
| expr LT expr { result =
|
127
|
-
| expr LTE expr { result =
|
128
|
-
| expr EQUAL expr { result =
|
129
|
-
| expr INEQUAL expr { result =
|
130
|
-
| NOT expr { result =
|
131
|
-
| IDENTIFER ASSIGN expr { result =
|
132
|
-
| expr PERIOD method { val[2].children[0] = val[0]; result = val[2] }
|
133
|
-
| expr AOPEN arguments ACLOSE {
|
134
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
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
|
-
|
154
|
-
|
155
|
-
|
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 =
|
161
|
-
| HOPEN pairs HCLOSE { result =
|
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 =
|
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],
|
184
|
+
arguments: params COMMA pairs { result = [*val[0], build(Hasher, :HASH, *val[2], position: pospoppush(3))] }
|
167
185
|
| params
|
168
|
-
| pairs { result = Hasher
|
169
|
-
method: IDENTIFER { result =
|
170
|
-
| IDENTIFER POPEN PCLOSE { result =
|
171
|
-
| IDENTIFER POPEN arguments PCLOSE {
|
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
|
-
|
177
|
-
|
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
|
182
|
-
@
|
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 =
|
187
|
-
@blocks =
|
188
|
-
@endblocks = Set.new(
|
189
|
-
|
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
|
-
|
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.
|
264
|
+
if @commands.key?(tcurr[1])
|
215
265
|
[:COMMAND, tcurr[1]]
|
216
|
-
elsif @blocks.
|
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
|
data/lib/hotcell/template.rb
CHANGED
@@ -4,25 +4,34 @@ module Hotcell
|
|
4
4
|
|
5
5
|
def self.parse source
|
6
6
|
new source,
|
7
|
-
commands: Hotcell.commands
|
8
|
-
blocks: Hotcell.blocks
|
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
|
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
|
-
|
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
|
data/lib/hotcell/version.rb
CHANGED
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
|