herb 0.1.0-x86_64-linux-gnu

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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/License.txt +21 -0
  3. data/Makefile +121 -0
  4. data/README.md +166 -0
  5. data/Rakefile +184 -0
  6. data/exe/herb +5 -0
  7. data/ext/herb/error_helpers.c +302 -0
  8. data/ext/herb/error_helpers.h +15 -0
  9. data/ext/herb/extconf.rb +75 -0
  10. data/ext/herb/extension.c +110 -0
  11. data/ext/herb/extension.h +6 -0
  12. data/ext/herb/extension_helpers.c +117 -0
  13. data/ext/herb/extension_helpers.h +24 -0
  14. data/ext/herb/nodes.c +936 -0
  15. data/ext/herb/nodes.h +12 -0
  16. data/herb.gemspec +49 -0
  17. data/lib/herb/3.0/herb.so +0 -0
  18. data/lib/herb/3.1/herb.so +0 -0
  19. data/lib/herb/3.2/herb.so +0 -0
  20. data/lib/herb/3.3/herb.so +0 -0
  21. data/lib/herb/3.4/herb.so +0 -0
  22. data/lib/herb/ast/node.rb +61 -0
  23. data/lib/herb/ast/nodes.rb +1542 -0
  24. data/lib/herb/ast.rb +6 -0
  25. data/lib/herb/cli.rb +164 -0
  26. data/lib/herb/errors.rb +352 -0
  27. data/lib/herb/lex_result.rb +20 -0
  28. data/lib/herb/libherb/array.rb +48 -0
  29. data/lib/herb/libherb/ast_node.rb +47 -0
  30. data/lib/herb/libherb/buffer.rb +53 -0
  31. data/lib/herb/libherb/extract_result.rb +17 -0
  32. data/lib/herb/libherb/lex_result.rb +29 -0
  33. data/lib/herb/libherb/libherb.rb +49 -0
  34. data/lib/herb/libherb/parse_result.rb +17 -0
  35. data/lib/herb/libherb/token.rb +43 -0
  36. data/lib/herb/libherb.rb +32 -0
  37. data/lib/herb/location.rb +42 -0
  38. data/lib/herb/parse_result.rb +26 -0
  39. data/lib/herb/position.rb +36 -0
  40. data/lib/herb/project.rb +361 -0
  41. data/lib/herb/range.rb +40 -0
  42. data/lib/herb/result.rb +21 -0
  43. data/lib/herb/token.rb +43 -0
  44. data/lib/herb/token_list.rb +11 -0
  45. data/lib/herb/version.rb +5 -0
  46. data/lib/herb.rb +32 -0
  47. data/src/analyze.c +989 -0
  48. data/src/analyze_helpers.c +241 -0
  49. data/src/analyzed_ruby.c +35 -0
  50. data/src/array.c +137 -0
  51. data/src/ast_node.c +81 -0
  52. data/src/ast_nodes.c +866 -0
  53. data/src/ast_pretty_print.c +588 -0
  54. data/src/buffer.c +199 -0
  55. data/src/errors.c +740 -0
  56. data/src/extract.c +110 -0
  57. data/src/herb.c +103 -0
  58. data/src/html_util.c +143 -0
  59. data/src/include/analyze.h +36 -0
  60. data/src/include/analyze_helpers.h +43 -0
  61. data/src/include/analyzed_ruby.h +33 -0
  62. data/src/include/array.h +33 -0
  63. data/src/include/ast_node.h +35 -0
  64. data/src/include/ast_nodes.h +303 -0
  65. data/src/include/ast_pretty_print.h +17 -0
  66. data/src/include/buffer.h +36 -0
  67. data/src/include/errors.h +125 -0
  68. data/src/include/extract.h +20 -0
  69. data/src/include/herb.h +32 -0
  70. data/src/include/html_util.h +13 -0
  71. data/src/include/io.h +9 -0
  72. data/src/include/json.h +28 -0
  73. data/src/include/lexer.h +13 -0
  74. data/src/include/lexer_peek_helpers.h +23 -0
  75. data/src/include/lexer_struct.h +32 -0
  76. data/src/include/location.h +25 -0
  77. data/src/include/macros.h +10 -0
  78. data/src/include/memory.h +12 -0
  79. data/src/include/parser.h +22 -0
  80. data/src/include/parser_helpers.h +33 -0
  81. data/src/include/position.h +22 -0
  82. data/src/include/pretty_print.h +53 -0
  83. data/src/include/prism_helpers.h +18 -0
  84. data/src/include/range.h +23 -0
  85. data/src/include/ruby_parser.h +6 -0
  86. data/src/include/token.h +25 -0
  87. data/src/include/token_matchers.h +21 -0
  88. data/src/include/token_struct.h +51 -0
  89. data/src/include/util.h +25 -0
  90. data/src/include/version.h +6 -0
  91. data/src/include/visitor.h +11 -0
  92. data/src/io.c +30 -0
  93. data/src/json.c +205 -0
  94. data/src/lexer.c +284 -0
  95. data/src/lexer_peek_helpers.c +59 -0
  96. data/src/location.c +41 -0
  97. data/src/main.c +162 -0
  98. data/src/memory.c +53 -0
  99. data/src/parser.c +704 -0
  100. data/src/parser_helpers.c +161 -0
  101. data/src/position.c +33 -0
  102. data/src/pretty_print.c +242 -0
  103. data/src/prism_helpers.c +50 -0
  104. data/src/range.c +38 -0
  105. data/src/ruby_parser.c +47 -0
  106. data/src/token.c +194 -0
  107. data/src/token_matchers.c +32 -0
  108. data/src/util.c +128 -0
  109. data/src/visitor.c +321 -0
  110. metadata +159 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Herb
6
+ class LexResult
7
+ extend Forwardable
8
+
9
+ def_delegators :@array, :items, :size, :capacity
10
+
11
+ attr_accessor :array
12
+
13
+ def initialize(pointer)
14
+ @array = LibHerb::Array.new(pointer, LibHerb::Token)
15
+ end
16
+
17
+ def as_json
18
+ JSON.parse(to_json)
19
+ end
20
+
21
+ def to_json(*_args)
22
+ "[#{@array.items.map(&:to_json).join(", ")}]"
23
+ end
24
+
25
+ def inspect
26
+ @array.items.map(&:inspect).join("\n")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "herb/libherb/ast_node"
4
+ require "herb/libherb/buffer"
5
+ require "herb/libherb/array"
6
+ require "herb/libherb/token"
7
+
8
+ require "herb/libherb/lex_result"
9
+ require "herb/libherb/parse_result"
10
+
11
+ module Herb
12
+ VERSION = LibHerb.herb_version.read_string
13
+
14
+ def self.parse(source)
15
+ ParseResult.new(
16
+ LibHerb.herb_parse(source)
17
+ )
18
+ end
19
+
20
+ def self.lex(source)
21
+ LexResult.new(
22
+ LibHerb.herb_lex(source)
23
+ )
24
+ end
25
+
26
+ def self.lex_to_json(source)
27
+ LibHerb::Buffer.with do |output|
28
+ LibHerb.herb_lex_json_to_buffer(source, output.pointer)
29
+
30
+ JSON.parse(output.read.force_encoding("utf-8"))
31
+ end
32
+ end
33
+
34
+ def self.extract_ruby(source)
35
+ LibHerb::Buffer.with do |output|
36
+ LibHerb.herb_extract_ruby_to_buffer(source, output.pointer)
37
+
38
+ output.read
39
+ end
40
+ end
41
+
42
+ def self.extract_html(source)
43
+ LibHerb::Buffer.with do |output|
44
+ LibHerb.herb_extract_html_to_buffer(source, output.pointer)
45
+
46
+ output.read
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Herb
6
+ class ParseResult
7
+ extend Forwardable
8
+
9
+ def_delegators :@root_node, :type, :child_count
10
+
11
+ attr_accessor :root_node
12
+
13
+ def initialize(pointer)
14
+ @root_node = LibHerb::ASTNode.new(pointer)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ module LibHerb
5
+ attach_function :token_to_string, [:pointer], :string
6
+ attach_function :token_to_json, [:pointer], :string
7
+ attach_function :token_type_to_string, [:int], :pointer
8
+ attach_function :token_value, [:pointer], :pointer
9
+ attach_function :token_type, [:pointer], :int
10
+
11
+ class Token
12
+ attr_reader :pointer
13
+
14
+ def initialize(pointer)
15
+ @pointer = pointer
16
+ end
17
+
18
+ def value
19
+ @value ||= LibHerb.token_value(pointer).read_string
20
+ end
21
+
22
+ def type
23
+ @type ||= LibHerb.token_type_to_string(type_int).read_string
24
+ end
25
+
26
+ def type_int
27
+ @type_int ||= LibHerb.token_type(pointer)
28
+ end
29
+
30
+ def inspect
31
+ LibHerb.token_to_string(pointer).force_encoding("utf-8")
32
+ end
33
+
34
+ def as_json
35
+ JSON.parse(to_json)
36
+ end
37
+
38
+ def to_json(*_args)
39
+ LibHerb.token_to_json(pointer).force_encoding("utf-8")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "rbconfig"
5
+
6
+ module Herb
7
+ module LibHerb
8
+ extend FFI::Library
9
+
10
+ def self.library_extension
11
+ RbConfig::CONFIG["DLEXT"]
12
+ end
13
+
14
+ def self.library_name
15
+ "libherb.#{library_extension}"
16
+ end
17
+
18
+ def self.library_path
19
+ File.expand_path("../../#{library_name}", __dir__)
20
+ end
21
+
22
+ ffi_lib(library_path)
23
+
24
+ attach_function :herb_lex_to_buffer, [:pointer, :pointer], :void
25
+ attach_function :herb_lex_json_to_buffer, [:pointer, :pointer], :void
26
+ attach_function :herb_lex, [:pointer], :pointer
27
+ attach_function :herb_parse, [:pointer], :pointer
28
+ attach_function :herb_extract_ruby_to_buffer, [:pointer, :pointer], :void
29
+ attach_function :herb_extract_html_to_buffer, [:pointer, :pointer], :void
30
+ attach_function :herb_version, [], :pointer
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ class Location
5
+ attr_reader :start, :end
6
+
7
+ def initialize(start_position, end_position)
8
+ @start = start_position
9
+ @end = end_position
10
+ end
11
+
12
+ def self.from(start_line, start_column, end_line, end_column)
13
+ new(
14
+ Position.new(start_line, start_column),
15
+ Position.new(end_line, end_column)
16
+ )
17
+ end
18
+
19
+ def self.[](...)
20
+ from(...)
21
+ end
22
+
23
+ def to_hash
24
+ {
25
+ start: start,
26
+ end: self.end,
27
+ }
28
+ end
29
+
30
+ def to_json(*args)
31
+ to_hash.to_json(*args)
32
+ end
33
+
34
+ def tree_inspect
35
+ %((location: #{start.tree_inspect}-#{self.end.tree_inspect}))
36
+ end
37
+
38
+ def inspect
39
+ %(#<Herb::Location #{tree_inspect}>)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Herb
6
+ class ParseResult < Result
7
+ attr_reader :value
8
+
9
+ def initialize(value, source, warnings, errors)
10
+ @value = value
11
+ super(source, warnings, errors)
12
+ end
13
+
14
+ def failed?
15
+ errors.any? || value.errors.any? # TODO: this should probably be recursive
16
+ end
17
+
18
+ def success?
19
+ !failed?
20
+ end
21
+
22
+ def pretty_errors
23
+ JSON.pretty_generate(errors + value.errors)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ class Position
5
+ attr_reader :line, :column
6
+
7
+ def initialize(line, column)
8
+ @line = line
9
+ @column = column
10
+ end
11
+
12
+ def self.[](...)
13
+ new(...)
14
+ end
15
+
16
+ def self.from(...)
17
+ new(...)
18
+ end
19
+
20
+ def to_hash
21
+ { line: line, column: column }
22
+ end
23
+
24
+ def to_json(*args)
25
+ to_hash.to_json(*args)
26
+ end
27
+
28
+ def tree_inspect
29
+ "(#{line}:#{column})"
30
+ end
31
+
32
+ def inspect
33
+ %(#<Herb::Position #{tree_inspect}>)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+ require "timeout"
5
+ require "tempfile"
6
+ require "pathname"
7
+ require "English"
8
+
9
+ module Herb
10
+ class Project
11
+ attr_accessor :project_path, :output_file
12
+
13
+ def initialize(project_path, output_file: nil)
14
+ @project_path = Pathname.new(
15
+ project_path ? File.expand_path(".", project_path) : File.expand_path("../..", __dir__)
16
+ )
17
+
18
+ date = Time.now.strftime("%Y-%m-%d_%H-%M-%S")
19
+ @output_file = output_file || "#{date}_erb_parsing_result_#{@project_path.basename}.log"
20
+ end
21
+
22
+ def glob
23
+ "**/*.html.erb"
24
+ end
25
+
26
+ def full_path_glob
27
+ project_path + glob
28
+ end
29
+
30
+ def absolute_path
31
+ File.expand_path(@project_path, File.expand_path("../..", __dir__))
32
+ end
33
+
34
+ def files
35
+ @files ||= Dir[full_path_glob]
36
+ end
37
+
38
+ def parse!
39
+ File.open(output_file, "w") do |log|
40
+ log.puts heading("METADATA")
41
+ log.puts "Herb Version: #{Herb.version}"
42
+ log.puts "Reported at: #{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}\n\n"
43
+
44
+ log.puts heading("PROJECT")
45
+ log.puts "Path: #{absolute_path}"
46
+ log.puts "Glob: #{"#{absolute_path}#{glob}"}\n\n"
47
+
48
+ log.puts heading("PROCESSED FILES")
49
+
50
+ if files.empty?
51
+ message = "No .html.erb files found using #{full_path_glob}"
52
+ log.puts message
53
+ puts message
54
+ next
55
+ end
56
+
57
+ print "\e[H\e[2J"
58
+
59
+ successful_files = []
60
+ failed_files = []
61
+ timeout_files = []
62
+ error_files = []
63
+ error_outputs = {}
64
+ file_contents = {}
65
+ parse_errors = {}
66
+
67
+ files.each_with_index do |file_path, index|
68
+ total_failed = failed_files.count
69
+ total_timeout = timeout_files.count
70
+ total_errors = error_files.count
71
+
72
+ lines_to_clear = 6 + total_failed + total_timeout + total_errors
73
+ lines_to_clear += 3 if total_failed.positive?
74
+ lines_to_clear += 3 if total_timeout.positive?
75
+ lines_to_clear += 3 if total_errors.positive?
76
+
77
+ lines_to_clear.times { print "\e[1A\e[K" } if index.positive?
78
+
79
+ puts "Parsing .html.erb files in: #{project_path}"
80
+ puts "Total files to process: #{files.count}\n"
81
+
82
+ relative_path = file_path.sub("#{project_path}/", "")
83
+
84
+ puts
85
+ puts progress_bar(index + 1, files.count)
86
+ puts
87
+ puts "Processing [#{index + 1}/#{files.count}]: #{relative_path}"
88
+
89
+ if failed_files.any?
90
+ puts
91
+ puts "Files that failed:"
92
+ failed_files.each { |file| puts " - #{file}" }
93
+ puts
94
+ end
95
+
96
+ if timeout_files.any?
97
+ puts
98
+ puts "Files that timed out:"
99
+ timeout_files.each { |file| puts " - #{file}" }
100
+ puts
101
+ end
102
+
103
+ if error_files.any?
104
+ puts
105
+ puts "Files with parse errors:"
106
+ error_files.each { |file| puts " - #{file}" }
107
+ puts
108
+ end
109
+
110
+ begin
111
+ file_content = File.read(file_path)
112
+
113
+ stdout_file = Tempfile.new("stdout")
114
+ stderr_file = Tempfile.new("stderr")
115
+ ast_file = Tempfile.new("ast")
116
+
117
+ Timeout.timeout(1) do
118
+ pid = Process.fork do
119
+ $stdout.reopen(stdout_file.path, "w")
120
+ $stderr.reopen(stderr_file.path, "w")
121
+
122
+ begin
123
+ result = Herb.parse(file_content)
124
+
125
+ if result.failed?
126
+ File.open(ast_file.path, "w") do |f|
127
+ f.puts result.value.inspect
128
+ end
129
+
130
+ exit!(2)
131
+ end
132
+
133
+ exit!(0)
134
+ rescue StandardError => e
135
+ warn "Ruby exception: #{e.class}: #{e.message}"
136
+ warn e.backtrace.join("\n") if e.backtrace
137
+ exit!(1)
138
+ end
139
+ end
140
+
141
+ Process.waitpid(pid)
142
+
143
+ stdout_file.rewind
144
+ stderr_file.rewind
145
+ stdout_content = stdout_file.read
146
+ stderr_content = stderr_file.read
147
+ ast = File.exist?(ast_file.path) ? File.read(ast_file.path) : ""
148
+
149
+ case $CHILD_STATUS.exitstatus
150
+ when 0
151
+ log.puts "✅ Parsed #{file_path} successfully"
152
+ successful_files << file_path
153
+ when 2
154
+ message = "⚠️ Parsing #{file_path} completed with errors"
155
+ log.puts message
156
+
157
+ parse_errors[file_path] = {
158
+ ast: ast,
159
+ stdout: stdout_content,
160
+ stderr: stderr_content,
161
+ }
162
+
163
+ file_contents[file_path] = file_content
164
+
165
+ error_files << file_path
166
+ else
167
+ message = "❌ Parsing #{file_path} failed"
168
+ log.puts message
169
+
170
+ error_outputs[file_path] = {
171
+ exit_code: $CHILD_STATUS.exitstatus,
172
+ stdout: stdout_content,
173
+ stderr: stderr_content,
174
+ }
175
+
176
+ file_contents[file_path] = file_content
177
+
178
+ failed_files << file_path
179
+ end
180
+ end
181
+
182
+ stdout_file.close
183
+ stdout_file.unlink
184
+ stderr_file.close
185
+ stderr_file.unlink
186
+ ast_file.close
187
+ ast_file.unlink
188
+ rescue Timeout::Error
189
+ message = "⏱️ Parsing #{file_path} timed out after 1 second"
190
+ log.puts message
191
+
192
+ begin
193
+ Process.kill("TERM", pid)
194
+ rescue StandardError
195
+ nil
196
+ end
197
+
198
+ timeout_files << file_path
199
+ file_contents[file_path] = file_content
200
+ rescue StandardError => e
201
+ message = "⚠️ Error processing #{file_path}: #{e.message}"
202
+ log.puts message
203
+
204
+ failed_files << file_path
205
+
206
+ begin
207
+ file_contents[file_path] = File.read(file_path)
208
+ rescue StandardError => read_error
209
+ log.puts " Could not read file content: #{read_error.message}"
210
+ end
211
+ end
212
+ end
213
+
214
+ print "\e[1A\e[K"
215
+ puts "Completed processing all files."
216
+
217
+ print "\e[H\e[2J"
218
+
219
+ log.puts ""
220
+
221
+ summary = [
222
+ heading("Summary"),
223
+ "Total files: #{files.count}",
224
+ "✅ Successful: #{successful_files.count}",
225
+ "❌ Failed: #{failed_files.count}",
226
+ "⚠️ Parse errors: #{error_files.count}",
227
+ "⏱️ Timed out: #{timeout_files.count}"
228
+ ]
229
+
230
+ summary.each do |line|
231
+ log.puts line
232
+ puts line
233
+ end
234
+
235
+ if failed_files.any?
236
+ log.puts "\n#{heading("Files that failed")}"
237
+ puts "\nFiles that failed:"
238
+
239
+ failed_files.each do |f|
240
+ log.puts "- #{f}"
241
+ puts " - #{f}"
242
+ end
243
+ end
244
+
245
+ if error_files.any?
246
+ log.puts "\n#{heading("Files with parse errors")}"
247
+ puts "\nFiles with parse errors:"
248
+
249
+ error_files.each do |f|
250
+ log.puts f
251
+ puts " - #{f}"
252
+ end
253
+ end
254
+
255
+ if timeout_files.any?
256
+ log.puts "\n#{heading("Files that timed out")}"
257
+ puts "\nFiles that timed out:"
258
+
259
+ timeout_files.each do |f|
260
+ log.puts f
261
+ puts " - #{f}"
262
+ end
263
+ end
264
+
265
+ problem_files = failed_files + timeout_files + error_files
266
+
267
+ if problem_files.any?
268
+ log.puts "\n#{heading("FILE CONTENTS AND DETAILS")}"
269
+
270
+ problem_files.each do |file|
271
+ next unless file_contents[file]
272
+
273
+ divider = "=" * [80, file.length].max
274
+
275
+ log.puts
276
+ log.puts divider
277
+ log.puts file
278
+ log.puts divider
279
+
280
+ log.puts "\n#{heading("CONTENT")}"
281
+ log.puts "```erb"
282
+ log.puts file_contents[file]
283
+ log.puts "```"
284
+
285
+ if error_outputs[file]
286
+ if error_outputs[file][:exit_code]
287
+ log.puts "\n#{heading("EXIT CODE")}"
288
+ log.puts error_outputs[file][:exit_code]
289
+ end
290
+
291
+ if error_outputs[file][:stderr].strip.length.positive?
292
+ log.puts "\n#{heading("ERROR OUTPUT")}"
293
+ log.puts "```"
294
+ log.puts error_outputs[file][:stderr]
295
+ log.puts "```"
296
+ end
297
+
298
+ if error_outputs[file][:stdout].strip.length.positive?
299
+ log.puts "\n#{heading("STANDARD OUTPUT")}"
300
+ log.puts "```"
301
+ log.puts error_outputs[file][:stdout]
302
+ log.puts "```"
303
+ log.puts
304
+ end
305
+ end
306
+
307
+ next unless parse_errors[file]
308
+
309
+ if parse_errors[file][:stdout].strip.length.positive?
310
+ log.puts "\n#{heading("STANDARD OUTPUT")}"
311
+ log.puts "```"
312
+ log.puts parse_errors[file][:stdout]
313
+ log.puts "```"
314
+ end
315
+
316
+ if parse_errors[file][:stderr].strip.length.positive?
317
+ log.puts "\n#{heading("ERROR OUTPUT")}"
318
+ log.puts "```"
319
+ log.puts parse_errors[file][:stderr]
320
+ log.puts "```"
321
+ end
322
+
323
+ next unless parse_errors[file][:ast]
324
+
325
+ log.puts "\n#{heading("AST")}"
326
+ log.puts "```"
327
+ log.puts parse_errors[file][:ast]
328
+ log.puts "```"
329
+ log.puts
330
+ end
331
+ end
332
+
333
+ puts "\nResults saved to #{output_file}"
334
+ end
335
+ end
336
+
337
+ private
338
+
339
+ def progress_bar(current, total, width = IO.console.winsize[1] - "[] 100% (#{total}/#{total})".length)
340
+ progress = current.to_f / total
341
+ completed_length = (progress * width).to_i
342
+ completed = "█" * completed_length
343
+
344
+ partial_index = ((progress * width) % 1 * 8).to_i
345
+ partial_chars = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
346
+ partial = partial_index.zero? ? "" : partial_chars[partial_index]
347
+
348
+ remaining = " " * (width - completed_length - (partial.empty? ? 0 : 1))
349
+ percentage = (progress * 100).to_i
350
+
351
+ # Format as [███████▋ ] 42% (123/292)
352
+ "[#{completed}#{partial}#{remaining}] #{percentage}% (#{current}/#{total})"
353
+ end
354
+
355
+ def heading(text)
356
+ prefix = "--- #{text.upcase} "
357
+
358
+ prefix + ("-" * (80 - prefix.length))
359
+ end
360
+ end
361
+ end
data/lib/herb/range.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ class Range
5
+ attr_reader :from, :to
6
+
7
+ def initialize(from, to)
8
+ @from = from
9
+ @to = to
10
+ end
11
+
12
+ def self.[](...)
13
+ new(...)
14
+ end
15
+
16
+ def self.from(...)
17
+ new(...)
18
+ end
19
+
20
+ def to_a
21
+ [from, to]
22
+ end
23
+
24
+ def to_json(*args)
25
+ to_a.to_json(*args)
26
+ end
27
+
28
+ def tree_inspect
29
+ to_a.to_s
30
+ end
31
+
32
+ def inspect
33
+ %(#<Herb::Range #{to_a}>)
34
+ end
35
+
36
+ def to_s
37
+ inspect
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ class Result
5
+ attr_reader :source, :warnings, :errors
6
+
7
+ def initialize(source, warnings, errors)
8
+ @source = source
9
+ @warnings = warnings
10
+ @errors = errors
11
+ end
12
+
13
+ def success?
14
+ false
15
+ end
16
+
17
+ def failed?
18
+ true
19
+ end
20
+ end
21
+ end