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,52 @@
|
|
|
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 single exception class raised for every template problem (syntax,
|
|
13
|
+
# runtime, sandbox and limit errors). +type+ is the Ruby-style error name
|
|
14
|
+
# from SPEC section 11 ("NoMethodError", "Math::DomainError", ...).
|
|
15
|
+
class Error < StandardError
|
|
16
|
+
attr_reader :type, :filename, :line
|
|
17
|
+
|
|
18
|
+
def initialize(type, message, filename = nil, line = nil)
|
|
19
|
+
@type = type
|
|
20
|
+
@raw_message = message.to_s
|
|
21
|
+
@filename = filename
|
|
22
|
+
@line = line
|
|
23
|
+
super(@raw_message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The human readable message without location information.
|
|
27
|
+
def message
|
|
28
|
+
@raw_message
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fills in the location if it is not known yet. Errors raised by built-in
|
|
32
|
+
# methods get the line of the call that raised them.
|
|
33
|
+
def fill(filename, line)
|
|
34
|
+
@filename ||= filename
|
|
35
|
+
@line ||= line
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Display form from SPEC section 11.
|
|
40
|
+
def to_s
|
|
41
|
+
"#{@filename}:#{@line}: #{@type}: #{@raw_message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def full_message(**)
|
|
45
|
+
to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def detailed_message(**)
|
|
49
|
+
to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
# format / sprintf / String#% (SPEC section 10).
|
|
13
|
+
#
|
|
14
|
+
# The format string is parsed here. Each directive is then rendered by
|
|
15
|
+
# calling Kernel#format with a spec rebuilt from validated pieces only
|
|
16
|
+
# (flags from "-+ 0#", digits, '.', and a type letter from a fixed set), so
|
|
17
|
+
# template text never reaches Kernel#format directly.
|
|
18
|
+
module Format
|
|
19
|
+
FLAGS = "-+ 0#"
|
|
20
|
+
INT_TYPES = %w[d i u].freeze
|
|
21
|
+
BASE_TYPES = %w[x X o b B].freeze
|
|
22
|
+
FLOAT_TYPES = %w[f e E g G].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def arg_error(msg)
|
|
27
|
+
Error.new("ArgumentError", msg)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format(it, fmt, args)
|
|
31
|
+
Builtins.need_str(fmt)
|
|
32
|
+
st = { args: args, argi: 0, mode: nil }
|
|
33
|
+
out = +""
|
|
34
|
+
i = 0
|
|
35
|
+
n = fmt.length
|
|
36
|
+
while i < n
|
|
37
|
+
c = fmt[i]
|
|
38
|
+
if c != "%"
|
|
39
|
+
j = fmt.index("%", i) || n
|
|
40
|
+
out << fmt[i...j]
|
|
41
|
+
i = j
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
i += 1
|
|
45
|
+
raise arg_error("incomplete format specifier; use %% (double %) instead") if i >= n
|
|
46
|
+
|
|
47
|
+
if fmt[i] == "%"
|
|
48
|
+
out << "%"
|
|
49
|
+
i += 1
|
|
50
|
+
next
|
|
51
|
+
end
|
|
52
|
+
flags = +""
|
|
53
|
+
width = nil
|
|
54
|
+
prec = nil
|
|
55
|
+
value = nil
|
|
56
|
+
has_value = false
|
|
57
|
+
brace = false
|
|
58
|
+
loop do
|
|
59
|
+
ch = fmt[i]
|
|
60
|
+
if ch && FLAGS.include?(ch)
|
|
61
|
+
flags << ch unless flags.include?(ch)
|
|
62
|
+
i += 1
|
|
63
|
+
elsif ch == "<" || ch == "{"
|
|
64
|
+
raise arg_error("named after a value was already given") if has_value
|
|
65
|
+
|
|
66
|
+
close = fmt.index(ch == "<" ? ">" : "}", i)
|
|
67
|
+
raise arg_error("malformed name - unmatched parenthesis") unless close
|
|
68
|
+
|
|
69
|
+
value = named(st, fmt[(i + 1)...close])
|
|
70
|
+
has_value = true
|
|
71
|
+
brace = ch == "{"
|
|
72
|
+
i = close + 1
|
|
73
|
+
break if brace
|
|
74
|
+
elsif ch&.match?(/[0-9]/) && width.nil?
|
|
75
|
+
j = i
|
|
76
|
+
j += 1 while fmt[j]&.match?(/[0-9]/)
|
|
77
|
+
if fmt[j] == "$" && !has_value
|
|
78
|
+
value = positional(st, fmt[i...j].to_i)
|
|
79
|
+
has_value = true
|
|
80
|
+
i = j + 1
|
|
81
|
+
else
|
|
82
|
+
width = fmt[i...j].to_i
|
|
83
|
+
i = j
|
|
84
|
+
end
|
|
85
|
+
elsif ch == "*" && width.nil?
|
|
86
|
+
i += 1
|
|
87
|
+
w = int_arg(next_arg(st))
|
|
88
|
+
if w.negative?
|
|
89
|
+
flags << "-" unless flags.include?("-")
|
|
90
|
+
w = -w
|
|
91
|
+
end
|
|
92
|
+
width = w
|
|
93
|
+
elsif ch == "." && prec.nil?
|
|
94
|
+
i += 1
|
|
95
|
+
if fmt[i] == "*"
|
|
96
|
+
i += 1
|
|
97
|
+
pv = int_arg(next_arg(st))
|
|
98
|
+
prec = pv.negative? ? nil : pv
|
|
99
|
+
else
|
|
100
|
+
j = i
|
|
101
|
+
j += 1 while fmt[j]&.match?(/[0-9]/)
|
|
102
|
+
prec = fmt[i...j].to_i
|
|
103
|
+
i = j
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
break
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
Builtins.cap(width) if width
|
|
110
|
+
Builtins.cap(prec) if prec
|
|
111
|
+
spec = "%#{flags}#{width}#{prec ? ".#{prec}" : ""}"
|
|
112
|
+
if brace
|
|
113
|
+
out << Kernel.format("#{spec}s", Builtins.to_s(value))
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
type = fmt[i]
|
|
117
|
+
raise arg_error("malformed format string - %#{type}") if type.nil? || !type.match?(/[a-zA-Z]/)
|
|
118
|
+
|
|
119
|
+
i += 1
|
|
120
|
+
unless INT_TYPES.include?(type) || BASE_TYPES.include?(type) || FLOAT_TYPES.include?(type) ||
|
|
121
|
+
%w[s p c].include?(type)
|
|
122
|
+
raise arg_error("malformed format string - %#{type}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
value = next_arg(st) unless has_value
|
|
126
|
+
out << directive(it, type, flags, spec, value)
|
|
127
|
+
end
|
|
128
|
+
out
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def next_arg(st)
|
|
132
|
+
raise arg_error("unnumbered mixed with named") if st[:mode] == :named
|
|
133
|
+
raise arg_error("unnumbered mixed with numbered") if st[:mode] == :abs
|
|
134
|
+
|
|
135
|
+
st[:mode] = :pos
|
|
136
|
+
raise arg_error("too few arguments") if st[:argi] >= st[:args].length
|
|
137
|
+
|
|
138
|
+
v = st[:args][st[:argi]]
|
|
139
|
+
st[:argi] += 1
|
|
140
|
+
v
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def positional(st, n)
|
|
144
|
+
raise arg_error("numbered(#{n}) after unnumbered") if st[:mode] == :pos
|
|
145
|
+
raise arg_error("numbered(#{n}) after named") if st[:mode] == :named
|
|
146
|
+
raise arg_error("invalid index - #{n}$") if n < 1 || n > st[:args].length
|
|
147
|
+
|
|
148
|
+
st[:mode] = :abs
|
|
149
|
+
st[:args][n - 1]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def named(st, name)
|
|
153
|
+
raise arg_error("named<#{name}> after unnumbered") if st[:mode] == :pos || st[:mode] == :abs
|
|
154
|
+
|
|
155
|
+
st[:mode] = :named
|
|
156
|
+
h = st[:args][0]
|
|
157
|
+
raise arg_error("one hash required") unless st[:args].length == 1 && h.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
key = name.to_sym
|
|
160
|
+
return h[key] if h.key?(key)
|
|
161
|
+
return h.default unless h.default.nil?
|
|
162
|
+
|
|
163
|
+
raise Error.new("KeyError", "key<#{name}> not found")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def int_arg(v)
|
|
167
|
+
case v
|
|
168
|
+
when Integer then v
|
|
169
|
+
when Float
|
|
170
|
+
raise Error.new("FloatDomainError", v.to_s) if v.nan? || v.infinite?
|
|
171
|
+
|
|
172
|
+
Builtins.ck(v.to_i)
|
|
173
|
+
when String then Builtins.ck(Kernel.Integer(v))
|
|
174
|
+
else raise Error.new("TypeError", "can't convert #{Builtins.tname(v)} into Integer")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def float_arg(v)
|
|
179
|
+
case v
|
|
180
|
+
when Integer, Float then v
|
|
181
|
+
when String then Kernel.Float(v)
|
|
182
|
+
else raise Error.new("TypeError", "can't convert #{Builtins.tname(v)} into Float")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def directive(it, type, flags, spec, v)
|
|
187
|
+
if INT_TYPES.include?(type)
|
|
188
|
+
Kernel.format("#{spec}d", int_arg(v))
|
|
189
|
+
elsif BASE_TYPES.include?(type)
|
|
190
|
+
iv = int_arg(v)
|
|
191
|
+
if iv.negative? && !flags.include?("+") && !flags.include?(" ")
|
|
192
|
+
raise arg_error("negative argument for %#{type} (two's complement output is not supported)")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
Kernel.format("#{spec}#{type}", iv)
|
|
196
|
+
elsif FLOAT_TYPES.include?(type)
|
|
197
|
+
Kernel.format("#{spec}#{type}", float_arg(v))
|
|
198
|
+
else
|
|
199
|
+
case type
|
|
200
|
+
when "s" then Kernel.format("#{spec}s", Builtins.to_s(v))
|
|
201
|
+
when "p" then Kernel.format("#{spec}s", Builtins.inspect(v))
|
|
202
|
+
else
|
|
203
|
+
case v
|
|
204
|
+
when Integer, Float then Kernel.format("#{spec}c", int_arg(v))
|
|
205
|
+
when String then Kernel.format("#{spec}c", v)
|
|
206
|
+
else raise Error.new("TypeError", "no implicit conversion of #{Builtins.tname(v)} into Integer")
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|