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(),
}