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