diff --git a/.rubocop.yml b/.rubocop.yml
index 934b53771..21fc7229a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -48,6 +48,10 @@ Metrics/CyclomaticComplexity:
Exclude:
- lib/herb/project.rb
- lib/herb/ast/nodes.rb
+ - lib/herb/range.rb
+ - lib/herb/parse_result.rb
+ - lib/herb/errors.rb
+ - lib/herb/cli.rb
Metrics/MethodLength:
Max: 20
@@ -55,26 +59,33 @@ Metrics/MethodLength:
- lib/herb/ast/nodes.rb
- lib/herb/cli.rb
- lib/herb/project.rb
+ - lib/herb/errors.rb
+ - lib/herb/range.rb
- templates/template.rb
- - test/fork_helper.rb
- - test/snapshot_utils.rb
+ - test/**
Metrics/AbcSize:
Exclude:
- lib/herb/ast/node.rb
- lib/herb/ast/nodes.rb
+ - lib/herb/lex_result.rb
- lib/herb/cli.rb
- lib/herb/errors.rb
- lib/herb/project.rb
+ - lib/herb/parse_result.rb
+ - lib/herb/range.rb
+ - lib/herb/token.rb
+ - lib/herb/lint_result.rb
- templates/template.rb
- - test/fork_helper.rb
- - test/snapshot_utils.rb
+ - test/**
Metrics/ClassLength:
Exclude:
- lib/herb/cli.rb
- lib/herb/project.rb
- lib/herb/visitor.rb
+ - lib/herb/backend.rb
+ - lib/herb/ast/nodes.rb
- test/**/*_test.rb
Metrics/BlockLength:
@@ -83,6 +94,7 @@ Metrics/BlockLength:
- Rakefile
- "*.gemspec"
- "**/*.rake"
+ - lib/herb/cli.rb
- lib/herb/project.rb
- test/**/*_test.rb
@@ -90,12 +102,17 @@ Metrics/ParameterLists:
Exclude:
- lib/herb/ast/nodes.rb
- lib/herb/errors.rb
+ - lib/herb/lint_offense.rb
Metrics/PerceivedComplexity:
+ Max: 15
Exclude:
- lib/herb/ast/nodes.rb
- lib/herb/cli.rb
- lib/herb/project.rb
+ - lib/herb/range.rb
+ # - lib/herb/errors.rb
+ # - lib/herb/lex_result.rb
- test/snapshot_utils.rb
Layout/LineLength:
@@ -104,6 +121,10 @@ Layout/LineLength:
- test/**/*_test.rb
- lib/herb/token.rb
- lib/herb/ast/nodes.rb
+ - lib/herb/errors.rb
+ - lib/herb/backend.rb
+ - lib/herb/backends/native_backend.rb
+ - lib/herb/backends/node_backend.rb
Layout/EmptyLines:
Exclude:
@@ -125,6 +146,18 @@ Layout/FirstHashElementIndentation:
Layout/LeadingCommentSpace:
Enabled: false
+Layout/IndentationWidth:
+ Exclude:
+ - lib/herb/ast/nodes.rb
+
+Layout/ElseAlignment:
+ Exclude:
+ - lib/herb/ast/nodes.rb
+
+Layout/EndAlignment:
+ Exclude:
+ - lib/herb/ast/nodes.rb
+
Security/Eval:
Exclude:
- Rakefile
diff --git a/Gemfile b/Gemfile
index 629808ed2..79d0de407 100644
--- a/Gemfile
+++ b/Gemfile
@@ -9,6 +9,7 @@ gem "prism", github: "ruby/prism", tag: "v1.4.0"
gem "lz_string"
gem "maxitest"
gem "minitest-difftastic", "~> 0.2"
+gem "nodo", "~> 1.8"
gem "rake", "~> 13.2"
gem "rake-compiler", "~> 1.2"
gem "rake-compiler-dock", "~> 1.9"
diff --git a/Gemfile.lock b/Gemfile.lock
index 6b12a48d6..b51d29a2b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -77,6 +77,8 @@ GEM
minitest-difftastic (0.2.1)
difftastic (~> 0.6)
mutex_m (0.3.0)
+ nodo (1.8.1)
+ logger
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
@@ -159,6 +161,7 @@ DEPENDENCIES
lz_string
maxitest
minitest-difftastic (~> 0.2)
+ nodo (~> 1.8)
prism!
rake (~> 13.2)
rake-compiler (~> 1.2)
diff --git a/Steepfile b/Steepfile
index 40d550b4c..e081a8b7d 100644
--- a/Steepfile
+++ b/Steepfile
@@ -12,4 +12,5 @@ target :lib do
ignore "lib/herb/libherb.rb"
ignore "lib/herb/libherb"
ignore "lib/herb/project.rb"
+ ignore "lib/herb/backends/node_backend.rb"
end
diff --git a/docs/docs/bindings/ruby/index.md b/docs/docs/bindings/ruby/index.md
index 68ba75d7b..1598d1141 100644
--- a/docs/docs/bindings/ruby/index.md
+++ b/docs/docs/bindings/ruby/index.md
@@ -46,4 +46,72 @@ require "herb"
```
:::
-You are now ready to parse HTML+ERB in Ruby.
+You are now ready to parse, format, lint, and print HTML+ERB in Ruby. See the [Quick Start](#quick-start) section below for basic usage.
+
+## Multi-Backend Architecture
+
+Herb for Ruby features a sophisticated multi-backend architecture that allows you to choose the best backend for your specific use case:
+
+- **Native Backend** (default) - Ruby C extension for parsing and lexing
+- **Node Backend** - Uses Node.js packages for advanced features like formatting and linting
+- **FFI Backend** - Foreign Function Interface backend (planned for future release)
+- **WASM Backend** - WebAssembly backend (planned for future release)
+
+### Backend Selection
+
+Herb automatically selects the best available backend, but you can choose manually:
+
+```ruby
+Herb.switch_backend(:native) # Native Ruby C-Extension Backend
+Herb.switch_backend(:node) # Full feature set
+
+Herb.current_backend
+# => "native"
+
+Herb.available_backends
+# => [:native, :node]
+```
+
+### Cross-Backend Operations
+
+You can use different backends for different operations without switching:
+
+
+```ruby
+# Parse with native backend
+result = Herb.parse(source)
+
+# Parse with Node backend
+formatted = Herb.parse(source, backend: :node)
+
+# Switch to Node backend
+Herb.switch_backend(:node)
+
+# Now uses Node backend for parsing
+result = Herb.parse(source)
+```
+
+## Quick Start
+
+Once installed, you can start parsing, formatting, and linting HTML+ERB templates:
+
+:::code-group
+```ruby
+require "herb"
+
+# Parse HTML+ERB templates
+result = Herb.parse('
Hello <%= user.name %>!
')
+
+# Format templates
+formatted = Herb.format('Messy
')
+
+# Lint templates for issues
+lint_result = Herb.lint('
')
+
+# Convert AST back to HTML
+node = result.value
+html_output = node.to_source
+```
+:::
+
+For detailed examples and API reference, see the [Ruby Reference](/bindings/ruby/reference) documentation.
diff --git a/docs/docs/bindings/ruby/reference.md b/docs/docs/bindings/ruby/reference.md
index 59b062018..a9928905d 100644
--- a/docs/docs/bindings/ruby/reference.md
+++ b/docs/docs/bindings/ruby/reference.md
@@ -4,25 +4,43 @@ outline: deep
# Ruby Reference
-The `Herb` module exposes a few methods for you to lex, extract and parse HTML+ERB source code.
+The `Herb` module provides a comprehensive API for lexing, parsing, formatting, linting, and printing HTML+ERB source code across multiple backend engines.
-## Ruby API
+## Core API Methods
-`Herb` provides the following key methods:
+### Parsing & Lexing
+* `Herb.lex(source, backend: nil)`
+* `Herb.lex_file(path, backend: nil)`
+* `Herb.parse(source, backend: nil)`
+* `Herb.parse_file(path, backend: nil)`
-* `Herb.lex(source)`
-* `Herb.lex_file(path)`
-* `Herb.parse(source)`
-* `Herb.parse_file(path)`
+### Formatting & Linting
+* `Herb.format(source, backend: :node)`
+* `Herb.format_file(path, backend: :node)`
+* `Herb.lint(source, backend: :node)`
+* `Herb.lint_file(path, backend: :node)`
+
+### Code Extraction
* `Herb.extract_ruby(source)`
* `Herb.extract_html(source)`
+
+### Node Printing
+* `Herb.print_node(node, backend: :node)`
+
+### Backend Management
+* `Herb.switch_backend(backend_name)`
+* `Herb.current_backend`
+* `Herb.available_backends`
+* `Herb.backend(backend_name)`
+
+### System Information
* `Herb.version`
## Lexing
The `Herb.lex` and `Herb.lex_file` methods allow you to tokenize an HTML document with embedded Ruby.
-### `Herb.lex(source)`
+### `Herb.lex(source, backend: nil)`
:::code-group
```ruby
@@ -38,7 +56,7 @@ Herb.lex(source).value
```
:::
-### `Herb.lex_file(path)`
+### `Herb.lex_file(path, backend: nil)`
:::code-group
```ruby
@@ -60,11 +78,28 @@ Herb.lex_file("./index.html.erb").value
The `Herb.parse` and `Herb.parse_file` methods allow you to parse an HTML document with embedded Ruby and returns you a parsed result of your document containing an Abstract Syntax Tree (AST) that you can use to structurally traverse the parsed document.
-### `Herb.parse(source)`
+### Basic Example
:::code-group
```ruby
-source = %(Hello <%= user.name %>
)
+require "herb"
+
+source = %(Hello <%= user.name %>!
)
+result = Herb.parse(source)
+
+if result.success?
+ puts result.value.class # => Herb::AST::DocumentNode
+else
+ puts "Parse errors: #{result.errors}"
+end
+```
+:::
+
+### `Herb.parse(source, backend: nil)`
+
+:::code-group
+```ruby
+source = %(Hello <%= user.name %>!
)
Herb.parse(source).value
# =>
@@ -100,7 +135,7 @@ Herb.parse(source).value
```
:::
-### `Herb.parse_file(path)`
+### `Herb.parse_file(path, backend: nil)`
:::code-group
```ruby
@@ -167,13 +202,338 @@ result.visit(visitor)
This allows you to analyze the parsed HTML+ERB programmatically.
-## Metadata
+## Formatting
+
+Herb provides advanced formatting capabilities through the Node backend using the `@herb-tools/formatter` package.
+
+> [!NOTE] Default Backend
+> Format operations default to the Node backend since it's the only backend that supports formatting. You can override this by specifying a different backend.
+
+### Basic Example
+
+:::code-group
+```ruby
+require "herb"
+
+# Messy ERB template
+messy_erb = 'Hello<%= user.name %>
'
+
+# Format with default options
+formatted = Herb.format(messy_erb)
+puts formatted
+# =>
+#
+# Hello
+# <%= user.name %>
+#
+```
+:::
+
+### `Herb.format(source, backend: :node, **options)`
+
+:::code-group
+```ruby
+# Format messy ERB template
+source = %(<%if active%>Active
<%else%>Inactive<%end%>)
+
+formatted = Herb.format(source)
+
+puts formatted
+# =>
+#
+#
Hello<%= user.name %>
+#
+#
+# <% if active %>
+# Active
+# <% else %>
+# Inactive
+# <% end %>
+```
+:::
+
+### `Herb.format_file(path, backend: :node, **options)`
+
+:::code-group
+```ruby
+# Format an ERB file
+formatted = Herb.format_file("./template.html.erb", backend: :node)
+File.write("./formatted_template.html.erb", formatted)
+```
+:::
+
+## Linting
+
+Herb provides comprehensive linting capabilities through the Node backend using the `@herb-tools/linter` package.
+
+> [!NOTE] Default Backend
+> Lint operations default to the Node backend since it's the only backend that supports linting. You can override this by specifying a different backend.
+
+### Basic Example
+
+:::code-group
+```ruby
+require "herb"
+
+template = %(
Content
)
+
+lint_result = Herb.lint(template)
+
+if lint_result.success?
+ puts "✓ No lint issues found"
+else
+ puts "Found #{lint_result.total_offenses} issues:"
+
+ lint_result.error_offenses.each do |offense|
+ puts " Error: #{offense.message} (#{offense.rule})"
+ end
+
+ lint_result.warning_offenses.each do |offense|
+ puts " Warning: #{offense.message} (#{offense.rule})"
+ end
+end
+```
+:::
+
+### `Herb.lint(source, backend: :node, **options)`
+
+:::code-group
+```ruby
+# Lint ERB template
+source = %(
Content
)
+
+lint_result = Herb.lint(source)
+
+if lint_result.success?
+ puts "✓ No lint issues found"
+else
+ puts "Found #{lint_result.total_offenses} issues:"
+
+ lint_result.error_offenses.each do |offense|
+ puts " Error: #{offense.message} (#{offense.rule})"
+ end
+
+ lint_result.warning_offenses.each do |offense|
+ puts " Warning: #{offense.message} (#{offense.rule})"
+ end
+end
+```
+:::
+
+### `Herb.lint_file(path, backend: :node, **options)`
+
+:::code-group
+```ruby
+# Lint an ERB file
+lint_result = Herb.lint_file("./template.html.erb", backend: :node)
+
+# Generate a report
+if !lint_result.clean?
+ puts lint_result.to_s
+ # => "✗ Found 3 lint offenses: 1 errors, 2 warnings"
+end
+```
+:::
+
+## Node Printing
+
+Convert AST nodes back to ERB/HTML strings with optional formatting.
+
+> [!NOTE] Default Backend
+> Print operations default to the Node backend since it's the only backend that supports node printing. You can override this by specifying a different backend.
+
+### Basic Example
+
+:::code-group
+```ruby
+require "herb"
+
+source = %(Hello <%= user.name %>!
)
+result = Herb.parse(source)
+
+if result.success?
+ node = result.value
+
+ formatted = node.to_source # Uses FormatPrinter
+ original = node.to_source(format: false) # Uses IdentityPrinter
+
+ puts formatted
+end
+```
+:::
+
+### Node Methods
+
+Every AST node supports these methods:
+
+#### `node.to_source(backend: :node, format: true, **options)`
+
+:::code-group
+```ruby
+result = Herb.parse("Hello <%= user.name %>!
")
+
+result.value.to_source
+# => "Hello <%= user.name %>!
"
+```
+:::
+
+## Backend Management
+
+Herb supports multiple backend engines with different capabilities:
+
+### Available Backends
+
+- **Native Backend** - Ruby C extension (default)
+ - ✓ Parsing, Lexing, Code Extraction
+ - ✗ Formatting, Linting, Node Printing
+
+- **Node Backend** - Node.js integration with full feature set
+ - ✓ All features including formatting, linting, and printing
+ - Requires `nodo` gem and Node.js packages
+
+- **FFI Backend** - Foreign Function Interface (not yet implemented)
+ - 🚧 Planned for future release
+ - Will provide direct access to libherb via FFI
+
+- **WASM Backend** - WebAssembly (not yet implemented)
+ - 🚧 Planned for future release
+ - Will enable browser and sandboxed environments
+
+### Backend Selection
+
+#### `Herb.switch_backend(backend_name)`
+
+:::code-group
+```ruby
+# Switch to native backend (no dependencies)
+Herb.switch_backend(:native)
+
+# Switch to node backend (all features)
+Herb.switch_backend(:node)
+
+Herb.current_backend # => "node"
+```
+:::
+
+#### `Herb.available_backends`
+
+:::code-group
+```ruby
+Herb.available_backends
+# => [:native, :node]
+```
+:::
+
+#### Cross-Backend Operations
+
+:::code-group
+```ruby
+# Keep native backend for fast parsing
+Herb.switch_backend(:native)
+
+# Parse with current (native) backend
+result = Herb.parse(source)
+
+# Format with Node backend without switching
+formatted = Herb.format(source)
+
+# Lint with Node backend
+lint_result = Herb.lint(source)
+
+# Print node
+html_output = result.value.to_source(format: true)
+html_output = result.value.to_source(format: false)
+
+Herb.current_backend # => "native"
+```
+:::
+
+## LintResult API
+
+The `Herb::LintResult` class provides detailed information about linting results:
+
+### Properties
+
+:::code-group
+```ruby
+lint_result = Herb.lint(template, backend: :node)
+
+# Offense counts
+lint_result.total_offenses # => 5
+lint_result.errors # => 2
+lint_result.warnings # => 2
+lint_result.infos # => 1
+lint_result.hints # => 0
+
+# Status checks
+lint_result.success? # => false (has errors)
+lint_result.clean? # => false (has any offenses)
+
+# Access offenses by severity
+lint_result.error_offenses # => [LintOffense, ...]
+lint_result.warning_offenses # => [LintOffense, ...]
+lint_result.info_offenses # => [LintOffense, ...]
+
+# Access offenses by rule
+lint_result.offenses_for_rule("html-img-require-alt") # => [LintOffense, ...]
+```
+:::
+
+### Output Formats
+
+:::code-group
+```ruby
+lint_result = Herb.lint(template, backend: :node)
+
+# String representation
+puts lint_result.to_s
+# => "✗ Found 3 lint offenses: 2 errors, 1 warnings"
+
+# Hash representation
+hash = lint_result.to_h
+# => { offenses: [...], errors: 2, warnings: 1, success: false }
+
+# JSON representation
+json = lint_result.to_json
+# => '{"offenses":[...],"errors":2,"warnings":1}'
+```
+:::
+
+### LintOffense API
+
+Each lint offense provides detailed information:
+
+:::code-group
+```ruby
+offense = lint_result.error_offenses.first
+
+offense.message # => "Missing alt attribute"
+offense.rule # => "html-img-require-alt"
+offense.severity # => "error"
+offense.code # => "E001"
+offense.source # => "Herb Linter"
+
+# Location information
+offense.location.start.line # => 1
+offense.location.start.column # => 5
+offense.location.end.line # => 1
+offense.location.end.column # => 20
+
+# Severity checks
+offense.error? # => true
+offense.warning? # => false
+offense.info? # => false
+offense.hint? # => false
+```
+:::
+
+## System Information
### `Herb.version`
:::code-group
```ruby
Herb.version
-# => "herb gem v0.0.1, libherb v0.0.1 (Ruby C native extension)"
+# => "Herb gem 0.6.0 - Herb::Backends::NativeBackend(native backend v0.6.0 via C extension)"
```
:::
diff --git a/ext/herb/extconf.rb b/ext/herb/extconf.rb
index 9abcbfc27..7f4f8186c 100644
--- a/ext/herb/extconf.rb
+++ b/ext/herb/extconf.rb
@@ -55,6 +55,7 @@
core_src_files = %w[
extension.c
+ native_backend.c
nodes.c
error_helpers.c
extension_helpers.c
diff --git a/ext/herb/extension.c b/ext/herb/extension.c
index e6e569e5e..01dd7810f 100644
--- a/ext/herb/extension.c
+++ b/ext/herb/extension.c
@@ -5,8 +5,6 @@
#include "extension_helpers.h"
#include "nodes.h"
-#include "../../src/include/analyze.h"
-
VALUE mHerb;
VALUE cPosition;
VALUE cLocation;
@@ -16,126 +14,7 @@ VALUE cResult;
VALUE cLexResult;
VALUE cParseResult;
-static VALUE Herb_lex(VALUE self, VALUE source) {
- char* string = (char*) check_string(source);
-
- array_T* tokens = herb_lex(string);
-
- VALUE result = create_lex_result(tokens, source);
-
- herb_free_tokens(&tokens);
-
- return result;
-}
-
-static VALUE Herb_lex_file(VALUE self, VALUE path) {
- char* file_path = (char*) check_string(path);
- array_T* tokens = herb_lex_file(file_path);
-
- VALUE source_value = read_file_to_ruby_string(file_path);
- VALUE result = create_lex_result(tokens, source_value);
-
- herb_free_tokens(&tokens);
-
- return result;
-}
-
-static VALUE Herb_parse(int argc, VALUE* argv, VALUE self) {
- VALUE source, options;
- rb_scan_args(argc, argv, "1:", &source, &options);
-
- char* string = (char*) check_string(source);
-
- parser_options_T* parser_options = NULL;
- parser_options_T opts = { 0 };
-
- if (!NIL_P(options)) {
- VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace"));
- if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); }
-
- if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) {
- opts.track_whitespace = true;
- parser_options = &opts;
- }
- }
-
- AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options);
-
- herb_analyze_parse_tree(root, string);
-
- VALUE result = create_parse_result(root, source);
-
- ast_node_free((AST_NODE_T*) root);
-
- return result;
-}
-
-static VALUE Herb_parse_file(VALUE self, VALUE path) {
- char* file_path = (char*) check_string(path);
-
- VALUE source_value = read_file_to_ruby_string(file_path);
- char* string = (char*) check_string(source_value);
-
- AST_DOCUMENT_NODE_T* root = herb_parse(string, NULL);
-
- VALUE result = create_parse_result(root, source_value);
-
- ast_node_free((AST_NODE_T*) root);
-
- return result;
-}
-
-static VALUE Herb_lex_to_json(VALUE self, VALUE source) {
- char* string = (char*) check_string(source);
- buffer_T output;
-
- if (!buffer_init(&output)) { return Qnil; }
-
- herb_lex_json_to_buffer(string, &output);
-
- VALUE result = rb_str_new(output.value, output.length);
-
- buffer_free(&output);
-
- return result;
-}
-
-static VALUE Herb_extract_ruby(VALUE self, VALUE source) {
- char* string = (char*) check_string(source);
- buffer_T output;
-
- if (!buffer_init(&output)) { return Qnil; }
-
- herb_extract_ruby_to_buffer(string, &output);
-
- VALUE result = rb_str_new_cstr(output.value);
- buffer_free(&output);
-
- return result;
-}
-
-static VALUE Herb_extract_html(VALUE self, VALUE source) {
- char* string = (char*) check_string(source);
- buffer_T output;
-
- if (!buffer_init(&output)) { return Qnil; }
-
- herb_extract_html_to_buffer(string, &output);
-
- VALUE result = rb_str_new_cstr(output.value);
- buffer_free(&output);
-
- return result;
-}
-
-static VALUE Herb_version(VALUE self) {
- VALUE gem_version = rb_const_get(self, rb_intern("VERSION"));
- VALUE libherb_version = rb_str_new_cstr(herb_version());
- VALUE libprism_version = rb_str_new_cstr(herb_prism_version());
- VALUE format_string = rb_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)");
-
- return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version);
-}
+void Init_native_backend(void);
void Init_herb(void) {
mHerb = rb_define_module("Herb");
@@ -147,12 +26,5 @@ void Init_herb(void) {
cLexResult = rb_define_class_under(mHerb, "LexResult", cResult);
cParseResult = rb_define_class_under(mHerb, "ParseResult", cResult);
- rb_define_singleton_method(mHerb, "parse", Herb_parse, -1);
- rb_define_singleton_method(mHerb, "lex", Herb_lex, 1);
- rb_define_singleton_method(mHerb, "parse_file", Herb_parse_file, 1);
- rb_define_singleton_method(mHerb, "lex_file", Herb_lex_file, 1);
- rb_define_singleton_method(mHerb, "lex_to_json", Herb_lex_to_json, 1);
- rb_define_singleton_method(mHerb, "extract_ruby", Herb_extract_ruby, 1);
- rb_define_singleton_method(mHerb, "extract_html", Herb_extract_html, 1);
- rb_define_singleton_method(mHerb, "version", Herb_version, 0);
+ Init_native_backend();
}
diff --git a/ext/herb/native_backend.c b/ext/herb/native_backend.c
new file mode 100644
index 000000000..908e46d40
--- /dev/null
+++ b/ext/herb/native_backend.c
@@ -0,0 +1,144 @@
+#include
+
+#include "error_helpers.h"
+#include "extension.h"
+#include "extension_helpers.h"
+#include "nodes.h"
+
+#include "../../src/include/analyze.h"
+
+VALUE cNativeBackend;
+
+static VALUE NativeBackend_perform_lex(VALUE self, VALUE source) {
+ char* string = (char*) check_string(source);
+
+ array_T* tokens = herb_lex(string);
+
+ VALUE result = create_lex_result(tokens, source);
+
+ herb_free_tokens(&tokens);
+
+ return result;
+}
+
+static VALUE NativeBackend_perform_lex_file(VALUE self, VALUE path) {
+ char* file_path = (char*) check_string(path);
+ array_T* tokens = herb_lex_file(file_path);
+
+ VALUE source_value = read_file_to_ruby_string(file_path);
+ VALUE result = create_lex_result(tokens, source_value);
+
+ herb_free_tokens(&tokens);
+
+ return result;
+}
+
+static VALUE NativeBackend_perform_parse(VALUE self, VALUE source, VALUE options) {
+ char* string = (char*) check_string(source);
+
+ parser_options_T* parser_options = NULL;
+ parser_options_T opts = { 0 };
+
+ if (!NIL_P(options)) {
+ VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace"));
+ if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); }
+
+ if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) {
+ opts.track_whitespace = true;
+ parser_options = &opts;
+ }
+ }
+
+ AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options);
+
+ herb_analyze_parse_tree(root, string);
+
+ VALUE result = create_parse_result(root, source);
+
+ ast_node_free((AST_NODE_T*) root);
+
+ return result;
+}
+
+static VALUE NativeBackend_perform_parse_file(VALUE self, VALUE path, VALUE options) {
+ char* file_path = (char*) check_string(path);
+
+ VALUE source_value = read_file_to_ruby_string(file_path);
+ char* string = (char*) check_string(source_value);
+
+ parser_options_T* parser_options = NULL;
+ parser_options_T opts = { 0 };
+
+ if (!NIL_P(options)) {
+ VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace"));
+ if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); }
+
+ if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) {
+ opts.track_whitespace = true;
+ parser_options = &opts;
+ }
+ }
+
+ AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options);
+
+ herb_analyze_parse_tree(root, string);
+
+ VALUE result = create_parse_result(root, source_value);
+
+ ast_node_free((AST_NODE_T*) root);
+
+ return result;
+}
+
+static VALUE NativeBackend_perform_extract_ruby(VALUE self, VALUE source) {
+ char* string = (char*) check_string(source);
+ buffer_T output;
+
+ if (!buffer_init(&output)) { return Qnil; }
+
+ herb_extract_ruby_to_buffer(string, &output);
+
+ VALUE result = rb_str_new_cstr(output.value);
+ buffer_free(&output);
+
+ return result;
+}
+
+static VALUE NativeBackend_perform_extract_html(VALUE self, VALUE source) {
+ char* string = (char*) check_string(source);
+ buffer_T output;
+
+ if (!buffer_init(&output)) { return Qnil; }
+
+ herb_extract_html_to_buffer(string, &output);
+
+ VALUE result = rb_str_new_cstr(output.value);
+ buffer_free(&output);
+
+ return result;
+}
+
+static VALUE NativeBackend_backend_version(VALUE self) {
+ VALUE mHerb = rb_const_get(rb_cObject, rb_intern("Herb"));
+ VALUE gem_version = rb_const_get(mHerb, rb_intern("VERSION"));
+ VALUE libherb_version = rb_str_new_cstr(herb_version());
+ VALUE libprism_version = rb_str_new_cstr(herb_prism_version());
+ VALUE format_string = rb_str_new_cstr("herb gem v%s, native backend (libprism v%s, libherb v%s via C extension)");
+
+ return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version);
+}
+
+void Init_native_backend(void) {
+ VALUE mHerb = rb_const_get(rb_cObject, rb_intern("Herb"));
+ VALUE mBackends = rb_const_get(mHerb, rb_intern("Backends"));
+
+ cNativeBackend = rb_const_get(mBackends, rb_intern("NativeBackend"));
+
+ rb_define_protected_method(cNativeBackend, "c_perform_lex", NativeBackend_perform_lex, 1);
+ rb_define_protected_method(cNativeBackend, "c_perform_lex_file", NativeBackend_perform_lex_file, 1);
+ rb_define_protected_method(cNativeBackend, "c_perform_parse", NativeBackend_perform_parse, 2);
+ rb_define_protected_method(cNativeBackend, "c_perform_parse_file", NativeBackend_perform_parse_file, 2);
+ rb_define_protected_method(cNativeBackend, "c_perform_extract_ruby", NativeBackend_perform_extract_ruby, 1);
+ rb_define_protected_method(cNativeBackend, "c_perform_extract_html", NativeBackend_perform_extract_html, 1);
+ rb_define_protected_method(cNativeBackend, "c_backend_version", NativeBackend_backend_version, 0);
+}
diff --git a/herb.gemspec b/herb.gemspec
index a3445ac6e..577a72b85 100644
--- a/herb.gemspec
+++ b/herb.gemspec
@@ -45,6 +45,4 @@ Gem::Specification.new do |spec|
spec.metadata["source_code_uri"] = "https://github.com/marcoroth/herb"
spec.metadata["bug_tracker_uri"] = "https://github.com/marcoroth/herb/issues"
spec.metadata["documentation_uri"] = "https://docs.herb-tools.dev"
-
- # spec.add_dependency "ffi"
end
diff --git a/lib/herb.rb b/lib/herb.rb
index e558ef60a..932dce965 100644
--- a/lib/herb.rb
+++ b/lib/herb.rb
@@ -18,6 +18,9 @@
require_relative "herb/errors"
require_relative "herb/warnings"
+require_relative "herb/diagnostic"
+require_relative "herb/lint_offense"
+require_relative "herb/lint_result"
require_relative "herb/cli"
require_relative "herb/project"
@@ -26,12 +29,110 @@
require_relative "herb/visitor"
-begin
- major, minor, _patch = RUBY_VERSION.split(".") #: [String, String, String]
- require_relative "herb/#{major}.#{minor}/herb"
-rescue LoadError
- require_relative "herb/herb"
-end
+require_relative "herb/backend"
+require_relative "herb/backend_loader"
module Herb
+ class << self
+ def load_backend(backend_name = nil)
+ @backend = BackendLoader.load(backend_name)
+ self
+ end
+
+ def backend_loaded?
+ @backend&.loaded?
+ end
+
+ def ensure_backend!
+ load_backend unless backend_loaded?
+ end
+
+ def lex(source)
+ ensure_backend!
+ @backend.lex(source)
+ end
+
+ def lex_file(path)
+ ensure_backend!
+ @backend.lex_file(path)
+ end
+
+ def parse(source, backend: nil, **options)
+ if backend
+ self.backend(backend).parse(source, options)
+ else
+ ensure_backend!
+ @backend.parse(source, options)
+ end
+ end
+
+ def parse_file(path, backend: nil, **options)
+ if backend
+ self.backend(backend).parse_file(path, options)
+ else
+ ensure_backend!
+ @backend.parse_file(path, options)
+ end
+ end
+
+ def extract_ruby(source)
+ ensure_backend!
+ @backend.extract_ruby(source)
+ end
+
+ def extract_html(source)
+ ensure_backend!
+ @backend.extract_html(source)
+ end
+
+ def version
+ ensure_backend!
+ @backend.version
+ end
+
+ def format(source, backend: :node, **options)
+ self.backend(backend).format(source, options)
+ end
+
+ def format_file(path, backend: :node, **options)
+ self.backend(backend).format_file(path, options)
+ end
+
+ def lint(source, backend: :node, **options)
+ self.backend(backend).lint(source, options)
+ end
+
+ def lint_file(path, backend: :node, **options)
+ self.backend(backend).lint_file(path, options)
+ end
+
+ def print_node(node, backend: :node, **options)
+ self.backend(backend).print_node(node, options)
+ end
+
+ def available_backends
+ BackendLoader.available_backends
+ end
+
+ def switch_backend(backend_name)
+ @backend = BackendLoader.load(backend_name)
+ self
+ end
+
+ def current_backend
+ @backend&.backend_name
+ end
+
+ def backend(backend_name)
+ BackendLoader.load(backend_name)
+ end
+ end
+
+ unless ENV["HERB_NO_AUTOLOAD"]
+ begin
+ Herb.load_backend
+ rescue StandardError
+ # Backend loading failed, but that's okay - methods will fail when called
+ end
+ end
end
diff --git a/lib/herb/ast/node.rb b/lib/herb/ast/node.rb
index e1e5e4270..0cd7551ca 100644
--- a/lib/herb/ast/node.rb
+++ b/lib/herb/ast/node.rb
@@ -5,10 +5,10 @@ module Herb
module AST
class Node
attr_reader :type #: String
- attr_reader :location #: Location
+ attr_reader :location #: Location?
attr_reader :errors #: Array[Herb::Errors::Error]
- #: (String, Location, Array[Herb::Errors::Error]) -> void
+ #: (String, Location?, Array[Herb::Errors::Error]) -> void
def initialize(type, location, errors = [])
@type = type
@location = location
@@ -97,6 +97,11 @@ def compact_child_nodes
def recursive_errors
errors + compact_child_nodes.flat_map(&:recursive_errors)
end
+
+ #: (?backend: Symbol, **untyped) -> String
+ def to_source(backend: :node, format: true, **options)
+ Herb.backend(backend).print_node(self, { format: format }.merge(options))
+ end
end
end
end
diff --git a/lib/herb/backend.rb b/lib/herb/backend.rb
new file mode 100644
index 000000000..910cdb50d
--- /dev/null
+++ b/lib/herb/backend.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+module Herb
+ class Backend
+ attr_reader :loaded
+
+ def initialize
+ @loaded = false
+ end
+
+ def load
+ return self if @loaded
+
+ perform_load
+ @loaded = true
+ self
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ def lex(source)
+ ensure_loaded!
+ perform_lex(source)
+ end
+
+ def lex_file(path)
+ ensure_loaded!
+ perform_lex_file(path)
+ end
+
+ def parse(source, options = {})
+ ensure_loaded!
+ perform_parse(source, options)
+ end
+
+ def parse_file(path, options = {})
+ ensure_loaded!
+ perform_parse_file(path, options)
+ end
+
+ def extract_ruby(source)
+ ensure_loaded!
+ perform_extract_ruby(source)
+ end
+
+ def extract_html(source)
+ ensure_loaded!
+ perform_extract_html(source)
+ end
+
+ def version
+ ensure_loaded!
+ backend_version
+ end
+
+ def format(source, options = {})
+ ensure_loaded!
+ perform_format(source, options)
+ end
+
+ def format_file(path, options = {})
+ ensure_loaded!
+ perform_format_file(path, options)
+ end
+
+ def lint(source, options = {})
+ ensure_loaded!
+ perform_lint(source, options)
+ end
+
+ def lint_file(path, options = {})
+ ensure_loaded!
+ perform_lint_file(path, options)
+ end
+
+ def print_node(node, options = {})
+ ensure_loaded!
+ perform_print_node(node, options)
+ end
+
+ def backend_name
+ self.class.name.split("::").last.sub(/Backend$/, "").downcase
+ end
+
+ protected
+
+ def perform_load
+ raise NotImplementedError, "#{self.class}#perform_load must be implemented"
+ end
+
+ def perform_lex(source)
+ raise NotImplementedError, "#{self.class}#perform_lex must be implemented"
+ end
+
+ def perform_lex_file(path)
+ raise NotImplementedError, "#{self.class}#perform_lex_file must be implemented"
+ end
+
+ def perform_parse(source, options)
+ raise NotImplementedError, "#{self.class}#perform_parse must be implemented"
+ end
+
+ def perform_parse_file(path, options)
+ raise NotImplementedError, "#{self.class}#perform_parse_file must be implemented"
+ end
+
+ def perform_extract_ruby(source)
+ raise NotImplementedError, "#{self.class}#perform_extract_ruby must be implemented"
+ end
+
+ def perform_extract_html(source)
+ raise NotImplementedError, "#{self.class}#perform_extract_html must be implemented"
+ end
+
+ def backend_version
+ raise NotImplementedError, "#{self.class}#backend_version must be implemented"
+ end
+
+ def perform_format(source, options)
+ raise NotImplementedError, "#{self.class}#perform_format not implemented. This backend does not support formatting."
+ end
+
+ def perform_format_file(path, options)
+ source = File.read(path, encoding: "UTF-8")
+ perform_format(source, options)
+ end
+
+ def perform_lint(source, options)
+ raise NotImplementedError, "#{self.class}#perform_lint not implemented. This backend does not support linting."
+ end
+
+ def perform_lint_file(path, options)
+ source = File.read(path, encoding: "UTF-8")
+ perform_lint(source, options)
+ end
+
+ def perform_print_node(node, options)
+ raise NotImplementedError, "#{self.class}#perform_print_node not implemented. This backend does not support node printing."
+ end
+
+ private
+
+ def ensure_loaded!
+ raise "Backend not loaded. Call #load first." unless loaded?
+ end
+ end
+end
diff --git a/lib/herb/backend_loader.rb b/lib/herb/backend_loader.rb
new file mode 100644
index 000000000..a0cca0739
--- /dev/null
+++ b/lib/herb/backend_loader.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Herb
+ class BackendLoader
+ BACKENDS = {
+ native: "backends/native_backend",
+ node: "backends/node_backend",
+ }.freeze
+
+ DEFAULT_PRIORITY = [:native, :node].freeze
+
+ class << self
+ def load(backend_name = nil)
+ backend_name ||= detect_backend
+
+ case backend_name.to_sym
+ when :native
+ load_native_backend
+ when :node
+ load_node_backend
+ else
+ raise ArgumentError, "Unknown backend: #{backend_name}. Available backends: #{BACKENDS.keys.join(", ")}"
+ end
+ end
+
+ def detect_backend
+ if ENV["HERB_BACKEND"]
+ backend = ENV["HERB_BACKEND"]&.downcase&.to_sym
+
+ return backend if BACKENDS.key?(backend)
+
+ warn "Warning: Unknown HERB_BACKEND '#{ENV["HERB_BACKEND"]}'. Falling back to auto-detection."
+ end
+
+ DEFAULT_PRIORITY.each do |backend_name|
+ backend = test_backend(backend_name)
+ return backend_name if backend
+ rescue LoadError, StandardError
+ # Try next backend
+ end
+
+ raise "No suitable Herb backend found. Please install required dependencies."
+ end
+
+ def available_backends
+ backends = [] #: Array[Herb::Backend]
+
+ BACKENDS.each_key do |name|
+ backend = test_backend(name)
+ backends << name if backend
+ rescue LoadError, NotImplementedError, StandardError
+ # Backend not available, skip it
+ end
+
+ backends
+ end
+
+ private
+
+ def load_native_backend
+ require_relative BACKENDS[:native]
+ backend = Herb::Backends::NativeBackend.new
+ backend.load
+ backend
+ rescue LoadError => e
+ raise LoadError, "Native backend not available. The C extension may not be compiled.\n#{e.message}"
+ end
+
+ def load_node_backend
+ require_relative BACKENDS[:node]
+ backend = Herb::Backends::NodeBackend.new
+ backend.load
+ backend
+ rescue LoadError => e
+ raise LoadError, "Node backend not available. Please install the 'nodo' gem.\n#{e.message}"
+ end
+
+ def test_backend(backend_name)
+ backend = case backend_name
+ when :native
+ require_relative BACKENDS[:native]
+ Herb::Backends::NativeBackend.new
+ when :node
+ require_relative BACKENDS[:node]
+ Herb::Backends::NodeBackend.new
+ end
+
+ backend&.load
+ backend
+ rescue NotImplementedError
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/herb/backends/ffi_backend.rb b/lib/herb/backends/ffi_backend.rb
new file mode 100644
index 000000000..440dbc119
--- /dev/null
+++ b/lib/herb/backends/ffi_backend.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require_relative "../backend"
+
+module Herb
+ module Backends
+ class FFIBackend < Backend
+ protected
+
+ def perform_load
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_lex(source)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_lex_file(path)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_parse(source, options)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_parse_file(path, options)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_extract_ruby(source)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_extract_html(source)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def backend_version
+ "ffi (not implemented)"
+ end
+
+ def perform_format(source, options)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for formatting."
+ end
+
+ def perform_lint(source, options)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for linting."
+ end
+
+ def perform_print_node(node, options)
+ raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for printing."
+ end
+ end
+ end
+end
diff --git a/lib/herb/backends/native_backend.rb b/lib/herb/backends/native_backend.rb
new file mode 100644
index 000000000..f493f537d
--- /dev/null
+++ b/lib/herb/backends/native_backend.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require_relative "../backend"
+
+module Herb
+ module Backends
+ class NativeBackend < Backend
+ protected
+
+ def perform_load
+ major, minor, _patch = RUBY_VERSION.split(".")
+
+ require_relative "../../herb/#{major}.#{minor}/herb"
+ rescue LoadError
+ begin
+ require_relative "../../herb/herb"
+ rescue LoadError
+ raise LoadError, "Native backend C extension not available. Please compile the extension with: bundle exec rake compile"
+ end
+ end
+
+ def perform_lex(source)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_lex, true)
+
+ c_perform_lex(source)
+ end
+
+ def perform_lex_file(path)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_lex_file, true)
+
+ c_perform_lex_file(path)
+ end
+
+ def perform_parse(source, options)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_parse, true)
+
+ c_perform_parse(source, options)
+ end
+
+ def perform_parse_file(path, options)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_parse_file, true)
+
+ c_perform_parse_file(path, options)
+ end
+
+ def perform_extract_ruby(source)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_extract_ruby, true)
+
+ c_perform_extract_ruby(source)
+ end
+
+ def perform_extract_html(source)
+ raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_extract_html, true)
+
+ c_perform_extract_html(source)
+ end
+
+ def backend_version
+ if respond_to?(:c_backend_version, true)
+ c_backend_version
+ else
+ %(herb gem v#{Herb::VERSION}, native backend (C extension not loaded))
+ end
+ end
+
+ def perform_format(source, options)
+ raise NotImplementedError, "Formatting is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)"
+ end
+
+ def perform_lint(source, options)
+ raise NotImplementedError, "Linting is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)"
+ end
+
+ def perform_print_node(node, options)
+ raise NotImplementedError, "Node printing is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)"
+ end
+ end
+ end
+end
diff --git a/lib/herb/backends/node_backend.rb b/lib/herb/backends/node_backend.rb
new file mode 100644
index 000000000..943ddc45f
--- /dev/null
+++ b/lib/herb/backends/node_backend.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+# typed: ignore
+
+# rbs_inline: disabled
+
+require_relative "../backend"
+require_relative "../lint_result"
+
+require "json"
+
+begin
+ require "nodo"
+rescue LoadError
+ # Will be handled in perform_load
+end
+
+module Herb
+ module Backends
+ class NodeBackend < Backend
+ attr_reader :nodo, :herb_node
+
+ protected
+
+ def perform_load
+ begin
+ require "nodo"
+ rescue LoadError
+ raise LoadError, "Node backend requires the 'nodo' gem. Please add it to your Gemfile or install it with: gem install nodo"
+ end
+
+ @herb_node_class = Herb::Backends::HerbNode
+ @herb_node = @herb_node_class.new
+ end
+
+ def perform_lex(source)
+ result_json = @herb_node.lex(source)
+
+ parse_lex_result(result_json)
+ end
+
+ def perform_lex_file(path)
+ result_json = @herb_node.lexFile(path)
+
+ parse_lex_result(result_json)
+ end
+
+ def perform_parse(source, options)
+ result_json = @herb_node.parse(source, convert_options(options))
+
+ parse_parse_result(result_json)
+ end
+
+ def perform_parse_file(path, options)
+ result_json = @herb_node.parseFile(path, convert_options(options))
+
+ parse_parse_result(result_json)
+ end
+
+ def perform_extract_ruby(source)
+ @herb_node.extractRuby(source)
+ end
+
+ def perform_extract_html(source)
+ @herb_node.extractHTML(source)
+ end
+
+ def perform_format(source, options)
+ @herb_node.format(source, convert_options(options))
+ end
+
+ def perform_lint(source, options)
+ result_json = @herb_node.lint(source, convert_options(options))
+
+ parse_lint_result(result_json)
+ end
+
+ def perform_print_node(node, options)
+ node_json = node.to_json
+
+ @herb_node.printNode(node_json, convert_options(options))
+ end
+
+ def backend_version
+ version = @herb_node.version
+
+ %(herb gem v#{VERSION}, node backend (via nodo), #{version})
+ end
+
+ private
+
+ def convert_options(options)
+ options.transform_keys { |key| key.to_s.gsub("_", "") }
+ end
+
+ def parse_lex_result(json_string)
+ data = JSON.parse(json_string, symbolize_names: true)
+
+ ::Herb::LexResult.from_hash(data)
+ end
+
+ def parse_parse_result(json_string)
+ data = JSON.parse(json_string, symbolize_names: true)
+
+ ::Herb::ParseResult.from_hash(data)
+ end
+
+ def parse_lint_result(json_string)
+ data = JSON.parse(json_string, symbolize_names: true)
+
+ ::Herb::LintResult.from_hash(data)
+ end
+ end
+
+ # Only define HerbNode if Nodo is available
+ if defined?(Nodo)
+ class HerbNode < Nodo::Core
+ require Herb: "@herb-tools/node-wasm"
+ require HerbCore: "@herb-tools/core"
+ require HerbFormatter: "@herb-tools/formatter"
+ require HerbLinter: "@herb-tools/linter"
+ require HerbPrinter: "@herb-tools/printer"
+
+ script <<~JS
+ let herbInstance = null;
+
+ async function ensureLoaded() {
+ if (!herbInstance) {
+ herbInstance = await Herb.Herb.load();
+ }
+
+ return herbInstance;
+ }
+ JS
+
+ function :lex, <<~JS
+ async (source) => {
+ const herb = await ensureLoaded();
+ const result = herb.lex(source);
+
+ return JSON.stringify(result);
+ }
+ JS
+
+ function :lexFile, <<~JS
+ async (path) => {
+ const herb = await ensureLoaded();
+ const result = herb.lexFile(path);
+
+ return JSON.stringify(result);
+ }
+ JS
+
+ function :parse, <<~JS
+ async (source, options = {}) => {
+ const herb = await ensureLoaded();
+ const result = herb.parse(source, options);
+
+ return JSON.stringify(result);
+ }
+ JS
+
+ function :parseFile, <<~JS
+ async (path, options = {}) => {
+ const herb = await ensureLoaded();
+ const result = herb.parseFile(path, options);
+
+ return JSON.stringify(result);
+ }
+ JS
+
+ function :extractRuby, <<~JS
+ async (source) => {
+ const herb = await ensureLoaded();
+
+ return herb.extractRuby(source);
+ }
+ JS
+
+ function :extractHTML, <<~JS
+ async (source) => {
+ const herb = await ensureLoaded();
+
+ return herb.extractHTML(source);
+ }
+ JS
+
+ function :version, <<~JS
+ async () => {
+ const herb = await ensureLoaded();
+
+ return herb.version;
+ }
+ JS
+
+ function :format, <<~JS
+ async (source, options = {}) => {
+ const herb = await ensureLoaded();
+
+ return new HerbFormatter.Formatter(herb).format(source, options);
+ }
+ JS
+
+ function :lint, <<~JS
+ async (source, options = {}) => {
+ const herb = await ensureLoaded();
+ const result = new HerbLinter.Linter(herb).lint(source, options);
+
+ return JSON.stringify(result);
+ }
+ JS
+
+ function :printNode, <<~JS
+ async (json, options = {}) => {
+ const herb = await ensureLoaded();
+ const serializedNode = JSON.parse(json);
+
+ const node = HerbCore.fromSerializedNode(serializedNode);
+
+ if (options.format === true) {
+ const defaultOptions = {
+ indentWidth: 2,
+ maxLineLength: 80,
+ ...options
+ };
+
+ const formatPrinter = new HerbFormatter.FormatPrinter("", defaultOptions);
+
+ return formatPrinter.print(node);
+ } else {
+ return HerbPrinter.IdentityPrinter.print(node, options);
+ }
+ }
+ JS
+ end
+ end
+ end
+end
diff --git a/lib/herb/backends/wasm_backend.rb b/lib/herb/backends/wasm_backend.rb
new file mode 100644
index 000000000..8462406b8
--- /dev/null
+++ b/lib/herb/backends/wasm_backend.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require_relative "../backend"
+
+module Herb
+ module Backends
+ class WASMBackend < Backend
+ protected
+
+ def perform_load
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_lex(source)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_lex_file(path)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_parse(source, options)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_parse_file(path, options)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_extract_ruby(source)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def perform_extract_html(source)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends."
+ end
+
+ def backend_version
+ "wasm (not implemented)"
+ end
+
+ def perform_format(source, options)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for formatting."
+ end
+
+ def perform_lint(source, options)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for linting."
+ end
+
+ def perform_print_node(node, options)
+ raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for printing."
+ end
+ end
+ end
+end
diff --git a/lib/herb/cli.rb b/lib/herb/cli.rb
index 51ebc68dc..79ae68a5e 100644
--- a/lib/herb/cli.rb
+++ b/lib/herb/cli.rb
@@ -6,7 +6,8 @@
require "optparse"
class Herb::CLI
- attr_accessor :json, :silent, :no_interactive, :no_log_file, :no_timing, :local
+ attr_accessor :json, :silent, :no_interactive, :no_log_file, :no_timing, :local, :indent_width,
+ :max_line_length
def initialize(args)
@args = args
@@ -86,6 +87,8 @@ def help(exit_code = 0)
Commands:
bundle exec herb lex [file] Lex a file.
bundle exec herb parse [file] Parse a file.
+ bundle exec herb format [file] Format an ERB file with proper indentation and spacing.
+ bundle exec herb lint [file] Lint an ERB file for potential issues and style violations.
bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project
bundle exec herb ruby [file] Extract Ruby from a file.
bundle exec herb html [file] Extract HTML from a file.
@@ -111,11 +114,16 @@ def result
project.no_log_file = no_log_file
project.no_timing = no_timing
has_issues = project.parse!
+
exit(has_issues ? 1 : 0)
when "parse"
Herb.parse(file_content)
when "lex"
Herb.lex(file_content)
+ when "format"
+ handle_format_command
+ when "lint"
+ handle_lint_command
when "ruby"
puts Herb.extract_ruby(file_content)
exit(0)
@@ -191,6 +199,15 @@ def option_parser
parser.on("--local", "Use localhost for playground command instead of herb-tools.dev") do
self.local = true
end
+
+ parser.on("-i", "--indent-width WIDTH", Integer, "Set indentation width for formatting (default: 2)") do |width|
+ self.indent_width = width
+ end
+
+ parser.on("-l", "--max-line-length LENGTH", Integer,
+ "Set maximum line length for formatting (default: 80)") do |length|
+ self.max_line_length = length
+ end
end
end
@@ -200,6 +217,62 @@ def options
private
+ def handle_format_command
+ options = {}
+ options[:indentWidth] = indent_width if indent_width
+ options[:maxLineLength] = max_line_length if max_line_length
+
+ formatted = Herb.format(file_content, **options)
+ puts formatted
+ exit(0)
+ rescue StandardError => e
+ puts "Error formatting file: #{e.message}"
+ exit(1)
+ end
+
+ def handle_lint_command
+ lint_result = Herb.lint(file_content)
+
+ if json
+ puts lint_result.to_json
+ elsif silent
+ exit(lint_result.success? ? 0 : 1)
+ else
+ puts lint_result
+
+ unless lint_result.clean?
+ puts
+ puts "Issues found:"
+
+ lint_result.error_offenses.each do |offense|
+ puts " ❌ Error: #{offense.message}"
+ puts " Rule: #{offense.rule}"
+ puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}"
+ puts
+ end
+
+ lint_result.warning_offenses.each do |offense|
+ puts " ⚠️ Warning: #{offense.message}"
+ puts " Rule: #{offense.rule}"
+ puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}"
+ puts
+ end
+
+ lint_result.info_offenses.each do |offense|
+ puts " ℹ️ Info: #{offense.message}"
+ puts " Rule: #{offense.rule}"
+ puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}"
+ puts
+ end
+ end
+ end
+
+ exit(lint_result.success? ? 0 : 1)
+ rescue StandardError => e
+ puts "Error linting file: #{e.message}"
+ exit(1)
+ end
+
def print_version
puts Herb.version
exit(0)
diff --git a/lib/herb/diagnostic.rb b/lib/herb/diagnostic.rb
new file mode 100644
index 000000000..fa757b034
--- /dev/null
+++ b/lib/herb/diagnostic.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+# typed: true
+
+# rbs_inline: enabled
+
+module Herb
+ DIAGNOSTIC_SEVERITIES = %w[error warning info hint].freeze #: Array[String]
+
+ class Diagnostic
+ attr_reader :message #: String
+ attr_reader :location #: Location?
+ attr_reader :severity #: String
+ attr_reader :code #: String?
+ attr_reader :source #: String?
+
+ #: (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void
+ def initialize(message:, location:, severity:, code: nil, source: nil)
+ @message = message
+ @location = location
+ @severity = validate_severity(severity)
+ @code = code
+ @source = source
+ end
+
+ #: () -> Hash[Symbol, untyped]
+ def to_h
+ {
+ message: @message,
+ location: @location.to_h,
+ severity: @severity,
+ code: @code,
+ source: @source,
+ }.compact
+ end
+
+ #: (*untyped) -> String
+ def to_json(state = nil)
+ to_h.to_json(state)
+ end
+
+ #: () -> String
+ def to_s
+ parts = [] #: Array[String]
+ parts << "[#{@severity.upcase}]" if @severity
+ parts << "[#{@code}]" if @code
+ parts << @message
+ parts << "at #{@location}" if @location
+ parts.join(" ")
+ end
+
+ #: () -> bool
+ def error?
+ @severity == "error"
+ end
+
+ #: () -> bool
+ def warning?
+ @severity == "warning"
+ end
+
+ #: () -> bool
+ def info?
+ @severity == "info"
+ end
+
+ #: () -> bool
+ def hint?
+ @severity == "hint"
+ end
+
+ private
+
+ #: (String) -> String
+ def validate_severity(severity)
+ unless DIAGNOSTIC_SEVERITIES.include?(severity)
+ raise ArgumentError, "Invalid severity '#{severity}'. Must be one of: #{DIAGNOSTIC_SEVERITIES.join(", ")}"
+ end
+
+ severity
+ end
+ end
+end
diff --git a/lib/herb/lex_result.rb b/lib/herb/lex_result.rb
index 5c32a76d1..844bbfe65 100644
--- a/lib/herb/lex_result.rb
+++ b/lib/herb/lex_result.rb
@@ -19,5 +19,19 @@ def success?
def failed?
errors.any?
end
+
+ #: (Hash[untyped, untyped]) -> LexResult
+ def self.from_hash(data)
+ tokens_data = data[:value] || data["value"] || []
+ warnings_data = data[:warnings] || data["warnings"] || []
+ errors_data = data[:errors] || data["errors"] || []
+ source = data[:source] || data["source"] || ""
+
+ tokens = tokens_data.map { |token_data| Token.from_hash(token_data) }
+ warnings = warnings_data.map { |warning_data| Herb::Warnings::Warning.from_hash(warning_data) }
+ errors = errors_data.map { |error_data| Herb::Errors::Error.from_hash(error_data) }
+
+ new(tokens, source, warnings, errors)
+ end
end
end
diff --git a/lib/herb/lint_offense.rb b/lib/herb/lint_offense.rb
new file mode 100644
index 000000000..d63b942fb
--- /dev/null
+++ b/lib/herb/lint_offense.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+# typed: true
+
+# rbs_inline: enabled
+
+require_relative "diagnostic"
+
+module Herb
+ class LintOffense < Diagnostic
+ attr_reader :rule #: String
+
+ #: (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void
+ def initialize(message:, location:, severity:, rule:, code: nil, source: "linter")
+ super(
+ message: message,
+ location: location,
+ severity: severity,
+ code: code,
+ source: source
+ )
+ @rule = rule
+ end
+
+ #: () -> Hash[Symbol, untyped]
+ def to_h
+ super.merge(rule: @rule)
+ end
+
+ #: () -> String
+ def to_s
+ parts = [] #: Array[String]
+
+ parts << "[#{@severity.upcase}]" if @severity
+ parts << "[#{@rule}]" if @rule
+ parts << "[#{@code}]" if @code
+ parts << @message
+ parts << "at #{@location}" if @location
+
+ parts.join(" ")
+ end
+
+ #: (Hash[untyped, untyped]) -> LintOffense
+ def self.from_hash(offense_data)
+ new(
+ message: offense_data[:message] || "Unknown lint offense",
+ location: Location.from_hash(offense_data[:location]),
+ severity: offense_data[:severity] || "error",
+ rule: offense_data[:rule] || "unknown",
+ code: offense_data[:code],
+ source: offense_data[:source] || "linter"
+ )
+ end
+ end
+end
diff --git a/lib/herb/lint_result.rb b/lib/herb/lint_result.rb
new file mode 100644
index 000000000..f5ab1a65c
--- /dev/null
+++ b/lib/herb/lint_result.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+# typed: true
+
+# rbs_inline: enabled
+
+require_relative "lint_offense"
+
+module Herb
+ class LintResult
+ attr_reader :offenses #: Array[LintOffense]
+
+ #: (?Array[LintOffense | Hash[untyped, untyped]]) -> void
+ def initialize(offenses = [])
+ @offenses = offenses.map { |offense|
+ offense.is_a?(LintOffense) ? offense : LintOffense.from_hash(offense)
+ }
+ end
+
+ #: () -> Integer
+ def errors
+ @offenses.count(&:error?)
+ end
+
+ #: () -> Integer
+ def warnings
+ @offenses.count(&:warning?)
+ end
+
+ #: () -> Integer
+ def infos
+ @offenses.count(&:info?)
+ end
+
+ #: () -> Integer
+ def hints
+ @offenses.count(&:hint?)
+ end
+
+ #: () -> Integer
+ def total_offenses
+ @offenses.length
+ end
+
+ #: () -> bool
+ def success?
+ errors.zero?
+ end
+
+ #: () -> bool
+ def clean?
+ @offenses.empty?
+ end
+
+ #: (String) -> Array[LintOffense]
+ def offenses_by_severity(severity)
+ @offenses.select { |offense| offense.severity == severity }
+ end
+
+ #: () -> Array[LintOffense]
+ def error_offenses
+ offenses_by_severity("error")
+ end
+
+ #: () -> Array[LintOffense]
+ def warning_offenses
+ offenses_by_severity("warning")
+ end
+
+ #: () -> Array[LintOffense]
+ def info_offenses
+ offenses_by_severity("info")
+ end
+
+ #: () -> Array[LintOffense]
+ def hint_offenses
+ offenses_by_severity("hint")
+ end
+
+ #: (String) -> Array[LintOffense]
+ def offenses_for_rule(rule_name)
+ @offenses.select { |offense| offense.rule == rule_name }
+ end
+
+ #: () -> Hash[Symbol, untyped]
+ def to_h
+ {
+ offenses: @offenses.map(&:to_h),
+ errors: errors,
+ warnings: warnings,
+ infos: infos,
+ hints: hints,
+ total_offenses: total_offenses,
+ success: success?,
+ clean: clean?,
+ }
+ end
+
+ #: (?untyped) -> String
+ def to_json(state = nil)
+ to_h.to_json(state)
+ end
+
+ #: () -> String
+ def to_s
+ return "✓ No lint offenses found" if clean?
+
+ parts = [] #: Array[String]
+ parts << "#{errors} errors" if errors.positive?
+ parts << "#{warnings} warnings" if warnings.positive?
+ parts << "#{infos} infos" if infos.positive?
+ parts << "#{hints} hints" if hints.positive?
+
+ "✗ Found #{total_offenses} lint offenses: #{parts.join(", ")}"
+ end
+
+ #: (Hash[untyped, untyped]) -> LintResult
+ def self.from_hash(hash_result)
+ offenses = (hash_result[:offenses] || hash_result["offenses"] || []).map do |offense_data|
+ LintOffense.from_hash(offense_data)
+ end
+
+ new(offenses)
+ end
+ end
+end
diff --git a/lib/herb/location.rb b/lib/herb/location.rb
index e7c4a1a10..9c921fba5 100644
--- a/lib/herb/location.rb
+++ b/lib/herb/location.rb
@@ -25,14 +25,34 @@ def self.[](start_line, start_column, end_line, end_column)
from(start_line, start_column, end_line, end_column)
end
+ #: (Hash[untyped, untyped]|nil) -> Location?
+ def self.from_hash(hash_data)
+ return nil if hash_data.nil?
+
+ start_data = hash_data[:start] || hash_data["start"]
+ end_data = hash_data[:end] || hash_data["end"]
+
+ start_position = Position.from_hash(start_data)
+ end_position = Position.from_hash(end_data)
+
+ return nil if start_position.nil? || end_position.nil?
+
+ new(start_position, end_position)
+ end
+
#: () -> serialized_location
def to_hash
{
- start: start,
- end: self.end,
+ start: start&.to_hash,
+ end: self.end&.to_hash,
} #: Herb::serialized_location
end
+ #: () -> serialized_location
+ def to_h
+ to_hash
+ end
+
#: (?untyped) -> String
def to_json(state = nil)
to_hash.to_json(state)
diff --git a/lib/herb/parse_result.rb b/lib/herb/parse_result.rb
index 9b0bc1764..37831e945 100644
--- a/lib/herb/parse_result.rb
+++ b/lib/herb/parse_result.rb
@@ -32,9 +32,30 @@ def pretty_errors
JSON.pretty_generate(errors)
end
+ #: () -> String
+ def to_source(...)
+ value.to_source(...)
+ end
+
#: (Visitor) -> void
def visit(visitor)
value.accept(visitor)
end
+
+ #: (Hash[untyped, untyped]) -> ParseResult
+ def self.from_hash(data)
+ value_date = data[:value] || data["value"]
+ warnings_data = data[:warnings] || data["warnings"] || [] #: Array[untyped]
+ errors_data = data[:errors] || data["errors"] || [] #: Array[untyped]
+ source = data[:source] || data["source"] || ""
+
+ value = value_date ? Herb::AST::Node.node_from_hash(value_date) : nil
+ raise "Missing document node" unless value.is_a?(Herb::AST::DocumentNode)
+
+ warnings = warnings_data.map { |warning_data| Herb::Warnings::Warning.from_hash(warning_data) }
+ errors = errors_data.map { |error_data| Herb::Errors::Error.from_hash(error_data) }
+
+ new(value, source, warnings, errors)
+ end
end
end
diff --git a/lib/herb/position.rb b/lib/herb/position.rb
index f7131d151..b919a5eac 100644
--- a/lib/herb/position.rb
+++ b/lib/herb/position.rb
@@ -22,11 +22,37 @@ def self.from(line, column)
new(line, column)
end
+ #: (Hash[untyped, untyped]|nil) -> Position?
+ def self.from_hash(hash_data)
+ return nil if hash_data.nil?
+
+ line = hash_data[:line] || hash_data["line"] || 1
+ column = hash_data[:column] || hash_data["column"] || 0
+
+ line = if line.is_a?(Integer)
+ line
+ else
+ (line.respond_to?(:to_i) ? line.to_i : 1)
+ end
+ column = if column.is_a?(Integer)
+ column
+ else
+ (column.respond_to?(:to_i) ? column.to_i : 0)
+ end
+
+ new(line, column)
+ end
+
#: () -> serialized_position
def to_hash
{ line: line, column: column } #: Herb::serialized_position
end
+ #: () -> serialized_position
+ def to_h
+ to_hash
+ end
+
#: (?untyped) -> String
def to_json(state = nil)
to_hash.to_json(state)
diff --git a/lib/herb/range.rb b/lib/herb/range.rb
index 7adb16c10..a75b64e2e 100644
--- a/lib/herb/range.rb
+++ b/lib/herb/range.rb
@@ -22,6 +22,43 @@ def self.from(from, to)
new(from, to)
end
+ #: (Array[untyped]|Hash[untyped, untyped]|nil) -> Range?
+ def self.from_hash(data)
+ return nil if data.nil?
+
+ case data
+ when Array
+ from = if data[0].is_a?(Integer)
+ data[0]
+ else
+ (data[0].respond_to?(:to_i) ? data[0].to_i : 0)
+ end
+ to = if data[1].is_a?(Integer)
+ data[1]
+ else
+ (data[1].respond_to?(:to_i) ? data[1].to_i : 0)
+ end
+
+ new(from, to)
+ when Hash
+ from_value = data[:start] || data["start"] || data[:from] || data["from"] || 0
+ to_value = data[:end] || data["end"] || data[:to] || data["to"] || 0
+
+ from = if from_value.is_a?(Integer)
+ from_value
+ else
+ (from_value.respond_to?(:to_i) ? from_value.to_i : 0)
+ end
+ to = if to_value.is_a?(Integer)
+ to_value
+ else
+ (to_value.respond_to?(:to_i) ? to_value.to_i : 0)
+ end
+
+ new(from, to)
+ end
+ end
+
#: () -> serialized_range
def to_a
[from, to] #: Herb::serialized_range
diff --git a/lib/herb/token.rb b/lib/herb/token.rb
index e12d3c4d4..ba16cba3c 100644
--- a/lib/herb/token.rb
+++ b/lib/herb/token.rb
@@ -26,6 +26,21 @@ def to_hash
} #: Herb::serialized_token
end
+ #: (Hash[untyped, untyped]?) -> Token?
+ def self.from_hash(data)
+ return if data.nil?
+
+ value = data[:value] || data["value"] || ""
+ type = data[:type] || data["type"] || ""
+
+ range = Herb::Range.from_hash(data[:range] || data["range"])
+ location = Location.from_hash(data[:location] || data["location"])
+
+ return nil if range.nil? || location.nil?
+
+ new(value, range, location, type)
+ end
+
#: (?untyped) -> String
def to_json(state = nil)
to_hash.to_json(state)
diff --git a/lib/herb/warnings.rb b/lib/herb/warnings.rb
index 9020a3d7d..a74b60090 100644
--- a/lib/herb/warnings.rb
+++ b/lib/herb/warnings.rb
@@ -5,10 +5,10 @@ module Herb
module Warnings
class Warning
attr_reader :type #: String
- attr_reader :location #: Location
+ attr_reader :location #: Location?
attr_reader :message #: String
- #: (String, Location, String) -> void
+ #: (String, Location?, String) -> void
def initialize(type, location, message)
@type = type
@location = location
@@ -24,6 +24,15 @@ def to_hash
}
end
+ #: (Hash[untyped, untyped]) -> Warning
+ def self.from_hash(data)
+ type = data[:type] || data["type"] || ""
+ location = Location.from_hash(data[:location] || data["location"])
+ message = data[:message] || data["message"] || ""
+
+ new(type, location, message)
+ end
+
#: () -> String
def class_name
self.class.name || "Warning"
diff --git a/sig/herb.rbs b/sig/herb.rbs
index 4195b972f..163167f03 100644
--- a/sig/herb.rbs
+++ b/sig/herb.rbs
@@ -1,4 +1,41 @@
# Generated from lib/herb.rb with RBS::Inline
module Herb
+ def self.load_backend: (?untyped backend_name) -> untyped
+
+ def self.backend_loaded?: () -> untyped
+
+ def self.ensure_backend!: () -> untyped
+
+ def self.lex: (untyped source) -> untyped
+
+ def self.lex_file: (untyped path) -> untyped
+
+ def self.parse: (untyped source, ?backend: untyped, **untyped options) -> untyped
+
+ def self.parse_file: (untyped path, ?backend: untyped, **untyped options) -> untyped
+
+ def self.extract_ruby: (untyped source) -> untyped
+
+ def self.extract_html: (untyped source) -> untyped
+
+ def self.version: () -> untyped
+
+ def self.format: (untyped source, ?backend: untyped, **untyped options) -> untyped
+
+ def self.format_file: (untyped path, ?backend: untyped, **untyped options) -> untyped
+
+ def self.lint: (untyped source, ?backend: untyped, **untyped options) -> untyped
+
+ def self.lint_file: (untyped path, ?backend: untyped, **untyped options) -> untyped
+
+ def self.print_node: (untyped node, ?backend: untyped, **untyped options) -> untyped
+
+ def self.available_backends: () -> untyped
+
+ def self.switch_backend: (untyped backend_name) -> untyped
+
+ def self.current_backend: () -> untyped
+
+ def self.backend: (untyped backend_name) -> untyped
end
diff --git a/sig/herb/ast/node.rbs b/sig/herb/ast/node.rbs
index c89780470..30518e7f1 100644
--- a/sig/herb/ast/node.rbs
+++ b/sig/herb/ast/node.rbs
@@ -5,12 +5,12 @@ module Herb
class Node
attr_reader type: String
- attr_reader location: Location
+ attr_reader location: Location?
attr_reader errors: Array[Herb::Errors::Error]
- # : (String, Location, Array[Herb::Errors::Error]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error]) -> void
# : () -> serialized_node
def to_hash: () -> serialized_node
@@ -46,6 +46,9 @@ module Herb
# : () -> Array[Herb::Errors::Error]
def recursive_errors: () -> Array[Herb::Errors::Error]
+
+ # : (?backend: Symbol, **untyped) -> String
+ def to_source: (?backend: Symbol, **untyped) -> String
end
end
end
diff --git a/sig/herb/ast/nodes.rbs b/sig/herb/ast/nodes.rbs
index fc211744b..136f2dc38 100644
--- a/sig/herb/ast/nodes.rbs
+++ b/sig/herb/ast/nodes.rbs
@@ -2,15 +2,24 @@
module Herb
module AST
+ # Dynamically reopen Node class to add node_from_hash method with knowledge of all node types
+ class Node
+ # : (Hash[untyped, untyped]?) -> Node?
+ def self.node_from_hash: (Hash[untyped, untyped]?) -> Node?
+ end
+
class DocumentNode < Node
attr_reader children: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
# : () -> serialized_document_node
def to_hash: () -> serialized_document_node
+ # : (Hash[untyped, untyped]) -> DocumentNode
+ def self.from_hash: (Hash[untyped, untyped]) -> DocumentNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -30,12 +39,15 @@ module Herb
class LiteralNode < Node
attr_reader content: String
- # : (String, Location, Array[Herb::Errors::Error], String) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], String) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], String) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], String) -> void
# : () -> serialized_literal_node
def to_hash: () -> serialized_literal_node
+ # : (Hash[untyped, untyped]) -> LiteralNode
+ def self.from_hash: (Hash[untyped, untyped]) -> LiteralNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -53,22 +65,25 @@ module Herb
end
class HTMLOpenTagNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader tag_name: Herb::Token
+ attr_reader tag_name: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
attr_reader is_void: bool
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], bool) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], bool) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], bool) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], bool) -> void
# : () -> serialized_html_open_tag_node
def to_hash: () -> serialized_html_open_tag_node
+ # : (Hash[untyped, untyped]) -> HTMLOpenTagNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLOpenTagNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -86,20 +101,23 @@ module Herb
end
class HTMLCloseTagNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader tag_name: Herb::Token
+ attr_reader tag_name: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
# : () -> serialized_html_close_tag_node
def to_hash: () -> serialized_html_close_tag_node
+ # : (Hash[untyped, untyped]) -> HTMLCloseTagNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLCloseTagNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -117,22 +135,25 @@ module Herb
end
class HTMLElementNode < Node
- attr_reader open_tag: Herb::AST::HTMLOpenTagNode
+ attr_reader open_tag: Herb::AST::HTMLOpenTagNode?
- attr_reader tag_name: Herb::Token
+ attr_reader tag_name: Herb::Token?
attr_reader body: Array[Herb::AST::Node]
- attr_reader close_tag: Herb::AST::HTMLCloseTagNode
+ attr_reader close_tag: Herb::AST::HTMLCloseTagNode?
attr_reader is_void: bool
- # : (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode, Herb::Token, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode, bool) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode, Herb::Token, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode, bool) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode?, bool) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode?, bool) -> void
# : () -> serialized_html_element_node
def to_hash: () -> serialized_html_element_node
+ # : (Hash[untyped, untyped]) -> HTMLElementNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLElementNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -150,20 +171,23 @@ module Herb
end
class HTMLAttributeValueNode < Node
- attr_reader open_quote: Herb::Token
+ attr_reader open_quote: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader close_quote: Herb::Token
+ attr_reader close_quote: Herb::Token?
attr_reader quoted: bool
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token, bool) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token, bool) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?, bool) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?, bool) -> void
# : () -> serialized_html_attribute_value_node
def to_hash: () -> serialized_html_attribute_value_node
+ # : (Hash[untyped, untyped]) -> HTMLAttributeValueNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeValueNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -183,12 +207,15 @@ module Herb
class HTMLAttributeNameNode < Node
attr_reader children: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void
# : () -> serialized_html_attribute_name_node
def to_hash: () -> serialized_html_attribute_name_node
+ # : (Hash[untyped, untyped]) -> HTMLAttributeNameNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeNameNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -206,18 +233,21 @@ module Herb
end
class HTMLAttributeNode < Node
- attr_reader name: Herb::AST::HTMLAttributeNameNode
+ attr_reader name: Herb::AST::HTMLAttributeNameNode?
- attr_reader equals: Herb::Token
+ attr_reader equals: Herb::Token?
- attr_reader value: Herb::AST::HTMLAttributeValueNode
+ attr_reader value: Herb::AST::HTMLAttributeValueNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode, Herb::Token, Herb::AST::HTMLAttributeValueNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode, Herb::Token, Herb::AST::HTMLAttributeValueNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode?, Herb::Token?, Herb::AST::HTMLAttributeValueNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode?, Herb::Token?, Herb::AST::HTMLAttributeValueNode?) -> void
# : () -> serialized_html_attribute_node
def to_hash: () -> serialized_html_attribute_node
+ # : (Hash[untyped, untyped]) -> HTMLAttributeNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -237,12 +267,15 @@ module Herb
class HTMLTextNode < Node
attr_reader content: String
- # : (String, Location, Array[Herb::Errors::Error], String) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], String) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], String) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], String) -> void
# : () -> serialized_html_text_node
def to_hash: () -> serialized_html_text_node
+ # : (Hash[untyped, untyped]) -> HTMLTextNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLTextNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -260,18 +293,21 @@ module Herb
end
class HTMLCommentNode < Node
- attr_reader comment_start: Herb::Token
+ attr_reader comment_start: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader comment_end: Herb::Token
+ attr_reader comment_end: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
# : () -> serialized_html_comment_node
def to_hash: () -> serialized_html_comment_node
+ # : (Hash[untyped, untyped]) -> HTMLCommentNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLCommentNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -289,18 +325,21 @@ module Herb
end
class HTMLDoctypeNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
# : () -> serialized_html_doctype_node
def to_hash: () -> serialized_html_doctype_node
+ # : (Hash[untyped, untyped]) -> HTMLDoctypeNode
+ def self.from_hash: (Hash[untyped, untyped]) -> HTMLDoctypeNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -318,18 +357,21 @@ module Herb
end
class XMLDeclarationNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
# : () -> serialized_xml_declaration_node
def to_hash: () -> serialized_xml_declaration_node
+ # : (Hash[untyped, untyped]) -> XMLDeclarationNode
+ def self.from_hash: (Hash[untyped, untyped]) -> XMLDeclarationNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -347,18 +389,21 @@ module Herb
end
class CDATANode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void
# : () -> serialized_cdata_node
def to_hash: () -> serialized_cdata_node
+ # : (Hash[untyped, untyped]) -> CDATANode
+ def self.from_hash: (Hash[untyped, untyped]) -> CDATANode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -376,14 +421,17 @@ module Herb
end
class WhitespaceNode < Node
- attr_reader value: Herb::Token
+ attr_reader value: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?) -> void
# : () -> serialized_whitespace_node
def to_hash: () -> serialized_whitespace_node
+ # : (Hash[untyped, untyped]) -> WhitespaceNode
+ def self.from_hash: (Hash[untyped, untyped]) -> WhitespaceNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -401,11 +449,11 @@ module Herb
end
class ERBContentNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader analyzed_ruby: nil
@@ -413,12 +461,15 @@ module Herb
attr_reader valid: bool
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, nil, bool, bool) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, nil, bool, bool) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, nil, bool, bool) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, nil, bool, bool) -> void
# : () -> serialized_erb_content_node
def to_hash: () -> serialized_erb_content_node
+ # : (Hash[untyped, untyped]) -> ERBContentNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBContentNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -436,18 +487,21 @@ module Herb
end
class ERBEndNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void
# : () -> serialized_erb_end_node
def to_hash: () -> serialized_erb_end_node
+ # : (Hash[untyped, untyped]) -> ERBEndNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBEndNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -465,20 +519,23 @@ module Herb
end
class ERBElseNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
# : () -> serialized_erb_else_node
def to_hash: () -> serialized_erb_else_node
+ # : (Hash[untyped, untyped]) -> ERBElseNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBElseNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -496,24 +553,27 @@ module Herb
end
class ERBIfNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader subsequent: Herb::AST::Node
+ attr_reader subsequent: Herb::AST::Node?
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::Node, Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::Node, Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::Node?, Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::Node?, Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_if_node
def to_hash: () -> serialized_erb_if_node
+ # : (Hash[untyped, untyped]) -> ERBIfNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBIfNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -531,22 +591,25 @@ module Herb
end
class ERBBlockNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader body: Array[Herb::AST::Node]
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_block_node
def to_hash: () -> serialized_erb_block_node
+ # : (Hash[untyped, untyped]) -> ERBBlockNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBBlockNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -564,20 +627,23 @@ module Herb
end
class ERBWhenNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
# : () -> serialized_erb_when_node
def to_hash: () -> serialized_erb_when_node
+ # : (Hash[untyped, untyped]) -> ERBWhenNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBWhenNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -595,26 +661,29 @@ module Herb
end
class ERBCaseNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
attr_reader conditions: Array[Herb::AST::ERBWhenNode]
- attr_reader else_clause: Herb::AST::ERBElseNode
+ attr_reader else_clause: Herb::AST::ERBElseNode?
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_case_node
def to_hash: () -> serialized_erb_case_node
+ # : (Hash[untyped, untyped]) -> ERBCaseNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBCaseNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -632,26 +701,29 @@ module Herb
end
class ERBCaseMatchNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader children: Array[Herb::AST::Node]
attr_reader conditions: Array[Herb::AST::ERBInNode]
- attr_reader else_clause: Herb::AST::ERBElseNode
+ attr_reader else_clause: Herb::AST::ERBElseNode?
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_case_match_node
def to_hash: () -> serialized_erb_case_match_node
+ # : (Hash[untyped, untyped]) -> ERBCaseMatchNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBCaseMatchNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -669,22 +741,25 @@ module Herb
end
class ERBWhileNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_while_node
def to_hash: () -> serialized_erb_while_node
+ # : (Hash[untyped, untyped]) -> ERBWhileNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBWhileNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -702,22 +777,25 @@ module Herb
end
class ERBUntilNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_until_node
def to_hash: () -> serialized_erb_until_node
+ # : (Hash[untyped, untyped]) -> ERBUntilNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBUntilNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -735,22 +813,25 @@ module Herb
end
class ERBForNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_for_node
def to_hash: () -> serialized_erb_for_node
+ # : (Hash[untyped, untyped]) -> ERBForNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBForNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -768,22 +849,25 @@ module Herb
end
class ERBRescueNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader subsequent: Herb::AST::ERBRescueNode
+ attr_reader subsequent: Herb::AST::ERBRescueNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?) -> void
# : () -> serialized_erb_rescue_node
def to_hash: () -> serialized_erb_rescue_node
+ # : (Hash[untyped, untyped]) -> ERBRescueNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBRescueNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -801,20 +885,23 @@ module Herb
end
class ERBEnsureNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
# : () -> serialized_erb_ensure_node
def to_hash: () -> serialized_erb_ensure_node
+ # : (Hash[untyped, untyped]) -> ERBEnsureNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBEnsureNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -832,28 +919,31 @@ module Herb
end
class ERBBeginNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader rescue_clause: Herb::AST::ERBRescueNode
+ attr_reader rescue_clause: Herb::AST::ERBRescueNode?
- attr_reader else_clause: Herb::AST::ERBElseNode
+ attr_reader else_clause: Herb::AST::ERBElseNode?
- attr_reader ensure_clause: Herb::AST::ERBEnsureNode
+ attr_reader ensure_clause: Herb::AST::ERBEnsureNode?
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode, Herb::AST::ERBElseNode, Herb::AST::ERBEnsureNode, Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode, Herb::AST::ERBElseNode, Herb::AST::ERBEnsureNode, Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?, Herb::AST::ERBElseNode?, Herb::AST::ERBEnsureNode?, Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?, Herb::AST::ERBElseNode?, Herb::AST::ERBEnsureNode?, Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_begin_node
def to_hash: () -> serialized_erb_begin_node
+ # : (Hash[untyped, untyped]) -> ERBBeginNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBBeginNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -871,24 +961,27 @@ module Herb
end
class ERBUnlessNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- attr_reader else_clause: Herb::AST::ERBElseNode
+ attr_reader else_clause: Herb::AST::ERBElseNode?
- attr_reader end_node: Herb::AST::ERBEndNode
+ attr_reader end_node: Herb::AST::ERBEndNode?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void
# : () -> serialized_erb_unless_node
def to_hash: () -> serialized_erb_unless_node
+ # : (Hash[untyped, untyped]) -> ERBUnlessNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBUnlessNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -906,18 +999,21 @@ module Herb
end
class ERBYieldNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void
# : () -> serialized_erb_yield_node
def to_hash: () -> serialized_erb_yield_node
+ # : (Hash[untyped, untyped]) -> ERBYieldNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBYieldNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
@@ -935,20 +1031,23 @@ module Herb
end
class ERBInNode < Node
- attr_reader tag_opening: Herb::Token
+ attr_reader tag_opening: Herb::Token?
- attr_reader content: Herb::Token
+ attr_reader content: Herb::Token?
- attr_reader tag_closing: Herb::Token
+ attr_reader tag_closing: Herb::Token?
attr_reader statements: Array[Herb::AST::Node]
- # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
- def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void
+ # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
+ def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void
# : () -> serialized_erb_in_node
def to_hash: () -> serialized_erb_in_node
+ # : (Hash[untyped, untyped]) -> ERBInNode
+ def self.from_hash: (Hash[untyped, untyped]) -> ERBInNode
+
# : (Visitor) -> void
def accept: (Visitor) -> void
diff --git a/sig/herb/backend.rbs b/sig/herb/backend.rbs
new file mode 100644
index 000000000..9f3a6d9ca
--- /dev/null
+++ b/sig/herb/backend.rbs
@@ -0,0 +1,69 @@
+# Generated from lib/herb/backend.rb with RBS::Inline
+
+module Herb
+ class Backend
+ attr_reader loaded: untyped
+
+ def initialize: () -> untyped
+
+ def load: () -> untyped
+
+ def loaded?: () -> untyped
+
+ def lex: (untyped source) -> untyped
+
+ def lex_file: (untyped path) -> untyped
+
+ def parse: (untyped source, ?untyped options) -> untyped
+
+ def parse_file: (untyped path, ?untyped options) -> untyped
+
+ def extract_ruby: (untyped source) -> untyped
+
+ def extract_html: (untyped source) -> untyped
+
+ def version: () -> untyped
+
+ def format: (untyped source, ?untyped options) -> untyped
+
+ def format_file: (untyped path, ?untyped options) -> untyped
+
+ def lint: (untyped source, ?untyped options) -> untyped
+
+ def lint_file: (untyped path, ?untyped options) -> untyped
+
+ def print_node: (untyped node, ?untyped options) -> untyped
+
+ def backend_name: () -> untyped
+
+ def perform_load: () -> untyped
+
+ def perform_lex: (untyped source) -> untyped
+
+ def perform_lex_file: (untyped path) -> untyped
+
+ def perform_parse: (untyped source, untyped options) -> untyped
+
+ def perform_parse_file: (untyped path, untyped options) -> untyped
+
+ def perform_extract_ruby: (untyped source) -> untyped
+
+ def perform_extract_html: (untyped source) -> untyped
+
+ def backend_version: () -> untyped
+
+ def perform_format: (untyped source, untyped options) -> untyped
+
+ def perform_format_file: (untyped path, untyped options) -> untyped
+
+ def perform_lint: (untyped source, untyped options) -> untyped
+
+ def perform_lint_file: (untyped path, untyped options) -> untyped
+
+ def perform_print_node: (untyped node, untyped options) -> untyped
+
+ private
+
+ def ensure_loaded!: () -> untyped
+ end
+end
diff --git a/sig/herb/backend_loader.rbs b/sig/herb/backend_loader.rbs
new file mode 100644
index 000000000..1eb9cd549
--- /dev/null
+++ b/sig/herb/backend_loader.rbs
@@ -0,0 +1,21 @@
+# Generated from lib/herb/backend_loader.rb with RBS::Inline
+
+module Herb
+ class BackendLoader
+ BACKENDS: untyped
+
+ DEFAULT_PRIORITY: untyped
+
+ def self.load: (?untyped backend_name) -> untyped
+
+ def self.detect_backend: () -> untyped
+
+ def self.available_backends: () -> untyped
+
+ private def self.load_native_backend: () -> untyped
+
+ private def self.load_node_backend: () -> untyped
+
+ private def self.test_backend: (untyped backend_name) -> untyped
+ end
+end
diff --git a/sig/herb/backends/ffi_backend.rbs b/sig/herb/backends/ffi_backend.rbs
new file mode 100644
index 000000000..8a0f08262
--- /dev/null
+++ b/sig/herb/backends/ffi_backend.rbs
@@ -0,0 +1,29 @@
+# Generated from lib/herb/backends/ffi_backend.rb with RBS::Inline
+
+module Herb
+ module Backends
+ class FFIBackend < Backend
+ def perform_load: () -> untyped
+
+ def perform_lex: (untyped source) -> untyped
+
+ def perform_lex_file: (untyped path) -> untyped
+
+ def perform_parse: (untyped source, untyped options) -> untyped
+
+ def perform_parse_file: (untyped path, untyped options) -> untyped
+
+ def perform_extract_ruby: (untyped source) -> untyped
+
+ def perform_extract_html: (untyped source) -> untyped
+
+ def backend_version: () -> untyped
+
+ def perform_format: (untyped source, untyped options) -> untyped
+
+ def perform_lint: (untyped source, untyped options) -> untyped
+
+ def perform_print_node: (untyped node, untyped options) -> untyped
+ end
+ end
+end
diff --git a/sig/herb/backends/native_backend.rbs b/sig/herb/backends/native_backend.rbs
new file mode 100644
index 000000000..b54c687d6
--- /dev/null
+++ b/sig/herb/backends/native_backend.rbs
@@ -0,0 +1,29 @@
+# Generated from lib/herb/backends/native_backend.rb with RBS::Inline
+
+module Herb
+ module Backends
+ class NativeBackend < Backend
+ def perform_load: () -> untyped
+
+ def perform_lex: (untyped source) -> untyped
+
+ def perform_lex_file: (untyped path) -> untyped
+
+ def perform_parse: (untyped source, untyped options) -> untyped
+
+ def perform_parse_file: (untyped path, untyped options) -> untyped
+
+ def perform_extract_ruby: (untyped source) -> untyped
+
+ def perform_extract_html: (untyped source) -> untyped
+
+ def backend_version: () -> untyped
+
+ def perform_format: (untyped source, untyped options) -> untyped
+
+ def perform_lint: (untyped source, untyped options) -> untyped
+
+ def perform_print_node: (untyped node, untyped options) -> untyped
+ end
+ end
+end
diff --git a/sig/herb/backends/node_backend.rbs b/sig/herb/backends/node_backend.rbs
new file mode 100644
index 000000000..6d7d4311e
--- /dev/null
+++ b/sig/herb/backends/node_backend.rbs
@@ -0,0 +1,49 @@
+# Generated from lib/herb/backends/node_backend.rb with RBS::Inline
+
+module Herb
+ module Backends
+ class NodeBackend < Backend
+ attr_reader nodo: untyped
+
+ attr_reader herb_node: untyped
+
+ def perform_load: () -> untyped
+
+ def perform_lex: (untyped source) -> untyped
+
+ def perform_lex_file: (untyped path) -> untyped
+
+ def perform_parse: (untyped source, untyped options) -> untyped
+
+ def perform_parse_file: (untyped path, untyped options) -> untyped
+
+ def perform_extract_ruby: (untyped source) -> untyped
+
+ def perform_extract_html: (untyped source) -> untyped
+
+ def perform_format: (untyped source, untyped options) -> untyped
+
+ def perform_lint: (untyped source, untyped options) -> untyped
+
+ def perform_print_node: (untyped node, untyped options) -> untyped
+
+ def backend_version: () -> untyped
+
+ private
+
+ def setup_node_environment: () -> untyped
+
+ def find_node_binary: () -> untyped
+
+ def find_node_modules_path: () -> untyped
+
+ def convert_options: (untyped options) -> untyped
+
+ def parse_lex_result: (untyped json_string) -> untyped
+
+ def parse_parse_result: (untyped json_string) -> untyped
+
+ def parse_lint_result: (untyped json_string) -> untyped
+ end
+ end
+end
diff --git a/sig/herb/backends/wasm_backend.rbs b/sig/herb/backends/wasm_backend.rbs
new file mode 100644
index 000000000..edfcc0baa
--- /dev/null
+++ b/sig/herb/backends/wasm_backend.rbs
@@ -0,0 +1,29 @@
+# Generated from lib/herb/backends/wasm_backend.rb with RBS::Inline
+
+module Herb
+ module Backends
+ class WASMBackend < Backend
+ def perform_load: () -> untyped
+
+ def perform_lex: (untyped source) -> untyped
+
+ def perform_lex_file: (untyped path) -> untyped
+
+ def perform_parse: (untyped source, untyped options) -> untyped
+
+ def perform_parse_file: (untyped path, untyped options) -> untyped
+
+ def perform_extract_ruby: (untyped source) -> untyped
+
+ def perform_extract_html: (untyped source) -> untyped
+
+ def backend_version: () -> untyped
+
+ def perform_format: (untyped source, untyped options) -> untyped
+
+ def perform_lint: (untyped source, untyped options) -> untyped
+
+ def perform_print_node: (untyped node, untyped options) -> untyped
+ end
+ end
+end
diff --git a/sig/herb/diagnostic.rbs b/sig/herb/diagnostic.rbs
new file mode 100644
index 000000000..61b1f647b
--- /dev/null
+++ b/sig/herb/diagnostic.rbs
@@ -0,0 +1,46 @@
+# Generated from lib/herb/diagnostic.rb with RBS::Inline
+
+module Herb
+ DIAGNOSTIC_SEVERITIES: Array[String]
+
+ class Diagnostic
+ attr_reader message: String
+
+ attr_reader location: Location?
+
+ attr_reader severity: String
+
+ attr_reader code: String?
+
+ attr_reader source: String?
+
+ # : (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void
+ def initialize: (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void
+
+ # : () -> Hash[Symbol, untyped]
+ def to_h: () -> Hash[Symbol, untyped]
+
+ # : (*untyped) -> String
+ def to_json: (*untyped) -> String
+
+ # : () -> String
+ def to_s: () -> String
+
+ # : () -> bool
+ def error?: () -> bool
+
+ # : () -> bool
+ def warning?: () -> bool
+
+ # : () -> bool
+ def info?: () -> bool
+
+ # : () -> bool
+ def hint?: () -> bool
+
+ private
+
+ # : (String) -> String
+ def validate_severity: (String) -> String
+ end
+end
diff --git a/sig/herb/errors.rbs b/sig/herb/errors.rbs
index ec1b47a57..f3aa80bc5 100644
--- a/sig/herb/errors.rbs
+++ b/sig/herb/errors.rbs
@@ -5,16 +5,19 @@ module Herb
class Error
attr_reader type: String
- attr_reader location: Location
+ attr_reader location: Location?
attr_reader message: String
- # : (String, Location, String) -> void
- def initialize: (String, Location, String) -> void
+ # : (String, Location?, String) -> void
+ def initialize: (String, Location?, String) -> void
# : () -> serialized_error
def to_hash: () -> serialized_error
+ # : (Hash[untyped, untyped]) -> Error
+ def self.from_hash: (Hash[untyped, untyped]) -> Error
+
# : () -> String
def class_name: () -> String
@@ -35,8 +38,8 @@ module Herb
attr_reader found: String
- # : (String, Location, String, String, String, String) -> void
- def initialize: (String, Location, String, String, String, String) -> void
+ # : (String, Location?, String, String, String, String) -> void
+ def initialize: (String, Location?, String, String, String, String) -> void
# : () -> String
def inspect: () -> String
@@ -44,6 +47,9 @@ module Herb
# : () -> serialized_unexpected_error
def to_hash: () -> serialized_unexpected_error
+ # : (Hash[untyped, untyped]) -> UnexpectedError
+ def self.from_hash: (Hash[untyped, untyped]) -> UnexpectedError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
@@ -51,10 +57,10 @@ module Herb
class UnexpectedTokenError < Error
attr_reader expected_type: String
- attr_reader found: Herb::Token
+ attr_reader found: Herb::Token?
- # : (String, Location, String, String, Herb::Token) -> void
- def initialize: (String, Location, String, String, Herb::Token) -> void
+ # : (String, Location?, String, String, Herb::Token?) -> void
+ def initialize: (String, Location?, String, String, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -62,15 +68,18 @@ module Herb
# : () -> serialized_unexpected_token_error
def to_hash: () -> serialized_unexpected_token_error
+ # : (Hash[untyped, untyped]) -> UnexpectedTokenError
+ def self.from_hash: (Hash[untyped, untyped]) -> UnexpectedTokenError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class MissingOpeningTagError < Error
- attr_reader closing_tag: Herb::Token
+ attr_reader closing_tag: Herb::Token?
- # : (String, Location, String, Herb::Token) -> void
- def initialize: (String, Location, String, Herb::Token) -> void
+ # : (String, Location?, String, Herb::Token?) -> void
+ def initialize: (String, Location?, String, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -78,15 +87,18 @@ module Herb
# : () -> serialized_missing_opening_tag_error
def to_hash: () -> serialized_missing_opening_tag_error
+ # : (Hash[untyped, untyped]) -> MissingOpeningTagError
+ def self.from_hash: (Hash[untyped, untyped]) -> MissingOpeningTagError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class MissingClosingTagError < Error
- attr_reader opening_tag: Herb::Token
+ attr_reader opening_tag: Herb::Token?
- # : (String, Location, String, Herb::Token) -> void
- def initialize: (String, Location, String, Herb::Token) -> void
+ # : (String, Location?, String, Herb::Token?) -> void
+ def initialize: (String, Location?, String, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -94,17 +106,20 @@ module Herb
# : () -> serialized_missing_closing_tag_error
def to_hash: () -> serialized_missing_closing_tag_error
+ # : (Hash[untyped, untyped]) -> MissingClosingTagError
+ def self.from_hash: (Hash[untyped, untyped]) -> MissingClosingTagError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class TagNamesMismatchError < Error
- attr_reader opening_tag: Herb::Token
+ attr_reader opening_tag: Herb::Token?
- attr_reader closing_tag: Herb::Token
+ attr_reader closing_tag: Herb::Token?
- # : (String, Location, String, Herb::Token, Herb::Token) -> void
- def initialize: (String, Location, String, Herb::Token, Herb::Token) -> void
+ # : (String, Location?, String, Herb::Token?, Herb::Token?) -> void
+ def initialize: (String, Location?, String, Herb::Token?, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -112,17 +127,20 @@ module Herb
# : () -> serialized_tag_names_mismatch_error
def to_hash: () -> serialized_tag_names_mismatch_error
+ # : (Hash[untyped, untyped]) -> TagNamesMismatchError
+ def self.from_hash: (Hash[untyped, untyped]) -> TagNamesMismatchError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class QuotesMismatchError < Error
- attr_reader opening_quote: Herb::Token
+ attr_reader opening_quote: Herb::Token?
- attr_reader closing_quote: Herb::Token
+ attr_reader closing_quote: Herb::Token?
- # : (String, Location, String, Herb::Token, Herb::Token) -> void
- def initialize: (String, Location, String, Herb::Token, Herb::Token) -> void
+ # : (String, Location?, String, Herb::Token?, Herb::Token?) -> void
+ def initialize: (String, Location?, String, Herb::Token?, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -130,19 +148,22 @@ module Herb
# : () -> serialized_quotes_mismatch_error
def to_hash: () -> serialized_quotes_mismatch_error
+ # : (Hash[untyped, untyped]) -> QuotesMismatchError
+ def self.from_hash: (Hash[untyped, untyped]) -> QuotesMismatchError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class VoidElementClosingTagError < Error
- attr_reader tag_name: Herb::Token
+ attr_reader tag_name: Herb::Token?
attr_reader expected: String
attr_reader found: String
- # : (String, Location, String, Herb::Token, String, String) -> void
- def initialize: (String, Location, String, Herb::Token, String, String) -> void
+ # : (String, Location?, String, Herb::Token?, String, String) -> void
+ def initialize: (String, Location?, String, Herb::Token?, String, String) -> void
# : () -> String
def inspect: () -> String
@@ -150,15 +171,18 @@ module Herb
# : () -> serialized_void_element_closing_tag_error
def to_hash: () -> serialized_void_element_closing_tag_error
+ # : (Hash[untyped, untyped]) -> VoidElementClosingTagError
+ def self.from_hash: (Hash[untyped, untyped]) -> VoidElementClosingTagError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
class UnclosedElementError < Error
- attr_reader opening_tag: Herb::Token
+ attr_reader opening_tag: Herb::Token?
- # : (String, Location, String, Herb::Token) -> void
- def initialize: (String, Location, String, Herb::Token) -> void
+ # : (String, Location?, String, Herb::Token?) -> void
+ def initialize: (String, Location?, String, Herb::Token?) -> void
# : () -> String
def inspect: () -> String
@@ -166,6 +190,9 @@ module Herb
# : () -> serialized_unclosed_element_error
def to_hash: () -> serialized_unclosed_element_error
+ # : (Hash[untyped, untyped]) -> UnclosedElementError
+ def self.from_hash: (Hash[untyped, untyped]) -> UnclosedElementError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
@@ -177,8 +204,8 @@ module Herb
attr_reader level: String
- # : (String, Location, String, String, String, String) -> void
- def initialize: (String, Location, String, String, String, String) -> void
+ # : (String, Location?, String, String, String, String) -> void
+ def initialize: (String, Location?, String, String, String, String) -> void
# : () -> String
def inspect: () -> String
@@ -186,6 +213,9 @@ module Herb
# : () -> serialized_ruby_parse_error
def to_hash: () -> serialized_ruby_parse_error
+ # : (Hash[untyped, untyped]) -> RubyParseError
+ def self.from_hash: (Hash[untyped, untyped]) -> RubyParseError
+
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
diff --git a/sig/herb/lex_result.rbs b/sig/herb/lex_result.rbs
index cfb7db0ed..a0895e396 100644
--- a/sig/herb/lex_result.rbs
+++ b/sig/herb/lex_result.rbs
@@ -12,5 +12,8 @@ module Herb
# : () -> bool
def failed?: () -> bool
+
+ # : (Hash[untyped, untyped]) -> LexResult
+ def self.from_hash: (Hash[untyped, untyped]) -> LexResult
end
end
diff --git a/sig/herb/lint_offense.rbs b/sig/herb/lint_offense.rbs
new file mode 100644
index 000000000..71e2a8907
--- /dev/null
+++ b/sig/herb/lint_offense.rbs
@@ -0,0 +1,19 @@
+# Generated from lib/herb/lint_offense.rb with RBS::Inline
+
+module Herb
+ class LintOffense < Diagnostic
+ attr_reader rule: String
+
+ # : (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void
+ def initialize: (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void
+
+ # : () -> Hash[Symbol, untyped]
+ def to_h: () -> Hash[Symbol, untyped]
+
+ # : () -> String
+ def to_s: () -> String
+
+ # : (Hash[untyped, untyped]) -> LintOffense
+ def self.from_hash: (Hash[untyped, untyped]) -> LintOffense
+ end
+end
diff --git a/sig/herb/lint_result.rbs b/sig/herb/lint_result.rbs
new file mode 100644
index 000000000..e5f914fb5
--- /dev/null
+++ b/sig/herb/lint_result.rbs
@@ -0,0 +1,61 @@
+# Generated from lib/herb/lint_result.rb with RBS::Inline
+
+module Herb
+ class LintResult
+ attr_reader offenses: Array[LintOffense]
+
+ # : (?Array[LintOffense | Hash[untyped, untyped]]) -> void
+ def initialize: (?Array[LintOffense | Hash[untyped, untyped]]) -> void
+
+ # : () -> Integer
+ def errors: () -> Integer
+
+ # : () -> Integer
+ def warnings: () -> Integer
+
+ # : () -> Integer
+ def infos: () -> Integer
+
+ # : () -> Integer
+ def hints: () -> Integer
+
+ # : () -> Integer
+ def total_offenses: () -> Integer
+
+ # : () -> bool
+ def success?: () -> bool
+
+ # : () -> bool
+ def clean?: () -> bool
+
+ # : (String) -> Array[LintOffense]
+ def offenses_by_severity: (String) -> Array[LintOffense]
+
+ # : () -> Array[LintOffense]
+ def error_offenses: () -> Array[LintOffense]
+
+ # : () -> Array[LintOffense]
+ def warning_offenses: () -> Array[LintOffense]
+
+ # : () -> Array[LintOffense]
+ def info_offenses: () -> Array[LintOffense]
+
+ # : () -> Array[LintOffense]
+ def hint_offenses: () -> Array[LintOffense]
+
+ # : (String) -> Array[LintOffense]
+ def offenses_for_rule: (String) -> Array[LintOffense]
+
+ # : () -> Hash[Symbol, untyped]
+ def to_h: () -> Hash[Symbol, untyped]
+
+ # : (?untyped) -> String
+ def to_json: (?untyped) -> String
+
+ # : () -> String
+ def to_s: () -> String
+
+ # : (Hash[untyped, untyped]) -> LintResult
+ def self.from_hash: (Hash[untyped, untyped]) -> LintResult
+ end
+end
diff --git a/sig/herb/location.rbs b/sig/herb/location.rbs
index b10799f1d..97c85b376 100644
--- a/sig/herb/location.rbs
+++ b/sig/herb/location.rbs
@@ -15,9 +15,15 @@ module Herb
# : (Integer, Integer, Integer, Integer) -> Location
def self.[]: (Integer, Integer, Integer, Integer) -> Location
+ # : (Hash[untyped, untyped]|nil) -> Location?
+ def self.from_hash: (Hash[untyped, untyped] | nil) -> Location?
+
# : () -> serialized_location
def to_hash: () -> serialized_location
+ # : () -> serialized_location
+ def to_h: () -> serialized_location
+
# : (?untyped) -> String
def to_json: (?untyped) -> String
diff --git a/sig/herb/parse_result.rbs b/sig/herb/parse_result.rbs
index 6e0801f38..bbfbfb82b 100644
--- a/sig/herb/parse_result.rbs
+++ b/sig/herb/parse_result.rbs
@@ -19,7 +19,13 @@ module Herb
# : () -> String
def pretty_errors: () -> String
+ # : () -> String
+ def to_source: () -> String
+
# : (Visitor) -> void
def visit: (Visitor) -> void
+
+ # : (Hash[untyped, untyped]) -> ParseResult
+ def self.from_hash: (Hash[untyped, untyped]) -> ParseResult
end
end
diff --git a/sig/herb/position.rbs b/sig/herb/position.rbs
index cb0c33ee4..5583c72dc 100644
--- a/sig/herb/position.rbs
+++ b/sig/herb/position.rbs
@@ -15,9 +15,15 @@ module Herb
# : (Integer, Integer) -> Position
def self.from: (Integer, Integer) -> Position
+ # : (Hash[untyped, untyped]|nil) -> Position?
+ def self.from_hash: (Hash[untyped, untyped] | nil) -> Position?
+
# : () -> serialized_position
def to_hash: () -> serialized_position
+ # : () -> serialized_position
+ def to_h: () -> serialized_position
+
# : (?untyped) -> String
def to_json: (?untyped) -> String
diff --git a/sig/herb/range.rbs b/sig/herb/range.rbs
index 9b6d02947..a85bec433 100644
--- a/sig/herb/range.rbs
+++ b/sig/herb/range.rbs
@@ -15,6 +15,9 @@ module Herb
# : (Integer, Integer) -> Range
def self.from: (Integer, Integer) -> Range
+ # : (Array[untyped]|Hash[untyped, untyped]|nil) -> Range?
+ def self.from_hash: (Array[untyped] | Hash[untyped, untyped] | nil) -> Range?
+
# : () -> serialized_range
def to_a: () -> serialized_range
diff --git a/sig/herb/token.rbs b/sig/herb/token.rbs
index d29c75c06..6ef729567 100644
--- a/sig/herb/token.rbs
+++ b/sig/herb/token.rbs
@@ -16,6 +16,9 @@ module Herb
# : () -> serialized_token
def to_hash: () -> serialized_token
+ # : (Hash[untyped, untyped]?) -> Token?
+ def self.from_hash: (Hash[untyped, untyped]?) -> Token?
+
# : (?untyped) -> String
def to_json: (?untyped) -> String
diff --git a/sig/herb/warnings.rbs b/sig/herb/warnings.rbs
index 4da04d0eb..8096091b7 100644
--- a/sig/herb/warnings.rbs
+++ b/sig/herb/warnings.rbs
@@ -5,16 +5,19 @@ module Herb
class Warning
attr_reader type: String
- attr_reader location: Location
+ attr_reader location: Location?
attr_reader message: String
- # : (String, Location, String) -> void
- def initialize: (String, Location, String) -> void
+ # : (String, Location?, String) -> void
+ def initialize: (String, Location?, String) -> void
# : () -> serialized_warning
def to_hash: () -> serialized_warning
+ # : (Hash[untyped, untyped]) -> Warning
+ def self.from_hash: (Hash[untyped, untyped]) -> Warning
+
# : () -> String
def class_name: () -> String
diff --git a/sig/native_backend.rbs b/sig/native_backend.rbs
new file mode 100644
index 000000000..e2b71491b
--- /dev/null
+++ b/sig/native_backend.rbs
@@ -0,0 +1,17 @@
+# For methods dynamically defined when requiring the Ruby C-Extension
+#
+# See: ext/herb/native_backend.c
+
+module Herb
+ module Backends
+ class NativeBackend < Backend
+ def c_perform_lex: (String) -> untyped
+ def c_perform_lex_file: (String) -> untyped
+ def c_perform_parse: (String, Hash[untyped, untyped]) -> untyped
+ def c_perform_parse_file: (String, Hash[untyped, untyped]) -> untyped
+ def c_perform_extract_ruby: (String) -> untyped
+ def c_perform_extract_html: (String) -> untyped
+ def c_backend_version: () -> String?
+ end
+ end
+end
diff --git a/stargazers-2025-08-23.txt b/stargazers-2025-08-23.txt
new file mode 100644
index 000000000..f559f1094
--- /dev/null
+++ b/stargazers-2025-08-23.txt
@@ -0,0 +1,665 @@
+0exp
+0rientd
+0xD7ba952CE8A0976e8d9852b7649bf01c30146
+0xMostafa
+29decibel
+3colorr
+a-chris
+aaronvenezia
+Abdulwahaab710
+acetinick
+acidtib
+AdamFreemer
+admtnnr
+adrianthedev
+adrienpoly
+aergonaut
+afalkear
+afmicc
+afr114
+aglio
+AGNutGH
+agoos-nbcuni
+AhmadMajid
+ajsharp
+aki77
+albertico
+alec-c4
+alexgufler
+alextrueman
+alfakini
+alfetahe
+alfredoreduarte
+alperenbozkurt
+alphabetek
+alxbnct
+am1006
+amiralles
+anderson
+AndresGarciadelaHuerta
+andrew
+andrewhwaller
+andrewmcodes
+andrewnovykov
+andy4thehuynh
+andycandrea
+andyhennie
+anil-adepu
+anonosuke
+anthonyamar
+appyspaces
+aprotsyk
+Apupsis
+arturaspiksrys
+as181920
+asenkovskiy
+aspleenic
+astrocket
+atulvishw240
+auslach
+axcochrane
+ayb
+b08x
+badenkov
+baku01
+basilmeer
+baxter2
+bcgianni
+bdavidxyz
+bdougie
+BedeDD
+Belibaste
+ben-greenwood
+ben-gy
+bendangelo
+benguillet
+benjbush
+Benoit-Baumann
+benwalks
+Bergrebell
+bibendi
+BilalBudhani
+billabel
+billthompson
+binilsn
+bishalg
+bitshaker
+bjarkevad
+bkenny
+blasterpal
+bluengreen
+bluerssen
+bmichelsen
+bongole
+bonty
+bookingninja
+bradgessler
+bradpurchase
+brandonzylstra
+brendanwb10
+brentkearney
+bricolage
+brkn
+brossi
+brucruz
+bryanbeshore
+bryanmorrow
+BuffaloDrew
+bunnahabhain
+candland
+carlson
+cdaviis
+celina-lopez
+chadwilken
+champagnepappi
+charlimmelman
+CheckerFlow
+Ches-ctrl
+chokkoyamada
+chops
+chrisedington
+chriskjaer
+christiangenco
+christopherstyles
+chtzvt
+chubchenko
+chukitow
+ciscoLegrand
+codescaptain
+colinux
+crespire
+cs3b
+cuneyter
+cyril
+cyrilmayance
+czhu12
+czj
+damireh
+dangerousbeans
+Daniel-Brai
+danielabar
+danielpuglisi
+danielwestendorf
+danwetherald
+dapeng2018
+davebream
+davedkg
+davidemerli
+davidstump
+davidteren
+db0sch
+dbates0623
+dchuk
+deanpcmad
+defeed
+defifund
+defkode
+deivinsontejeda
+denjamio
+dennispaagman
+dev-psi
+Developerayo
+dfioretti
+diegofigueroa
+dineyw23
+dkaplan88
+dlysenko
+dmeremyanin
+DmitrySychev
+dongjinahn
+douglara
+doutatsu
+dpaluy
+DRBragg
+dreamingblackcat
+duckworth
+dwaynemac
+dyoshikawa
+e12e
+ebababi
+edwinwills
+egemen-dev
+eiskrenkov
+ekroon
+elalemanyo
+elarabyelaidy19
+eleazarer
+emanuelhfarias
+emaraschio
+embs
+emilford
+emilianodellacasa
+enprop
+epugh
+Eric-Guo
+ericpowell03
+ericrochford
+Erol
+erozas
+ersatzryan
+ervinismu
+estebarb
+etewiah
+Eth3rnit3
+Eunix
+evdevdev
+ewertoncodes
+fagnerpereira
+fdr
+felipefontoura
+ferrislucas
+fetijashari
+filipemendespi
+finbarr
+florianfynnweber
+flyerhzm
+fnmunhoz
+frabr
+fredoliveira
+friendlyantz
+Galathius
+gamead
+gando001
+garyhtou
+geetfun
+georgebaskervil
+georgebellos
+georgkreimer
+gessivamjr
+giedriusr
+gilmda
+gingermusketeer
+gitpkj
+gkosmo
+glory-mfv
+god4saken
+goofansu
+gregogalante
+gregoryfm
+grodowski
+guilhermeteodoro
+guizaols
+GyozaGuy
+hades0932
+hara-y-u
+harshalbhakta
+hayesr
+headius
+HenriqueRicardoFigueira
+henrydjacob
+heyitscoco
+hiasinho
+hiromitsusasaki
+hkf
+hoelmer
+hokageCV
+holamendi
+hossamamer
+howmuchcomputer
+hsbt
+huseyinbiyik
+iamcoreyg
+iaurg
+iFloris
+ignar
+igorvazz
+igrabes
+igrigorik
+ihoka
+Ikass
+ikraamg
+ineedjet
+intertwingle
+iravikhatri
+IrvanFza
+ismaels
+israeljrs
+ivobenedito
+ivy
+j-manu
+jake-duchesne
+janko
+janstol
+jaredsmithse
+jaryl
+jason-riddle
+jasonfantasia
+jasperfurniss
+JavaKoala
+javid-gulamaliyev
+jayelkaake
+jaygooby
+jbwl
+jcsanti
+jespr
+jess
+jgarber-cisco
+jhash
+Jholloway12
+jhuckabee
+jimjimovich
+jisuanjixue
+jjstafford
+jleo3
+jnettome
+johannesschobel
+johnlukeG
+jon-sully
+jonathanmdavies
+jonbaer
+JonCrawford
+JordanForeman
+JorgeDDW
+josefarias
+joshh45
+joshRpowell
+joshshifman
+joshuajansen
+JoungSik
+jpbalarini
+jrramon
+juandazapata
+juliends
+juliocabrera820
+juliusdelta
+juniortada
+justi
+jylamont
+kaspth
+kbuckler
+kcdragon
+kdmgs110
+kemallette
+kengreeff
+KeyAmam
+khalilgharbaoui
+khasinski
+kieranklaassen
+kierr
+kigster
+kimihito
+kirankarki
+kjf
+kjlape
+kladaFOX
+kmc79
+KodyKendall
+koke2y
+korakotlee
+kristinadd
+kristjanakkermann
+krongk
+krzkrzkrz
+kurioscreative
+kwent
+landrywj
+Largo
+lazybios
+lebedevilya
+leemcalilly
+leh
+leonmezu1
+lfv89
+lHydra
+LiamBateman
+llenodo
+localops-root
+Lokideos
+LordoftheManor7
+lorismaz
+loumarven
+luiscobot
+lululau
+lundie
+mabras
+mabutou
+madddybit
+magnum
+MaherSaif
+manuelmeurer
+marclerodrigues
+marcoroth
+markets
+MarkSurfas
+masonhensley
+master-of-null
+masterT
+mathieubrunpicard
+mattparlane
+MattRogish
+mattwigham
+mattzollinhofer
+maxhungry
+mcdonald-conor
+mcusatech
+mdomga
+meg-gutshall
+memon
+meneerprins
+Michael9311
+michaelroudnitski
+michaelwapp
+mikeastock
+mikekosulin
+mikeletscher
+mikereczek
+mikezaby
+mikker
+MiltonRen
+MindRave
+mintuhouse
+MiroslavCsonka
+mitchthebaker
+mjberg
+mnort9
+moneill
+Morganjackson
+MostafaAdly
+mpressen
+mqzhang
+mtrefilek
+musik
+n-at-han-k
+n00bvn
+namiwang
+namuit
+natanaelmaia
+nathanfishcircleci
+nathanpalmer
+navjeetc
+nbluis
+nemuba
+neocoin
+nertzy
+netzfisch
+neutrino
+newdogcow
+ngpestelos
+nicieja
+nickisnoble
+nicogaldamez
+nicolangr
+niculistana
+nik
+niklasbabel
+Nittarab
+No-YE
+nobodyiscertain
+Novtopro
+nrevko
+Nuzair46
+nwarwick
+ocarreterom
+olimart
+olinelson
+omarluq
+onk
+Orphist
+osjjames
+OskarsEzerins
+osmanok
+otrub
+oztofer
+pablomarti
+paderinandrey
+panckreous
+patvice
+paulhenri-l
+PauloPhagula
+paultursuru
+pfeiffer
+phuwanart
+pikitgb
+pilgunboris
+PiotrMocan
+piscespieces
+pkananen
+plabaj
+plattenschieber
+playcase217
+postpostmodern
+pranavbabu
+preetpalS
+princetechs
+psylone
+qen
+qinmingyuan
+raderj89
+raguila8
+railsdev
+rajraj
+ralder
+rancar2
+randynov
+rapcal
+raphaelivan
+ravinderrana
+raysapida
+relyks
+renehernandez
+renshuki
+revans
+rhiannon-io
+ribeirojose
+RichardBlair
+ricsdeol
+rince
+riyaadh-abrahams
+rockwellll
+rogeriomarques
+roma-glushko
+RomanHood
+ronb54
+Roriz
+roshanabey2
+rpanachi
+rslhdyt
+rstrangh
+ryanmaynard
+rzkmr
+S-H-GAMELINKS
+safakferhatkaya
+samuelcouch
+samuelgiles
+sanchal-coder
+Sanchezdav
+sarahg423
+sarbadaj
+satotakumi
+say2memohitverma
+schappim
+schmidie
+Schwad
+scicco
+ScotterC
+scottwhudson
+seanhogge
+SeanLF
+SebastianSzturo
+Sebhastien
+SegundoRP
+sergii
+servatti
+SerylLns
+ShawnAukstak
+shenaor
+shkm
+shouya31
+shreyas-makes
+shunhikita
+siegbenn
+simon-marks-tcb
+simonknoll
+Sinhyeok
+sirwolfgang
+sisodiaa
+skelz0r
+Skullamortis
+smathieu
+spacechurro
+spacepolice10
+SrAnthony
+stancheta
+stefanvermaas
+steveclarke
+stevegeek
+StevenJL
+stlucasgarcia
+stream7
+strzibny
+stuyam
+su-sh
+suaron
+subelsky
+sugi511
+suho
+SujayPrabhu96
+sunenilausen
+suras
+swinner2
+tabishiqbal
+talltorp
+taltas
+tamersalama
+tanukiti1987
+tarakish
+tarellel
+taylorbryant
+tehnuge
+tgezginis
+TheCovenant
+thedarkside
+thedayisntgray
+thedumbtechguy
+theill
+therealadam
+TheRealNeil
+theSteveMitchell
+thiagopecanha
+thisIsLoading
+thisugee
+thomasgalibert
+thomasklemm
+thomaswitt
+Thrry
+tiknaosman
+timsco
+todddickerson
+toddkummer
+tomasmuller
+TonsOfFun
+tonymastrorio
+topher6345
+tostca
+tpaulshippy
+trevorturk
+tristanoneil
+truongduyng
+tvergara
+ujackson
+umairashas
+umbrella-h
+unabris
+Uysim
+varun2407
+versun
+viamin
+victor-github
+viniciusmeneses
+vishvish
+visini
+volpeo
+vormwald
+vsppedro
+vsrnth
+vtno
+w3villa-deepanshu
+WADI31dev
+wagnerpereira
+webmatze
+websymphony
+widget501
+williamherry
+williamkennedy
+wlaurance
+wolfgang555
+wooyakob
+wowinter13
+xkraty
+xleotranx
+xzgyb
+yangtheman
+yaroslav
+yatish27
+yinho999
+yjacquin
+ykpythemind
+yogadevs
+YuheiNakasaka
+zackgilbert
+zhephyn
+znuamaan
+zokioki
+ZPVIP
+zumkorn
+zzJZzz
diff --git a/templates/lib/herb/ast/nodes.rb.erb b/templates/lib/herb/ast/nodes.rb.erb
index e51d71902..97ee648c8 100644
--- a/templates/lib/herb/ast/nodes.rb.erb
+++ b/templates/lib/herb/ast/nodes.rb.erb
@@ -1,12 +1,30 @@
module Herb
module AST
+ # Dynamically reopen Node class to add node_from_hash method with knowledge of all node types
+ class Node
+ #: (Hash[untyped, untyped]?) -> Node?
+ def self.node_from_hash(data)
+ return nil unless data
+
+ node_type = data[:type] || data["type"]
+ return nil unless node_type
+
+ case node_type
+ <%- nodes.each do |node| -%>
+ when "<%= node.type %>"
+ <%= node.name %>.from_hash(data)
+ <%- end -%>
+ end
+ end
+ end
+
<%- nodes.each do |node| -%>
class <%= node.name -%> < Node
<%- node.fields.each do |field| -%>
attr_reader :<%= field.name %> #: <%= field.ruby_type %>
<%- end -%>
- #: (<%= ["String", "Location", "Array[Herb::Errors::Error]", *node.fields.map(&:ruby_type)].join(", ") %>) -> void
+ #: (<%= ["String", "Location?", "Array[Herb::Errors::Error]", *node.fields.map(&:ruby_type)].join(", ") %>) -> void
def initialize(<%= ["type", "location", "errors", *node.fields.map(&:name)].join(", ") %>)
super(type, location, errors)
<%- node.fields.each do |field| -%>
@@ -22,11 +40,62 @@ module Herb
def to_hash
super.merge({
<%- node.fields.each do |field| -%>
+ <%- case field -%>
+ <%- when Herb::Template::NodeField -%>
+ <%= field.name %>: <%= field.name %>&.to_hash,
+ <%- when Herb::Template::ArrayField -%>
+ <%- if field.specific_kind&.end_with?("Node") -%>
+ <%= field.name %>: <%= field.name %>.map(&:to_hash),
+ <%- else -%>
<%= field.name %>: <%= field.name %>,
<%- end -%>
+ <%- when Herb::Template::TokenField -%>
+ <%= field.name %>: <%= field.name %>&.to_hash,
+ <%- else -%>
+ <%= field.name %>: <%= field.name %>,
+ <%- end -%>
+ <%- end -%>
}) #: Herb::serialized_<%= node.human %>
end
+ #: (Hash[untyped, untyped]) -> <%= node.name %>
+ def self.from_hash(data)
+ location = Location.from_hash(data[:location] || data["location"])
+ errors = (data[:errors] || data["errors"] || []).map { |error| Herb::Errors::Error.from_hash(error) }
+ <%- node.fields.each do |field| -%>
+ <%- case field -%>
+ <%- when Herb::Template::StringField -%>
+ <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || ""
+ <%- when Herb::Template::TokenField -%>
+ <%= field.name %> = data[:<%= field.name %>] ? Herb::Token.from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil
+ <%- when Herb::Template::BooleanField -%>
+ <%= field.name %> = if data.key?(:<%= field.name %>)
+ data[:<%= field.name %>]
+ else
+ data.key?("<%= field.name %>") ? data["<%= field.name %>"] : false
+ end
+ <%- when Herb::Template::NodeField -%>
+ <%- if field.specific_kind -%>
+ <%= field.name %>_node = data[:<%= field.name %>] ? Herb::AST::Node.node_from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil
+ <%= field.name %> = <%= field.name %>_node #: Herb::AST::<%= field.specific_kind %>?
+ <%- else -%>
+ <%= field.name %> = data[:<%= field.name %>] ? Herb::AST::Node.node_from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil
+ <%- end -%>
+ <%- when Herb::Template::ArrayField -%>
+ <%- if field.specific_kind&.end_with?("Node") -%>
+ <%= field.name %>_nodes = (data[:<%= field.name %>] || data["<%= field.name %>"] || []).map { |node_data| Herb::AST::Node.node_from_hash(node_data) }.compact
+ <%= field.name %> = <%= field.name %>_nodes #: Array[Herb::AST::<%= field.specific_kind %>]
+ <%- else -%>
+ <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || []
+ <%- end -%>
+ <%- else -%>
+ <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"]
+ <%- end -%>
+ <%- end -%>
+
+ new("<%= node.type %>", location, errors, <%= node.fields.map(&:name).join(", ") %>)
+ end
+
#: (Visitor) -> void
def accept(visitor)
visitor.visit_<%= node.human %>(self)
@@ -60,7 +129,7 @@ module Herb
output = +""
output += "@ #{node_name} "
- output += location.tree_inspect
+ output += location&.tree_inspect || "(?)"
output += "\n"
output += inspect_errors(prefix: "<%= node.fields.any? ? "│ " : " " %>")
diff --git a/templates/lib/herb/errors.rb.erb b/templates/lib/herb/errors.rb.erb
index cc750f2fa..31ae943c9 100644
--- a/templates/lib/herb/errors.rb.erb
+++ b/templates/lib/herb/errors.rb.erb
@@ -1,4 +1,4 @@
-<%- base_arguments = [["type", "String"], ["location", "Location"], ["message", "String"]] -%>
+<%- base_arguments = [["type", "String"], ["location", "Location?"], ["message", "String"]] -%>
module Herb
module Errors
class Error
@@ -22,6 +22,22 @@ module Herb
}
end
+ #: (Hash[untyped, untyped]) -> Error
+ def self.from_hash(data)
+ type = data[:type] || data["type"] || ""
+ location = Location.from_hash(data[:location] || data["location"])
+ message = data[:message] || data["message"] || ""
+
+ case type
+ <%- errors.each do |error| -%>
+ when "<%= error.type %>"
+ <%= error.name %>.from_hash(data)
+ <%- end -%>
+ else
+ new(type, location, message)
+ end
+ end
+
#: () -> String
def class_name
self.class.name || "Error"
@@ -72,11 +88,30 @@ module Herb
}) #: Herb::serialized_<%= error.human %>
end
+ #: (Hash[untyped, untyped]) -> <%= error.name %>
+ def self.from_hash(data)
+ type = data[:type] || data["type"] || ""
+ location = Location.from_hash(data[:location] || data["location"])
+ message = data[:message] || data["message"] || ""
+ <%- error.fields.each do |field| -%>
+ <%- case field -%>
+ <%- when Herb::Template::StringField -%>
+ <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || ""
+ <%- when Herb::Template::TokenField -%>
+ <%= field.name %> = data[:<%= field.name %>] ? Herb::Token.from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil
+ <%- else -%>
+ <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"]
+ <%- end -%>
+ <%- end -%>
+
+ new(type, location, message, <%= error.fields.map(&:name).join(", ") %>)
+ end
+
#: (?Integer) -> String
def tree_inspect(indent = 0)
output = +""
- output += %(@ #{error_name} #{location.tree_inspect}\n)
+ output += %(@ #{error_name} #{location&.tree_inspect || "?"}\n)
<%- symbol = error.fields.none? ? "└──" : "├──" -%>
output += %(<%= symbol %> message: #{message.inspect}\n)
<%- error.fields.each do |field| -%>
diff --git a/templates/template.rb b/templates/template.rb
index 4029554bd..7ad8251e5 100755
--- a/templates/template.rb
+++ b/templates/template.rb
@@ -65,7 +65,7 @@ def c_type
end
def ruby_type
- "Herb::AST::#{specific_kind || "Node"}"
+ "Herb::AST::#{specific_kind || "Node"}?"
end
def specific_kind
@@ -79,7 +79,7 @@ def union_kind
class TokenField < Field
def ruby_type
- "Herb::Token"
+ "Herb::Token?"
end
def c_type
diff --git a/test/backend_format_lint_test.rb b/test/backend_format_lint_test.rb
new file mode 100644
index 000000000..5c655f088
--- /dev/null
+++ b/test/backend_format_lint_test.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class BackendFormatLintTest < Minitest::Test
+ def setup
+ @template = <<~ERB.strip
+
+ <% if user.logged_in? %>
+
Welcome, <%= user.name %>!
+ <% end %>
+
+ ERB
+
+ @messy_template = <<~ERB.strip
+
+ <%if user.logged_in?%>
+
Welcome, <%= user.name %>!
+ <%end%>
+
+ ERB
+ end
+
+ def test_native_backend_format_uses_default_node_backend
+ Herb.switch_backend(:native)
+
+ skip "Node backend not available" unless node_backend_available?
+
+ result = Herb.format(@messy_template)
+ assert_instance_of String, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+
+ def test_native_backend_lint_uses_default_node_backend
+ Herb.switch_backend(:native)
+
+ skip "Node backend not available" unless node_backend_available?
+
+ result = Herb.lint(@template)
+ assert_instance_of Herb::LintResult, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+
+ def test_native_backend_format_file_uses_default_node_backend
+ require "tempfile"
+
+ Herb.switch_backend(:native)
+ skip "Node backend not available" unless node_backend_available?
+
+ Tempfile.create(["test", ".html.erb"]) do |file|
+ file.write(@messy_template)
+ file.flush
+
+ result = Herb.format_file(file.path)
+ assert_instance_of String, result
+ end
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+
+ def test_native_backend_lint_file_uses_default_node_backend
+ require "tempfile"
+
+ Herb.switch_backend(:native)
+ skip "Node backend not available" unless node_backend_available?
+
+ Tempfile.create(["test", ".html.erb"]) do |file|
+ file.write(@template)
+ file.flush
+
+ result = Herb.lint_file(file.path)
+ assert_instance_of Herb::LintResult, result
+ end
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+
+ def test_node_backend_format_functionality
+ skip "Node backend not available" unless node_backend_available?
+
+ Herb.switch_backend(:node)
+
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :format_file
+
+ begin
+ result = Herb.format(@messy_template)
+ assert_instance_of String, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+ end
+
+ def test_node_backend_lint_functionality
+ skip "Node backend not available" unless node_backend_available?
+
+ Herb.switch_backend(:node)
+
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb, :lint_file
+
+ begin
+ result = Herb.lint(@template)
+ assert_instance_of Herb::LintResult, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+ end
+
+ def test_format_and_lint_methods_exist_on_available_backends
+ [:native, :node].each do |backend_name|
+ next unless backend_available?(backend_name)
+
+ Herb.switch_backend(backend_name)
+
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :format_file
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb, :lint_file
+
+ current_backend = Herb.backend(backend_name)
+ assert_respond_to current_backend, :format
+ assert_respond_to current_backend, :format_file
+ assert_respond_to current_backend, :lint
+ assert_respond_to current_backend, :lint_file
+ end
+ end
+
+ private
+
+ def backend_available?(backend_name)
+ case backend_name
+ when :native
+ true
+ when :node
+ node_backend_available?
+ else
+ false
+ end
+ end
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/backend_system_test.rb b/test/backend_system_test.rb
new file mode 100644
index 000000000..f9187ea81
--- /dev/null
+++ b/test/backend_system_test.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class BackendSystemTest < Minitest::Test
+ def setup
+ @original_backend = Herb.current_backend
+ end
+
+ def teardown
+ Herb.switch_backend(@original_backend) if @original_backend
+ end
+
+ def test_herb_has_format_and_lint_methods
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :format_file
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb, :lint_file
+ end
+
+ def test_backend_abstract_methods_are_defined
+ backend = Herb::Backend.new
+ backend.instance_variable_set(:@loaded, true)
+
+ assert_raises(NotImplementedError) { backend.format("test", {}) }
+ assert_raises(NotImplementedError) { backend.lint("test", {}) }
+ end
+
+ def test_backend_format_and_lint_methods_exist
+ backend = Herb::Backend.new
+
+ assert_respond_to backend, :format
+ assert_respond_to backend, :format_file
+ assert_respond_to backend, :lint
+ assert_respond_to backend, :lint_file
+ end
+
+ def test_backend_format_and_lint_require_loaded_backend
+ backend = Herb::Backend.new
+
+ error = assert_raises(RuntimeError) do
+ backend.format("test")
+ end
+
+ assert_includes error.message, "Backend not loaded"
+
+ error = assert_raises(RuntimeError) do
+ backend.lint("test")
+ end
+
+ assert_includes error.message, "Backend not loaded"
+ end
+
+ def test_native_backend_has_helpful_error_messages
+ error = assert_raises(NotImplementedError) do
+ Herb.format("test", backend: :native)
+ end
+
+ assert_includes error.message, "native backend"
+ assert_includes error.message, "Node backend"
+ assert_includes error.message, "switch_backend(:node)"
+
+ error = assert_raises(NotImplementedError) do
+ Herb.lint("test", backend: :native)
+ end
+
+ assert_includes error.message, "native backend"
+ assert_includes error.message, "Node backend"
+ assert_includes error.message, "switch_backend(:node)"
+ end
+
+ def test_format_file_reads_from_filesystem
+ require "tempfile"
+
+ Tempfile.create(["test", ".html.erb"]) do |file|
+ file.write("")
+ file.flush
+
+ error = assert_raises(NotImplementedError) do
+ Herb.format_file(file.path, backend: :native)
+ end
+
+ assert_includes error.message, "Formatting is not implemented"
+ refute_includes error.message.downcase, "file"
+ refute_includes error.message.downcase, "read"
+ end
+ end
+
+ def test_lint_file_reads_from_filesystem
+ require "tempfile"
+
+ Tempfile.create(["test", ".html.erb"]) do |file|
+ file.write("")
+ file.flush
+
+ error = assert_raises(NotImplementedError) do
+ Herb.lint_file(file.path, backend: :native)
+ end
+
+ assert_includes error.message, "Linting is not implemented"
+ refute_includes error.message.downcase, "file"
+ refute_includes error.message.downcase, "read"
+ end
+ end
+
+ private
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/cli_test.rb b/test/cli_test.rb
new file mode 100644
index 000000000..a8884fd42
--- /dev/null
+++ b/test/cli_test.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+require "stringio"
+
+class CLITest < Minitest::Test
+ def setup
+ @cli_class = Herb::CLI
+ @original_stdout = $stdout
+ @original_stderr = $stderr
+ $stdout = StringIO.new
+ $stderr = StringIO.new
+ end
+
+ def teardown
+ $stdout = @original_stdout
+ $stderr = @original_stderr
+ end
+
+ def test_cli_class_exists
+ assert_equal Herb::CLI, @cli_class
+ end
+
+ def test_cli_has_new_attributes
+ cli = @cli_class.new([])
+
+ assert_respond_to cli, :indent_width
+ assert_respond_to cli, :indent_width=
+ assert_respond_to cli, :max_line_length
+ assert_respond_to cli, :max_line_length=
+ end
+
+ def test_help_includes_new_commands
+ cli = @cli_class.new(["help"])
+
+ cli.call
+
+ assert_respond_to cli, :help
+ end
+
+ def test_option_parser_includes_new_options
+ cli = @cli_class.new([])
+ parser = cli.option_parser
+
+ parser_help = parser.to_s
+
+ assert_includes parser_help, "--indent-width"
+ assert_includes parser_help, "--max-line-length"
+ end
+
+ def test_format_command_handler_exists
+ cli = @cli_class.new(["format"])
+
+ assert_includes cli.private_methods, :handle_format_command
+ end
+
+ def test_lint_command_handler_exists
+ cli = @cli_class.new(["lint"])
+
+ assert_includes cli.private_methods, :handle_lint_command
+ end
+
+ def test_format_and_lint_commands_in_help_text
+ @cli_class.new([])
+
+ require "tempfile"
+
+ Tempfile.create(["test", ".html.erb"]) do |file|
+ file.write("Hello World
")
+ file.flush
+
+ format_cli = @cli_class.new(["format", file.path])
+ assert_equal "format", format_cli.instance_variable_get(:@command)
+
+ lint_cli = @cli_class.new(["lint", file.path])
+ assert_equal "lint", lint_cli.instance_variable_get(:@command)
+ end
+ end
+
+ def test_indent_width_option_parsing
+ cli = @cli_class.new(["format", "test.erb", "--indent-width", "4"])
+ cli.options
+
+ assert_equal 4, cli.indent_width
+ end
+
+ def test_formatting_options_parsing
+ cli = @cli_class.new(["format", "test.erb", "--indent-width", "4", "--max-line-length", "120"])
+ cli.options
+
+ assert_equal 4, cli.indent_width
+ assert_equal 120, cli.max_line_length
+ end
+end
diff --git a/test/complete_format_lint_test.rb b/test/complete_format_lint_test.rb
new file mode 100644
index 000000000..b58c1e988
--- /dev/null
+++ b/test/complete_format_lint_test.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class CompleteFormatLintTest < Minitest::Test
+ def test_format_and_lint_functionality_summary
+ assert defined?(Herb::Diagnostic)
+ assert defined?(Herb::LintOffense)
+ assert defined?(Herb::LintResult)
+
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :format_file
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb, :lint_file
+
+ diagnostic = Herb::Diagnostic.new(
+ message: "Test diagnostic",
+ location: create_test_location,
+ severity: "error"
+ )
+
+ assert_instance_of Herb::Diagnostic, diagnostic
+ assert diagnostic.error?
+
+ lint_offense = Herb::LintOffense.new(
+ message: "Test lint offense",
+ location: create_test_location,
+ severity: "warning",
+ rule: "test-rule"
+ )
+
+ assert_instance_of Herb::LintOffense, lint_offense
+ assert_kind_of Herb::Diagnostic, lint_offense
+ assert lint_offense.warning?
+ assert_equal "test-rule", lint_offense.rule
+ end
+
+ def test_lint_result_integration
+ error_offense = Herb::LintOffense.new(
+ message: "Missing alt attribute",
+ location: create_test_location,
+ severity: "error",
+ rule: "html-img-require-alt"
+ )
+
+ warning_offense = Herb::LintOffense.new(
+ message: "Consider semantic HTML",
+ location: create_test_location,
+ severity: "warning",
+ rule: "html-semantic"
+ )
+
+ lint_result = Herb::LintResult.new([error_offense, warning_offense])
+
+ assert_equal 2, lint_result.total_offenses
+ assert_equal 1, lint_result.errors
+ assert_equal 1, lint_result.warnings
+ assert_equal 0, lint_result.infos
+ assert_equal 0, lint_result.hints
+
+ refute lint_result.success?
+ refute lint_result.clean?
+
+ assert_equal 1, lint_result.error_offenses.length
+ assert_equal 1, lint_result.warning_offenses.length
+ assert_equal 1, lint_result.offenses_for_rule("html-img-require-alt").length
+
+ hash = lint_result.to_h
+ assert_instance_of Hash, hash
+ assert_equal 2, hash[:total_offenses]
+
+ json = lint_result.to_json
+ assert_instance_of String, json
+ assert_includes json, "Missing alt attribute"
+
+ string_output = lint_result.to_s
+ assert_includes string_output, "2 lint offenses"
+ assert_includes string_output, "1 errors"
+ assert_includes string_output, "1 warnings"
+ end
+
+ def test_backend_integration_with_lint_result
+ original_backend = Herb.current_backend
+ skip "Node backend not available" unless node_backend_available?
+
+ begin
+ Herb.switch_backend(:native)
+
+ result = Herb.lint("")
+
+ assert_instance_of Herb::LintResult, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ def test_javascript_data_conversion
+ offense_data = {
+ message: "Missing alt attribute",
+ severity: "error",
+ rule: "html-img-require-alt",
+ code: "E001",
+ location: {
+ start: { line: 1, column: 5 },
+ end: { line: 1, column: 20 },
+ },
+ }
+
+ offense = Herb::LintOffense.from_hash(offense_data)
+
+ assert_equal "Missing alt attribute", offense.message
+ assert_equal "error", offense.severity
+ assert_equal "html-img-require-alt", offense.rule
+ assert_equal "E001", offense.code
+ assert_equal 1, offense.location.start.line
+ assert_equal 5, offense.location.start.column
+
+ result_data = {
+ offenses: [offense_data],
+ }
+
+ result = Herb::LintResult.from_hash(result_data)
+
+ assert_equal 1, result.total_offenses
+ assert_equal 1, result.errors
+ assert_equal 0, result.warnings
+ refute result.success?
+ end
+
+ private
+
+ def create_test_location
+ start_pos = Herb::Position.new(1, 5)
+ end_pos = Herb::Position.new(1, 15)
+ Herb::Location.new(start_pos, end_pos)
+ end
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/cross_backend_test.rb b/test/cross_backend_test.rb
new file mode 100644
index 000000000..41bec1e00
--- /dev/null
+++ b/test/cross_backend_test.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class CrossBackendTest < Minitest::Test
+ def test_backend_method_gets_specific_backend
+ native_backend = Herb.backend(:native)
+ assert_instance_of Herb::Backends::NativeBackend, native_backend
+ assert native_backend.loaded?
+
+ current = Herb.current_backend
+ Herb.backend(:native)
+ assert_equal current, Herb.current_backend
+ end
+
+ def test_backend_kwarg_support
+ template = "Hello World
"
+
+ Herb.parse(template, backend: :native)
+ result = Herb.parse(template)
+
+ assert result.value.respond_to?(:to_source) if result.success?
+
+ assert true
+ end
+
+ def test_backend_method_access
+ native_backend = Herb.backend(:native)
+ assert_instance_of Herb::Backends::NativeBackend, native_backend
+
+ current = Herb.current_backend
+ Herb.backend(:native)
+
+ assert_equal current, Herb.current_backend
+ end
+
+ def test_cross_backend_usage_with_kwarg
+ skip "Node backend dependencies not available" unless node_backend_available?
+
+ original_backend = Herb.current_backend
+
+ begin
+ Herb.switch_backend(:native)
+ template = "HelloWorld
"
+ result = Herb.parse(template)
+
+ assert result.success?
+ assert_equal "native", Herb.current_backend
+
+ formatted_output = Herb.format(template, backend: :node)
+ lint_result = Herb.lint(template, backend: :node)
+
+ node_formatted = result.value.to_source(backend: :node)
+ node_identity = result.value.to_source(backend: :node)
+
+ assert_equal "native", Herb.current_backend
+
+ assert_instance_of String, formatted_output
+ assert_instance_of Herb::LintResult, lint_result
+ assert_instance_of String, node_formatted
+ assert_instance_of String, node_identity
+
+ assert_includes formatted_output, "Hello"
+ assert_includes node_formatted, "Hello"
+ assert_includes node_identity, "Hello"
+ rescue Nodo::DependencyError
+ skip "Node.js packages not installed"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ def test_backend_kwarg_functionality
+ skip "Node backend dependencies not available" unless node_backend_available?
+
+ original_backend = Herb.current_backend
+
+ begin
+ Herb.switch_backend(:native)
+ template = "MessyHTML
"
+
+ formatted = Herb.format(template, backend: :node)
+ lint_result = Herb.lint(template, backend: :node)
+ parsed = Herb.parse(template, backend: :native)
+
+ assert_instance_of String, formatted
+ assert_instance_of Herb::LintResult, lint_result
+ assert_instance_of Herb::ParseResult, parsed
+
+ assert_equal "native", Herb.current_backend
+ rescue Nodo::DependencyError
+ skip "Node.js packages not installed"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ private
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/format_lint_summary_test.rb b/test/format_lint_summary_test.rb
new file mode 100644
index 000000000..3c0aad777
--- /dev/null
+++ b/test/format_lint_summary_test.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class FormatLintSummaryTest < Minitest::Test
+ def test_format_and_lint_methods_exist
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :format_file
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb, :lint_file
+ end
+
+ def test_format_lint_use_node_backend_by_default
+ Herb.switch_backend(:native)
+
+ skip "Node backend not available" unless node_backend_available?
+
+ begin
+ result = Herb.format("")
+ assert_instance_of String, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+
+ begin
+ result = Herb.lint("")
+ assert_instance_of Herb::LintResult, result
+ rescue StandardError => e
+ assert_instance_of StandardError, e
+ end
+ end
+
+ def test_available_backends_returns_available_list
+ backends = Herb.available_backends
+
+ assert_instance_of Array, backends
+ assert_includes backends, :native
+
+ return unless node_backend_available?
+
+ assert_includes backends, :node
+ end
+
+ def test_node_backend_methods_exist_when_available
+ skip "Node backend dependencies not available" unless node_backend_available?
+
+ begin
+ Herb.switch_backend(:node)
+
+ assert_respond_to Herb, :format
+ assert_respond_to Herb, :lint
+ assert_respond_to Herb.backend(:node), :format
+ assert_respond_to Herb.backend(:node), :lint
+ rescue Nodo::DependencyError, StandardError
+ skip "Node packages not installed, but backend switching worked"
+ end
+ end
+
+ def test_multi_backend_format_lint_behavior
+ original_backend = Herb.current_backend
+
+ skip "Node backend not available" unless node_backend_available?
+
+ begin
+ Herb.switch_backend(:native)
+
+ result = Herb.format("")
+ assert_instance_of String, result
+
+ result = Herb.lint("")
+ assert_instance_of Herb::LintResult, result
+
+ Herb.switch_backend(:node)
+
+ result = Herb.format("")
+ assert_instance_of String, result
+
+ result = Herb.lint("")
+ assert_instance_of Herb::LintResult, result
+ rescue Nodo::DependencyError
+ skip "Node backend available but packages not installed"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ private
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/lint_result_test.rb b/test/lint_result_test.rb
new file mode 100644
index 000000000..03baf149e
--- /dev/null
+++ b/test/lint_result_test.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class LintResultTest < Minitest::Test
+ def setup
+ @position = Herb::Position.new(1, 5)
+ @location = Herb::Location.new(@position, Herb::Position.new(1, 15))
+
+ @error_offense = Herb::LintOffense.new(
+ message: "Missing alt attribute",
+ location: @location,
+ severity: "error",
+ rule: "html-img-require-alt"
+ )
+
+ @warning_offense = Herb::LintOffense.new(
+ message: "Consider using semantic HTML",
+ location: @location,
+ severity: "warning",
+ rule: "html-semantic-elements"
+ )
+ end
+
+ def test_diagnostic_creation
+ diagnostic = Herb::Diagnostic.new(
+ message: "Test message",
+ location: @location,
+ severity: "error",
+ code: "E001",
+ source: "test"
+ )
+
+ assert_equal "Test message", diagnostic.message
+ assert_equal @location, diagnostic.location
+ assert_equal "error", diagnostic.severity
+ assert_equal "E001", diagnostic.code
+ assert_equal "test", diagnostic.source
+ assert diagnostic.error?
+ refute diagnostic.warning?
+ end
+
+ def test_diagnostic_severity_validation
+ error = assert_raises(ArgumentError) do
+ Herb::Diagnostic.new(
+ message: "Test",
+ location: @location,
+ severity: "invalid"
+ )
+ end
+
+ assert_includes error.message, "Invalid severity"
+ assert_includes error.message, "error, warning, info, hint"
+ end
+
+ def test_lint_offense_creation
+ offense = Herb::LintOffense.new(
+ message: "Test offense",
+ location: @location,
+ severity: "warning",
+ rule: "test-rule"
+ )
+
+ assert_equal "Test offense", offense.message
+ assert_equal "warning", offense.severity
+ assert_equal "test-rule", offense.rule
+ assert_equal "linter", offense.source
+ assert offense.warning?
+ end
+
+ def test_lint_offense_from_hash
+ data = {
+ message: "Missing alt attribute",
+ severity: "error",
+ rule: "html-img-require-alt",
+ location: {
+ start: { line: 1, column: 5 },
+ end: { line: 1, column: 15 },
+ },
+ }
+
+ offense = Herb::LintOffense.from_hash(data)
+
+ assert_equal "Missing alt attribute", offense.message
+ assert_equal "error", offense.severity
+ assert_equal "html-img-require-alt", offense.rule
+ assert_equal 1, offense.location.start.line
+ assert_equal 5, offense.location.start.column
+ end
+
+ def test_lint_offense_from_hash_with_minimal_data
+ data = {
+ message: "Test message",
+ }
+
+ offense = Herb::LintOffense.from_hash(data)
+
+ assert_equal "Test message", offense.message
+ assert_equal "error", offense.severity
+ assert_equal "unknown", offense.rule
+ assert_equal 1, offense.location.start.line
+ end
+
+ def test_lint_result_creation
+ result = Herb::LintResult.new([@error_offense, @warning_offense])
+
+ assert_equal 2, result.total_offenses
+ assert_equal 1, result.errors
+ assert_equal 1, result.warnings
+ assert_equal 0, result.infos
+ assert_equal 0, result.hints
+
+ refute result.success?
+ refute result.clean?
+ end
+
+ def test_lint_result_empty
+ result = Herb::LintResult.new([])
+
+ assert_equal 0, result.total_offenses
+ assert_equal 0, result.errors
+ assert_equal 0, result.warnings
+
+ assert result.success?
+ assert result.clean?
+ end
+
+ def test_lint_result_success_with_warnings
+ result = Herb::LintResult.new([@warning_offense])
+
+ assert_equal 1, result.total_offenses
+ assert_equal 0, result.errors
+ assert_equal 1, result.warnings
+
+ assert result.success?
+ refute result.clean?
+ end
+
+ def test_lint_result_offense_filtering
+ info_offense = Herb::LintOffense.new(
+ message: "Info message",
+ location: @location,
+ severity: "info",
+ rule: "info-rule"
+ )
+
+ result = Herb::LintResult.new([@error_offense, @warning_offense, info_offense])
+
+ assert_equal 1, result.error_offenses.length
+ assert_equal 1, result.warning_offenses.length
+ assert_equal 1, result.info_offenses.length
+ assert_equal 0, result.hint_offenses.length
+
+ assert_equal @error_offense, result.error_offenses.first
+ assert_equal @warning_offense, result.warning_offenses.first
+ end
+
+ def test_lint_result_offense_by_rule
+ other_error = Herb::LintOffense.new(
+ message: "Another error",
+ location: @location,
+ severity: "error",
+ rule: "html-img-require-alt"
+ )
+
+ result = Herb::LintResult.new([@error_offense, @warning_offense, other_error])
+
+ img_offenses = result.offenses_for_rule("html-img-require-alt")
+ assert_equal 2, img_offenses.length
+
+ semantic_offenses = result.offenses_for_rule("html-semantic-elements")
+ assert_equal 1, semantic_offenses.length
+
+ nonexistent_offenses = result.offenses_for_rule("nonexistent-rule")
+ assert_equal 0, nonexistent_offenses.length
+ end
+
+ def test_lint_result_from_hash
+ data = {
+ offenses: [
+ {
+ message: "Missing alt attribute",
+ severity: "error",
+ rule: "html-img-require-alt",
+ location: {
+ start: { line: 1, column: 5 },
+ end: { line: 1, column: 15 },
+ },
+ },
+ {
+ message: "Consider semantic HTML",
+ severity: "warning",
+ rule: "html-semantic-elements",
+ }
+ ],
+ }
+
+ result = Herb::LintResult.from_hash(data)
+
+ assert_equal 2, result.total_offenses
+ assert_equal 1, result.errors
+ assert_equal 1, result.warnings
+
+ error_offense = result.error_offenses.first
+
+ assert_equal "Missing alt attribute", error_offense.message
+ assert_equal "html-img-require-alt", error_offense.rule
+ end
+
+ def test_lint_result_to_s
+ clean_result = Herb::LintResult.new([])
+ assert_equal "✓ No lint offenses found", clean_result.to_s
+
+ result = Herb::LintResult.new([@error_offense, @warning_offense])
+ result_string = result.to_s
+
+ assert_includes result_string, "✗ Found 2 lint offenses"
+ assert_includes result_string, "1 errors"
+ assert_includes result_string, "1 warnings"
+ end
+
+ def test_position_from_hash
+ data = { line: 5, column: 10 }
+ position = Herb::Position.from_hash(data)
+
+ assert_equal 5, position.line
+ assert_equal 10, position.column
+ end
+
+ def test_location_from_hash
+ data = {
+ start: { line: 1, column: 5 },
+ end: { line: 2, column: 15 },
+ }
+ location = Herb::Location.from_hash(data)
+
+ assert_equal 1, location.start.line
+ assert_equal 5, location.start.column
+
+ assert_equal 2, location.end.line
+ assert_equal 15, location.end.column
+ end
+end
diff --git a/test/node_backend_integration_test.rb b/test/node_backend_integration_test.rb
new file mode 100644
index 000000000..45da669db
--- /dev/null
+++ b/test/node_backend_integration_test.rb
@@ -0,0 +1,248 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class NodeBackendIntegrationTest < Minitest::Test
+ def setup
+ skip "Node backend dependencies not available" unless node_backend_fully_available?
+
+ @template = <<~ERB.strip
+
+ <% if user.logged_in? %>
+
Welcome, <%= user.name %>!
+ <% end %>
+
+ ERB
+
+ @messy_template = <<~ERB.strip
+
+ <%if user.logged_in?%>
+
Welcome, <%= user.name %>!
+ <%end%>
+
+ ERB
+
+ @template_with_issues = <<~ERB.strip
+
+ <%if user.logged_in?%>
+
Welcome, <%= user.name %>!
+ <%end%>
+
+ ERB
+
+ Herb.switch_backend(:node)
+ end
+
+ def test_format_returns_string
+ skip_if_packages_missing
+
+ result = Herb.format(@messy_template)
+
+ assert_instance_of String, result
+ assert result.length.positive?
+ refute_equal @messy_template, result
+ end
+
+ def test_format_improves_formatting
+ skip_if_packages_missing
+
+ result = Herb.format(@messy_template)
+
+ refute_includes result, "<%if"
+ refute_includes result, "<%end%>"
+ refute_includes result, "")
+ rescue Nodo::DependencyError, StandardError => e
+ raise e unless e.message.include?("Cannot find package")
+
+ skip "Node.js packages not installed: #{e.message.lines.first.strip}"
+ end
+end
diff --git a/test/parser/parser_test.rb b/test/parser/parser_test.rb
index ff61e334f..18a37504f 100644
--- a/test/parser/parser_test.rb
+++ b/test/parser/parser_test.rb
@@ -39,8 +39,8 @@ class ParserTest < Minitest::Spec
│ ├── tag_opening: "<%=" (location: (1:4)-(1:7))
│ ├── content: " RUBY_VERSION " (location: (1:7)-(1:21))
│ ├── tag_closing: "%>" (location: (1:21)-(1:23))
- │ ├── parsed: false
- │ └── valid: false
+ │ ├── parsed: true
+ │ └── valid: true
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:23)-(1:28))
diff --git a/test/print_node_test.rb b/test/print_node_test.rb
new file mode 100644
index 000000000..9fa09b759
--- /dev/null
+++ b/test/print_node_test.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class PrintNodeTest < Minitest::Test
+ def test_node_to_source_method_exists
+ template = "Hello World
"
+ result = Herb.parse(template)
+
+ assert result.success?
+ assert_respond_to result.value, :to_source
+ end
+
+ def test_native_backend_print_node_not_implemented
+ original_backend = Herb.current_backend
+
+ begin
+ Herb.switch_backend(:native)
+ template = "Hello World
"
+ result = Herb.parse(template)
+
+ error = assert_raises(NotImplementedError) do
+ result.value.to_source(backend: :native)
+ end
+
+ assert_includes error.message, "Node printing is not implemented in the native backend"
+ assert_includes error.message, "Node backend"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ def test_node_backend_print_node_works
+ skip "Node backend dependencies not available" unless node_backend_available?
+
+ original_backend = Herb.current_backend
+
+ begin
+ Herb.switch_backend(:node)
+ template = "Hello World
"
+ result = Herb.parse(template)
+
+ assert result.success?
+
+ erb_output = result.value.to_source
+
+ assert_instance_of String, erb_output
+ assert erb_output.length.positive?
+
+ assert_includes erb_output, "div"
+ assert_includes erb_output, "Hello World"
+ rescue Nodo::DependencyError
+ skip "Node.js packages not installed"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ def test_node_backend_format_option
+ skip "Node backend dependencies not available" unless node_backend_available?
+
+ original_backend = Herb.current_backend
+
+ begin
+ Herb.switch_backend(:node)
+ messy_template = "HelloWorld
"
+ result = Herb.parse(messy_template)
+
+ assert result.success?
+
+ source_default = result.value.to_source
+ source_identity = result.value.to_source(format: false)
+
+ assert_instance_of String, source_default
+ assert_instance_of String, source_identity
+
+ assert_includes source_default, "div"
+ assert_includes source_default, "span"
+ assert_includes source_identity, "div"
+ assert_includes source_identity, "span"
+ assert_includes source_identity, "Hello"
+
+ assert source_default.length.positive?
+ assert source_identity.length.positive?
+ rescue Nodo::DependencyError
+ skip "Node.js packages not installed"
+ ensure
+ Herb.switch_backend(original_backend) if original_backend
+ end
+ end
+
+ private
+
+ def node_backend_available?
+ require "nodo"
+ true
+ rescue LoadError
+ false
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 164c52243..ace408d97 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -14,6 +14,24 @@
Minitest::Spec::DSL.send(:alias_method, :test, :it)
Minitest::Spec::DSL.send(:alias_method, :xtest, :xit)
+class Minitest::Test
+ def setup
+ super
+
+ @__original_herb_backend = Herb.current_backend
+
+ Herb.switch_backend(:native) unless Herb.current_backend == :native
+ end
+
+ def teardown
+ if @__original_herb_backend && @__original_herb_backend != Herb.current_backend
+ Herb.switch_backend(@__original_herb_backend)
+ end
+
+ super
+ end
+end
+
def cyclic_string(length)
sequence = ("a".."z").to_a + ("0".."9").to_a
sequence.cycle.take(length).join