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,192 @@
|
|
|
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
|
+
# The only file system access templates have (SPEC section 9.1). Every path
|
|
13
|
+
# is resolved against the real path of base_dir, symlinks included, and
|
|
14
|
+
# must stay inside it.
|
|
15
|
+
module Sandbox
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def require_base(state)
|
|
19
|
+
return state.real_base if state.real_base
|
|
20
|
+
|
|
21
|
+
raise Error.new("SecurityError", "file access is disabled (no base_dir was given)")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_path_string(path)
|
|
25
|
+
unless path.is_a?(String)
|
|
26
|
+
raise Error.new("TypeError", "no implicit conversion of #{Builtins.tname(path)} into String")
|
|
27
|
+
end
|
|
28
|
+
raise Error.new("ArgumentError", "string contains null byte") if path.include?("\0")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inside?(real, base)
|
|
32
|
+
return true if real == base
|
|
33
|
+
|
|
34
|
+
prefix = base.end_with?("/") ? base : "#{base}/"
|
|
35
|
+
real.start_with?(prefix)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Lexically normalized absolute path for +path+ under +base+. A leading
|
|
39
|
+
# "~" is never expanded to a home directory.
|
|
40
|
+
def expand(path, base)
|
|
41
|
+
p = path.start_with?("~") ? "./#{path}" : path
|
|
42
|
+
File.expand_path(p, base)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Resolves +path+ to a real path inside the sandbox or raises
|
|
46
|
+
# SecurityError. Missing trailing components are allowed (the caller
|
|
47
|
+
# reports IOError for them); the deepest existing ancestor is resolved
|
|
48
|
+
# with File.realpath so symlinks cannot escape.
|
|
49
|
+
def resolve(state, path)
|
|
50
|
+
base = require_base(state)
|
|
51
|
+
check_path_string(path)
|
|
52
|
+
full = expand(path, base)
|
|
53
|
+
rest = []
|
|
54
|
+
cur = full
|
|
55
|
+
until File.exist?(cur)
|
|
56
|
+
parent = File.dirname(cur)
|
|
57
|
+
break if parent == cur
|
|
58
|
+
|
|
59
|
+
rest.unshift(File.basename(cur))
|
|
60
|
+
cur = parent
|
|
61
|
+
end
|
|
62
|
+
real = begin
|
|
63
|
+
File.realpath(cur)
|
|
64
|
+
rescue SystemCallError
|
|
65
|
+
cur
|
|
66
|
+
end
|
|
67
|
+
real = File.join(real, *rest) unless rest.empty?
|
|
68
|
+
raise Error.new("SecurityError", "path is outside the sandbox: #{path}") unless inside?(real, base)
|
|
69
|
+
|
|
70
|
+
real
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read(state, path)
|
|
74
|
+
real = resolve(state, path)
|
|
75
|
+
raise Error.new("IOError", "No such file or directory - #{path}") unless File.exist?(real)
|
|
76
|
+
raise Error.new("IOError", "Is a directory - #{path}") if File.directory?(real)
|
|
77
|
+
|
|
78
|
+
File.binread(real).force_encoding(Encoding::UTF_8)
|
|
79
|
+
rescue SystemCallError => e
|
|
80
|
+
raise Error.new("IOError", "#{e.message} - #{path}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check(state, path, kind)
|
|
84
|
+
require_base(state)
|
|
85
|
+
check_path_string(path)
|
|
86
|
+
real = begin
|
|
87
|
+
resolve(state, path)
|
|
88
|
+
rescue Error => e
|
|
89
|
+
return false if e.type == "SecurityError"
|
|
90
|
+
|
|
91
|
+
raise
|
|
92
|
+
end
|
|
93
|
+
case kind
|
|
94
|
+
when :exist then File.exist?(real)
|
|
95
|
+
when :file then File.file?(real)
|
|
96
|
+
else File.directory?(real)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Dir.glob with *, ** and ? only. Results are relative to base_dir and
|
|
101
|
+
# sorted.
|
|
102
|
+
def glob(state, pattern)
|
|
103
|
+
base = require_base(state)
|
|
104
|
+
check_path_string(pattern)
|
|
105
|
+
full = expand(pattern, base)
|
|
106
|
+
raise Error.new("SecurityError", "glob pattern is outside the sandbox: #{pattern}") unless inside?(full, base)
|
|
107
|
+
|
|
108
|
+
rel = full == base ? "" : full[(base.end_with?("/") ? base.length : base.length + 1)..]
|
|
109
|
+
segs = rel.split("/").reject(&:empty?)
|
|
110
|
+
out = []
|
|
111
|
+
walk(base, base, "", segs, out) unless segs.empty?
|
|
112
|
+
out.uniq.sort
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def children(dir)
|
|
116
|
+
Dir.children(dir).sort
|
|
117
|
+
rescue SystemCallError
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def safe_dir?(path, base)
|
|
122
|
+
File.directory?(path) && inside?(File.realpath(path), base)
|
|
123
|
+
rescue SystemCallError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def walk(base, dir, prefix, segs, out)
|
|
128
|
+
seg = segs[0]
|
|
129
|
+
last = segs.length == 1
|
|
130
|
+
join = ->(name) { prefix.empty? ? name : "#{prefix}/#{name}" }
|
|
131
|
+
if seg == "**" && !last
|
|
132
|
+
walk(base, dir, prefix, segs[1..], out)
|
|
133
|
+
children(dir).each do |name|
|
|
134
|
+
next if name.start_with?(".")
|
|
135
|
+
|
|
136
|
+
path = File.join(dir, name)
|
|
137
|
+
next if File.symlink?(path) || !safe_dir?(path, base)
|
|
138
|
+
|
|
139
|
+
walk(base, path, join.call(name), segs, out)
|
|
140
|
+
end
|
|
141
|
+
elsif seg.include?("*") || seg.include?("?")
|
|
142
|
+
children(dir).each do |name|
|
|
143
|
+
next if name.start_with?(".") && !seg.start_with?(".")
|
|
144
|
+
next unless wildcard_match?(seg, name)
|
|
145
|
+
|
|
146
|
+
path = File.join(dir, name)
|
|
147
|
+
if last
|
|
148
|
+
out << join.call(name)
|
|
149
|
+
elsif safe_dir?(path, base)
|
|
150
|
+
walk(base, path, join.call(name), segs[1..], out)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
path = File.join(dir, seg)
|
|
155
|
+
return unless File.exist?(path) || File.symlink?(path)
|
|
156
|
+
|
|
157
|
+
if last
|
|
158
|
+
out << join.call(seg)
|
|
159
|
+
elsif safe_dir?(path, base)
|
|
160
|
+
walk(base, path, join.call(seg), segs[1..], out)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Matches +name+ against a glob segment where * is any run of characters
|
|
166
|
+
# and ? is one character.
|
|
167
|
+
def wildcard_match?(pat, name)
|
|
168
|
+
p = 0
|
|
169
|
+
n = 0
|
|
170
|
+
star_p = nil
|
|
171
|
+
star_n = 0
|
|
172
|
+
while n < name.length
|
|
173
|
+
if p < pat.length && (pat[p] == "?" || pat[p] == name[n])
|
|
174
|
+
p += 1
|
|
175
|
+
n += 1
|
|
176
|
+
elsif p < pat.length && pat[p] == "*"
|
|
177
|
+
star_p = p
|
|
178
|
+
star_n = n
|
|
179
|
+
p += 1
|
|
180
|
+
elsif star_p
|
|
181
|
+
p = star_p + 1
|
|
182
|
+
star_n += 1
|
|
183
|
+
n = star_n
|
|
184
|
+
else
|
|
185
|
+
return false
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
p += 1 while p < pat.length && pat[p] == "*"
|
|
189
|
+
p == pat.length
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
# A built-in class or module constant (Integer, Comparable, Math, File, ...).
|
|
13
|
+
# Templates can only reach the instances created in Builtins::CONSTANTS.
|
|
14
|
+
class ClassValue
|
|
15
|
+
attr_reader :name
|
|
16
|
+
|
|
17
|
+
def initialize(name, is_module)
|
|
18
|
+
@name = name.freeze
|
|
19
|
+
@module = is_module
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def module?
|
|
23
|
+
@module
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
@name.dup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inspect
|
|
31
|
+
@name.dup
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# What an iterator method returns when it is called without a block.
|
|
36
|
+
# Iterating it calls method +meth+ on +recv+ with +args+ and a block.
|
|
37
|
+
class Enum
|
|
38
|
+
attr_reader :recv, :meth, :args
|
|
39
|
+
|
|
40
|
+
def initialize(recv, meth, args)
|
|
41
|
+
@recv = recv
|
|
42
|
+
@meth = meth
|
|
43
|
+
@args = args
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def inspect
|
|
47
|
+
a = @args.empty? ? "" : "(#{@args.map { |x| Builtins.inspect(x) }.join(", ")})"
|
|
48
|
+
if @meth == "step" && arithmetic_sequence?
|
|
49
|
+
# Ruby returns an ArithmeticSequence here, which prints differently.
|
|
50
|
+
recv = Builtins.inspect(@recv)
|
|
51
|
+
return @recv.is_a?(Range) ? "((#{recv}).step#{a})" : "(#{recv}.step#{a})"
|
|
52
|
+
end
|
|
53
|
+
"#<Enumerator: #{Builtins.inspect(@recv)}:#{@meth}#{a}>"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def arithmetic_sequence?
|
|
57
|
+
num = ->(x) { x.is_a?(Integer) || x.is_a?(Float) }
|
|
58
|
+
return @args.all?(&num) if @recv.is_a?(Range)
|
|
59
|
+
|
|
60
|
+
(@recv.is_a?(Integer) || @recv.is_a?(Float)) && @args.all? { |x| num.call(x) || x.nil? }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_s
|
|
64
|
+
inspect
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Local variable scope. Each block call gets its own Env whose +blk+ is the
|
|
69
|
+
# UserBlock being run, so `break`/`next` know where to go.
|
|
70
|
+
class Env
|
|
71
|
+
attr_reader :vars, :parent, :blk
|
|
72
|
+
|
|
73
|
+
def initialize(parent, blk = nil)
|
|
74
|
+
@vars = {}
|
|
75
|
+
@parent = parent
|
|
76
|
+
@blk = blk
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A block written in the template ({ |x| ... } or do |x| ... end).
|
|
81
|
+
class UserBlock
|
|
82
|
+
attr_reader :next_tag
|
|
83
|
+
|
|
84
|
+
def initialize(interp, node, env, value_used = true)
|
|
85
|
+
@it = interp
|
|
86
|
+
@node = node
|
|
87
|
+
@env = env
|
|
88
|
+
@value_used = value_used
|
|
89
|
+
@next_tag = Object.new
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def call(*vals)
|
|
93
|
+
@it.step!
|
|
94
|
+
e = Env.new(@env, self)
|
|
95
|
+
params = @node.params
|
|
96
|
+
vals = vals[0] if params.length > 1 && vals.length == 1 && vals[0].is_a?(Array)
|
|
97
|
+
params.each_with_index { |p, i| bind(p, vals[i], e) }
|
|
98
|
+
@it.want = @value_used
|
|
99
|
+
catch(@next_tag) { @it.ev(@node.body, e) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def bind(param, value, env)
|
|
105
|
+
if param.is_a?(Array)
|
|
106
|
+
arr = value.is_a?(Array) ? value : [value]
|
|
107
|
+
param.each_with_index { |q, j| bind(q, arr[j], env) }
|
|
108
|
+
else
|
|
109
|
+
env.vars[param] = value
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# &:name - calls method +name+ (through the allowlist) on the first value.
|
|
115
|
+
class SymBlock
|
|
116
|
+
def initialize(interp, name)
|
|
117
|
+
@it = interp
|
|
118
|
+
@name = name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def call(*vals)
|
|
122
|
+
@it.step!
|
|
123
|
+
raise Error.new("ArgumentError", "no receiver given") if vals.empty?
|
|
124
|
+
|
|
125
|
+
@it.call_method(vals[0], @name, vals[1..], nil)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A block implemented by the interpreter itself (for loops, enumerators).
|
|
130
|
+
class NativeBlock
|
|
131
|
+
def initialize(&body)
|
|
132
|
+
@body = body
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def call(*vals)
|
|
136
|
+
@body.call(*vals)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
VERSION = "0.5.0"
|
|
13
|
+
end
|
data/lib/erblite.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
require_relative "erblite/version"
|
|
12
|
+
require_relative "erblite/error"
|
|
13
|
+
require_relative "erblite/nodes"
|
|
14
|
+
require_relative "erblite/lexer"
|
|
15
|
+
require_relative "erblite/parser"
|
|
16
|
+
require_relative "erblite/values"
|
|
17
|
+
require_relative "erblite/builtins"
|
|
18
|
+
require_relative "erblite/format"
|
|
19
|
+
require_relative "erblite/json"
|
|
20
|
+
require_relative "erblite/sandbox"
|
|
21
|
+
require_relative "erblite/interpreter"
|
|
22
|
+
|
|
23
|
+
# ERBLite: a sandboxed template engine with ERB syntax and a small subset of
|
|
24
|
+
# Ruby. Templates are lexed, parsed into an AST and run by a tree-walking
|
|
25
|
+
# interpreter; no template text is ever evaluated as Ruby code.
|
|
26
|
+
module ERBLite
|
|
27
|
+
NAME_RE = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
28
|
+
|
|
29
|
+
# A compiled template. The source is tokenized once; the token stream is
|
|
30
|
+
# parsed per set of host variable names (host variables are locals of the
|
|
31
|
+
# template, which affects parsing as in Ruby).
|
|
32
|
+
class Template
|
|
33
|
+
attr_reader :filename
|
|
34
|
+
|
|
35
|
+
def initialize(source, filename: "(erblite)")
|
|
36
|
+
@filename = filename
|
|
37
|
+
@source = ERBLite.normalize_source(source, filename)
|
|
38
|
+
@tokens = Lexer.tokenize(@source, filename)
|
|
39
|
+
@asts = {}
|
|
40
|
+
rescue SystemStackError
|
|
41
|
+
raise Error.new("LimitError", "nesting is too deep (stack exhausted)", filename, nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Variables may be passed as a Hash or as extra keyword arguments
|
|
45
|
+
# (render(name: "x")); base_dir and max_steps are always options.
|
|
46
|
+
def render(vars = {}, base_dir: nil, max_steps: nil, **more)
|
|
47
|
+
vars = vars.merge(more) unless more.empty?
|
|
48
|
+
locals = ERBLite.convert_vars(vars)
|
|
49
|
+
names = locals.keys
|
|
50
|
+
ast = @asts[names.sort] ||= Parser.parse(@tokens, @filename, names)
|
|
51
|
+
state = State.new(base_dir, max_steps)
|
|
52
|
+
Interpreter.new(state, @filename).run(ast, locals)
|
|
53
|
+
rescue SystemStackError
|
|
54
|
+
raise Error.new("LimitError", "nesting is too deep (stack exhausted)", @filename, nil)
|
|
55
|
+
rescue NoMemoryError
|
|
56
|
+
raise Error.new("LimitError", "out of memory", @filename, nil)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
def render(source, vars = {}, base_dir: nil, filename: "(erblite)", max_steps: nil, **more)
|
|
63
|
+
Template.new(source, filename: filename).render(vars, base_dir: base_dir, max_steps: max_steps, **more)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_file(path, vars = {}, base_dir: nil, filename: nil, max_steps: nil, **more)
|
|
67
|
+
vars = vars.merge(more) unless more.empty?
|
|
68
|
+
source = begin
|
|
69
|
+
File.binread(path)
|
|
70
|
+
rescue SystemCallError => e
|
|
71
|
+
raise Error.new("IOError", e.message, path.to_s, nil)
|
|
72
|
+
end
|
|
73
|
+
base_dir ||= File.dirname(File.expand_path(path))
|
|
74
|
+
Template.new(source, filename: filename || path.to_s).render(vars, base_dir: base_dir, max_steps: max_steps)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def normalize_source(source, filename)
|
|
78
|
+
raise Error.new("TypeError", "template source must be a String", filename, nil) unless source.is_a?(String)
|
|
79
|
+
|
|
80
|
+
s = source.dup.force_encoding(Encoding::UTF_8)
|
|
81
|
+
raise Error.new("SyntaxError", "template is not valid UTF-8", filename, 1) unless s.valid_encoding?
|
|
82
|
+
|
|
83
|
+
s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Host variables: Symbol or String keys naming valid identifiers. Values
|
|
87
|
+
# are copied into fresh template values (Arrays/Hashes are copied, sharing
|
|
88
|
+
# is preserved), so templates never see host objects.
|
|
89
|
+
def convert_vars(vars)
|
|
90
|
+
raise Error.new("TypeError", "variables must be a Hash") unless vars.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
memo = {}.compare_by_identity
|
|
93
|
+
out = {}
|
|
94
|
+
vars.each do |k, v|
|
|
95
|
+
unless k.is_a?(Symbol) || k.is_a?(String)
|
|
96
|
+
raise Error.new("TypeError", "variable names must be Symbols or Strings, got #{k.inspect}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
name = k.to_s
|
|
100
|
+
next unless name.match?(NAME_RE) && !CodeLexer::KEYWORD_SET[name]
|
|
101
|
+
|
|
102
|
+
out[name] = convert_value(v, memo)
|
|
103
|
+
end
|
|
104
|
+
out
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def convert_value(v, memo)
|
|
108
|
+
case v
|
|
109
|
+
when nil, true, false, Float, Symbol then v
|
|
110
|
+
when Integer then Builtins.ck(v)
|
|
111
|
+
when String
|
|
112
|
+
s = v.dup
|
|
113
|
+
s.force_encoding(Encoding::UTF_8) unless s.encoding == Encoding::UTF_8
|
|
114
|
+
s
|
|
115
|
+
when Array
|
|
116
|
+
return memo[v] if memo.key?(v)
|
|
117
|
+
|
|
118
|
+
arr = memo[v] = []
|
|
119
|
+
v.each { |x| arr << convert_value(x, memo) }
|
|
120
|
+
arr
|
|
121
|
+
when Hash
|
|
122
|
+
return memo[v] if memo.key?(v)
|
|
123
|
+
|
|
124
|
+
h = memo[v] = {}
|
|
125
|
+
h.default = convert_value(v.default, memo) unless v.default_proc
|
|
126
|
+
v.each { |k, x| h[convert_value(k, memo)] = convert_value(x, memo) }
|
|
127
|
+
h
|
|
128
|
+
when Range
|
|
129
|
+
Range.new(convert_value(v.begin, memo), convert_value(v.end, memo), v.exclude_end?)
|
|
130
|
+
else
|
|
131
|
+
raise Error.new("TypeError", "unsupported variable value of class #{v.class}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: erblite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OpenC3
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: ERBLite renders ERB templates with a tree-walking interpreter for a small,
|
|
13
|
+
sandboxed subset of Ruby. It never evals template code.
|
|
14
|
+
executables:
|
|
15
|
+
- erblite
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE.md
|
|
20
|
+
- README.md
|
|
21
|
+
- bin/erblite
|
|
22
|
+
- lib/erblite.rb
|
|
23
|
+
- lib/erblite/builtins.rb
|
|
24
|
+
- lib/erblite/error.rb
|
|
25
|
+
- lib/erblite/format.rb
|
|
26
|
+
- lib/erblite/interpreter.rb
|
|
27
|
+
- lib/erblite/json.rb
|
|
28
|
+
- lib/erblite/lexer.rb
|
|
29
|
+
- lib/erblite/nodes.rb
|
|
30
|
+
- lib/erblite/parser.rb
|
|
31
|
+
- lib/erblite/sandbox.rb
|
|
32
|
+
- lib/erblite/values.rb
|
|
33
|
+
- lib/erblite/version.rb
|
|
34
|
+
homepage: https://github.com/OpenC3/erblite
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
source_code_uri: https://github.com/OpenC3/erblite
|
|
39
|
+
bug_tracker_uri: https://github.com/OpenC3/erblite/issues
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.2'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.6.9
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Sandboxed ERB-syntax template engine running a safe subset of Ruby
|
|
57
|
+
test_files: []
|