typedef struct POSITION_STRUCT {
@@ -8,4 +10,7 @@ typedef struct POSITION_STRUCT {
uint32_t column;
} position_T;
+position_T position_from_source_with_offset(const char* source, size_t offset);
+bool position_is_within_range(position_T position, position_T start, position_T end);
+
#endif
diff --git a/src/include/prism_helpers.h b/src/include/prism_helpers.h
index ebfb53b4f..89c61baac 100644
--- a/src/include/prism_helpers.h
+++ b/src/include/prism_helpers.h
@@ -16,6 +16,10 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error(
pm_parser_t* parser
);
-position_T position_from_source_with_offset(const char* source, size_t offset);
+RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error_with_positions(
+ const pm_diagnostic_t* error,
+ position_T start,
+ position_T end
+);
#endif
diff --git a/src/position.c b/src/position.c
new file mode 100644
index 000000000..29246b429
--- /dev/null
+++ b/src/position.c
@@ -0,0 +1,27 @@
+#include "include/position.h"
+#include "include/util.h"
+
+position_T position_from_source_with_offset(const char* source, size_t offset) {
+ position_T position = { .line = 1, .column = 0 };
+
+ for (size_t i = 0; i < offset; i++) {
+ if (is_newline(source[i])) {
+ position.line++;
+ position.column = 0;
+ } else {
+ position.column++;
+ }
+ }
+
+ return position;
+}
+
+bool position_is_within_range(position_T position, position_T start, position_T end) {
+ if (position.line < start.line) { return false; }
+ if (position.line == start.line && position.column < start.column) { return false; }
+
+ if (position.line > end.line) { return false; }
+ if (position.line == end.line && position.column > end.column) { return false; }
+
+ return true;
+}
diff --git a/src/prism_helpers.c b/src/prism_helpers.c
index 06ac155b5..5cd1c9d29 100644
--- a/src/prism_helpers.c
+++ b/src/prism_helpers.c
@@ -16,21 +16,6 @@ const char* pm_error_level_to_string(pm_error_level_t level) {
}
}
-position_T position_from_source_with_offset(const char* source, size_t offset) {
- position_T position = { .line = 1, .column = 0 };
-
- for (size_t i = 0; i < offset; i++) {
- if (is_newline(source[i])) {
- position.line++;
- position.column = 0;
- } else {
- position.column++;
- }
- }
-
- return position;
-}
-
RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error(
const pm_diagnostic_t* error,
const AST_NODE_T* node,
@@ -51,3 +36,17 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error(
end
);
}
+
+RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error_with_positions(
+ const pm_diagnostic_t* error,
+ position_T start,
+ position_T end
+) {
+ return ruby_parse_error_init(
+ error->message,
+ pm_diagnostic_id_human(error->diag_id),
+ pm_error_level_to_string(error->level),
+ start,
+ end
+ );
+}
diff --git a/test/analyze/if_test.rb b/test/analyze/if_test.rb
index 48f0ac305..683a3b5b5 100644
--- a/test/analyze/if_test.rb
+++ b/test/analyze/if_test.rb
@@ -152,5 +152,19 @@ class IfTest < Minitest::Spec
>
HTML
end
+
+ test "if with missing conditional" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if %>
+ <% end %>
+ HTML
+ end
+
+ test "if with invalid syntax" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if true true %>
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/c/test_extract.c b/test/c/test_extract.c
index d707622c7..0d268f01b 100644
--- a/test/c/test_extract.c
+++ b/test/c/test_extract.c
@@ -4,11 +4,11 @@
#include "../../src/include/extract.h"
#include "../../src/include/util/hb_buffer.h"
-TEST(extract_ruby_single_erb_no_semicolon)
+TEST(extract_ruby_single_erb_with_semicolons)
char* source = "<% if %>\n<% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " if \n end ";
+ char expected[] = " if ;\n end ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -18,7 +18,7 @@ TEST(extract_ruby_multiple_erb_same_line_with_semicolon)
char* source = "<% x = 1 %> <% y = 2 %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " x = 1 ; y = 2 ");
+ ck_assert_str_eq(result, " x = 1 ; y = 2 ;");
free(result);
END
@@ -27,16 +27,16 @@ TEST(extract_ruby_three_erb_same_line_with_semicolons)
char* source = "<% a = 1 %> <% b = 2 %> <% c = 3 %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " a = 1 ; b = 2 ; c = 3 ");
+ ck_assert_str_eq(result, " a = 1 ; b = 2 ; c = 3 ;");
free(result);
END
-TEST(extract_ruby_different_lines_no_semicolons)
+TEST(extract_ruby_different_lines_with_semicolons)
char* source = "<% x = 1 %>\n<% y = 2 %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " x = 1 \n y = 2 ";
+ char expected[] = " x = 1 ;\n y = 2 ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -46,7 +46,7 @@ TEST(extract_ruby_mixed_lines)
char* source = "<% a = 1 %> <% b = 2 %>\n<% c = 3 %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " a = 1 ; b = 2 \n c = 3 ";
+ char expected[] = " a = 1 ; b = 2 ;\n c = 3 ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -56,7 +56,7 @@ TEST(extract_ruby_output_tags_same_line)
char* source = "<%= x %> <%= y %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " x ; y ");
+ ck_assert_str_eq(result, " x ; y ;");
free(result);
END
@@ -65,7 +65,7 @@ TEST(extract_ruby_empty_erb_same_line)
char* source = "<% %> <% %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " ; ");
+ ck_assert_str_eq(result, " ; ;");
free(result);
END
@@ -74,7 +74,7 @@ TEST(extract_ruby_comments_skipped)
char* source = "<%# comment %> <% code %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " code ");
+ ck_assert_str_eq(result, " code ;");
free(result);
END
@@ -83,7 +83,7 @@ TEST(extract_ruby_issue_135_if_without_condition)
char* source = "<% if %>\n<% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " if \n end ";
+ char expected[] = " if ;\n end ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -93,7 +93,7 @@ TEST(extract_ruby_inline_comment_same_line)
char* source = "<% if true %><% # Comment here %><% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " if true ; end ");
+ ck_assert_str_eq(result, " if true ; end ;");
free(result);
END
@@ -102,7 +102,7 @@ TEST(extract_ruby_inline_comment_with_newline)
char* source = "<% if true %><% # Comment here %>\n<% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " if true ; \n end ";
+ char expected[] = " if true ; \n end ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -112,7 +112,7 @@ TEST(extract_ruby_inline_comment_with_spaces)
char* source = "<% # Comment %> <% code %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " code ");
+ ck_assert_str_eq(result, " code ;");
free(result);
END
@@ -121,7 +121,7 @@ TEST(extract_ruby_inline_comment_multiline)
char* source = "<% # Comment\nmore %> <% code %>";
char* result = herb_extract_ruby_with_semicolons(source);
- char expected[] = " # Comment\nmore ; code ";
+ char expected[] = " # Comment\nmore ; code ;";
ck_assert_str_eq(result, expected);
free(result);
@@ -131,7 +131,7 @@ TEST(extract_ruby_inline_comment_between_code)
char* source = "<% if true %><% # Comment here %><%= hello %><% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " if true ; hello ; end ");
+ ck_assert_str_eq(result, " if true ; hello ; end ;");
free(result);
END
@@ -140,7 +140,7 @@ TEST(extract_ruby_inline_comment_complex)
char* source = "<% # Comment here %><% if true %><% # Comment here %><%= hello %><% end %>";
char* result = herb_extract_ruby_with_semicolons(source);
- ck_assert_str_eq(result, " if true ; hello ; end ");
+ ck_assert_str_eq(result, " if true ; hello ; end ;");
free(result);
END
@@ -148,10 +148,10 @@ END
TCase *extract_tests(void) {
TCase *extract = tcase_create("Extract");
- tcase_add_test(extract, extract_ruby_single_erb_no_semicolon);
+ tcase_add_test(extract, extract_ruby_single_erb_with_semicolons);
tcase_add_test(extract, extract_ruby_multiple_erb_same_line_with_semicolon);
tcase_add_test(extract, extract_ruby_three_erb_same_line_with_semicolons);
- tcase_add_test(extract, extract_ruby_different_lines_no_semicolons);
+ tcase_add_test(extract, extract_ruby_different_lines_with_semicolons);
tcase_add_test(extract, extract_ruby_mixed_lines);
tcase_add_test(extract, extract_ruby_output_tags_same_line);
tcase_add_test(extract, extract_ruby_empty_erb_same_line);
diff --git a/test/extractor/extract_ruby_test.rb b/test/extractor/extract_ruby_test.rb
index 2724827ab..0f5f260b8 100644
--- a/test/extractor/extract_ruby_test.rb
+++ b/test/extractor/extract_ruby_test.rb
@@ -7,13 +7,13 @@ class ExtractRubyTest < Minitest::Spec
test "basic silent" do
ruby = Herb.extract_ruby("<% RUBY_VERSION %>
")
- assert_equal " RUBY_VERSION ", ruby
+ assert_equal " RUBY_VERSION ; ", ruby
end
test "basic loud" do
ruby = Herb.extract_ruby("<%= RUBY_VERSION %>
")
- assert_equal " RUBY_VERSION ", ruby
+ assert_equal " RUBY_VERSION ; ", ruby
end
test "with newlines" do
@@ -23,7 +23,7 @@ class ExtractRubyTest < Minitest::Spec
HTML
- assert_equal " \n RUBY_VERSION \n \n", actual
+ assert_equal " \n RUBY_VERSION ;\n \n", actual
end
test "nested" do
@@ -37,7 +37,7 @@ class ExtractRubyTest < Minitest::Spec
HTML
- expected = " array = [1, 2, 3] \n\n \n array.each do |item| \n item \n end \n \n"
+ expected = " array = [1, 2, 3] ;\n\n \n array.each do |item| ;\n item ; \n end ;\n \n"
assert_equal expected, actual
end
@@ -100,7 +100,7 @@ class ExtractRubyTest < Minitest::Spec
<% if %><%# comment %><% end %>
HTML
- expected = " if end \n"
+ expected = " if ; end ;\n"
assert_equal expected, actual
end
@@ -110,7 +110,7 @@ class ExtractRubyTest < Minitest::Spec
<% if %><% # comment %><% end %>
HTML
- expected = " if # comment end \n"
+ expected = " if ; # comment ; end ;\n"
assert_equal expected, actual
end
diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb
index 285b0d8e3..746ed5e8d 100644
--- a/test/parser/erb_test.rb
+++ b/test/parser/erb_test.rb
@@ -309,5 +309,76 @@ class ERBTest < Minitest::Spec
<% end %>
HTML
end
+
+ test "hash with trailing key across tags" do
+ assert_parsed_snapshot(<<~HTML)
+ <%= render "form", f: %>
+ <%= f.input :name %>
+ HTML
+ end
+
+ test "single hash with trailing key in div" do
+ assert_parsed_snapshot(<<~HTML)
+
+ <%= render "something", thing: %>
+
+ HTML
+ end
+
+ test "multiple hash with trailing key in div" do
+ assert_parsed_snapshot(<<~HTML)
+
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+
+ HTML
+ end
+
+ test "many hash with trailing key in div" do
+ assert_parsed_snapshot(<<~HTML)
+
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+
+ HTML
+ end
+
+ test "hash shorthand in parentheses across tags" do
+ assert_parsed_snapshot(<<~HTML)
+
+ <%= something(thing:) %>
+ <%= something(thing:) %>
+
+ HTML
+ end
+
+ test "complex form with trailing hash keys" do
+ assert_parsed_snapshot(<<~HTML)
+ <%= form_with url:, model: custom_field, scope: :custom_field, data: {turbo_frame: :_top}, class: "my-2" do |form| %>
+ <%= form.hidden_field :type, value: custom_field.type %>
+ <%= form.input :name %>
+ <%= form.check_box :required %>
+ <%= form.check_box :nullable %>
+
+ <%= component "custom_field_forms/\#{custom_field.model_name.element.underscore}", form: %>
+
+
+ <%= component :link, path: custom_fields_path, variant: :button, data: {turbo_frame: :_top} do %>
+ <%= t("buttons.back") %>
+ <% end %>
+
+ <%= component :button, state: :primary do %>
+ <%= button_text %>
+ <% end %>
+
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt b/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt
index c3504979f..60db0740b 100644
--- a/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt
+++ b/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt
@@ -19,7 +19,7 @@ input: |2-
│ │ ├── diagnostic_id: "unexpected_token_close_context"
│ │ └── level: "syntax"
│ │
-│ └── @ RubyParseError (location: (4:0)-(4:0))
+│ └── @ RubyParseError (location: (3:9)-(3:9))
│ ├── message: "block_term_brace: expected a block beginning with `{` to end with `}`"
│ ├── error_message: "expected a block beginning with `{` to end with `}`"
│ ├── diagnostic_id: "block_term_brace"
diff --git a/test/snapshots/analyze/if_test/test_0016_if_with_missing_conditional_ee03fbe2e88a2b56ca5c044200fe19bf.txt b/test/snapshots/analyze/if_test/test_0016_if_with_missing_conditional_ee03fbe2e88a2b56ca5c044200fe19bf.txt
new file mode 100644
index 000000000..ee80a7d71
--- /dev/null
+++ b/test/snapshots/analyze/if_test/test_0016_if_with_missing_conditional_ee03fbe2e88a2b56ca5c044200fe19bf.txt
@@ -0,0 +1,33 @@
+---
+source: "Analyze::IfTest#test_0016_if with missing conditional"
+input: |2-
+<% if %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(3:0))
+├── errors: (1 error)
+│ └── @ RubyParseError (location: (1:3)-(1:5))
+│ ├── message: "conditional_if_predicate: expected a predicate expression for the `if` statement"
+│ ├── error_message: "expected a predicate expression for the `if` statement"
+│ ├── diagnostic_id: "conditional_if_predicate"
+│ └── level: "syntax"
+│
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(2:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if " (location: (1:2)-(1:6))
+ │ ├── tag_closing: "%>" (location: (1:6)-(1:8))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:8)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (2:0)-(2:9))
+ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ ├── content: " end " (location: (2:2)-(2:7))
+ │ └── tag_closing: "%>" (location: (2:7)-(2:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (2:9)-(3:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/if_test/test_0017_if_with_invalid_syntax_1c3b5ad1abe4440ca61ee62d2db330f2.txt b/test/snapshots/analyze/if_test/test_0017_if_with_invalid_syntax_1c3b5ad1abe4440ca61ee62d2db330f2.txt
new file mode 100644
index 000000000..37de27eec
--- /dev/null
+++ b/test/snapshots/analyze/if_test/test_0017_if_with_invalid_syntax_1c3b5ad1abe4440ca61ee62d2db330f2.txt
@@ -0,0 +1,33 @@
+---
+source: "Analyze::IfTest#test_0017_if with invalid syntax"
+input: |2-
+<% if true true %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(3:0))
+├── errors: (1 error)
+│ └── @ RubyParseError (location: (1:11)-(1:15))
+│ ├── message: "conditional_predicate_term: expected `then` or `;` or '\\n'"
+│ ├── error_message: "expected `then` or `;` or '\\n'"
+│ ├── diagnostic_id: "conditional_predicate_term"
+│ └── level: "syntax"
+│
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(2:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if true true " (location: (1:2)-(1:16))
+ │ ├── tag_closing: "%>" (location: (1:16)-(1:18))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:18)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (2:0)-(2:9))
+ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ ├── content: " end " (location: (2:2)-(2:7))
+ │ └── tag_closing: "%>" (location: (2:7)-(2:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (2:9)-(3:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt b/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt
index b79785e60..fb1dc6402 100644
--- a/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt
+++ b/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt
@@ -3,21 +3,15 @@ source: "Parser::ERBTest#test_0039_incomplete erb tag"
input: "<%= 1 + %>"
---
@ DocumentNode (location: (1:0)-(1:10))
-├── errors: (2 errors)
-│ ├── @ RubyParseError (location: (1:10)-(1:10))
-│ │ ├── message: "expect_expression_after_operator: unexpected end-of-input; expected an expression after the operator"
-│ │ ├── error_message: "unexpected end-of-input; expected an expression after the operator"
-│ │ ├── diagnostic_id: "expect_expression_after_operator"
-│ │ └── level: "syntax"
-│ │
-│ └── @ RubyParseError (location: (1:10)-(1:10))
-│ ├── message: "unexpected_token_close_context: unexpected end-of-input, assuming it is closing the parent top level context"
-│ ├── error_message: "unexpected end-of-input, assuming it is closing the parent top level context"
-│ ├── diagnostic_id: "unexpected_token_close_context"
-│ └── level: "syntax"
-│
└── children: (1 item)
└── @ ERBContentNode (location: (1:0)-(1:10))
+ ├── errors: (1 error)
+ │ └── @ RubyParseError (location: (1:0)-(1:10))
+ │ ├── message: "expect_expression_after_operator: unexpected end-of-input; expected an expression after the operator"
+ │ ├── error_message: "unexpected end-of-input; expected an expression after the operator"
+ │ ├── diagnostic_id: "expect_expression_after_operator"
+ │ └── level: "syntax"
+ │
├── tag_opening: "<%=" (location: (1:0)-(1:3))
├── content: " 1 + " (location: (1:3)-(1:8))
├── tag_closing: "%>" (location: (1:8)-(1:10))
diff --git a/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt b/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt
index f90424832..065295c18 100644
--- a/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt
+++ b/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt
@@ -5,17 +5,11 @@ input: |2-
<% end %>
---
@ DocumentNode (location: (1:0)-(3:0))
-├── errors: (2 errors)
-│ ├── @ RubyParseError (location: (1:3)-(1:5))
-│ │ ├── message: "conditional_if_predicate: expected a predicate expression for the `if` statement"
-│ │ ├── error_message: "expected a predicate expression for the `if` statement"
-│ │ ├── diagnostic_id: "conditional_if_predicate"
-│ │ └── level: "syntax"
-│ │
-│ └── @ RubyParseError (location: (2:3)-(2:6))
-│ ├── message: "conditional_predicate_term: expected `then` or `;` or '\\n'"
-│ ├── error_message: "expected `then` or `;` or '\\n'"
-│ ├── diagnostic_id: "conditional_predicate_term"
+├── errors: (1 error)
+│ └── @ RubyParseError (location: (1:3)-(1:5))
+│ ├── message: "conditional_if_predicate: expected a predicate expression for the `if` statement"
+│ ├── error_message: "expected a predicate expression for the `if` statement"
+│ ├── diagnostic_id: "conditional_if_predicate"
│ └── level: "syntax"
│
└── children: (2 items)
diff --git a/test/snapshots/parser/erb_test/test_0052_hash_with_trailing_key_across_tags_5c81bac82089f414dd42d8becdaa4ef3.txt b/test/snapshots/parser/erb_test/test_0052_hash_with_trailing_key_across_tags_5c81bac82089f414dd42d8becdaa4ef3.txt
new file mode 100644
index 000000000..b676d932b
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0052_hash_with_trailing_key_across_tags_5c81bac82089f414dd42d8becdaa4ef3.txt
@@ -0,0 +1,27 @@
+---
+source: "Parser::ERBTest#test_0052_hash with trailing key across tags"
+input: |2-
+<%= render "form", f: %>
+<%= f.input :name %>
+---
+@ DocumentNode (location: (1:0)-(3:0))
+└── children: (4 items)
+ ├── @ ERBContentNode (location: (1:0)-(1:24))
+ │ ├── tag_opening: "<%=" (location: (1:0)-(1:3))
+ │ ├── content: " render "form", f: " (location: (1:3)-(1:22))
+ │ ├── tag_closing: "%>" (location: (1:22)-(1:24))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ ├── @ HTMLTextNode (location: (1:24)-(2:0))
+ │ └── content: "\n"
+ │
+ ├── @ ERBContentNode (location: (2:0)-(2:20))
+ │ ├── tag_opening: "<%=" (location: (2:0)-(2:3))
+ │ ├── content: " f.input :name " (location: (2:3)-(2:18))
+ │ ├── tag_closing: "%>" (location: (2:18)-(2:20))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (2:20)-(3:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0053_single_hash_with_trailing_key_in_div_6b3ce8ff3d1ac8f4a5cf2a377ddcbae4.txt b/test/snapshots/parser/erb_test/test_0053_single_hash_with_trailing_key_in_div_6b3ce8ff3d1ac8f4a5cf2a377ddcbae4.txt
new file mode 100644
index 000000000..5663f391e
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0053_single_hash_with_trailing_key_in_div_6b3ce8ff3d1ac8f4a5cf2a377ddcbae4.txt
@@ -0,0 +1,45 @@
+---
+source: "Parser::ERBTest#test_0053_single hash with trailing key in div"
+input: |2-
+
+ <%= render "something", thing: %>
+
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(3:6))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:5))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ │ ├── tag_closing: ">" (location: (1:4)-(1:5))
+ │ │ ├── children: []
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:5)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (2:5)-(2:33))
+ │ │ │ ├── tag_closing: "%>" (location: (2:33)-(2:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:35)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (3:0)-(3:6))
+ │ │ ├── tag_opening: "" (location: (3:0)-(3:2))
+ │ │ ├── tag_name: "div" (location: (3:2)-(3:5))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (3:5)-(3:6))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (3:6)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0054_multiple_hash_with_trailing_key_in_div_566fdf7ce4fea5ab67f856680ed3dbba.txt b/test/snapshots/parser/erb_test/test_0054_multiple_hash_with_trailing_key_in_div_566fdf7ce4fea5ab67f856680ed3dbba.txt
new file mode 100644
index 000000000..01bcd5b8e
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0054_multiple_hash_with_trailing_key_in_div_566fdf7ce4fea5ab67f856680ed3dbba.txt
@@ -0,0 +1,56 @@
+---
+source: "Parser::ERBTest#test_0054_multiple hash with trailing key in div"
+input: |2-
+
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+
+---
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(4:6))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:5))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ │ ├── tag_closing: ">" (location: (1:4)-(1:5))
+ │ │ ├── children: []
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ ├── body: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (1:5)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (2:5)-(2:33))
+ │ │ │ ├── tag_closing: "%>" (location: (2:33)-(2:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:35)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (3:5)-(3:33))
+ │ │ │ ├── tag_closing: "%>" (location: (3:33)-(3:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (3:35)-(4:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (4:0)-(4:6))
+ │ │ ├── tag_opening: "" (location: (4:0)-(4:2))
+ │ │ ├── tag_name: "div" (location: (4:2)-(4:5))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (4:5)-(4:6))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (4:6)-(5:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0055_many_hash_with_trailing_key_in_div_bc4767160a429bf77c068d0263c18645.txt b/test/snapshots/parser/erb_test/test_0055_many_hash_with_trailing_key_in_div_bc4767160a429bf77c068d0263c18645.txt
new file mode 100644
index 000000000..8b9a47862
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0055_many_hash_with_trailing_key_in_div_bc4767160a429bf77c068d0263c18645.txt
@@ -0,0 +1,122 @@
+---
+source: "Parser::ERBTest#test_0055_many hash with trailing key in div"
+input: |2-
+
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+ <%= render "something", thing: %>
+
+---
+@ DocumentNode (location: (1:0)-(11:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(10:6))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:5))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ │ ├── tag_closing: ">" (location: (1:4)-(1:5))
+ │ │ ├── children: []
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ ├── body: (17 items)
+ │ │ ├── @ HTMLTextNode (location: (1:5)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (2:5)-(2:33))
+ │ │ │ ├── tag_closing: "%>" (location: (2:33)-(2:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:35)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (3:5)-(3:33))
+ │ │ │ ├── tag_closing: "%>" (location: (3:33)-(3:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (3:35)-(4:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (4:2)-(4:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (4:2)-(4:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (4:5)-(4:33))
+ │ │ │ ├── tag_closing: "%>" (location: (4:33)-(4:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (4:35)-(5:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (5:2)-(5:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (5:2)-(5:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (5:5)-(5:33))
+ │ │ │ ├── tag_closing: "%>" (location: (5:33)-(5:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (5:35)-(6:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (6:2)-(6:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (6:5)-(6:33))
+ │ │ │ ├── tag_closing: "%>" (location: (6:33)-(6:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (6:35)-(7:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (7:2)-(7:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (7:2)-(7:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (7:5)-(7:33))
+ │ │ │ ├── tag_closing: "%>" (location: (7:33)-(7:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (7:35)-(8:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (8:2)-(8:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (8:2)-(8:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (8:5)-(8:33))
+ │ │ │ ├── tag_closing: "%>" (location: (8:33)-(8:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (8:35)-(9:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (9:2)-(9:35))
+ │ │ │ ├── tag_opening: "<%=" (location: (9:2)-(9:5))
+ │ │ │ ├── content: " render "something", thing: " (location: (9:5)-(9:33))
+ │ │ │ ├── tag_closing: "%>" (location: (9:33)-(9:35))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (9:35)-(10:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (10:0)-(10:6))
+ │ │ ├── tag_opening: "" (location: (10:0)-(10:2))
+ │ │ ├── tag_name: "div" (location: (10:2)-(10:5))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (10:5)-(10:6))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (10:6)-(11:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0056_hash_shorthand_in_parentheses_across_tags_8cafde6814ee57b2ee27e3392f23c2ae.txt b/test/snapshots/parser/erb_test/test_0056_hash_shorthand_in_parentheses_across_tags_8cafde6814ee57b2ee27e3392f23c2ae.txt
new file mode 100644
index 000000000..9821690fd
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0056_hash_shorthand_in_parentheses_across_tags_8cafde6814ee57b2ee27e3392f23c2ae.txt
@@ -0,0 +1,56 @@
+---
+source: "Parser::ERBTest#test_0056_hash shorthand in parentheses across tags"
+input: |2-
+
+ <%= something(thing:) %>
+ <%= something(thing:) %>
+
+---
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(4:6))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:5))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ │ ├── tag_closing: ">" (location: (1:4)-(1:5))
+ │ │ ├── children: []
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ ├── body: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (1:5)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:26))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " something(thing:) " (location: (2:5)-(2:24))
+ │ │ │ ├── tag_closing: "%>" (location: (2:24)-(2:26))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:26)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:26))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " something(thing:) " (location: (3:5)-(3:24))
+ │ │ │ ├── tag_closing: "%>" (location: (3:24)-(3:26))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (3:26)-(4:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (4:0)-(4:6))
+ │ │ ├── tag_opening: "" (location: (4:0)-(4:2))
+ │ │ ├── tag_name: "div" (location: (4:2)-(4:5))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (4:5)-(4:6))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (4:6)-(5:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0057_complex_form_with_trailing_hash_keys_5aa8c6b298cf6e7261db5fe1341a4eb1.txt b/test/snapshots/parser/erb_test/test_0057_complex_form_with_trailing_hash_keys_5aa8c6b298cf6e7261db5fe1341a4eb1.txt
new file mode 100644
index 000000000..7483ba1e2
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0057_complex_form_with_trailing_hash_keys_5aa8c6b298cf6e7261db5fe1341a4eb1.txt
@@ -0,0 +1,194 @@
+---
+source: "Parser::ERBTest#test_0057_complex form with trailing hash keys"
+input: |2-
+<%= form_with url:, model: custom_field, scope: :custom_field, data: {turbo_frame: :_top}, class: "my-2" do |form| %>
+ <%= form.hidden_field :type, value: custom_field.type %>
+ <%= form.input :name %>
+ <%= form.check_box :required %>
+ <%= form.check_box :nullable %>
+
+ <%= component "custom_field_forms/#{custom_field.model_name.element.underscore}", form: %>
+
+
+ <%= component :link, path: custom_fields_path, variant: :button, data: {turbo_frame: :_top} do %>
+ <%= t("buttons.back") %>
+ <% end %>
+
+ <%= component :button, state: :primary do %>
+ <%= button_text %>
+ <% end %>
+
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(19:0))
+└── children: (2 items)
+ ├── @ ERBBlockNode (location: (1:0)-(18:9))
+ │ ├── tag_opening: "<%=" (location: (1:0)-(1:3))
+ │ ├── content: " form_with url:, model: custom_field, scope: :custom_field, data: {turbo_frame: :_top}, class: "my-2" do |form| " (location: (1:3)-(1:115))
+ │ ├── tag_closing: "%>" (location: (1:115)-(1:117))
+ │ ├── body: (13 items)
+ │ │ ├── @ HTMLTextNode (location: (1:117)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:58))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " form.hidden_field :type, value: custom_field.type " (location: (2:5)-(2:56))
+ │ │ │ ├── tag_closing: "%>" (location: (2:56)-(2:58))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:58)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:25))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " form.input :name " (location: (3:5)-(3:23))
+ │ │ │ ├── tag_closing: "%>" (location: (3:23)-(3:25))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (3:25)-(4:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (4:2)-(4:33))
+ │ │ │ ├── tag_opening: "<%=" (location: (4:2)-(4:5))
+ │ │ │ ├── content: " form.check_box :required " (location: (4:5)-(4:31))
+ │ │ │ ├── tag_closing: "%>" (location: (4:31)-(4:33))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (4:33)-(5:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (5:2)-(5:33))
+ │ │ │ ├── tag_opening: "<%=" (location: (5:2)-(5:5))
+ │ │ │ ├── content: " form.check_box :nullable " (location: (5:5)-(5:31))
+ │ │ │ ├── tag_closing: "%>" (location: (5:31)-(5:33))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (5:33)-(7:2))
+ │ │ │ └── content: "\n\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (7:2)-(7:92))
+ │ │ │ ├── tag_opening: "<%=" (location: (7:2)-(7:5))
+ │ │ │ ├── content: " component "custom_field_forms/#{custom_field.model_name.element.underscore}", form: " (location: (7:5)-(7:90))
+ │ │ │ ├── tag_closing: "%>" (location: (7:90)-(7:92))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (7:92)-(9:2))
+ │ │ │ └── content: "\n\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (9:2)-(17:8))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ HTMLOpenTagNode (location: (9:2)-(9:47))
+ │ │ │ │ ├── tag_opening: "<" (location: (9:2)-(9:3))
+ │ │ │ │ ├── tag_name: "div" (location: (9:3)-(9:6))
+ │ │ │ │ ├── tag_closing: ">" (location: (9:46)-(9:47))
+ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ └── @ HTMLAttributeNode (location: (9:7)-(9:46))
+ │ │ │ │ │ ├── name:
+ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (9:7)-(9:12))
+ │ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ │ └── @ LiteralNode (location: (9:7)-(9:12))
+ │ │ │ │ │ │ └── content: "class"
+ │ │ │ │ │ │
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── equals: "=" (location: (9:12)-(9:13))
+ │ │ │ │ │ └── value:
+ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (9:13)-(9:46))
+ │ │ │ │ │ ├── open_quote: """ (location: (9:13)-(9:14))
+ │ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ │ └── @ LiteralNode (location: (9:14)-(9:45))
+ │ │ │ │ │ │ └── content: "flex justify-end mt-4 space-x-4"
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── close_quote: """ (location: (9:45)-(9:46))
+ │ │ │ │ │ └── quoted: true
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ └── is_void: false
+ │ │ │ │
+ │ │ │ ├── tag_name: "div" (location: (9:3)-(9:6))
+ │ │ │ ├── body: (5 items)
+ │ │ │ │ ├── @ HTMLTextNode (location: (9:47)-(10:4))
+ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │
+ │ │ │ │ ├── @ ERBBlockNode (location: (10:4)-(12:13))
+ │ │ │ │ │ ├── tag_opening: "<%=" (location: (10:4)-(10:7))
+ │ │ │ │ │ ├── content: " component :link, path: custom_fields_path, variant: :button, data: {turbo_frame: :_top} do " (location: (10:7)-(10:99))
+ │ │ │ │ │ ├── tag_closing: "%>" (location: (10:99)-(10:101))
+ │ │ │ │ │ ├── body: (3 items)
+ │ │ │ │ │ │ ├── @ HTMLTextNode (location: (10:101)-(11:6))
+ │ │ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── @ ERBContentNode (location: (11:6)-(11:30))
+ │ │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (11:6)-(11:9))
+ │ │ │ │ │ │ │ ├── content: " t("buttons.back") " (location: (11:9)-(11:28))
+ │ │ │ │ │ │ │ ├── tag_closing: "%>" (location: (11:28)-(11:30))
+ │ │ │ │ │ │ │ ├── parsed: true
+ │ │ │ │ │ │ │ └── valid: true
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ └── @ HTMLTextNode (location: (11:30)-(12:4))
+ │ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │ │
+ │ │ │ │ │ └── end_node:
+ │ │ │ │ │ └── @ ERBEndNode (location: (12:4)-(12:13))
+ │ │ │ │ │ ├── tag_opening: "<%" (location: (12:4)-(12:6))
+ │ │ │ │ │ ├── content: " end " (location: (12:6)-(12:11))
+ │ │ │ │ │ └── tag_closing: "%>" (location: (12:11)-(12:13))
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ ├── @ HTMLTextNode (location: (12:13)-(14:4))
+ │ │ │ │ │ └── content: "\n\n "
+ │ │ │ │ │
+ │ │ │ │ ├── @ ERBBlockNode (location: (14:4)-(16:13))
+ │ │ │ │ │ ├── tag_opening: "<%=" (location: (14:4)-(14:7))
+ │ │ │ │ │ ├── content: " component :button, state: :primary do " (location: (14:7)-(14:46))
+ │ │ │ │ │ ├── tag_closing: "%>" (location: (14:46)-(14:48))
+ │ │ │ │ │ ├── body: (3 items)
+ │ │ │ │ │ │ ├── @ HTMLTextNode (location: (14:48)-(15:6))
+ │ │ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── @ ERBContentNode (location: (15:6)-(15:24))
+ │ │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (15:6)-(15:9))
+ │ │ │ │ │ │ │ ├── content: " button_text " (location: (15:9)-(15:22))
+ │ │ │ │ │ │ │ ├── tag_closing: "%>" (location: (15:22)-(15:24))
+ │ │ │ │ │ │ │ ├── parsed: true
+ │ │ │ │ │ │ │ └── valid: true
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ └── @ HTMLTextNode (location: (15:24)-(16:4))
+ │ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │ │
+ │ │ │ │ │ └── end_node:
+ │ │ │ │ │ └── @ ERBEndNode (location: (16:4)-(16:13))
+ │ │ │ │ │ ├── tag_opening: "<%" (location: (16:4)-(16:6))
+ │ │ │ │ │ ├── content: " end " (location: (16:6)-(16:11))
+ │ │ │ │ │ └── tag_closing: "%>" (location: (16:11)-(16:13))
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ └── @ HTMLTextNode (location: (16:13)-(17:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── close_tag:
+ │ │ │ │ └── @ HTMLCloseTagNode (location: (17:2)-(17:8))
+ │ │ │ │ ├── tag_opening: "" (location: (17:2)-(17:4))
+ │ │ │ │ ├── tag_name: "div" (location: (17:4)-(17:7))
+ │ │ │ │ ├── children: []
+ │ │ │ │ └── tag_closing: ">" (location: (17:7)-(17:8))
+ │ │ │ │
+ │ │ │ ├── is_void: false
+ │ │ │ └── source: "HTML"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (17:8)-(18:0))
+ │ │ └── content: "\n"
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (18:0)-(18:9))
+ │ ├── tag_opening: "<%" (location: (18:0)-(18:2))
+ │ ├── content: " end " (location: (18:2)-(18:7))
+ │ └── tag_closing: "%>" (location: (18:7)-(18:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (18:9)-(19:0))
+ └── content: "\n"
\ No newline at end of file