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,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