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
data/lib/erblite/json.rb
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
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
|
+
# JSON.parse / JSON.generate / JSON.pretty_generate / #to_json.
|
|
13
|
+
#
|
|
14
|
+
# Self-contained so output does not depend on the installed json gem. The
|
|
15
|
+
# parser follows Ruby's json gem: comments are allowed, trailing commas,
|
|
16
|
+
# leading zeros, raw control characters and lone surrogates are errors.
|
|
17
|
+
module JSONSupport
|
|
18
|
+
MAX_NESTING = 100
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# -- parsing ---------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def parse(str, opts)
|
|
25
|
+
Builtins.need_str(str)
|
|
26
|
+
sym = false
|
|
27
|
+
unless opts.nil?
|
|
28
|
+
Builtins.need_hash(opts)
|
|
29
|
+
opts.each do |k, v|
|
|
30
|
+
sym = !(v.nil? || v == false) if k == :symbolize_names || k == "symbolize_names"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
s = str.encoding == Encoding::UTF_8 ? str : str.dup.force_encoding(Encoding::UTF_8)
|
|
34
|
+
parse_error("source is not valid UTF-8") unless s.valid_encoding?
|
|
35
|
+
Parser.new(s, sym).parse
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_error(msg)
|
|
39
|
+
raise Error.new("JSON::ParserError", msg)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Parser
|
|
43
|
+
NUMBER_RE = /-?(?:0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/
|
|
44
|
+
|
|
45
|
+
def initialize(src, symbolize)
|
|
46
|
+
@s = src
|
|
47
|
+
@i = 0
|
|
48
|
+
@sym = symbolize
|
|
49
|
+
@depth = 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse
|
|
53
|
+
skip_ws
|
|
54
|
+
JSONSupport.parse_error("unexpected end of input") if @i >= @s.length
|
|
55
|
+
v = value
|
|
56
|
+
skip_ws
|
|
57
|
+
error("unexpected character") if @i < @s.length
|
|
58
|
+
v
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def error(msg)
|
|
64
|
+
JSONSupport.parse_error("#{msg} at offset #{@i}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def skip_ws
|
|
68
|
+
loop do
|
|
69
|
+
c = @s[@i]
|
|
70
|
+
if c == " " || c == "\t" || c == "\n" || c == "\r"
|
|
71
|
+
@i += 1
|
|
72
|
+
elsif c == "/" && @s[@i + 1] == "*"
|
|
73
|
+
close = @s.index("*/", @i + 2)
|
|
74
|
+
error("unterminated comment") unless close
|
|
75
|
+
@i = close + 2
|
|
76
|
+
elsif c == "/" && @s[@i + 1] == "/"
|
|
77
|
+
nl = @s.index("\n", @i + 2)
|
|
78
|
+
@i = nl ? nl + 1 : @s.length
|
|
79
|
+
else
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def value
|
|
86
|
+
c = @s[@i]
|
|
87
|
+
case c
|
|
88
|
+
when "{" then object
|
|
89
|
+
when "[" then array
|
|
90
|
+
when '"' then string
|
|
91
|
+
when "t" then literal("true", true)
|
|
92
|
+
when "f" then literal("false", false)
|
|
93
|
+
when "n" then literal("null", nil)
|
|
94
|
+
when "-", "0".."9" then number
|
|
95
|
+
when nil then error("unexpected end of input")
|
|
96
|
+
else error("unexpected character #{c.inspect}")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def literal(word, v)
|
|
101
|
+
error("unexpected token") unless @s[@i, word.length] == word
|
|
102
|
+
@i += word.length
|
|
103
|
+
v
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def number
|
|
107
|
+
m = NUMBER_RE.match(@s, @i)
|
|
108
|
+
error("invalid number") unless m && m.begin(0) == @i && m[0] != "-"
|
|
109
|
+
text = m[0]
|
|
110
|
+
@i += text.length
|
|
111
|
+
error("invalid number") if @s[@i]&.match?(/[0-9A-Za-z_.+-]/)
|
|
112
|
+
if m[1] || m[2]
|
|
113
|
+
Float(text)
|
|
114
|
+
else
|
|
115
|
+
Builtins.ck(text.to_i)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def nest
|
|
120
|
+
@depth += 1
|
|
121
|
+
# json 2.21 accepts 101 levels when parsing (it rejects 102).
|
|
122
|
+
error("nesting of #{@depth} is too deep") if @depth > MAX_NESTING + 1
|
|
123
|
+
yield
|
|
124
|
+
ensure
|
|
125
|
+
@depth -= 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def array
|
|
129
|
+
nest do
|
|
130
|
+
@i += 1
|
|
131
|
+
out = []
|
|
132
|
+
skip_ws
|
|
133
|
+
if @s[@i] == "]"
|
|
134
|
+
@i += 1
|
|
135
|
+
return out
|
|
136
|
+
end
|
|
137
|
+
loop do
|
|
138
|
+
skip_ws
|
|
139
|
+
out << value
|
|
140
|
+
skip_ws
|
|
141
|
+
c = @s[@i]
|
|
142
|
+
@i += 1
|
|
143
|
+
break if c == "]"
|
|
144
|
+
|
|
145
|
+
error("expected ',' or ']'") unless c == ","
|
|
146
|
+
end
|
|
147
|
+
out
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def object
|
|
152
|
+
nest do
|
|
153
|
+
@i += 1
|
|
154
|
+
out = {}
|
|
155
|
+
skip_ws
|
|
156
|
+
if @s[@i] == "}"
|
|
157
|
+
@i += 1
|
|
158
|
+
return out
|
|
159
|
+
end
|
|
160
|
+
loop do
|
|
161
|
+
skip_ws
|
|
162
|
+
error("expected a string key") unless @s[@i] == '"'
|
|
163
|
+
k = string
|
|
164
|
+
k = k.to_sym if @sym
|
|
165
|
+
skip_ws
|
|
166
|
+
error("expected ':'") unless @s[@i] == ":"
|
|
167
|
+
@i += 1
|
|
168
|
+
skip_ws
|
|
169
|
+
out[k] = value
|
|
170
|
+
skip_ws
|
|
171
|
+
c = @s[@i]
|
|
172
|
+
@i += 1
|
|
173
|
+
break if c == "}"
|
|
174
|
+
|
|
175
|
+
error("expected ',' or '}'") unless c == ","
|
|
176
|
+
end
|
|
177
|
+
out
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
ESC = { '"' => '"', "\\" => "\\", "/" => "/", "b" => "\b", "f" => "\f", "n" => "\n",
|
|
182
|
+
"r" => "\r", "t" => "\t" }.freeze
|
|
183
|
+
|
|
184
|
+
def string
|
|
185
|
+
@i += 1
|
|
186
|
+
out = +""
|
|
187
|
+
loop do
|
|
188
|
+
c = @s[@i]
|
|
189
|
+
error("unterminated string") if c.nil?
|
|
190
|
+
@i += 1
|
|
191
|
+
if c == '"'
|
|
192
|
+
return out
|
|
193
|
+
elsif c == "\\"
|
|
194
|
+
d = @s[@i]
|
|
195
|
+
@i += 1
|
|
196
|
+
if ESC[d]
|
|
197
|
+
out << ESC[d]
|
|
198
|
+
elsif d == "u"
|
|
199
|
+
cp = hex4
|
|
200
|
+
if cp.between?(0xD800, 0xDBFF)
|
|
201
|
+
error("invalid surrogate pair") unless @s[@i, 2] == "\\u"
|
|
202
|
+
@i += 2
|
|
203
|
+
lo = hex4
|
|
204
|
+
error("invalid surrogate pair") unless lo.between?(0xDC00, 0xDFFF)
|
|
205
|
+
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
|
|
206
|
+
elsif cp.between?(0xDC00, 0xDFFF)
|
|
207
|
+
error("invalid surrogate")
|
|
208
|
+
end
|
|
209
|
+
out << cp.chr(Encoding::UTF_8)
|
|
210
|
+
else
|
|
211
|
+
error("invalid escape character in string")
|
|
212
|
+
end
|
|
213
|
+
elsif c.ord < 0x20
|
|
214
|
+
error("invalid control character in string")
|
|
215
|
+
else
|
|
216
|
+
out << c
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def hex4
|
|
222
|
+
h = @s[@i, 4]
|
|
223
|
+
error("incomplete unicode escape") unless h&.match?(/\A[0-9a-fA-F]{4}\z/)
|
|
224
|
+
@i += 4
|
|
225
|
+
h.to_i(16)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# -- generation ------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def generate(obj)
|
|
232
|
+
out = +""
|
|
233
|
+
gen(obj, out, nil, 0)
|
|
234
|
+
out
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def pretty_generate(obj)
|
|
238
|
+
out = +""
|
|
239
|
+
gen(obj, out, "", 0)
|
|
240
|
+
out
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def gen_error(msg)
|
|
244
|
+
raise Error.new("JSON::GeneratorError", msg)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# +indent+ is nil for compact output, else the current indentation.
|
|
248
|
+
def gen(v, out, indent, depth)
|
|
249
|
+
case v
|
|
250
|
+
when nil then out << "null"
|
|
251
|
+
when true then out << "true"
|
|
252
|
+
when false then out << "false"
|
|
253
|
+
when Integer then out << v.to_s
|
|
254
|
+
when Float then out << float(v)
|
|
255
|
+
when String then string(v, out)
|
|
256
|
+
when Symbol then string(v.name, out)
|
|
257
|
+
when Array
|
|
258
|
+
gen_error("nesting of #{depth + 1} is too deep") if depth + 1 > MAX_NESTING
|
|
259
|
+
if v.empty?
|
|
260
|
+
out << "[]"
|
|
261
|
+
elsif indent
|
|
262
|
+
inner = "#{indent} "
|
|
263
|
+
out << "[\n"
|
|
264
|
+
v.each_with_index do |x, i|
|
|
265
|
+
out << ",\n" if i.positive?
|
|
266
|
+
out << inner
|
|
267
|
+
gen(x, out, inner, depth + 1)
|
|
268
|
+
end
|
|
269
|
+
out << "\n" << indent << "]"
|
|
270
|
+
else
|
|
271
|
+
out << "["
|
|
272
|
+
v.each_with_index do |x, i|
|
|
273
|
+
out << "," if i.positive?
|
|
274
|
+
gen(x, out, nil, depth + 1)
|
|
275
|
+
end
|
|
276
|
+
out << "]"
|
|
277
|
+
end
|
|
278
|
+
when Hash
|
|
279
|
+
gen_error("nesting of #{depth + 1} is too deep") if depth + 1 > MAX_NESTING
|
|
280
|
+
if v.empty?
|
|
281
|
+
out << "{}"
|
|
282
|
+
else
|
|
283
|
+
inner = indent ? "#{indent} " : nil
|
|
284
|
+
out << (indent ? "{\n" : "{")
|
|
285
|
+
first = true
|
|
286
|
+
v.each do |k, x|
|
|
287
|
+
out << (indent ? ",\n" : ",") unless first
|
|
288
|
+
first = false
|
|
289
|
+
out << inner if indent
|
|
290
|
+
string(k.is_a?(String) ? k : Builtins.to_s(k), out)
|
|
291
|
+
out << (indent ? ": " : ":")
|
|
292
|
+
gen(x, out, inner, depth + 1)
|
|
293
|
+
end
|
|
294
|
+
out << "\n" << indent if indent
|
|
295
|
+
out << "}"
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
string(Builtins.to_s(v), out)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def string(s, out)
|
|
303
|
+
gen_error("source sequence is illegal/malformed utf-8") unless s.valid_encoding?
|
|
304
|
+
|
|
305
|
+
out << '"'
|
|
306
|
+
s.each_char do |c|
|
|
307
|
+
case c
|
|
308
|
+
when '"' then out << '\\"'
|
|
309
|
+
when "\\" then out << "\\\\"
|
|
310
|
+
when "\n" then out << "\\n"
|
|
311
|
+
when "\r" then out << "\\r"
|
|
312
|
+
when "\t" then out << "\\t"
|
|
313
|
+
when "\b" then out << "\\b"
|
|
314
|
+
when "\f" then out << "\\f"
|
|
315
|
+
else
|
|
316
|
+
o = c.ord
|
|
317
|
+
o < 0x20 ? out << format("\\u%04x", o) : out << c
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
out << '"'
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# [ERBLite] Floats are written with Float#to_s (1.0e+20), as Ruby 3.4's
|
|
324
|
+
# bundled json 2.9.1 does.
|
|
325
|
+
def float(f)
|
|
326
|
+
gen_error("#{f} not allowed in JSON") if f.nan? || f.infinite?
|
|
327
|
+
|
|
328
|
+
f.to_s
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|