ebsm 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f67cacb0b8f96cb6a3d1772bba3e2b8fb49095e7cd7088127a4c14e080aa792
4
+ data.tar.gz: 4efb427fbed1f5ffba5b13cd4b448223d1bf1a41c6e46873e03c539296e7e0fa
5
+ SHA512:
6
+ metadata.gz: 672b49abb7e7081fa96ea0077a845a50d6745e792b5b5a4d4923593cd5a1c9c4f165742128b20d345b611b60d6608835ef3b57603d1f771c893e9fdc2238c878
7
+ data.tar.gz: 87aefd8f087c5d27941b64af730fd96083cc8b7f19cc779776ca39a2df4bd4f3c1ee89d6c52b971c6f7833fcd564c9c2a4be5b3a2a5b234f0c3d5e6f90f6aa14
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in ebsm.gemspec
6
+ gemspec
7
+
8
+ # Specify development dependencies here and not in the gemspec
9
+ gem 'markspec'
10
+ gem 'rake'
11
+ gem 'rspec'
12
+ gem 'rubocop'
13
+ gem 'rubocop-rake'
14
+ gem 'rubocop-rspec'
15
+ gem 'rubocop-yard'
16
+ gem 'yard'
17
+ gem 'yard-rspec'
data/ebsm.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/ebsm/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ebsm'
7
+ spec.version = Ebsm::VERSION
8
+ spec.authors = ['David Siaw']
9
+ spec.email = ['874280+davidsiaw@users.noreply.github.com']
10
+
11
+ spec.summary = 'Ebsm gem'
12
+ spec.description = 'Does not do anything'
13
+ spec.homepage = 'https://github.com/davidsiaw/ebsm'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0')
16
+
17
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/davidsiaw/ebsm'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/davidsiaw/ebsm'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir['{exe,data,lib}/**/*'] + %w[Gemfile ebsm.gemspec]
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'bsm'
30
+ end
data/exe/ebsm ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'ebsm'
5
+ require 'bsm'
6
+
7
+ # When set, print the expanded bsm2 literate text (the output of
8
+ # Ebsm::Renderer#render) instead of compiling it to binary. Useful for
9
+ # debugging ebsm scripting: you see exactly what the templating produced
10
+ # before bsm2 turns it into bytes.
11
+ output_bsm2 = ARGV.delete('--bsm') || ARGV.delete('-b')
12
+
13
+ content = ARGF.read
14
+
15
+ begin
16
+ expanded = Ebsm::Renderer.new.render(content)
17
+ if output_bsm2
18
+ print expanded
19
+ else
20
+ print Ebsm::Renderer.new.bsm2_evaluate(content)
21
+ end
22
+ rescue Ebsm::Error => e
23
+ warn e.format
24
+ exit 1
25
+ end
data/lib/ebsm/dsl.rb ADDED
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ebsm/hex'
4
+
5
+ module Ebsm
6
+ # The evaluation context for an ebsm template -- the object whose binding an
7
+ # ERB template is evaluated against. Any public method here is available
8
+ # inside a [expr] interpolation or a <% ... %> code block.
9
+ #
10
+ # Helper methods are expected to return strings that the bsm2 lexer can
11
+ # consume on a '; ' data line -- typically space-separated hex byte pairs, so
12
+ # that e.g. [le32 0x12345678] expands to "; 78 56 34 12".
13
+ class Dsl
14
+ def initialize
15
+ @emitted = []
16
+ end
17
+
18
+ # Source lines of the template being rendered, set by the renderer before
19
+ # evaluation so #eval_at can report the text of an offending line.
20
+ attr_writer :lines
21
+
22
+ # The [source_line, expanded_text] pairs emitted by #line, in order. The
23
+ # renderer reads this to feed each ; line to bsm2 individually so a bsm2
24
+ # error can be reported against the original source line.
25
+ attr_reader :emitted
26
+
27
+ # --- Byte helpers -------------------------------------------------------
28
+ # Each helper emits +value+ as space-separated hex byte pairs, raising
29
+ # Ebsm::Error if +value+ is not an Integer or does not fit the width.
30
+
31
+ # Emit +value+ as a single hex byte pair (0..0xff).
32
+ # @param value [Integer] the byte value, 0..255.
33
+ # @return [String] two lowercase hex digits.
34
+ def byte(value)
35
+ hex.pack_hex(value, 'C', 0xff)
36
+ end
37
+
38
+ # Emit +count+ copies of +value+ as space-separated lowercase hex pairs.
39
+ # Handy for padding/filling, e.g. [repeat 0x00, 16] pads 16 zero bytes.
40
+ # +value+ must be an Integer 0..255; +count+ must be a non-negative Integer.
41
+ # @param value [Integer] the byte to repeat, 0..255.
42
+ # @param count [Integer] non-negative number of copies.
43
+ # @return [String] space-separated hex byte pairs.
44
+ def repeat(value, count)
45
+ unless count.is_a?(Integer) && !count.negative?
46
+ raise Ebsm::Error,
47
+ "#{count.inspect} is not a non-negative Integer"
48
+ end
49
+
50
+ hex_val = hex.hex_pair(value)
51
+ Array.new(count, hex_val).join(' ')
52
+ end
53
+
54
+ # Emit every byte of +value+ as space-separated lowercase hex pairs.
55
+ # Accepts a String (its bytes) or an Enumerable of Integers (each 0..255).
56
+ # Useful for dumping a whole string or byte array in one call, e.g.
57
+ # [bytes chunk] where chunk is a binary String, or [bytes arr] where arr
58
+ # is an Array of Integers assigned on a # line.
59
+ # @param value [String, Enumerable<Integer>] the bytes to emit.
60
+ # @return [String] space-separated hex byte pairs.
61
+ def bytes(value)
62
+ ints = value.is_a?(String) ? value.bytes : value.to_a
63
+ ints.map { |b| hex.hex_pair(b) }.join(' ')
64
+ end
65
+
66
+ # Emit +value+ as 2 little-endian hex byte pairs (0..0xffff).
67
+ # @param value [Integer] 0..0xffff.
68
+ # @return [String] two space-separated hex byte pairs.
69
+ def le16(value)
70
+ hex.pack_hex(value, 'v', 0xffff)
71
+ end
72
+
73
+ # Emit +value+ as 2 big-endian hex byte pairs (0..0xffff).
74
+ # @param value [Integer] 0..0xffff.
75
+ # @return [String] two space-separated hex byte pairs.
76
+ def be16(value)
77
+ hex.pack_hex(value, 'n', 0xffff)
78
+ end
79
+
80
+ # Emit +value+ as 4 little-endian hex byte pairs (0..0xffffffff).
81
+ # @param value [Integer] 0..0xffffffff.
82
+ # @return [String] four space-separated hex byte pairs.
83
+ def le32(value)
84
+ hex.pack_hex(value, 'V', 0xffffffff)
85
+ end
86
+
87
+ # Emit +value+ as 4 big-endian hex byte pairs (0..0xffffffff).
88
+ # @param value [Integer] 0..0xffffffff.
89
+ # @return [String] four space-separated hex byte pairs.
90
+ def be32(value)
91
+ hex.pack_hex(value, 'N', 0xffffffff)
92
+ end
93
+
94
+ # Emit +value+ as 8 little-endian hex byte pairs.
95
+ # @param value [Integer] 0..0xffffffffffffffff.
96
+ # @return [String] eight space-separated hex byte pairs.
97
+ def le64(value)
98
+ hex.pack_hex(value, 'Q<', 0xffffffffffffffff)
99
+ end
100
+
101
+ # Emit +value+ as 8 big-endian hex byte pairs.
102
+ # @param value [Integer] 0..0xffffffffffffffff.
103
+ # @return [String] eight space-separated hex byte pairs.
104
+ def be64(value)
105
+ hex.pack_hex(value, 'Q>', 0xffffffffffffffff)
106
+ end
107
+
108
+ # Emit +value+ as a 32-bit (single-precision) little-endian IEEE 754 float.
109
+ # @param value [Float] the float to encode.
110
+ # @return [String] four space-separated hex byte pairs.
111
+ def lef32(value)
112
+ hex.pack_float(value, 'e')
113
+ end
114
+
115
+ # Emit +value+ as a 32-bit (single-precision) big-endian IEEE 754 float.
116
+ # @param value [Float] the float to encode.
117
+ # @return [String] four space-separated hex byte pairs.
118
+ def bef32(value)
119
+ hex.pack_float(value, 'g')
120
+ end
121
+
122
+ # Emit +value+ as a 64-bit (double-precision) little-endian IEEE 754 float.
123
+ # @param value [Float] the float to encode.
124
+ # @return [String] eight space-separated hex byte pairs.
125
+ def lef64(value)
126
+ hex.pack_float(value, 'E')
127
+ end
128
+
129
+ # Emit +value+ as a 64-bit (double-precision) big-endian IEEE 754 float.
130
+ # @param value [Float] the float to encode.
131
+ # @return [String] eight space-separated hex byte pairs.
132
+ def bef64(value)
133
+ hex.pack_float(value, 'G')
134
+ end
135
+
136
+ # Evaluate a [expr] interpolation, annotating any error with the source
137
+ # position of the bracket so the CLI can draw an arrow/tilde. Ebsm::Error
138
+ # (from helpers) is passed through with position attached; any other
139
+ # StandardError raised by the expression (NoMethodError, ArgumentError,
140
+ # etc.) is wrapped in an Ebsm::Error so it gets the same diagnostic.
141
+ # @param line_number [Integer] 1-indexed source line of the bracket.
142
+ # @param column [Integer] 1-indexed column of the opening bracket.
143
+ # @param char_length [Integer] length of the [...] bracket span.
144
+ # @return [Object] the result of the block.
145
+ # @yieldreturn [Object] the result of the [expr] expression.
146
+ def eval_at(line_number, column, char_length)
147
+ yield
148
+ rescue Ebsm::Error => e
149
+ raise annotate(e, line_number, column, char_length)
150
+ rescue StandardError => e
151
+ raise annotate(Ebsm::Error.new("#{e.message} (#{e.class})"),
152
+ line_number, column, char_length)
153
+ end
154
+
155
+ # The binding the renderer evaluates the ERB template against. Methods set
156
+ # or called in <% %> / <%= %> resolve on this object.
157
+ # @return [Binding] this DSL's binding.
158
+ def erb_binding
159
+ binding
160
+ end
161
+
162
+ private
163
+
164
+ # Attach source position to +error+ and return it for re-raising.
165
+ def annotate(error, line_number, column, char_length)
166
+ error.line_number = line_number
167
+ error.column = column
168
+ error.char_length = char_length
169
+ error.line_text = @lines[line_number - 1]
170
+ error.source_lines = @lines
171
+ error
172
+ end
173
+
174
+ # Record that a ; data line (the fully-expanded +text+) came from
175
+ # +source_line+, and return +text+ so ERB still emits it. The renderer
176
+ # reads #emitted to bsm2_evaluate each ; line individually.
177
+ # @param source_line [Integer] 1-indexed source line of the ; line.
178
+ # @return [String] the expanded ; line text.
179
+ # @yieldreturn [String] the expanded ; line text.
180
+ def line(source_line)
181
+ text = yield
182
+ @emitted << [source_line, text]
183
+ text
184
+ end
185
+
186
+ # @return [Ebsm::Hex] a fresh Hex formatter.
187
+ def hex
188
+ Hex.new
189
+ end
190
+ end
191
+ end
data/lib/ebsm/hex.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebsm
4
+ # Formats numbers as the hex byte pairs the bsm2 lexer consumes on a '; '
5
+ # data line. Owns the responsibility of validating a number (Integer in range,
6
+ # or Float) and rendering it as lowercase hex pairs, so that Ebsm::Renderer can
7
+ # stay focused on templating.
8
+ class Hex
9
+ # Convert +value+ to a byte sequence via Array#pack using +directive+ and
10
+ # render it as space-separated lowercase hex pairs. Raises Ebsm::Error if
11
+ # +value+ is not an Integer or lies outside 0..+max+.
12
+ # @param value [Integer] the integer to encode.
13
+ # @param directive [String] an Array#pack directive (e.g. 'V', 'N', 'C').
14
+ # @param max [Integer] the inclusive upper bound for +value+.
15
+ # @return [String] space-separated lowercase hex byte pairs.
16
+ def pack_hex(value, directive, max)
17
+ raise Ebsm::Error, "#{value.inspect} is not an Integer" unless value.is_a?(Integer)
18
+
19
+ if value.negative? || value > max
20
+ raise Ebsm::Error, format('0x%<v>s out of range 0..0x%<m>x',
21
+ v: value.to_s(16), m: max)
22
+ end
23
+
24
+ [value].pack(directive).bytes.map { |b| format('%02x', b) }.join(' ')
25
+ end
26
+
27
+ # Render a single Integer byte (0..255) as a two-digit lowercase hex pair,
28
+ # raising Ebsm::Error if it is not an Integer or is out of range.
29
+ # @param byte [Integer] the byte value, 0..255.
30
+ # @return [String] two lowercase hex digits.
31
+ def hex_pair(byte)
32
+ raise Ebsm::Error, "#{byte.inspect} is not an Integer" unless byte.is_a?(Integer)
33
+
34
+ raise Ebsm::Error, format('0x%<v>s out of range 0..0xff', v: byte.to_s(16)) if byte.negative? || byte > 0xff
35
+
36
+ format('%02x', byte)
37
+ end
38
+
39
+ # Convert a Float +value+ to its IEEE 754 binary representation via
40
+ # Array#pack using +directive+ and render it as space-separated lowercase
41
+ # hex pairs. Raises Ebsm::Error if +value+ is not a Float.
42
+ # @param value [Float] the float to encode.
43
+ # @param directive [String] an Array#pack float directive ('e','g','E','G').
44
+ # @return [String] space-separated lowercase hex byte pairs.
45
+ def pack_float(value, directive)
46
+ raise Ebsm::Error, "#{value.inspect} is not a Float" unless value.is_a?(Float)
47
+
48
+ [value].pack(directive).bytes.map { |b| format('%02x', b) }.join(' ')
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'ebsm/dsl'
5
+
6
+ module Ebsm
7
+ # Renders ebsm source to bsm2 literate text. Owns the source -> ERB template
8
+ # translation; the byte helpers users call inside [...] live on Ebsm::Dsl,
9
+ # whose binding the template is evaluated against.
10
+ class Renderer
11
+ # Translate ebsm source into an ERB program, evaluate it against a Dsl
12
+ # instance, and return the resulting bsm2 literate text. Each [expr] on a
13
+ # '; ' data line is wrapped so that an Ebsm::Error raised while evaluating
14
+ # it is annotated with its source line, column, and length for diagnostics.
15
+ # @param content [String] the ebsm source.
16
+ # @return [String] the expanded bsm2 literate text.
17
+ def render(content)
18
+ dsl = evaluate(content)
19
+ ERB.new(build_template(content), trim_mode: '-').result(dsl.erb_binding)
20
+ end
21
+
22
+ # Evaluate the template, then feed each emitted ; line to bsm2 individually
23
+ # so a bsm2 lexer error is reported against the original source line (not
24
+ # the expanded text's line number). Returns the concatenated binary bytes.
25
+ # @param content [String] the ebsm source.
26
+ # @return [String] the concatenated binary bytes.
27
+ def bsm2_evaluate(content)
28
+ dsl = evaluate(content)
29
+ ERB.new(build_template(content), trim_mode: '-').result(dsl.erb_binding)
30
+ generator = Bsm::Generator.new
31
+ source_lines = content.lines.map(&:chomp)
32
+ dsl.emitted.each_with_object(+'') do |(source_line, text), output|
33
+ output << generate_line(generator, source_line, text, source_lines)
34
+ end
35
+ end
36
+
37
+ # Compile a single expanded ; line, re-raising a bsm2 error as an
38
+ # Ebsm::Error annotated with the original +source_line+ and source context.
39
+ # @param generator [Bsm::Generator] the bsm2 generator.
40
+ # @param source_line [Integer] 1-indexed source line of the ; line.
41
+ # @param text [String] the expanded ; line text.
42
+ # @param source_lines [Array<String>] all source lines (chomped).
43
+ # @return [String] the compiled binary bytes for this line.
44
+ def generate_line(generator, source_line, text, source_lines)
45
+ generator.generate("#{text}\n")
46
+ rescue Bsm::InvalidInput => e
47
+ raise bsm_error(e, source_line, text, source_lines)
48
+ end
49
+
50
+ # Translate ebsm source into an ERB program. A '# ' line becomes an ERB
51
+ # code block, a '; ' line is wrapped in a #line call (so the renderer can
52
+ # track which source line each expanded ; line came from), anything else
53
+ # passes through verbatim (bsm2 ignores it).
54
+ # @param content [String] the ebsm source.
55
+ # @return [String] the ERB program.
56
+ def build_template(content)
57
+ content.lines.each_with_index.map do |line, index|
58
+ stripped = line.chomp
59
+ if stripped.start_with?('#')
60
+ "<% #{stripped[1..]} -%>\n"
61
+ elsif stripped.start_with?(';')
62
+ "<%= line(#{index + 1}) { #{expand_brackets(stripped, index + 1)} } %>\n"
63
+ else
64
+ line
65
+ end
66
+ end.join
67
+ end
68
+
69
+ # Build a Ruby double-quoted string expression that evaluates to the
70
+ # expanded ; line, with each [expr] replaced by an `eval_at(N, col, len)`
71
+ # interpolation. Literal text is escaped for a double-quoted Ruby string.
72
+ # An unclosed [ is left literal.
73
+ # @param stripped [String] the chomped ; source line.
74
+ # @param line_number [Integer] 1-indexed source line number.
75
+ # @return [String] a Ruby double-quoted string expression.
76
+ def expand_brackets(stripped, line_number)
77
+ expr = +'"'
78
+ index = 0
79
+ expr, index = expand_segment(expr, stripped, index, line_number) while index < stripped.length
80
+ "#{expr}\""
81
+ end
82
+
83
+ # Expand one segment of a ; line starting at +index+, returning the updated
84
+ # [expr, next_index]. A [expr] bracket becomes an eval_at interpolation; a
85
+ # literal run is escaped; an unclosed [ ends the walk.
86
+ # @param expr [String] the accumulator expression so far.
87
+ # @param stripped [String] the chomped ; source line.
88
+ # @param index [Integer] the current position in +stripped+.
89
+ # @param line_number [Integer] 1-indexed source line number.
90
+ # @return [Array(String, Integer)] the updated [expr, next_index].
91
+ def expand_segment(expr, stripped, index, line_number)
92
+ if stripped[index] == '['
93
+ close = stripped.index(']', index)
94
+ return [expr, stripped.length] unless close
95
+
96
+ [expr + bracket_expr(stripped, index, close, line_number), close + 1]
97
+ else
98
+ chunk_end = stripped.index('[', index) || stripped.length
99
+ [expr + escape_dq(stripped[index...chunk_end]), chunk_end]
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def evaluate(content)
106
+ dsl = Dsl.new
107
+ dsl.lines = content.lines.map(&:chomp)
108
+ dsl
109
+ end
110
+
111
+ # The `eval_at(...)` interpolation expression for a [expr] bracket
112
+ # spanning +open+..+close+ in +stripped+.
113
+ def bracket_expr(stripped, open, close, line_number)
114
+ inner = stripped[(open + 1)...close]
115
+ column = open + 1
116
+ length = close - open + 1
117
+ "\#{eval_at(#{line_number}, #{column}, #{length}) { #{inner} }}"
118
+ end
119
+
120
+ # Escape a literal chunk for a double-quoted Ruby string: backslash,
121
+ # double-quote, and # when it would start an interpolation.
122
+ def escape_dq(chunk)
123
+ chunk.gsub(/[\\"]|\#(?=[{@$])/) { |m| "\\#{m}" }
124
+ end
125
+
126
+ # Build an Ebsm::Error from a bsm2 error, pointing at +source_line+ with
127
+ # +expanded_text+ as the arrow target and +source_lines+ for context.
128
+ def bsm_error(error, source_line, expanded_text, source_lines)
129
+ result = Ebsm::Error.new(error.message)
130
+ result.line_number = source_line
131
+ result.column = error.column
132
+ result.char_length = error.length
133
+ result.line_text = error.line_text
134
+ result.source_lines = source_lines
135
+ result.expanded_line = expanded_text unless expanded_text == source_lines[source_line - 1]
136
+ result
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ebsm is a literate binary generator: a templating layer over bsm2 that lets
4
+ # you mix prose, Ruby scripting (# lines), and byte data (; lines) in one
5
+ # source file. See Ebsm::Renderer and Ebsm::Dsl.
6
+ module Ebsm
7
+ # The ebsm gem version.
8
+ VERSION = '0.1.0'
9
+ end
data/lib/ebsm.rb ADDED
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ebsm/version'
4
+ require 'ebsm/hex'
5
+ require 'ebsm/dsl'
6
+ require 'ebsm/renderer'
7
+ require 'bsm'
8
+
9
+ # Ebsm is a literate binary generator: a templating layer over bsm2 that lets
10
+ # you mix prose, Ruby scripting (# lines), and byte data (; lines) in one
11
+ # source file. See Ebsm::Renderer and Ebsm::Dsl.
12
+ module Ebsm
13
+ # An error raised while evaluating ebsm source. Carries the source position
14
+ # of the offending line plus (for bsm2-stage errors) the expanded bsm2 line
15
+ # the arrow actually points at, so the CLI can show both source and expanded
16
+ # context like bsm2 does.
17
+ class Error < StandardError
18
+ # Source position of the offending line/bracket, filled in by the renderer.
19
+ attr_accessor :line_number, :column, :char_length, :line_text
20
+ # All source lines (chomped), for printing context lines above the error.
21
+ attr_accessor :source_lines
22
+ # The expanded bsm2 line the arrow points at. nil for ebsm-stage helper
23
+ # errors (where the source line is the arrow target).
24
+ attr_accessor :expanded_line
25
+
26
+ # Format this error as a diagnostic with source context (2 lines above)
27
+ # and, for bsm2-stage errors, the expanded line the arrow points at:
28
+ #
29
+ # EBSM: bsm2 error at source line 2 (expanded column 17)
30
+ # ---
31
+ # 1 | # x = 0x12345678
32
+ # > 2 | ; [le32 x] 45 asd
33
+ # expanded:
34
+ # ; 78 56 34 12 45 asd
35
+ # ^ hex byte needs two hex digits
36
+ # @return [String] the formatted diagnostic.
37
+ def format
38
+ body = "#{header}\n---\n"
39
+ body << context_lines
40
+ body << expanded_section if expanded_line
41
+ body << arrow_line
42
+ body
43
+ end
44
+
45
+ # The header line, noting whether the column is expanded (bsm2-stage) or
46
+ # source (ebsm-stage).
47
+ # @return [String] the header line.
48
+ def header
49
+ if expanded_line
50
+ "EBSM: bsm2 error at source line #{line_number} (expanded column #{column})"
51
+ else
52
+ "EBSM: error at source line #{line_number} (column #{column})"
53
+ end
54
+ end
55
+
56
+ # The "expanded:" label and the expanded bsm2 line -- shown only for
57
+ # bsm2-stage errors where the arrow target differs from source.
58
+ # @return [String] the expanded section.
59
+ def expanded_section
60
+ pad = ' ' * prefix_width
61
+ "#{pad}expanded:\n#{pad}#{expanded_line}\n"
62
+ end
63
+
64
+ # The arrow (with leading spaces to align under the displayed line) and the
65
+ # error message.
66
+ # @return [String] the arrow line with the message.
67
+ def arrow_line
68
+ indent = ' ' * (prefix_width + (column - 1))
69
+ "#{indent}#{arrow} #{message}\n"
70
+ end
71
+
72
+ # The numbered source lines from 2 above the error up to the error line,
73
+ # with `> ` marking the error line. Line numbers are right-padded to the
74
+ # error line's digit width so the `|` pipes align. When the error is near
75
+ # the top of the file and there are fewer than 2 source lines above, a
76
+ # single `beginning of file` marker fills the gap.
77
+ # @return [String] the numbered context lines.
78
+ def context_lines
79
+ width = line_number.to_s.length
80
+ first = [line_number - 2, 1].max
81
+ bof = line_number < 3 ? [bof_line(width)] : []
82
+ real = (first..line_number).map { |n| context_line(n, width) }
83
+ (bof + real).join
84
+ end
85
+
86
+ private
87
+
88
+ def prefix_width
89
+ "> #{line_number} | ".length
90
+ end
91
+
92
+ def arrow
93
+ "^#{'~' * (char_length - 1)}"
94
+ end
95
+
96
+ def context_line(number, width)
97
+ prefix = number == line_number ? '> ' : ' '
98
+ "#{prefix}#{number.to_s.rjust(width)} | #{source_lines[number - 1]}\n"
99
+ end
100
+
101
+ def bof_line(width)
102
+ " #{' ' * width} | *beginning of file*\n"
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ebsm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Siaw
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bsm
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Does not do anything
28
+ email:
29
+ - 874280+davidsiaw@users.noreply.github.com
30
+ executables:
31
+ - ebsm
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - ebsm.gemspec
37
+ - exe/ebsm
38
+ - lib/ebsm.rb
39
+ - lib/ebsm/dsl.rb
40
+ - lib/ebsm/hex.rb
41
+ - lib/ebsm/renderer.rb
42
+ - lib/ebsm/version.rb
43
+ homepage: https://github.com/davidsiaw/ebsm
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ allowed_push_host: https://rubygems.org
48
+ homepage_uri: https://github.com/davidsiaw/ebsm
49
+ source_code_uri: https://github.com/davidsiaw/ebsm
50
+ changelog_uri: https://github.com/davidsiaw/ebsm
51
+ rubygems_mfa_required: 'true'
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.5.22
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Ebsm gem
71
+ test_files: []