Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to have non void self closing HTML tags #123

Merged
merged 9 commits into from
Jul 15, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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)
Expand Down
92 changes: 87 additions & 5 deletions formatter/src/formatter/element.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::formatter::Formatter;
use crate::{formatter::Formatter, ClosingTagStyle};

use rstml::node::{Node, NodeAttribute, NodeElement};
use syn::spanned::Spanned;
Expand All @@ -7,22 +7,25 @@ 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());
V4ldum marked this conversation as resolved.
Show resolved Hide resolved
self.opening_tag(element, is_void);
let is_self_closing = is_self_closing(is_void, self.settings.closing_tag_style);
let is_empty = element.children.is_empty();

if !is_void {
self.opening_tag(element, is_self_closing, is_empty);

if !(is_self_closing && is_empty) {
self.children(&element.children, element.attributes().len());
self.flush_comments(element.close_tag.span().end().line - 1);
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, is_empty: bool) {
self.printer.word("<");
self.node_name(&element.open_tag.name);

self.attributes(element.attributes());

if is_void {
if is_self_closing && is_empty {
self.printer.word("/>");
} else {
self.printer.word(">")
Expand Down Expand Up @@ -138,11 +141,17 @@ fn is_void_element(name: &str, has_children: bool) -> bool {
}
}

fn is_self_closing(is_void: bool, closing_tag_style: ClosingTagStyle) -> bool {
V4ldum marked this conversation as resolved.
Show resolved Hide resolved
closing_tag_style == ClosingTagStyle::SelfClosing
|| closing_tag_style == ClosingTagStyle::Preserve && is_void
}

#[cfg(test)]
mod tests {
use indoc::indoc;

use crate::{
formatter::ClosingTagStyle,
formatter::FormatterSettings,
test_helpers::{element, format_element_from_string, format_with},
};
Expand All @@ -155,6 +164,14 @@ mod tests {
})
}};
}
macro_rules! format_element_with_self_closing_tag {
V4ldum marked this conversation as resolved.
Show resolved Hide resolved
($($tt:tt)*) => {{
let element = element! { $($tt)* };
format_with(FormatterSettings { max_width: 40, closing_tag_style: ClosingTagStyle::SelfClosing, ..Default::default() }, |formatter| {
formatter.element(&element)
})
}};
}
macro_rules! format_element_from_string {
($val:expr) => {{
format_element_from_string(
Expand Down Expand Up @@ -409,4 +426,69 @@ mod tests {
</div>
"#);
}

// Self Closing Tag
V4ldum marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn self_closing_void_element_no_children_separate_closing_tag() {
let formatted = format_element_with_self_closing_tag! { < input >< / input > };
insta::assert_snapshot!(formatted, @"<input/>");
V4ldum marked this conversation as resolved.
Show resolved Hide resolved
}

#[test]
fn self_closing_void_element_no_children_self_closing_tag() {
let formatted = format_element_with_self_closing_tag! { < input / > };
insta::assert_snapshot!(formatted, @"<input/>");
}

#[test]
fn self_closing_non_void_element_with_child() {
let formatted = format_element_with_self_closing_tag! { < div > "Child" < / div > };
insta::assert_snapshot!(formatted, @r#"<div>"Child"</div>"#);
}

#[test]
fn self_closing_non_void_element_no_children_separate_closing_tag() {
let formatted = format_element_with_self_closing_tag! { < div >< / div > };
insta::assert_snapshot!(formatted, @"<div/>");
}

#[test]
fn self_closing_non_void_element_no_children_self_closing_tag() {
let formatted = format_element_with_self_closing_tag! { < div / > };
insta::assert_snapshot!(formatted, @"<div/>");
}

// Non Self Closing Tag
// TODO uncomment when macro is properly working

// #[test]
// fn non_self_closing_void_element_no_children_separate_closing_tag() {
// let formatted = format_element_with_self_closing_tag! { < input >< / input > };
// insta::assert_snapshot!(formatted, @"<input></input>");
// }

// #[test]
// fn non_self_closing_void_element_no_children_self_closing_tag() {
// let formatted = format_element_with_self_closing_tag! { < input / > };
// insta::assert_snapshot!(formatted, @"<input></input>");
// }

// #[test]
// fn non_self_closing_non_void_element_with_child() {
// let formatted = format_element_with_self_closing_tag! { < div > "Child" < / div > };
// insta::assert_snapshot!(formatted, @r#"<div>"Hello"</div>"#);
// }

// #[test]
// fn non_self_closing_non_void_element_no_children_separate_closing_tag() {
// let formatted = format_element_with_self_closing_tag! { < div >< / div > };
// insta::assert_snapshot!(formatted, @"<div></div>");
// }

// #[test]
// fn non_self_closing_non_void_element_no_children_self_closing_tag() {
// let formatted = format_element_with_self_closing_tag! { < div / > };
// insta::assert_snapshot!(formatted, @"<div></div>");
// }
}
14 changes: 14 additions & 0 deletions formatter/src/formatter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<div></div>` formats to `<div />`
SelfClosing,
/// Separate closing tag for elements with no children: `<div />` formats to `<div></div>`
NonSelfClosing,
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum AttributeValueBraceStyle {
Always,
Expand Down Expand Up @@ -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<String>,

Expand All @@ -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(),
}
Expand Down