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 |
`----
- 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 |
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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 |