diff --git a/src/html/tag.rs b/src/html/tag.rs index 20ad90db..6406785f 100644 --- a/src/html/tag.rs +++ b/src/html/tag.rs @@ -105,6 +105,29 @@ macro_rules! tag_is_one_of { }; } +impl Tag { + /// Whether `name` is one of the [void elements] of the HTML namespace + /// (plus the obsolete ones the tree builder still treats as void). + /// + /// [void elements]: https://html.spec.whatwg.org/multipage/syntax.html#void-elements + #[inline] + #[must_use] + pub(crate) fn is_void_html_element(name: &impl PartialEq) -> bool { + // NOTE: fast path for the most commonly used elements + if tag_is_one_of!(*name, [Div, A, Span, Li]) { + return false; + } + + tag_is_one_of!( + *name, + [ + Area, Base, Basefont, Bgsound, Br, Col, Embed, Hr, Img, Input, Keygen, Link, Meta, + Param, Source, Track, Wbr + ] + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/rewritable_units/element.rs b/src/rewritable_units/element.rs index d9da98b8..743cb9f5 100644 --- a/src/rewritable_units/element.rs +++ b/src/rewritable_units/element.rs @@ -5,6 +5,7 @@ use super::{ }; use crate::HandlerResult; use crate::base::{BytesCow, SourceLocation}; +use crate::html::{LocalNameHash, Namespace, Tag}; use crate::rewriter::{HandlerTypes, LocalHandlerTypes}; use encoding_rs::Encoding; use std::any::Any; @@ -128,18 +129,31 @@ impl<'rewriter, 'input_token, H: HandlerTypes> Element<'rewriter, 'input_token, /// Sets the tag name of the element. /// - /// The new tag name must be in the same namespace, have the same content model, and be valid in its location. + /// The new tag name must be in the same namespace and be valid in its location. /// Otherwise change of the tag name may cause the resulting document to be parsed in an unexpected way, /// out of sync with this library. + /// + /// If the element is a [void element] (``, `
`, ...) and the new name is not, an end tag + /// `` is written right after the start tag, so that the renamed element stays empty instead of + /// swallowing the siblings that follow it. The element still [can't have content][Self::can_have_content]: + /// [`prepend`][Self::prepend], [`append`][Self::append] and [`set_inner_content`][Self::set_inner_content] + /// remain no-ops, and the written end tag doesn't run [end tag handlers][Self::end_tag_handlers]. + /// + /// [void element]: https://html.spec.whatwg.org/multipage/syntax.html#void-elements #[inline] pub fn set_tag_name(&mut self, name: &str) -> Result<(), TagNameError> { - let name = self.tag_name_bytes_from_str(name)?; + let name_bytes = self.tag_name_bytes_from_str(name)?; if self.can_have_content { - self.modified_end_tag_name = Some((*name).into()); + self.modified_end_tag_name = Some((*name_bytes).into()); + } else if self.start_tag.namespace() == Namespace::Html { + // A self-closing tag in foreign content is closed by its `/>` whatever its name is, + // but an HTML void element is closed by its name alone. + let emit_end_tag = !Tag::is_void_html_element(&LocalNameHash::from(name)); + self.start_tag.set_emit_end_tag(emit_end_tag); } - self.start_tag.set_name_raw(name); + self.start_tag.set_name_raw(name_bytes); Ok(()) } @@ -905,8 +919,8 @@ mod tests { ); assert_eq!( out, - " - --> + " + -->
" ); } @@ -922,8 +936,8 @@ mod tests { ); assert_eq!( out, - "

--> -

-->" + "

--> +

-->" ); } @@ -935,7 +949,10 @@ mod tests { "img", |el| el.set_tag_name("we-have-scripts").unwrap(), ); - assert_eq!(out, r#"