diff --git a/README.md b/README.md index bbe3f81..03e7e77 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ indentation_style = "Auto" # "Tabs", "Spaces" or "Auto" newline_style = "Auto" # "Unix", "Windows" or "Auto" attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve" macro_names = [ "leptos::view", "view" ] # Macro names which will be formatted +closing_tag_style = "Preserve" # "Preserve", "SelfClosing" or "NonSelfClosing" # Attribute values can be formatted by custom formatters # Every attribute name may only select one formatter (this might change later on) diff --git a/formatter/src/formatter/element.rs b/formatter/src/formatter/element.rs index 7adc51b..df811dc 100644 --- a/formatter/src/formatter/element.rs +++ b/formatter/src/formatter/element.rs @@ -1,4 +1,4 @@ -use crate::formatter::Formatter; +use crate::{formatter::Formatter, ClosingTagStyle}; use rstml::node::{Node, NodeAttribute, NodeElement}; use syn::spanned::Spanned; @@ -6,24 +6,25 @@ use syn::spanned::Spanned; impl Formatter<'_> { pub fn element(&mut self, element: &NodeElement) { let name = element.name().to_string(); - let is_void = is_void_element(&name, !element.children.is_empty()); - self.opening_tag(element, is_void); + let is_self_closing = is_self_closing(element, &name, self.settings.closing_tag_style); - if !is_void { + self.opening_tag(element, is_self_closing); + + if !is_self_closing { self.children(&element.children, element.attributes().len()); self.flush_comments(element.close_tag.span().end().line - 1, true); self.closing_tag(element) } } - fn opening_tag(&mut self, element: &NodeElement, is_void: bool) { + fn opening_tag(&mut self, element: &NodeElement, is_self_closing: bool) { self.printer.word("<"); self.node_name(&element.open_tag.name); leptosfmt_prettyplease::unparse_generics(&element.open_tag.generics, self.printer); - self.attributes(element.attributes()); + self.attributes(element.attributes(), is_self_closing); - if is_void { + if is_self_closing { self.printer.word("/>"); } else { self.printer.word(">") @@ -36,31 +37,43 @@ impl Formatter<'_> { self.printer.word(">"); } - fn attributes(&mut self, attributes: &[NodeAttribute]) { - if attributes.is_empty() { - return; - } + fn attributes(&mut self, attributes: &[NodeAttribute], trailing_space: bool) { + match attributes { + [] => { + if trailing_space { + self.printer.nbsp(); + } + } + [attribute] => { + self.printer.cbox(0); + self.printer.nbsp(); + self.attribute(attribute); - if let [attribute] = attributes { - self.printer.cbox(0); - self.printer.nbsp(); - self.attribute(attribute); - self.printer.end(); - } else { - self.printer.cbox_indent(); - self.printer.space(); + if trailing_space { + self.printer.nbsp(); + } + self.printer.end(); + } + _ => { + self.printer.cbox_indent(); + self.printer.space(); - let mut iter = attributes.iter().peekable(); - while let Some(attr) = iter.next() { - self.attribute(attr); + let mut iter = attributes.iter().peekable(); + while let Some(attr) = iter.next() { + self.attribute(attr); - if iter.peek().is_some() { - self.printer.space() + if iter.peek().is_some() { + self.printer.space() + } } - } - self.printer.zerobreak(); - self.printer.end_dedent(); + if trailing_space { + self.printer.space(); // Only results in a space if the consistent box didn't break + } else { + self.printer.zerobreak(); + } + self.printer.end_dedent(); + } } } @@ -115,27 +128,40 @@ impl Formatter<'_> { } } -fn is_void_element(name: &str, has_children: bool) -> bool { - if name.chars().next().unwrap().is_uppercase() { - !has_children - } else { - matches!( - name, - "area" - | "base" - | "br" - | "col" - | "embed" - | "hr" - | "img" - | "input" - | "link" - | "meta" - | "param" - | "source" - | "track" - | "wbr" - ) +fn is_void_element(name: &str) -> bool { + matches!( + name, + "area" + | "base" + | "br" + | "col" + | "embed" + | "hr" + | "img" + | "input" + | "link" + | "meta" + | "param" + | "source" + | "track" + | "wbr" + ) +} + +fn is_self_closing(element: &NodeElement, name: &str, closing_tag_style: ClosingTagStyle) -> bool { + if !element.children.is_empty() { + return false; + } + + if is_void_element(name) { + return true; + }; + + // At this point, it must be a non-void element that has no children + match closing_tag_style { + ClosingTagStyle::Preserve => element.close_tag.is_none(), + ClosingTagStyle::SelfClosing => true, + ClosingTagStyle::NonSelfClosing => false, } } @@ -144,18 +170,35 @@ mod tests { use indoc::indoc; use crate::{ + formatter::ClosingTagStyle, formatter::FormatterSettings, test_helpers::{element, format_element_from_string, format_with}, }; macro_rules! format_element { ($($tt:tt)*) => {{ + format_element_with!(Default::default(), $($tt)*) + }}; + } + + macro_rules! format_element_with_closing_style { + ($style:expr, $($tt:tt)*) => {{ + format_element_with!(FormatterSettings { + closing_tag_style: $style, + ..Default::default() + }, $($tt)*) + }}; + } + + macro_rules! format_element_with { + ($settings:expr, $($tt:tt)*) => {{ let element = element! { $($tt)* }; - format_with(FormatterSettings { max_width: 40, ..Default::default() }, |formatter| { + format_with(FormatterSettings { max_width: 40, ..$settings }, |formatter| { formatter.element(&element) }) }}; } + macro_rules! format_element_from_string { ($val:expr) => {{ format_element_from_string( @@ -333,17 +376,17 @@ mod tests { fn single_empty_line() { let formatted = format_element_from_string!(indoc! {r#"
-
"#}); insta::assert_snapshot!(formatted, @r###"
-
"###); } @@ -352,19 +395,19 @@ mod tests { fn multiple_empty_lines() { let formatted = format_element_from_string!(indoc! {r#"
-
"#}); insta::assert_snapshot!(formatted, @r###"
-
"###); } @@ -374,16 +417,16 @@ mod tests { let formatted = format_element_from_string!(indoc! {r#"
-
"#}); insta::assert_snapshot!(formatted, @r###"
-
"###); } @@ -414,6 +457,111 @@ mod tests { #[test] fn with_generics() { let formatted = format_element! { /> }; - insta::assert_snapshot!(formatted, @"/>"); + insta::assert_snapshot!(formatted, @" />"); + } + + // Closing Tags Behaviour + + #[test] + fn void_element_no_children_separate_closing_tag() { + let preserve_formatted = + format_element_with_closing_style! { ClosingTagStyle::Preserve, < input >< / input > }; + let self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < input >< / input > }; + let non_self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < input >< / input > }; + + insta::assert_snapshot!(preserve_formatted, @""); + insta::assert_snapshot!(self_closing_formatted, @""); + insta::assert_snapshot!(non_self_closing_formatted, @""); + } + + #[test] + fn void_element_no_children_self_closing_tag_one_line() { + let preserve_formatted = + format_element_with_closing_style! { ClosingTagStyle::Preserve, < input / > }; + let self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < input / > }; + let non_self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < input / > }; + + insta::assert_snapshot!(preserve_formatted, @""); + insta::assert_snapshot!(self_closing_formatted, @""); + insta::assert_snapshot!(non_self_closing_formatted, @""); + } + + #[test] + fn void_element_no_children_self_closing_tag_single_attr() { + let preserve_formatted = + format_element_with_closing_style! { ClosingTagStyle::Preserve, < input key=1 / > }; + let self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < input key=1 / > }; + let non_self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < input key=1 / > }; + + insta::assert_snapshot!(preserve_formatted, @""); + insta::assert_snapshot!(self_closing_formatted, @""); + insta::assert_snapshot!(non_self_closing_formatted, @""); + } + + #[test] + fn void_element_no_children_self_closing_tag_multi_line() { + let preserve_formatted = format_element_with_closing_style! { ClosingTagStyle::Preserve, < input key=1 class="veryveryvery longlonglong attributesattributesattributes listlistlist" / > }; + let self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < input key=1 class="veryveryvery longlonglong attributesattributesattributes listlistlist" / > }; + let non_self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < input key=1 class="veryveryvery longlonglong attributesattributesattributes listlistlist" / > }; + + insta::assert_snapshot!(preserve_formatted, @r#" + + "#); + insta::assert_snapshot!(self_closing_formatted, @r#" + + "#); + insta::assert_snapshot!(non_self_closing_formatted, @r#" + + "#); + } + + #[test] + fn non_void_element_with_child() { + let preserve_formatted = format_element_with_closing_style! { ClosingTagStyle::Preserve, < div > "Child" < / div > }; + let self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < div > "Child" < / div > }; + let non_self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < div > "Child" < / div > }; + + insta::assert_snapshot!(preserve_formatted, @r#"
"Child"
"#); + insta::assert_snapshot!(self_closing_formatted, @r#"
"Child"
"#); + insta::assert_snapshot!(non_self_closing_formatted, @r#"
"Child"
"#); + } + + #[test] + fn non_void_element_no_children_separate_closing_tag() { + let preserve_formatted = + format_element_with_closing_style! { ClosingTagStyle::Preserve, < div >< / div > }; + let self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < div >< / div > }; + let non_self_closing_formatted = format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < div >< / div > }; + + insta::assert_snapshot!(preserve_formatted, @"
"); + insta::assert_snapshot!(self_closing_formatted, @"
"); + insta::assert_snapshot!(non_self_closing_formatted, @"
"); + } + + #[test] + fn non_void_element_no_children_self_closing_tag() { + let preserve_formatted = + format_element_with_closing_style! { ClosingTagStyle::Preserve, < div / > }; + let self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::SelfClosing, < div / > }; + let non_self_closing_formatted = + format_element_with_closing_style! { ClosingTagStyle::NonSelfClosing, < div / > }; + + insta::assert_snapshot!(preserve_formatted, @"
"); + insta::assert_snapshot!(self_closing_formatted, @"
"); + insta::assert_snapshot!(non_self_closing_formatted, @"
"); } } diff --git a/formatter/src/formatter/mod.rs b/formatter/src/formatter/mod.rs index 5f5f15e..a2d1420 100644 --- a/formatter/src/formatter/mod.rs +++ b/formatter/src/formatter/mod.rs @@ -19,6 +19,16 @@ pub use mac::{ParentIndent, ViewMacro}; use serde::Deserialize; use serde::Serialize; +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub enum ClosingTagStyle { + /// Preserve the original closing tag style (self-closing or a separate closing tag) + Preserve, + /// Self closing tag for elements with no children: `
` formats to `
` + SelfClosing, + /// Separate closing tag for elements with no children: `
` formats to `
` + NonSelfClosing, +} + #[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] pub enum AttributeValueBraceStyle { Always, @@ -73,6 +83,9 @@ pub struct FormatterSettings { /// Determines placement of braces around single expression attribute values pub attr_value_brace_style: AttributeValueBraceStyle, + /// Preferred style for closing tags (self-closing or not) when a non-void element has no children + pub closing_tag_style: ClosingTagStyle, + /// Determines macros to be formatted. Default: leptos::view, view pub macro_names: Vec, @@ -88,6 +101,7 @@ impl Default for FormatterSettings { attr_value_brace_style: AttributeValueBraceStyle::WhenRequired, indentation_style: IndentationStyle::Auto, newline_style: NewlineStyle::Auto, + closing_tag_style: ClosingTagStyle::Preserve, macro_names: vec!["leptos::view".to_string(), "view".to_string()], attr_values: HashMap::new(), }