diff --git a/crates/zensical/src/compat/mkdocs/plugin/minify/html.rs b/crates/zensical/src/compat/mkdocs/plugin/minify/html.rs index 8aec48b..6417914 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/minify/html.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/minify/html.rs @@ -245,6 +245,23 @@ mod tests { ); } + #[test] + fn separates_self_closing_tags_from_unquoted_attribute_values() { + let input = concat!( + r#""#, + r#""#, + r#""#, + ); + assert_eq!( + minify(input, &options(), false, false), + concat!( + r#""#, + r#""#, + r#""#, + ) + ); + } + #[test] fn minifies_supported_inline_languages() { let input = r#" diff --git a/crates/zensical/src/compat/mkdocs/plugin/minify/html/serializer.rs b/crates/zensical/src/compat/mkdocs/plugin/minify/html/serializer.rs index 33ed55f..36d3463 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/minify/html/serializer.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/minify/html/serializer.rs @@ -229,6 +229,7 @@ impl<'a> Serializer<'a> { self.output.push_str(&tag.output_name); let mut preserve = false; + let mut unquoted = false; for attribute in &tag.attributes { // A configured prefix protects an attribute from normalization; // the prefix itself is omitted from rendered output. @@ -265,6 +266,7 @@ impl<'a> Serializer<'a> { self.output.push(' '); self.output.push_str(output_name); let Some(value) = &attribute.value else { + unquoted = false; continue; }; @@ -274,6 +276,7 @@ impl<'a> Serializer<'a> { || (self.options.reduce_boolean_attributes && is_boolean_attribute(&tag.name, name)) { + unquoted = false; continue; } @@ -285,7 +288,7 @@ impl<'a> Serializer<'a> { } else { value.decoded.as_str() }; - serialize_attribute_value( + unquoted = serialize_attribute_value( &mut self.output, value, self.options.remove_optional_attribute_quotes, @@ -294,6 +297,11 @@ impl<'a> Serializer<'a> { } if self_closing && !is_void(&tag.name) { + // Without whitespace, the solidus is part of an unquoted value, + // so the HTML parser does not acknowledge the self-closing flag. + if unquoted { + self.output.push(' '); + } self.output.push_str("/>"); } else { self.output.push('>'); diff --git a/crates/zensical/src/compat/mkdocs/plugin/minify/html/syntax.rs b/crates/zensical/src/compat/mkdocs/plugin/minify/html/syntax.rs index ec63c8c..7a04eed 100644 --- a/crates/zensical/src/compat/mkdocs/plugin/minify/html/syntax.rs +++ b/crates/zensical/src/compat/mkdocs/plugin/minify/html/syntax.rs @@ -148,17 +148,19 @@ pub fn escape_text(output: &mut String, value: &str) { } /// Serializes an attribute value with minimal safe quoting. +/// +/// Returns whether the serialized value is unquoted. pub fn serialize_attribute_value( output: &mut String, value: &str, remove_quotes: bool, preserve_references: bool, -) { +) -> bool { if remove_quotes && !value.is_empty() && value.chars().all(is_unquoted) { if preserve_references { output.push_str(value); } else { escape_ampersands(output, value); } - return; + return true; } let single = value.matches('\'').count(); @@ -174,6 +176,7 @@ pub fn serialize_attribute_value( } } output.push(quote); + false } /// Escapes ampersands in an attribute value.