erblite 0.5.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/LICENSE.md +21 -0
- data/README.md +19 -0
- data/bin/erblite +53 -0
- data/lib/erblite/builtins.rb +1332 -0
- data/lib/erblite/error.rb +52 -0
- data/lib/erblite/format.rb +212 -0
- data/lib/erblite/interpreter.rb +486 -0
- data/lib/erblite/json.rb +331 -0
- data/lib/erblite/lexer.rb +649 -0
- data/lib/erblite/nodes.rb +46 -0
- data/lib/erblite/parser.rb +953 -0
- data/lib/erblite/sandbox.rb +192 -0
- data/lib/erblite/values.rb +139 -0
- data/lib/erblite/version.rb +13 -0
- data/lib/erblite.rb +134 -0
- metadata +57 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# Copyright 2026 OpenC3, Inc.
|
|
2
|
+
# All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# This program is distributed in the hope that it will be useful,
|
|
5
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
6
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
7
|
+
# See LICENSE.md for more details.
|
|
8
|
+
|
|
9
|
+
# frozen_string_literal: true
|
|
10
|
+
|
|
11
|
+
module ERBLite
|
|
12
|
+
# Render-wide state shared by a template and all of its partials.
|
|
13
|
+
class State
|
|
14
|
+
attr_accessor :steps
|
|
15
|
+
attr_reader :max_steps, :base_dir, :real_base, :cache
|
|
16
|
+
|
|
17
|
+
def initialize(base_dir, max_steps)
|
|
18
|
+
@steps = 0
|
|
19
|
+
@max_steps = max_steps
|
|
20
|
+
@base_dir = base_dir
|
|
21
|
+
@real_base = nil
|
|
22
|
+
if base_dir
|
|
23
|
+
begin
|
|
24
|
+
@real_base = File.realpath(base_dir.to_s)
|
|
25
|
+
rescue SystemCallError => e
|
|
26
|
+
raise Error.new("IOError", "base_dir is not accessible: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
@cache = {}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Tree-walking interpreter. Every method call on a template value goes
|
|
34
|
+
# through call_method, which only dispatches through Builtins' allowlist
|
|
35
|
+
# tables.
|
|
36
|
+
class Interpreter
|
|
37
|
+
include Nodes
|
|
38
|
+
|
|
39
|
+
NO_RECV = Object.new.freeze
|
|
40
|
+
EMPTY = [].freeze
|
|
41
|
+
MAX_RENDER_DEPTH = 32
|
|
42
|
+
# Methods that ignore what their block returns.
|
|
43
|
+
BLOCK_RESULT_UNUSED = %w[each each_with_index each_with_object each_slice each_cons each_char each_line
|
|
44
|
+
each_pair each_key each_value each_index reverse_each times upto downto step
|
|
45
|
+
loop tap].to_h { |m| [m, true] }.freeze
|
|
46
|
+
|
|
47
|
+
attr_reader :state, :filename, :depth
|
|
48
|
+
|
|
49
|
+
def initialize(state, filename, depth = 0)
|
|
50
|
+
@state = state
|
|
51
|
+
@filename = filename
|
|
52
|
+
@depth = depth
|
|
53
|
+
@loops = []
|
|
54
|
+
@line = 1
|
|
55
|
+
@out = +""
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run(ast, vars)
|
|
59
|
+
@want = false
|
|
60
|
+
env = Env.new(nil)
|
|
61
|
+
vars.each { |k, v| env.vars[k] = v }
|
|
62
|
+
ev(ast, env)
|
|
63
|
+
@out
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def step!
|
|
67
|
+
st = @state
|
|
68
|
+
st.steps += 1
|
|
69
|
+
return unless st.max_steps && st.steps > st.max_steps
|
|
70
|
+
|
|
71
|
+
raise Error.new("LimitError", "step limit of #{st.max_steps} exceeded", @filename, @line)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def truthy(v)
|
|
75
|
+
!(v.nil? || false.equal?(v))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A Ruby block that forwards to a template block, keeping the exact
|
|
79
|
+
# number of yielded values.
|
|
80
|
+
def bp(blk)
|
|
81
|
+
return nil unless blk
|
|
82
|
+
|
|
83
|
+
proc { |*a| blk.call(*a) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# -- evaluation ------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
attr_writer :want
|
|
89
|
+
|
|
90
|
+
# @want is true when the value of the node being evaluated can be used.
|
|
91
|
+
# Text and <%= %> statements only build their value (a snapshot of the
|
|
92
|
+
# output buffer [ERBLite]) when it is wanted, so loops that end in text
|
|
93
|
+
# do not copy the whole buffer on every iteration.
|
|
94
|
+
def ev(n, env)
|
|
95
|
+
step!
|
|
96
|
+
w = @want
|
|
97
|
+
@want = true
|
|
98
|
+
case n
|
|
99
|
+
when Call then ev_call(n, env)
|
|
100
|
+
when LVar then env_at(env, n.depth).vars[n.name]
|
|
101
|
+
when Lit then n.value
|
|
102
|
+
when Str then ev_str(n.parts, env)
|
|
103
|
+
when Seq
|
|
104
|
+
r = nil
|
|
105
|
+
last = n.stmts.length - 1
|
|
106
|
+
n.stmts.each_with_index do |s, i|
|
|
107
|
+
@want = i == last ? w : false
|
|
108
|
+
r = ev(s, env)
|
|
109
|
+
end
|
|
110
|
+
r
|
|
111
|
+
when Text
|
|
112
|
+
@out << n.text
|
|
113
|
+
w ? @out.dup : nil
|
|
114
|
+
when Output
|
|
115
|
+
v = ev(n.expr, env)
|
|
116
|
+
@line = n.line
|
|
117
|
+
@out << Builtins.to_s(v)
|
|
118
|
+
w ? @out.dup : nil
|
|
119
|
+
when LAsgn then env_at(env, n.depth).vars[n.name] = ev(n.value, env)
|
|
120
|
+
when If
|
|
121
|
+
c = truthy(ev(n.cond, env))
|
|
122
|
+
@want = w
|
|
123
|
+
if c
|
|
124
|
+
n.thn ? ev(n.thn, env) : nil
|
|
125
|
+
else
|
|
126
|
+
n.els ? ev(n.els, env) : nil
|
|
127
|
+
end
|
|
128
|
+
when And
|
|
129
|
+
l = ev(n.left, env)
|
|
130
|
+
truthy(l) ? ev(n.right, env) : l
|
|
131
|
+
when Or
|
|
132
|
+
l = ev(n.left, env)
|
|
133
|
+
truthy(l) ? l : ev(n.right, env)
|
|
134
|
+
when Not then !truthy(ev(n.expr, env))
|
|
135
|
+
when FCall then ev_fcall(n, env)
|
|
136
|
+
when VCall then ev_vcall(n, env)
|
|
137
|
+
when Arr then n.elems.map { |e| ev(e, env) }
|
|
138
|
+
when HashLit
|
|
139
|
+
h = {}
|
|
140
|
+
n.pairs.each { |k, v| h[ev(k, env)] = ev(v, env) }
|
|
141
|
+
h
|
|
142
|
+
when RangeLit
|
|
143
|
+
lo = ev(n.lo, env)
|
|
144
|
+
hi = n.hi ? ev(n.hi, env) : nil
|
|
145
|
+
@line = n.line
|
|
146
|
+
Builtins.make_range(lo, hi, n.excl)
|
|
147
|
+
when OpAsgn then ev_opasgn(n, env)
|
|
148
|
+
when IndexAsgn
|
|
149
|
+
r = ev(n.recv, env)
|
|
150
|
+
args = n.args.map { |a| ev(a, env) }
|
|
151
|
+
v = ev(n.value, env)
|
|
152
|
+
@line = n.line
|
|
153
|
+
invoke(r, "[]=", args + [v], nil, env, n.line)
|
|
154
|
+
v
|
|
155
|
+
when AttrAsgn
|
|
156
|
+
r = ev(n.recv, env)
|
|
157
|
+
v = ev(n.value, env)
|
|
158
|
+
@line = n.line
|
|
159
|
+
invoke(r, "#{n.name}=", [v], nil, env, n.line)
|
|
160
|
+
v
|
|
161
|
+
when MAsgn then ev_masgn(n, env)
|
|
162
|
+
when While then ev_while(n, env)
|
|
163
|
+
when For then ev_for(n, env)
|
|
164
|
+
when Case then ev_case(n, env, w)
|
|
165
|
+
when Break
|
|
166
|
+
v = n.value ? ev(n.value, env) : nil
|
|
167
|
+
throw(n.kind == :loop ? @loops.last[0] : env.blk, v)
|
|
168
|
+
when Next
|
|
169
|
+
v = n.value ? ev(n.value, env) : nil
|
|
170
|
+
throw(n.kind == :loop ? @loops.last[1] : env.blk.next_tag, v)
|
|
171
|
+
when Const
|
|
172
|
+
@line = n.line
|
|
173
|
+
Builtins.lookup_constant(n.name) ||
|
|
174
|
+
raise(Error.new("NameError", "uninitialized constant #{n.name}", @filename, n.line))
|
|
175
|
+
when Colon2
|
|
176
|
+
scope = ev(n.scope, env)
|
|
177
|
+
@line = n.line
|
|
178
|
+
Builtins.scoped_constant(scope, n.name, @filename, n.line)
|
|
179
|
+
when DSym then ev_str(n.parts, env).to_sym
|
|
180
|
+
else
|
|
181
|
+
raise Error.new("SyntaxError", "unsupported construct", @filename, @line)
|
|
182
|
+
end
|
|
183
|
+
rescue Error => e
|
|
184
|
+
e.fill(@filename, n.line)
|
|
185
|
+
raise
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def env_at(env, depth)
|
|
189
|
+
depth.times { env = env.parent }
|
|
190
|
+
env
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def ev_str(parts, env)
|
|
194
|
+
return parts[0].dup if parts.length == 1 && parts[0].is_a?(String)
|
|
195
|
+
|
|
196
|
+
s = +""
|
|
197
|
+
parts.each { |p| s << (p.is_a?(String) ? p : Builtins.to_s(ev(p, env))) }
|
|
198
|
+
s
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def ev_call(n, env)
|
|
202
|
+
return ev_shovel(n, env)[0] if n.name == "<<" && n.args.length == 1 && n.block.nil?
|
|
203
|
+
|
|
204
|
+
recv = ev(n.recv, env)
|
|
205
|
+
return nil if n.safe && recv.nil?
|
|
206
|
+
|
|
207
|
+
args = n.args.empty? ? EMPTY : n.args.map { |a| ev(a, env) }
|
|
208
|
+
@line = n.line
|
|
209
|
+
invoke(recv, n.name, args, n.block, env, n.line)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def ev_fcall(n, env)
|
|
213
|
+
args = n.args.empty? ? EMPTY : n.args.map { |a| ev(a, env) }
|
|
214
|
+
@line = n.line
|
|
215
|
+
invoke(NO_RECV, n.name, args, n.block, env, n.line)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ev_vcall(n, env)
|
|
219
|
+
@line = n.line
|
|
220
|
+
return invoke(NO_RECV, n.name, EMPTY, nil, env, n.line) if Builtins::FUNCS[n.name]
|
|
221
|
+
|
|
222
|
+
raise Error.new("NameError", "undefined local variable or method '#{n.name}' for main", @filename, n.line)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Calls +name+ on +recv+ (or the function +name+ when recv is NO_RECV),
|
|
226
|
+
# setting up the template block if there is one. `break` inside the
|
|
227
|
+
# block is a throw to the UserBlock, caught here.
|
|
228
|
+
def invoke(recv, name, args, bnode, env, line)
|
|
229
|
+
if bnode.is_a?(BlockNode)
|
|
230
|
+
blk = UserBlock.new(self, bnode, env, !(BLOCK_RESULT_UNUSED[name] && !recv.is_a?(Enum)))
|
|
231
|
+
catch(blk) { dispatch(recv, name, args, blk) }
|
|
232
|
+
elsif bnode
|
|
233
|
+
dispatch(recv, name, args, SymBlock.new(self, bnode.name))
|
|
234
|
+
else
|
|
235
|
+
dispatch(recv, name, args, nil)
|
|
236
|
+
end
|
|
237
|
+
rescue Error => e
|
|
238
|
+
e.fill(@filename, line)
|
|
239
|
+
raise
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def dispatch(recv, name, args, blk)
|
|
243
|
+
recv.equal?(NO_RECV) ? call_function(name, args, blk) : call_method(recv, name, args, blk)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def call_function(name, args, blk)
|
|
247
|
+
f = Builtins::FUNCS[name]
|
|
248
|
+
raise Error.new("NoMethodError", "undefined method '#{name}' for main:Object") unless f
|
|
249
|
+
|
|
250
|
+
Builtins.check_arity(f, args.length)
|
|
251
|
+
f.fn.call(self, nil, args, blk)
|
|
252
|
+
rescue Error
|
|
253
|
+
raise
|
|
254
|
+
rescue ::StandardError => e
|
|
255
|
+
raise Builtins.map_error(e)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def call_method(recv, name, args, blk)
|
|
259
|
+
m = Builtins.lookup(recv, name)
|
|
260
|
+
unless m
|
|
261
|
+
raise Error.new("NoMethodError", "undefined method '#{name}' for #{Builtins.describe(recv)}")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
Builtins.check_arity(m, args.length)
|
|
265
|
+
return Enum.new(recv, name, args.dup) if blk.nil? && Builtins.enumerator_call?(recv, name, args)
|
|
266
|
+
|
|
267
|
+
m.fn.call(self, recv, args, blk)
|
|
268
|
+
rescue Error
|
|
269
|
+
raise
|
|
270
|
+
rescue ::StandardError => e
|
|
271
|
+
raise Builtins.map_error(e)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def responds?(recv, name)
|
|
275
|
+
!Builtins.lookup(recv, name).nil?
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# -- assignment helpers ------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
# Evaluates an expression that may be written back to (for `<<` on
|
|
281
|
+
# strings and op-assignment). Returns [value, setter or nil].
|
|
282
|
+
def lvalue(node, env)
|
|
283
|
+
case node
|
|
284
|
+
when LVar
|
|
285
|
+
target = env_at(env, node.depth)
|
|
286
|
+
[target.vars[node.name], ->(x) { target.vars[node.name] = x }]
|
|
287
|
+
when Call
|
|
288
|
+
if node.name == "[]" && node.recv && !node.safe && node.block.nil?
|
|
289
|
+
r = ev(node.recv, env)
|
|
290
|
+
a = node.args.map { |x| ev(x, env) }
|
|
291
|
+
@line = node.line
|
|
292
|
+
v = invoke(r, "[]", a, nil, env, node.line)
|
|
293
|
+
[v, lambda { |x|
|
|
294
|
+
invoke(r, "[]=", a + [x], nil, env, node.line)
|
|
295
|
+
x
|
|
296
|
+
}]
|
|
297
|
+
elsif node.name == "<<" && node.args.length == 1 && node.block.nil?
|
|
298
|
+
ev_shovel(node, env)
|
|
299
|
+
else
|
|
300
|
+
[ev(node, env), nil]
|
|
301
|
+
end
|
|
302
|
+
else
|
|
303
|
+
[ev(node, env), nil]
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# `a << b`. Strings are values [ERBLite]: when +a+ is a String, the new
|
|
308
|
+
# string is assigned back to whatever +a+ was read from.
|
|
309
|
+
def ev_shovel(n, env)
|
|
310
|
+
step!
|
|
311
|
+
v, setter = lvalue(n.recv, env)
|
|
312
|
+
arg = ev(n.args[0], env)
|
|
313
|
+
@line = n.line
|
|
314
|
+
res = invoke(v, "<<", [arg], nil, env, n.line)
|
|
315
|
+
if v.is_a?(String)
|
|
316
|
+
setter&.call(res)
|
|
317
|
+
[res, setter]
|
|
318
|
+
else
|
|
319
|
+
[res, nil]
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def ev_opasgn(n, env)
|
|
324
|
+
t = n.target
|
|
325
|
+
if t.is_a?(Call) && t.name != "[]"
|
|
326
|
+
r = ev(t.recv, env)
|
|
327
|
+
@line = n.line
|
|
328
|
+
v = invoke(r, t.name, EMPTY, nil, env, n.line)
|
|
329
|
+
setter = lambda { |x|
|
|
330
|
+
invoke(r, "#{t.name}=", [x], nil, env, n.line)
|
|
331
|
+
x
|
|
332
|
+
}
|
|
333
|
+
else
|
|
334
|
+
v, setter = lvalue(t, env)
|
|
335
|
+
end
|
|
336
|
+
case n.op
|
|
337
|
+
when "||" then truthy(v) ? v : setter.call(ev(n.value, env))
|
|
338
|
+
when "&&" then truthy(v) ? setter.call(ev(n.value, env)) : v
|
|
339
|
+
else
|
|
340
|
+
rhs = ev(n.value, env)
|
|
341
|
+
@line = n.line
|
|
342
|
+
setter.call(invoke(v, n.op, [rhs], nil, env, n.line))
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def ev_masgn(n, env)
|
|
347
|
+
if n.values.length == 1
|
|
348
|
+
result = ev(n.values[0], env)
|
|
349
|
+
arr = result.is_a?(Array) ? result : [result]
|
|
350
|
+
else
|
|
351
|
+
result = arr = n.values.map { |v| ev(v, env) }
|
|
352
|
+
end
|
|
353
|
+
n.targets.each_with_index { |(name, depth), i| env_at(env, depth).vars[name] = arr[i] }
|
|
354
|
+
result
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# -- control flow ------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
def ev_while(n, env)
|
|
360
|
+
brk = Object.new
|
|
361
|
+
nxt = Object.new
|
|
362
|
+
@loops.push([brk, nxt])
|
|
363
|
+
begin
|
|
364
|
+
catch(brk) do
|
|
365
|
+
while true # rubocop:disable Style/InfiniteLoop
|
|
366
|
+
c = truthy(ev(n.cond, env))
|
|
367
|
+
break if n.neg ? c : !c
|
|
368
|
+
|
|
369
|
+
step!
|
|
370
|
+
@want = false
|
|
371
|
+
catch(nxt) { ev(n.body, env) }
|
|
372
|
+
end
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
ensure
|
|
376
|
+
@loops.pop
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def ev_for(n, env)
|
|
381
|
+
iter = ev(n.iter, env)
|
|
382
|
+
brk = Object.new
|
|
383
|
+
nxt = Object.new
|
|
384
|
+
@loops.push([brk, nxt])
|
|
385
|
+
begin
|
|
386
|
+
catch(brk) do
|
|
387
|
+
blk = NativeBlock.new do |*vals|
|
|
388
|
+
step!
|
|
389
|
+
v = vals.length == 1 ? vals[0] : vals
|
|
390
|
+
if n.vars.length == 1
|
|
391
|
+
name, depth = n.vars[0]
|
|
392
|
+
env_at(env, depth).vars[name] = v
|
|
393
|
+
else
|
|
394
|
+
arr = v.is_a?(Array) ? v : [v]
|
|
395
|
+
n.vars.each_with_index { |(name, depth), i| env_at(env, depth).vars[name] = arr[i] }
|
|
396
|
+
end
|
|
397
|
+
@want = false
|
|
398
|
+
catch(nxt) { ev(n.body, env) }
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
@line = n.line
|
|
402
|
+
begin
|
|
403
|
+
call_method(iter, "each", EMPTY, blk)
|
|
404
|
+
rescue Error => e
|
|
405
|
+
e.fill(@filename, n.line)
|
|
406
|
+
raise
|
|
407
|
+
end
|
|
408
|
+
iter
|
|
409
|
+
end
|
|
410
|
+
ensure
|
|
411
|
+
@loops.pop
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def ev_case(n, env, want)
|
|
416
|
+
v = n.subject ? ev(n.subject, env) : nil
|
|
417
|
+
n.whens.each do |conds, body|
|
|
418
|
+
conds.each do |c|
|
|
419
|
+
hit = n.subject ? case_eq(ev(c, env), v) : truthy(ev(c, env))
|
|
420
|
+
next unless hit
|
|
421
|
+
|
|
422
|
+
@want = want
|
|
423
|
+
return ev(body, env)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
@want = want
|
|
427
|
+
n.els ? ev(n.els, env) : nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def case_eq(pat, v)
|
|
431
|
+
Builtins.case_eq(pat, v)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# -- enumerators -------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def enum_each(e, blk)
|
|
437
|
+
call_method(e.recv, e.meth, e.args, blk)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def enum_to_a(e)
|
|
441
|
+
res = []
|
|
442
|
+
enum_each(e, NativeBlock.new { |*v|
|
|
443
|
+
res << (v.length == 1 ? v[0] : v)
|
|
444
|
+
nil
|
|
445
|
+
})
|
|
446
|
+
res
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# The collection used by the shared Enumerable methods.
|
|
450
|
+
def coll(r)
|
|
451
|
+
case r
|
|
452
|
+
when Enum then enum_to_a(r)
|
|
453
|
+
when Range
|
|
454
|
+
# Native Enumerable methods walk the whole range without counting
|
|
455
|
+
# steps, so bound the work like any other generated collection.
|
|
456
|
+
Builtins.cap(r.size) if r.begin.is_a?(Integer) && r.end.is_a?(Integer)
|
|
457
|
+
r
|
|
458
|
+
else r
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# -- partials ------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
def render_partial(path, locals)
|
|
465
|
+
if @depth + 1 > MAX_RENDER_DEPTH
|
|
466
|
+
raise Error.new("LimitError", "render nesting is deeper than #{MAX_RENDER_DEPTH}")
|
|
467
|
+
end
|
|
468
|
+
raise Error.new("TypeError", "render locals must be a Hash") unless locals.is_a?(Hash)
|
|
469
|
+
|
|
470
|
+
vars = {}
|
|
471
|
+
locals.each do |k, v|
|
|
472
|
+
unless k.is_a?(Symbol) || k.is_a?(String)
|
|
473
|
+
raise Error.new("TypeError", "render local names must be Symbols or Strings")
|
|
474
|
+
end
|
|
475
|
+
vars[k.to_s] = v
|
|
476
|
+
end
|
|
477
|
+
source = Sandbox.read(@state, path)
|
|
478
|
+
key = [Sandbox.resolve(@state, path), vars.keys.sort]
|
|
479
|
+
ast = @state.cache[key] ||= begin
|
|
480
|
+
src = ERBLite.normalize_source(source, path)
|
|
481
|
+
Parser.parse(Lexer.tokenize(src, path), path, vars.keys)
|
|
482
|
+
end
|
|
483
|
+
Interpreter.new(@state, path, @depth + 1).run(ast, vars)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|