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.
- 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
|