diff --git a/crates/swc_html_ast/src/token.rs b/crates/swc_html_ast/src/token.rs index 867f219f3d9..17fff0e4a8a 100644 --- a/crates/swc_html_ast/src/token.rs +++ b/crates/swc_html_ast/src/token.rs @@ -11,7 +11,7 @@ pub struct TokenAndSpan { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Attribute { pub name: JsWord, - pub value: JsWord, + pub value: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/crates/swc_html_codegen/src/lib.rs b/crates/swc_html_codegen/src/lib.rs index 4b8fd2a7780..3e617555f14 100644 --- a/crates/swc_html_codegen/src/lib.rs +++ b/crates/swc_html_codegen/src/lib.rs @@ -98,17 +98,16 @@ where for attribute in attributes { start_tag.push(' '); start_tag.push_str(&attribute.name); - start_tag.push('='); - let quote = if attribute.value.contains('"') { - '\'' - } else { - '"' - }; + if let Some(value) = &attribute.value { + start_tag.push('='); - start_tag.push(quote); - start_tag.push_str(&attribute.value); - start_tag.push(quote); + let quote = if value.contains('"') { '\'' } else { '"' }; + + start_tag.push(quote); + start_tag.push_str(value); + start_tag.push(quote); + } } if *self_closing { @@ -132,8 +131,16 @@ where for attribute in attributes { start_tag.push(' '); start_tag.push_str(&attribute.name); - start_tag.push('='); - start_tag.push_str(&attribute.value); + + if let Some(value) = &attribute.value { + start_tag.push('='); + + let quote = if value.contains('"') { '\'' } else { '"' }; + + start_tag.push(quote); + start_tag.push_str(value); + start_tag.push(quote); + } } start_tag.push('>'); diff --git a/crates/swc_html_codegen/tests/fixture/attributes/input.html b/crates/swc_html_codegen/tests/fixture/attributes/input.html new file mode 100644 index 00000000000..0edbdc55f8b --- /dev/null +++ b/crates/swc_html_codegen/tests/fixture/attributes/input.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/swc_html_codegen/tests/fixture/attributes/output.html b/crates/swc_html_codegen/tests/fixture/attributes/output.html new file mode 100644 index 00000000000..048642c9f1a --- /dev/null +++ b/crates/swc_html_codegen/tests/fixture/attributes/output.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/swc_html_codegen/tests/fixture/attributes/output.min.html b/crates/swc_html_codegen/tests/fixture/attributes/output.min.html new file mode 100644 index 00000000000..048642c9f1a --- /dev/null +++ b/crates/swc_html_codegen/tests/fixture/attributes/output.min.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/swc_html_parser/src/lexer/mod.rs b/crates/swc_html_parser/src/lexer/mod.rs index 694cb27e089..48fbec8a993 100644 --- a/crates/swc_html_parser/src/lexer/mod.rs +++ b/crates/swc_html_parser/src/lexer/mod.rs @@ -1757,6 +1757,7 @@ where // Start a new attribute in the current tag token. Set that attribute's name // to the current input character, and its value to the empty string. Switch // to the attribute name state. + // We set `None` for `value` to support boolean attributes in AST Some(c @ '=') => { self.emit_error(ErrorKind::UnexpectedEqualsSignBeforeAttributeName); if let Some(ref mut token) = self.cur_token { @@ -1764,13 +1765,13 @@ where Token::StartTag { attributes, .. } => { attributes.push(Attribute { name: c.to_string().into(), - value: "".into(), + value: None, }); } Token::EndTag { attributes, .. } => { attributes.push(Attribute { name: c.to_string().into(), - value: "".into(), + value: None, }); } _ => {} @@ -1781,19 +1782,20 @@ where // Anything else // Start a new attribute in the current tag token. Set that attribute name // and value to the empty string. Reconsume in the attribute name state. + // We set `None` for `value` to support boolean attributes in AST _ => { if let Some(ref mut token) = self.cur_token { match token { Token::StartTag { attributes, .. } => { attributes.push(Attribute { name: "".into(), - value: "".into(), + value: None, }); } Token::EndTag { attributes, .. } => { attributes.push(Attribute { name: "".into(), - value: "".into(), + value: None, }); } _ => {} @@ -1943,6 +1945,7 @@ where // Anything else // Start a new attribute in the current tag token. Set that attribute name // and value to the empty string. Reconsume in the attribute name state. + // We set `None` for `value` to support boolean attributes in AST _ => { if let Some(ref mut token) = self.cur_token { match token { @@ -1950,7 +1953,7 @@ where | Token::EndTag { attributes, .. } => { attributes.push(Attribute { name: "".into(), - value: "".into(), + value: None, }); } _ => {} @@ -1970,9 +1973,7 @@ where // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. - Some('\x09' | '\x0a' | '\x0c' | '\x20') => { - self.state = State::BeforeAttributeName; - } + Some('\x09' | '\x0a' | '\x0c' | '\x20') => {} // U+0022 QUOTATION MARK (") // Switch to the attribute value (double-quoted) state. Some('"') => { @@ -2028,10 +2029,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(REPLACEMENT_CHARACTER); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -2054,10 +2061,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(c); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -2095,10 +2108,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(REPLACEMENT_CHARACTER); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -2121,10 +2140,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(c); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -2171,10 +2196,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(REPLACEMENT_CHARACTER); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -2207,10 +2238,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(c); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} @@ -4416,10 +4453,16 @@ where if let Some(attribute) = attributes.last_mut() { let mut new_value = String::new(); - new_value.push_str(&attribute.value); + match &attribute.value { + Some(value) => { + new_value.push_str(value); + } + None => {} + } + new_value.push(c); - attribute.value = new_value.into(); + attribute.value = Some(new_value.into()); } } _ => {} diff --git a/crates/swc_html_parser/tests/fixture/attribute-without-quotes/span.rust-debug b/crates/swc_html_parser/tests/fixture/attribute-without-quotes/span.rust-debug index 00544df2c68..4f66494caca 100644 --- a/crates/swc_html_parser/tests/fixture/attribute-without-quotes/span.rust-debug +++ b/crates/swc_html_parser/tests/fixture/attribute-without-quotes/span.rust-debug @@ -56,7 +56,7 @@ 5 | This is a link `---- - x StartTag { tag_name: Atom('a' type=inline), self_closing: false, attributes: [Attribute { name: Atom('href' type=inline), value: Atom('https://www.w3schools.com' type=dynamic) }] } + x StartTag { tag_name: Atom('a' type=inline), self_closing: false, attributes: [Attribute { name: Atom('href' type=inline), value: Some(Atom('https://www.w3schools.com' type=dynamic)) }] } ,-[$DIR/tests/fixture/attribute-without-quotes/input.html:5:1] 5 | This is a link : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/swc_html_parser/tests/fixture/images/span.rust-debug b/crates/swc_html_parser/tests/fixture/images/span.rust-debug index e2b47e28a8d..6319b1a2dc4 100644 --- a/crates/swc_html_parser/tests/fixture/images/span.rust-debug +++ b/crates/swc_html_parser/tests/fixture/images/span.rust-debug @@ -415,9 +415,9 @@ 8 | W3Schools.com `---- - x StartTag { tag_name: Atom('img' type=inline), self_closing: false, attributes: [Attribute { name: Atom('src' type=inline), value: Atom('w3schools.jpg' type=dynamic) }, Attribute { name: - | Atom('alt' type=inline), value: Atom('W3Schools.com' type=dynamic) }, Attribute { name: Atom('width' type=inline), value: Atom('104' type=inline) }, Attribute { name: Atom('height' type=inline), - | value: Atom('142' type=inline) }] } + x StartTag { tag_name: Atom('img' type=inline), self_closing: false, attributes: [Attribute { name: Atom('src' type=inline), value: Some(Atom('w3schools.jpg' type=dynamic)) }, Attribute { name: + | Atom('alt' type=inline), value: Some(Atom('W3Schools.com' type=dynamic)) }, Attribute { name: Atom('width' type=inline), value: Some(Atom('104' type=inline)) }, Attribute { name: Atom('height' + | type=inline), value: Some(Atom('142' type=inline)) }] } ,-[$DIR/tests/fixture/images/input.html:8:1] 8 | W3Schools.com : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/swc_html_parser/tests/fixture/script-cdata/span.rust-debug b/crates/swc_html_parser/tests/fixture/script-cdata/span.rust-debug index 5262d84b92c..6780c91978b 100644 --- a/crates/swc_html_parser/tests/fixture/script-cdata/span.rust-debug +++ b/crates/swc_html_parser/tests/fixture/script-cdata/span.rust-debug @@ -8,7 +8,7 @@ 5 | `-> `---- - x StartTag { tag_name: Atom('script' type=inline), self_closing: false, attributes: [Attribute { name: Atom('type' type=static), value: Atom('text/javascript' type=dynamic) }] } + x StartTag { tag_name: Atom('script' type=inline), self_closing: false, attributes: [Attribute { name: Atom('type' type=static), value: Some(Atom('text/javascript' type=dynamic)) }] } ,-[$DIR/tests/fixture/script-cdata/input.html:1:1] 1 |