Skip to content

Commit

Permalink
feat: customisable self-closing tag behaviour on non-void elements (#123
Browse files Browse the repository at this point in the history
)

* Add possibility to have non void self closing HTML tags

* Update README.md

* Fix forgotten usecase & add more tests

* Use an enum to set the formatting style & prune the tests

* Add space before "/>" & Update test macros and is_self_closing behaviour

* Fix conflict

* Update new test

* Fix space for self closing tag & add test for formatting with one attribute
  • Loading branch information
V4ldum authored Jul 15, 2024
1 parent 35d7ae0 commit 148869b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 62 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
272 changes: 210 additions & 62 deletions formatter/src/formatter/element.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
use crate::formatter::Formatter;
use crate::{formatter::Formatter, ClosingTagStyle};

use rstml::node::{Node, NodeAttribute, NodeElement};
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(">")
Expand All @@ -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();
}
}
}

Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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(
Expand Down Expand Up @@ -333,17 +376,17 @@ mod tests {
fn single_empty_line() {
let formatted = format_element_from_string!(indoc! {r#"
<div>
<Nav/>
<Nav />
<Main/>
<Main />
</div>
"#});

insta::assert_snapshot!(formatted, @r###"
<div>
<Nav/>
<Nav />
<Main/>
<Main />
</div>
"###);
}
Expand All @@ -352,19 +395,19 @@ mod tests {
fn multiple_empty_lines() {
let formatted = format_element_from_string!(indoc! {r#"
<div>
<Nav/>
<Nav />
<Main/>
<Main />
</div>
"#});

insta::assert_snapshot!(formatted, @r###"
<div>
<Nav/>
<Nav />
<Main/>
<Main />
</div>
"###);
}
Expand All @@ -374,16 +417,16 @@ mod tests {
let formatted = format_element_from_string!(indoc! {r#"
<div>
<Nav/>
<Main/>
<Nav />
<Main />
</div>
"#});

insta::assert_snapshot!(formatted, @r###"
<div>
<Nav/>
<Main/>
<Nav />
<Main />
</div>
"###);
}
Expand Down Expand Up @@ -414,6 +457,111 @@ mod tests {
#[test]
fn with_generics() {
let formatted = format_element! { <SizeOf<String>/> };
insta::assert_snapshot!(formatted, @"<SizeOf<String>/>");
insta::assert_snapshot!(formatted, @"<SizeOf<String> />");
}

// 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, @"<input />");
insta::assert_snapshot!(self_closing_formatted, @"<input />");
insta::assert_snapshot!(non_self_closing_formatted, @"<input />");
}

#[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, @"<input />");
insta::assert_snapshot!(self_closing_formatted, @"<input />");
insta::assert_snapshot!(non_self_closing_formatted, @"<input />");
}

#[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, @"<input key=1 />");
insta::assert_snapshot!(self_closing_formatted, @"<input key=1 />");
insta::assert_snapshot!(non_self_closing_formatted, @"<input key=1 />");
}

#[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#"
<input
key=1
class="veryveryvery longlonglong attributesattributesattributes listlistlist"
/>
"#);
insta::assert_snapshot!(self_closing_formatted, @r#"
<input
key=1
class="veryveryvery longlonglong attributesattributesattributes listlistlist"
/>
"#);
insta::assert_snapshot!(non_self_closing_formatted, @r#"
<input
key=1
class="veryveryvery longlonglong attributesattributesattributes listlistlist"
/>
"#);
}

#[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#"<div>"Child"</div>"#);
insta::assert_snapshot!(self_closing_formatted, @r#"<div>"Child"</div>"#);
insta::assert_snapshot!(non_self_closing_formatted, @r#"<div>"Child"</div>"#);
}

#[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, @"<div></div>");
insta::assert_snapshot!(self_closing_formatted, @"<div />");
insta::assert_snapshot!(non_self_closing_formatted, @"<div></div>");
}

#[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, @"<div />");
insta::assert_snapshot!(self_closing_formatted, @"<div />");
insta::assert_snapshot!(non_self_closing_formatted, @"<div></div>");
}
}
Loading

0 comments on commit 148869b

Please sign in to comment.