Skip to content

Commit 5e6a8af

Browse files
bors[bot]totorigolo
andcommitted
Merge #523
523: html!: fix mixed self-closing and non-sc tags r=jstarry a=totorigolo Before, self-closing tags where considered as open tags in `verify_end`, causing codes like this to not compile: ```rust html! { <div> <div/> // <- considered as open tag </div> } ``` ``` error: this open tag has no corresponding close tag --> src/lib.rs:264:17 | ... | <div> | ^^^^^ ``` However, this fix isn't ideal because it peeks the buffer twice for non self-closing tags. I did it that way in order to keep the peek thing. An alternative would be to turn `HtmlSelfClosingTag::peek` into a standard function returning `(ident, is_self_closing)`. Fixes: #522 Co-authored-by: Thomas Lacroix <toto.rigolo@free.fr>
2 parents 4c30319 + 5908d33 commit 5e6a8af

File tree

4 files changed

+152
-3
lines changed

4 files changed

+152
-3
lines changed

crates/macro/src/html_tree/html_tag/mod.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::HtmlPropSuffix as TagSuffix;
66
use super::HtmlTree;
77
use crate::Peek;
88
use boolinator::Boolinator;
9-
use proc_macro2::Span;
9+
use proc_macro2::{Delimiter, Span};
1010
use quote::{quote, quote_spanned, ToTokens};
1111
use syn::buffer::Cursor;
1212
use syn::parse;
@@ -42,6 +42,7 @@ impl Parse for HtmlTag {
4242
}
4343

4444
let open = input.parse::<HtmlTagOpen>()?;
45+
// Return early if it's a self-closing tag
4546
if open.div.is_some() {
4647
return Ok(HtmlTag {
4748
ident: open.ident,
@@ -60,7 +61,7 @@ impl Parse for HtmlTag {
6061
let mut children: Vec<HtmlTree> = vec![];
6162
loop {
6263
if let Some(next_close_ident) = HtmlTagClose::peek(input.cursor()) {
63-
if open.ident.to_string() == next_close_ident.to_string() {
64+
if open.ident == next_close_ident {
6465
break;
6566
}
6667
}
@@ -162,7 +163,9 @@ impl HtmlTag {
162163
fn verify_end(mut cursor: Cursor, open_ident: &Ident) -> bool {
163164
let mut tag_stack_count = 1;
164165
loop {
165-
if let Some(next_open_ident) = HtmlTagOpen::peek(cursor) {
166+
if HtmlSelfClosingTag::peek(cursor).is_some() {
167+
// Do nothing
168+
} else if let Some(next_open_ident) = HtmlTagOpen::peek(cursor) {
166169
if open_ident.to_string() == next_open_ident.to_string() {
167170
tag_stack_count += 1;
168171
}
@@ -185,6 +188,66 @@ impl HtmlTag {
185188
}
186189
}
187190

191+
/// This struct is only used for its Peek implementation in verify_end. Parsing
192+
/// is done with HtmlTagOpen with `div` set to true.
193+
struct HtmlSelfClosingTag;
194+
195+
impl Peek<Ident> for HtmlSelfClosingTag {
196+
fn peek(cursor: Cursor) -> Option<Ident> {
197+
let (punct, cursor) = cursor.punct()?;
198+
(punct.as_char() == '<').as_option()?;
199+
200+
let (ident, cursor) = cursor.ident()?;
201+
(ident.to_string().to_lowercase() == ident.to_string()).as_option()?;
202+
203+
let mut cursor = cursor;
204+
let mut after_slash = false;
205+
loop {
206+
if let Some((punct, next_cursor)) = cursor.punct() {
207+
match punct.as_char() {
208+
'/' => after_slash = true,
209+
'>' if after_slash => return Some(ident),
210+
'>' if !after_slash => {
211+
// We need to read after the '>' for cases like this:
212+
// <div onblur=|_| 2 > 1 />
213+
// ^ in order to handle this
214+
//
215+
// Because those cases are NOT handled by the html!
216+
// macro, so we want nice error messages.
217+
//
218+
// This idea here is that, in valid "JSX", after a tag,
219+
// only '<' or '{ ... }' can follow. (that should be
220+
// enough for reasonable cases)
221+
//
222+
let is_next_lt = next_cursor
223+
.punct()
224+
.map(|(p, _)| p.as_char() == '<')
225+
.unwrap_or(false);
226+
let is_next_brace = next_cursor.group(Delimiter::Brace).is_some();
227+
let no_next = next_cursor.token_tree().is_none();
228+
if is_next_lt || is_next_brace || no_next {
229+
return None;
230+
} else {
231+
// TODO: Use proc-macro's Diagnostic when stable
232+
eprintln!(
233+
"HELP: You must wrap expressions containing \
234+
'>' in braces or parenthesis. See #523."
235+
);
236+
}
237+
}
238+
_ => after_slash = false,
239+
}
240+
cursor = next_cursor;
241+
} else if let Some((_, next)) = cursor.token_tree() {
242+
after_slash = false;
243+
cursor = next;
244+
} else {
245+
return None;
246+
}
247+
}
248+
}
249+
}
250+
188251
struct HtmlTagOpen {
189252
lt: Token![<],
190253
ident: Ident,

tests/macro/html-tag-fail.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ fn compile_fail() {
3030
html! { <input onclick=|| () /> };
3131
html! { <input onclick=|a, b| () /> };
3232
html! { <input onclick=|a: String| () /> };
33+
34+
// This is a known limitation. Put braces or parenthesis around expressions
35+
// that contain '>'.
36+
html! { <div> <div onblur=|_| 2 > 1 /> </div> };
3337
}
3438

3539
fn main() {}

tests/macro/html-tag-fail.stderr

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ error: invalid closure argument
118118
32 | html! { <input onclick=|a: String| () /> };
119119
| ^^^^^^^^^^^
120120

121+
HELP: You must wrap expressions containing '>' in braces or parenthesis. See #523.
122+
error: expected valid html element
123+
--> $DIR/html-tag-fail.rs:36:39
124+
|
125+
36 | html! { <div> <div onblur=|_| 2 > 1 /> </div> };
126+
| ^
127+
121128
error[E0308]: mismatched types
122129
--> $DIR/html-tag-fail.rs:22:28
123130
|

tests/vtag_test.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,48 @@ impl Renderable<Comp> for Comp {
2828
}
2929
}
3030

31+
struct CompInt;
32+
33+
impl Component for CompInt {
34+
type Message = u32;
35+
type Properties = ();
36+
37+
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
38+
CompInt
39+
}
40+
41+
fn update(&mut self, _: Self::Message) -> ShouldRender {
42+
unimplemented!();
43+
}
44+
}
45+
46+
impl Renderable<CompInt> for CompInt {
47+
fn view(&self) -> Html<Self> {
48+
unimplemented!();
49+
}
50+
}
51+
52+
struct CompBool;
53+
54+
impl Component for CompBool {
55+
type Message = bool;
56+
type Properties = ();
57+
58+
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
59+
CompBool
60+
}
61+
62+
fn update(&mut self, _: Self::Message) -> ShouldRender {
63+
unimplemented!();
64+
}
65+
}
66+
67+
impl Renderable<CompBool> for CompBool {
68+
fn view(&self) -> Html<Self> {
69+
unimplemented!();
70+
}
71+
}
72+
3173
#[test]
3274
fn it_compares_tags() {
3375
let a: VNode<Comp> = html! {
@@ -258,3 +300,36 @@ fn it_allows_aria_attributes() {
258300
panic!("vtag expected");
259301
}
260302
}
303+
304+
#[test]
305+
fn it_checks_mixed_closing_tags() {
306+
let a: VNode<Comp> = html! { <div> <div/> </div> };
307+
let b: VNode<Comp> = html! { <div> <div></div> </div> };
308+
assert_eq!(a, b);
309+
310+
let a: VNode<Comp> = html! { <div> <div data-val={ 2 / 1 }/> </div> };
311+
let b: VNode<Comp> = html! { <div> <div data-val={ 2 }></div> </div> };
312+
assert_eq!(a, b);
313+
314+
let a: VNode<Comp> = html! { <div> <div data-val={ 2 > 1 }/> </div> };
315+
let b: VNode<Comp> = html! { <div> <div data-val={ true }></div> </div> };
316+
assert_eq!(a, b);
317+
318+
let a: VNode<CompInt> = html! { <div> <div onblur=|_| 2 / 1/> </div> };
319+
let b: VNode<CompInt> = html! { <div> <div onblur=|_| 3></div> </div> };
320+
assert_eq!(a, b); // NB: assert_eq! doesn't (cannot) compare the closures
321+
322+
// This is a known limitation of the html! macro:
323+
//
324+
// html! { <div> <img onblur=|_| 2 > 1 /> </div> }
325+
//
326+
// You have to put braces or parenthesis around the expression:
327+
//
328+
// html! { <div> <img onblur=|_| { 2 > 1 } /> </div> }
329+
//
330+
let a: VNode<CompBool> = html! { <div> <div onblur=|_| { 2 > 1 } /> </div> };
331+
let b: VNode<CompBool> = html! { <div> <div onblur=|_| ( 2 > 1 ) /> </div> };
332+
let c: VNode<CompBool> = html! { <div> <div onblur=|_| false></div> </div> };
333+
assert_eq!(a, c); // NB: assert_eq! doesn't (cannot) compare the closures
334+
assert_eq!(b, c);
335+
}

0 commit comments

Comments
 (0)