Skip to content

Commit 892f049

Browse files
authored
Parser: Introduce StrayERBClosingTagError (#1256)
This pull request introduces a new `StrayERBClosingTagError` that detects `%>` appearing outside of ERB tags. Inside an open tag, the `%>` is always reported as an error. The parser consumes both tokens, adds a `LiteralNode` to preserve the source, and continues parsing the tag. Therefore subsequent attributes and the real `>` closing are not lost anymore: ```html+erb <div class="foo" %> id="bar"> ``` At the document top level, the `%>` is reported only in strict mode. In non-strict mode it is silently absorbed as text, matching Erubi's behavior for backwards compatibility, if needed. ```html+erb <% content %> %> ``` The error message suggests using `&percnt;&gt;` HTML entities for cases where a literal `%>` is intended.
1 parent faf5af8 commit 892f049

File tree

22 files changed

+793
-0
lines changed

22 files changed

+793
-0
lines changed

β€Žconfig.ymlβ€Ž

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,15 @@ errors:
307307
- name: opening_tag
308308
type: token
309309

310+
- name: StrayERBClosingTagError
311+
message:
312+
template: "Stray `%%>` found at (%u:%u). This closing delimiter is not part of an ERB tag and will be treated as plain text. If you want a literal `%%>`, use the HTML entities `&percnt;&gt;` instead."
313+
arguments:
314+
- start.line
315+
- start.column
316+
317+
fields: []
318+
310319
- name: NestedERBTagError
311320
message:
312321
template: "ERB tag `%s` at (%u:%u) was terminated by nested `<%%` tag at (%zu:%zu). Nesting `<%%` tags is not supported."

β€Žsig/herb/errors.rbsβ€Ž

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žsrc/parser.cβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,31 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_arra
275275
return NULL;
276276
}
277277

278+
if (parser->options.strict && parser->current_token->type == TOKEN_PERCENT) {
279+
lexer_T lexer_copy = *parser->lexer;
280+
token_T* peek_token = lexer_next_token(&lexer_copy);
281+
282+
if (peek_token && peek_token->type == TOKEN_HTML_TAG_END) {
283+
position_T stray_start = parser->current_token->location.start;
284+
position_T stray_end = peek_token->location.end;
285+
token_free(peek_token);
286+
287+
append_strayerb_closing_tag_error(stray_start, stray_end, document_errors);
288+
289+
token_T* percent = parser_advance(parser);
290+
hb_buffer_append(&content, percent->value);
291+
token_free(percent);
292+
293+
token_T* gt = parser_advance(parser);
294+
hb_buffer_append(&content, gt->value);
295+
token_free(gt);
296+
297+
continue;
298+
}
299+
300+
token_free(peek_token);
301+
}
302+
278303
token_T* token = parser_advance(parser);
279304
hb_buffer_append(&content, token->value);
280305
token_free(token);
@@ -1004,6 +1029,32 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
10041029
token_free(next_token);
10051030
}
10061031

1032+
if (parser->current_token->type == TOKEN_PERCENT) {
1033+
lexer_T lexer_copy = *parser->lexer;
1034+
token_T* peek_token = lexer_next_token(&lexer_copy);
1035+
1036+
if (peek_token && peek_token->type == TOKEN_HTML_TAG_END) {
1037+
position_T stray_start = parser->current_token->location.start;
1038+
position_T stray_end = peek_token->location.end;
1039+
token_free(peek_token);
1040+
1041+
append_strayerb_closing_tag_error(stray_start, stray_end, errors);
1042+
1043+
token_T* percent = parser_advance(parser);
1044+
token_T* gt = parser_advance(parser);
1045+
1046+
AST_LITERAL_NODE_T* literal = ast_literal_node_init("%>", stray_start, stray_end, NULL);
1047+
hb_array_append(children, literal);
1048+
1049+
token_free(percent);
1050+
token_free(gt);
1051+
1052+
continue;
1053+
}
1054+
1055+
token_free(peek_token);
1056+
}
1057+
10071058
parser_append_unexpected_error(
10081059
parser,
10091060
"Unexpected Token",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../test_helper"
4+
5+
module Parser
6+
class StrayERBClosingTagTest < Minitest::Spec
7+
include SnapshotUtils
8+
9+
test "stray %> in open tag after unquoted ERB attribute value" do
10+
template = <<~HTML
11+
<div id=<%= dom_id(@slot) %> %>
12+
HTML
13+
14+
assert_parsed_snapshot(template, strict: true)
15+
assert_parsed_snapshot(template, strict: false)
16+
end
17+
18+
test "stray %> in open tag after quoted ERB attribute value" do
19+
template = <<~HTML
20+
<div id="<%= dom_id(@slot) %>" %>
21+
HTML
22+
23+
assert_parsed_snapshot(template, strict: true)
24+
assert_parsed_snapshot(template, strict: false)
25+
end
26+
27+
test "stray %> in open tag with no preceding ERB tag" do
28+
template = <<~HTML
29+
<div %>
30+
HTML
31+
32+
assert_parsed_snapshot(template, strict: true)
33+
assert_parsed_snapshot(template, strict: false)
34+
end
35+
36+
test "stray %> at document top level" do
37+
template = <<~HTML
38+
hello %> world
39+
HTML
40+
41+
assert_parsed_snapshot(template, strict: true)
42+
assert_parsed_snapshot(template, strict: false)
43+
end
44+
45+
test "stray %> at document top level after ERB tag" do
46+
template = <<~HTML
47+
<% content %> %>
48+
HTML
49+
50+
assert_parsed_snapshot(template, strict: true)
51+
assert_parsed_snapshot(template, strict: false)
52+
end
53+
54+
test "%> inside quoted attribute value is not stray" do
55+
template = <<~HTML
56+
<div class="foo %> bar">hello</div>
57+
HTML
58+
59+
assert_parsed_snapshot(template, strict: true)
60+
assert_parsed_snapshot(template, strict: false)
61+
end
62+
63+
test "multiple stray %> in open tag" do
64+
template = <<~HTML
65+
<div %> %>
66+
HTML
67+
68+
assert_parsed_snapshot(template, strict: true)
69+
assert_parsed_snapshot(template, strict: false)
70+
end
71+
72+
test "stray %> between attributes in open tag" do
73+
template = <<~HTML
74+
<div class="foo" %> id="bar"></div>
75+
HTML
76+
77+
assert_parsed_snapshot(template, strict: true)
78+
assert_parsed_snapshot(template, strict: false)
79+
end
80+
end
81+
end

β€Žtest/snapshots/parser/erb_test/test_0038_erb_tag_followed_by_literal_closing_delimiter_15e84c87986bd8a2aba06dcee2be46b8.txtβ€Ž

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žtest/snapshots/parser/error_recovery_test/test_0012_nested_ERB_tag_inside_ERB_comment_6056f70bdc685fbeb3e960f8bff735c2.txtβ€Ž

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žtest/snapshots/parser/stray_erb_closing_tag_test/test_0001_stray_%gt_in_open_tag_after_unquoted_ERB_attribute_value_c4a5f2fd004a3774105147a3180c68be-08996694f7ff1e6e34277dc32f646630.txtβ€Ž

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žtest/snapshots/parser/stray_erb_closing_tag_test/test_0001_stray_%gt_in_open_tag_after_unquoted_ERB_attribute_value_c4a5f2fd004a3774105147a3180c68be-2ffd01e26b8c99a6c7a6dad67c9489dc.txtβ€Ž

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žtest/snapshots/parser/stray_erb_closing_tag_test/test_0002_stray_%gt_in_open_tag_after_quoted_ERB_attribute_value_48f9d68f5fe0c3029c21c387e097b518-08996694f7ff1e6e34277dc32f646630.txtβ€Ž

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
Β (0)