diff --git a/src/analyze.c b/src/analyze.c
index 61cf86463..57f325e6d 100644
--- a/src/analyze.c
+++ b/src/analyze.c
@@ -90,6 +90,138 @@ static size_t process_subsequent_block(
control_type_t parent_type
);
+typedef struct {
+ control_type_t type;
+ uint32_t offset;
+ bool found;
+} earliest_control_keyword_t;
+
+typedef struct {
+ earliest_control_keyword_t* result;
+ const uint8_t* source_start;
+} location_walker_context_t;
+
+static bool find_earliest_control_keyword_walker(const pm_node_t* node, void* data) {
+ if (!node) { return true; }
+
+ location_walker_context_t* context = (location_walker_context_t*) data;
+ earliest_control_keyword_t* result = context->result;
+
+ control_type_t current_type = CONTROL_TYPE_UNKNOWN;
+ uint32_t keyword_offset = UINT32_MAX;
+
+ switch (node->type) {
+ case PM_IF_NODE: {
+ pm_if_node_t* if_node = (pm_if_node_t*) node;
+ current_type = CONTROL_TYPE_IF;
+ keyword_offset = (uint32_t) (if_node->if_keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_UNLESS_NODE: {
+ pm_unless_node_t* unless_node = (pm_unless_node_t*) node;
+ current_type = CONTROL_TYPE_UNLESS;
+ keyword_offset = (uint32_t) (unless_node->keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_CASE_NODE: {
+ pm_case_node_t* case_node = (pm_case_node_t*) node;
+ current_type = CONTROL_TYPE_CASE;
+ keyword_offset = (uint32_t) (case_node->case_keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_CASE_MATCH_NODE: {
+ pm_case_match_node_t* case_match_node = (pm_case_match_node_t*) node;
+ current_type = CONTROL_TYPE_CASE_MATCH;
+ keyword_offset = (uint32_t) (case_match_node->case_keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_WHILE_NODE: {
+ pm_while_node_t* while_node = (pm_while_node_t*) node;
+ current_type = CONTROL_TYPE_WHILE;
+ keyword_offset = (uint32_t) (while_node->keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_UNTIL_NODE: {
+ pm_until_node_t* until_node = (pm_until_node_t*) node;
+ current_type = CONTROL_TYPE_UNTIL;
+ keyword_offset = (uint32_t) (until_node->keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_FOR_NODE: {
+ pm_for_node_t* for_node = (pm_for_node_t*) node;
+ current_type = CONTROL_TYPE_FOR;
+ keyword_offset = (uint32_t) (for_node->for_keyword_loc.start - context->source_start);
+ break;
+ }
+
+ case PM_BEGIN_NODE: {
+ pm_begin_node_t* begin_node = (pm_begin_node_t*) node;
+ current_type = CONTROL_TYPE_BEGIN;
+
+ if (begin_node->begin_keyword_loc.start != NULL) {
+ keyword_offset = (uint32_t) (begin_node->begin_keyword_loc.start - context->source_start);
+ } else {
+ keyword_offset = (uint32_t) (node->location.start - context->source_start);
+ }
+ break;
+ }
+
+ case PM_YIELD_NODE: {
+ current_type = CONTROL_TYPE_YIELD;
+ keyword_offset = (uint32_t) (node->location.start - context->source_start);
+ break;
+ }
+
+ case PM_CALL_NODE: {
+ pm_call_node_t* call = (pm_call_node_t*) node;
+
+ if (call->block != NULL) {
+ current_type = CONTROL_TYPE_BLOCK;
+ keyword_offset = (uint32_t) (node->location.start - context->source_start);
+ }
+ break;
+ }
+
+ case PM_NEXT_NODE:
+ case PM_BREAK_NODE:
+ case PM_RETURN_NODE: {
+ current_type = CONTROL_TYPE_UNKNOWN;
+ keyword_offset = (uint32_t) (node->location.start - context->source_start);
+ break;
+ }
+
+ default: break;
+ }
+
+ if (keyword_offset != UINT32_MAX) {
+ if (!result->found || keyword_offset < result->offset) {
+ result->type = current_type;
+ result->offset = keyword_offset;
+ result->found = true;
+ }
+ }
+
+ return true;
+}
+
+static control_type_t find_earliest_control_keyword(pm_node_t* root, const uint8_t* source_start) {
+ if (!root) { return CONTROL_TYPE_UNKNOWN; }
+
+ earliest_control_keyword_t result = { .type = CONTROL_TYPE_UNKNOWN, .offset = UINT32_MAX, .found = false };
+
+ location_walker_context_t context = { .result = &result, .source_start = source_start };
+
+ pm_visit_node(root, find_earliest_control_keyword_walker, &context);
+
+ return result.found ? result.type : CONTROL_TYPE_UNKNOWN;
+}
+
static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) {
if (!erb_node || erb_node->base.type != AST_ERB_CONTENT_NODE) { return CONTROL_TYPE_UNKNOWN; }
@@ -99,26 +231,18 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) {
if (ruby->valid) { return CONTROL_TYPE_UNKNOWN; }
- if (has_block_node(ruby)) { return CONTROL_TYPE_BLOCK; }
- if (has_if_node(ruby)) { return CONTROL_TYPE_IF; }
+ pm_node_t* root = ruby->root;
+
if (has_elsif_node(ruby)) { return CONTROL_TYPE_ELSIF; }
if (has_else_node(ruby)) { return CONTROL_TYPE_ELSE; }
if (has_end(ruby)) { return CONTROL_TYPE_END; }
- if (has_case_node(ruby)) { return CONTROL_TYPE_CASE; }
- if (has_case_match_node(ruby)) { return CONTROL_TYPE_CASE_MATCH; }
if (has_when_node(ruby)) { return CONTROL_TYPE_WHEN; }
if (has_in_node(ruby)) { return CONTROL_TYPE_IN; }
- if (has_begin_node(ruby)) { return CONTROL_TYPE_BEGIN; }
if (has_rescue_node(ruby)) { return CONTROL_TYPE_RESCUE; }
if (has_ensure_node(ruby)) { return CONTROL_TYPE_ENSURE; }
- if (has_unless_node(ruby)) { return CONTROL_TYPE_UNLESS; }
- if (has_while_node(ruby)) { return CONTROL_TYPE_WHILE; }
- if (has_until_node(ruby)) { return CONTROL_TYPE_UNTIL; }
- if (has_for_node(ruby)) { return CONTROL_TYPE_FOR; }
if (has_block_closing(ruby)) { return CONTROL_TYPE_BLOCK_CLOSE; }
- if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; }
- return CONTROL_TYPE_UNKNOWN;
+ return find_earliest_control_keyword(root, ruby->parser.start);
}
static bool is_subsequent_type(control_type_t parent_type, control_type_t child_type) {
diff --git a/src/analyze_helpers.c b/src/analyze_helpers.c
index 36117367c..6580a5966 100644
--- a/src/analyze_helpers.c
+++ b/src/analyze_helpers.c
@@ -110,12 +110,20 @@ bool search_block_nodes(const pm_node_t* node, void* data) {
analyzed_ruby_T* analyzed = (analyzed_ruby_T*) data;
if (node->type == PM_BLOCK_NODE) {
- analyzed->has_block_node = true;
- return true;
- } else {
- pm_visit_child_nodes(node, search_block_nodes, analyzed);
+ pm_block_node_t* block_node = (pm_block_node_t*) node;
+
+ size_t opening_length = block_node->opening_loc.end - block_node->opening_loc.start;
+
+ if ((opening_length == 2 && block_node->opening_loc.start[0] == 'd' && block_node->opening_loc.start[1] == 'o')
+ || (opening_length == 1 && block_node->opening_loc.start[0] == '{')) {
+ analyzed->has_block_node = true;
+
+ return true;
+ }
}
+ pm_visit_child_nodes(node, search_block_nodes, analyzed);
+
return false;
}
diff --git a/test/analyze/block_test.rb b/test/analyze/block_test.rb
index 5d4dd7326..05cde2cdd 100644
--- a/test/analyze/block_test.rb
+++ b/test/analyze/block_test.rb
@@ -164,5 +164,35 @@ class BlockTest < Minitest::Spec
<%= yield(:header) if content_for?(:header) %>
HTML
end
+
+ test "unclosed brace block should error" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each { |item| %>
+ <%= item %>
+ <% } %>
+ HTML
+ end
+
+ test "unclosed brace block with end should error" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each { |item| %>
+ <%= item %>
+ <% end %>
+ HTML
+ end
+
+ test "closed brace block in single tag is not a block" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.map { |item| item.name } %>
+ HTML
+ end
+
+ test "do/end block works as expected" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each do |item| %>
+ <%= item %>
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/analyze/if_test.rb b/test/analyze/if_test.rb
index 7902ffc18..48f0ac305 100644
--- a/test/analyze/if_test.rb
+++ b/test/analyze/if_test.rb
@@ -134,6 +134,14 @@ class IfTest < Minitest::Spec
HTML
end
+ test "if with yield in condition" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if yield(:a) %>
+ content
+ <% end %>
+ HTML
+ end
+
test "conditional attribute value" do
skip
diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb
index 4306ec697..285b0d8e3 100644
--- a/test/parser/erb_test.rb
+++ b/test/parser/erb_test.rb
@@ -258,5 +258,56 @@ class ERBTest < Minitest::Spec
more %> <% code %>
HTML
end
+
+ test "if/then/else with trimming and nested output tags (real-world RDoc example)" do
+ assert_parsed_snapshot(<<~'HTML')
+ <%- if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then %>
+ ">
+ <%- else %>
+
+ <%- end %>
+ HTML
+ end
+
+ test "if/elsif with block syntax in condition" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if value %>
+
+ <% elsif items.any? { |item| item.true? } %>
+
+ <% end %>
+ HTML
+ end
+
+ test "if/elsif with symbol to proc in condition" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if value %>
+
+ <% elsif items.any?(&:true?) %>
+
+ <% end %>
+ HTML
+ end
+
+ test "if/elsif/else with multiple block conditions and output (real-world form errors)" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if f.object.errors.any? { |e| e.type == :blank } %>
+ Name is required.
+ <% elsif f.object.errors.any? { |e| e.type == :taken } %>
+ Coffee bag with this name and roast date already exists on this roaster.
+ <% else %>
+ <%= f.object.errors.first.message %>
+ <% end %>
+ HTML
+ end
+
+ test "if/elsif/else with assignment and block in condition" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if something = @some.find { |t| t.id == 1 } %>
+ <% elsif other_condition %>
+ <% else %>
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt b/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt
new file mode 100644
index 000000000..10177aa24
--- /dev/null
+++ b/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt
@@ -0,0 +1,36 @@
+---
+source: "Analyze::BlockTest#test_0019_unclosed brace block should error"
+input: |2-
+<% items.each { |item| %>
+ <%= item %>
+<% } %>
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBBlockNode (location: (1:0)-(3:7))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " items.each { |item| " (location: (1:2)-(1:23))
+ │ ├── tag_closing: "%>" (location: (1:23)-(1:25))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:25)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:13))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " item " (location: (2:5)-(2:11))
+ │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:13)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (3:0)-(3:7))
+ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ ├── content: " } " (location: (3:2)-(3:5))
+ │ └── tag_closing: "%>" (location: (3:5)-(3:7))
+ │
+ │
+ └── @ HTMLTextNode (location: (3:7)-(4:0))
+ └── content: "\n"
\ No newline at end of file
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
new file mode 100644
index 000000000..c3504979f
--- /dev/null
+++ b/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt
@@ -0,0 +1,55 @@
+---
+source: "Analyze::BlockTest#test_0020_unclosed brace block with end should error"
+input: |2-
+<% items.each { |item| %>
+ <%= item %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(4:0))
+├── errors: (3 errors)
+│ ├── @ RubyParseError (location: (3:3)-(3:6))
+│ │ ├── message: "unexpected_token_ignore: unexpected 'end', ignoring it"
+│ │ ├── error_message: "unexpected 'end', ignoring it"
+│ │ ├── diagnostic_id: "unexpected_token_ignore"
+│ │ └── level: "syntax"
+│ │
+│ ├── @ RubyParseError (location: (3:9)-(4:0))
+│ │ ├── 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"
+│ │
+│ └── @ RubyParseError (location: (4:0)-(4:0))
+│ ├── 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"
+│ └── level: "syntax"
+│
+└── children: (2 items)
+ ├── @ ERBBlockNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " items.each { |item| " (location: (1:2)-(1:23))
+ │ ├── tag_closing: "%>" (location: (1:23)-(1:25))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:25)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:13))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " item " (location: (2:5)-(2:11))
+ │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:13)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (3:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ ├── content: " end " (location: (3:2)-(3:7))
+ │ └── tag_closing: "%>" (location: (3:7)-(3:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (3:9)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt b/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt
new file mode 100644
index 000000000..8b1dddfe4
--- /dev/null
+++ b/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt
@@ -0,0 +1,16 @@
+---
+source: "Analyze::BlockTest#test_0021_closed brace block in single tag is not a block"
+input: |2-
+<% items.map { |item| item.name } %>
+---
+@ DocumentNode (location: (1:0)-(2:0))
+└── children: (2 items)
+ ├── @ ERBContentNode (location: (1:0)-(1:36))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " items.map { |item| item.name } " (location: (1:2)-(1:34))
+ │ ├── tag_closing: "%>" (location: (1:34)-(1:36))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (1:36)-(2:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt b/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt
new file mode 100644
index 000000000..253da4566
--- /dev/null
+++ b/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt
@@ -0,0 +1,36 @@
+---
+source: "Analyze::BlockTest#test_0022_do/end block works as expected"
+input: |2-
+<% items.each do |item| %>
+ <%= item %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBBlockNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " items.each do |item| " (location: (1:2)-(1:24))
+ │ ├── tag_closing: "%>" (location: (1:24)-(1:26))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:26)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:13))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " item " (location: (2:5)-(2:11))
+ │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:13)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (3:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ ├── content: " end " (location: (3:2)-(3:7))
+ │ └── tag_closing: "%>" (location: (3:7)-(3:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (3:9)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt b/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt
new file mode 100644
index 000000000..3ee603fd9
--- /dev/null
+++ b/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt
@@ -0,0 +1,27 @@
+---
+source: "Analyze::IfTest#test_0014_if with yield in condition"
+input: |2-
+<% if yield(:a) %>
+ content
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if yield(:a) " (location: (1:2)-(1:16))
+ │ ├── tag_closing: "%>" (location: (1:16)-(1:18))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:18)-(3:0))
+ │ │ └── content: "\n content\n"
+ │ │
+ │ ├── subsequent: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (3:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ ├── content: " end " (location: (3:2)-(3:7))
+ │ └── tag_closing: "%>" (location: (3:7)-(3:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (3:9)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt b/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt
new file mode 100644
index 000000000..8c2e17c58
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt
@@ -0,0 +1,165 @@
+---
+source: "Parser::ERBTest#test_0047_if/then/else with trimming and nested output tags (real-world RDoc example)"
+input: |2-
+<%- if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then %>
+ ">
+<%- else %>
+
+<%- end %>
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(5:10))
+ │ ├── tag_opening: "<%-" (location: (1:0)-(1:3))
+ │ ├── content: " if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then " (location: (1:3)-(1:101))
+ │ ├── tag_closing: "%>" (location: (1:101)-(1:103))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:103)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (2:2)-(2:89))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:89))
+ │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3))
+ │ │ │ │ ├── tag_name: "meta" (location: (2:3)-(2:7))
+ │ │ │ │ ├── tag_closing: ">" (location: (2:88)-(2:89))
+ │ │ │ │ ├── children: (2 items)
+ │ │ │ │ │ ├── @ HTMLAttributeNode (location: (2:8)-(2:26))
+ │ │ │ │ │ │ ├── name:
+ │ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (2:8)-(2:12))
+ │ │ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ │ │ └── @ LiteralNode (location: (2:8)-(2:12))
+ │ │ │ │ │ │ │ └── content: "name"
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── equals: "=" (location: (2:12)-(2:13))
+ │ │ │ │ │ │ └── value:
+ │ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (2:13)-(2:26))
+ │ │ │ │ │ │ ├── open_quote: """ (location: (2:13)-(2:14))
+ │ │ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ │ │ └── @ LiteralNode (location: (2:14)-(2:25))
+ │ │ │ │ │ │ │ └── content: "description"
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── close_quote: """ (location: (2:25)-(2:26))
+ │ │ │ │ │ │ └── quoted: true
+ │ │ │ │ │ │
+ │ │ │ │ │ │
+ │ │ │ │ │ └── @ HTMLAttributeNode (location: (2:27)-(2:88))
+ │ │ │ │ │ ├── name:
+ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (2:27)-(2:34))
+ │ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ │ └── @ LiteralNode (location: (2:27)-(2:34))
+ │ │ │ │ │ │ └── content: "content"
+ │ │ │ │ │ │
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── equals: "=" (location: (2:34)-(2:35))
+ │ │ │ │ │ └── value:
+ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (2:35)-(2:88))
+ │ │ │ │ │ ├── open_quote: """ (location: (2:35)-(2:36))
+ │ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ │ └── @ ERBContentNode (location: (2:36)-(2:87))
+ │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (2:36)-(2:39))
+ │ │ │ │ │ │ ├── content: " h "#{@title}: #{excerpt(main_page.comment)}" " (location: (2:39)-(2:85))
+ │ │ │ │ │ │ ├── tag_closing: "%>" (location: (2:85)-(2:87))
+ │ │ │ │ │ │ ├── parsed: true
+ │ │ │ │ │ │ └── valid: true
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── close_quote: """ (location: (2:87)-(2:88))
+ │ │ │ │ │ └── quoted: true
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ └── is_void: false
+ │ │ │ │
+ │ │ │ ├── tag_name: "meta" (location: (2:3)-(2:7))
+ │ │ │ ├── body: []
+ │ │ │ ├── close_tag: ∅
+ │ │ │ ├── is_void: true
+ │ │ │ └── source: "HTML"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:89)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBElseNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%-" (location: (3:0)-(3:3))
+ │ │ ├── content: " else " (location: (3:3)-(3:9))
+ │ │ ├── tag_closing: "%>" (location: (3:9)-(3:11))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (3:11)-(4:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (4:2)-(4:71))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ HTMLOpenTagNode (location: (4:2)-(4:71))
+ │ │ │ │ ├── tag_opening: "<" (location: (4:2)-(4:3))
+ │ │ │ │ ├── tag_name: "meta" (location: (4:3)-(4:7))
+ │ │ │ │ ├── tag_closing: ">" (location: (4:70)-(4:71))
+ │ │ │ │ ├── children: (2 items)
+ │ │ │ │ │ ├── @ HTMLAttributeNode (location: (4:8)-(4:26))
+ │ │ │ │ │ │ ├── name:
+ │ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (4:8)-(4:12))
+ │ │ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ │ │ └── @ LiteralNode (location: (4:8)-(4:12))
+ │ │ │ │ │ │ │ └── content: "name"
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── equals: "=" (location: (4:12)-(4:13))
+ │ │ │ │ │ │ └── value:
+ │ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (4:13)-(4:26))
+ │ │ │ │ │ │ ├── open_quote: """ (location: (4:13)-(4:14))
+ │ │ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ │ │ └── @ LiteralNode (location: (4:14)-(4:25))
+ │ │ │ │ │ │ │ └── content: "description"
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ ├── close_quote: """ (location: (4:25)-(4:26))
+ │ │ │ │ │ │ └── quoted: true
+ │ │ │ │ │ │
+ │ │ │ │ │ │
+ │ │ │ │ │ └── @ HTMLAttributeNode (location: (4:27)-(4:70))
+ │ │ │ │ │ ├── name:
+ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (4:27)-(4:34))
+ │ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ │ └── @ LiteralNode (location: (4:27)-(4:34))
+ │ │ │ │ │ │ └── content: "content"
+ │ │ │ │ │ │
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── equals: "=" (location: (4:34)-(4:35))
+ │ │ │ │ │ └── value:
+ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (4:35)-(4:70))
+ │ │ │ │ │ ├── open_quote: """ (location: (4:35)-(4:36))
+ │ │ │ │ │ ├── children: (2 items)
+ │ │ │ │ │ │ ├── @ LiteralNode (location: (4:36)-(4:54))
+ │ │ │ │ │ │ │ └── content: "Documentation for "
+ │ │ │ │ │ │ │
+ │ │ │ │ │ │ └── @ ERBContentNode (location: (4:54)-(4:69))
+ │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (4:54)-(4:57))
+ │ │ │ │ │ │ ├── content: " h @title " (location: (4:57)-(4:67))
+ │ │ │ │ │ │ ├── tag_closing: "%>" (location: (4:67)-(4:69))
+ │ │ │ │ │ │ ├── parsed: true
+ │ │ │ │ │ │ └── valid: true
+ │ │ │ │ │ │
+ │ │ │ │ │ ├── close_quote: """ (location: (4:69)-(4:70))
+ │ │ │ │ │ └── quoted: true
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ └── is_void: false
+ │ │ │ │
+ │ │ │ ├── tag_name: "meta" (location: (4:3)-(4:7))
+ │ │ │ ├── body: []
+ │ │ │ ├── close_tag: ∅
+ │ │ │ ├── is_void: true
+ │ │ │ └── source: "HTML"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (4:71)-(5:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (5:0)-(5:10))
+ │ ├── tag_opening: "<%-" (location: (5:0)-(5:3))
+ │ ├── content: " end " (location: (5:3)-(5:8))
+ │ └── tag_closing: "%>" (location: (5:8)-(5:10))
+ │
+ │
+ └── @ HTMLTextNode (location: (5:10)-(6:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt b/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt
new file mode 100644
index 000000000..9ba485285
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt
@@ -0,0 +1,40 @@
+---
+source: "Parser::ERBTest#test_0048_if/elsif with block syntax in condition"
+input: |2-
+<% if value %>
+
+<% elsif items.any? { |item| item.true? } %>
+
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if value " (location: (1:2)-(1:12))
+ │ ├── tag_closing: "%>" (location: (1:12)-(1:14))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:14)-(3:0))
+ │ │ └── content: "\n\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBIfNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " elsif items.any? { |item| item.true? } " (location: (3:2)-(3:42))
+ │ │ ├── tag_closing: "%>" (location: (3:42)-(3:44))
+ │ │ ├── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (3:44)-(5:0))
+ │ │ │ └── content: "\n\n"
+ │ │ │
+ │ │ ├── subsequent: ∅
+ │ │ └── end_node: ∅
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (5:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (5:0)-(5:2))
+ │ ├── content: " end " (location: (5:2)-(5:7))
+ │ └── tag_closing: "%>" (location: (5:7)-(5:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (5:9)-(6:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt b/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt
new file mode 100644
index 000000000..547351476
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt
@@ -0,0 +1,40 @@
+---
+source: "Parser::ERBTest#test_0049_if/elsif with symbol to proc in condition"
+input: |2-
+<% if value %>
+
+<% elsif items.any?(&:true?) %>
+
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if value " (location: (1:2)-(1:12))
+ │ ├── tag_closing: "%>" (location: (1:12)-(1:14))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:14)-(3:0))
+ │ │ └── content: "\n\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBIfNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " elsif items.any?(&:true?) " (location: (3:2)-(3:29))
+ │ │ ├── tag_closing: "%>" (location: (3:29)-(3:31))
+ │ │ ├── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (3:31)-(5:0))
+ │ │ │ └── content: "\n\n"
+ │ │ │
+ │ │ ├── subsequent: ∅
+ │ │ └── end_node: ∅
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (5:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (5:0)-(5:2))
+ │ ├── content: " end " (location: (5:2)-(5:7))
+ │ └── tag_closing: "%>" (location: (5:7)-(5:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (5:9)-(6:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt b/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt
new file mode 100644
index 000000000..148687af5
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt
@@ -0,0 +1,61 @@
+---
+source: "Parser::ERBTest#test_0050_if/elsif/else with multiple block conditions and output (real-world form errors)"
+input: |2-
+<% if f.object.errors.any? { |e| e.type == :blank } %>
+ Name is required.
+<% elsif f.object.errors.any? { |e| e.type == :taken } %>
+ Coffee bag with this name and roast date already exists on this roaster.
+<% else %>
+ <%= f.object.errors.first.message %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(8:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(7:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if f.object.errors.any? { |e| e.type == :blank } " (location: (1:2)-(1:52))
+ │ ├── tag_closing: "%>" (location: (1:52)-(1:54))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:54)-(3:0))
+ │ │ └── content: "\n Name is required.\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBIfNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " elsif f.object.errors.any? { |e| e.type == :taken } " (location: (3:2)-(3:55))
+ │ │ ├── tag_closing: "%>" (location: (3:55)-(3:57))
+ │ │ ├── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (3:57)-(5:0))
+ │ │ │ └── content: "\n Coffee bag with this name and roast date already exists on this roaster.\n"
+ │ │ │
+ │ │ ├── subsequent:
+ │ │ │ └── @ ERBElseNode (location: (5:0)-(7:0))
+ │ │ │ ├── tag_opening: "<%" (location: (5:0)-(5:2))
+ │ │ │ ├── content: " else " (location: (5:2)-(5:8))
+ │ │ │ ├── tag_closing: "%>" (location: (5:8)-(5:10))
+ │ │ │ └── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (5:10)-(6:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ ERBContentNode (location: (6:2)-(6:38))
+ │ │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5))
+ │ │ │ │ ├── content: " f.object.errors.first.message " (location: (6:5)-(6:36))
+ │ │ │ │ ├── tag_closing: "%>" (location: (6:36)-(6:38))
+ │ │ │ │ ├── parsed: true
+ │ │ │ │ └── valid: true
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (6:38)-(7:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ │
+ │ │ └── end_node: ∅
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (7:0)-(7:9))
+ │ ├── tag_opening: "<%" (location: (7:0)-(7:2))
+ │ ├── content: " end " (location: (7:2)-(7:7))
+ │ └── tag_closing: "%>" (location: (7:7)-(7:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (7:9)-(8:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt b/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt
new file mode 100644
index 000000000..cddde86ad
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt
@@ -0,0 +1,48 @@
+---
+source: "Parser::ERBTest#test_0051_if/elsif/else with assignment and block in condition"
+input: |2-
+<% if something = @some.find { |t| t.id == 1 } %>
+<% elsif other_condition %>
+<% else %>
+<% end %>
+---
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if something = @some.find { |t| t.id == 1 } " (location: (1:2)-(1:47))
+ │ ├── tag_closing: "%>" (location: (1:47)-(1:49))
+ │ ├── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:49)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBIfNode (location: (2:0)-(3:0))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " elsif other_condition " (location: (2:2)-(2:25))
+ │ │ ├── tag_closing: "%>" (location: (2:25)-(2:27))
+ │ │ ├── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (2:27)-(3:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ ├── subsequent:
+ │ │ │ └── @ ERBElseNode (location: (3:0)-(4:0))
+ │ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ │ ├── content: " else " (location: (3:2)-(3:8))
+ │ │ │ ├── tag_closing: "%>" (location: (3:8)-(3:10))
+ │ │ │ └── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (3:10)-(4:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ │
+ │ │ └── end_node: ∅
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (4:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (4:0)-(4:2))
+ │ ├── content: " end " (location: (4:2)-(4:7))
+ │ └── tag_closing: "%>" (location: (4:7)-(4:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (4:9)-(5:0))
+ └── content: "\n"
\ No newline at end of file