Skip to content

Commit 4473f8c

Browse files
feat: avoid multiline opening tag for single attribute (#42)
1 parent eb7a012 commit 4473f8c

30 files changed

+387
-303
lines changed

dprint_plugin/tests/integration/biome/quotes.vue.snap

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ source: dprint_plugin/tests/integration.rs
55
<div v-if="(label = 'a')"></div>
66
<div v-if="(label = 'a')"></div>
77

8-
<button
9-
@click="(content += '{&quot;hello&quot;: &quot;I\'m a button!&quot;}')"
10-
>
8+
<button @click="(content += '{&quot;hello&quot;: &quot;I\'m a button!&quot;}')">
119
</button>
1210

1311
<input :value="''">

dprint_plugin/tests/integration/biome/style-attr.html.snap

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ source: dprint_plugin/tests/integration.rs
33
---
44
<div style="width: 1px; height: 1px"></div>
55

6-
<ul
7-
style="display: grid; grid-template-columns: 50% 50%; justify-items: stretch; padding: 0"
8-
>
6+
<ul style="display: grid; grid-template-columns: 50% 50%; justify-items: stretch; padding: 0">
97
</ul>

dprint_plugin/tests/integration/dprint_ts/style-attr.html.snap

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ source: dprint_plugin/tests/integration.rs
33
---
44
<div style="width: 1px; height: 1px"></div>
55

6-
<ul
7-
style="display: grid; grid-template-columns: 50% 50%; justify-items: stretch; padding: 0"
8-
>
6+
<ul style="display: grid; grid-template-columns: 50% 50%; justify-items: stretch; padding: 0">
97
</ul>

markup_fmt/src/printer.rs

Lines changed: 148 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -400,19 +400,24 @@ impl<'s> DocGen<'s> for Element<'s> {
400400
.split_once(':')
401401
.and_then(|(namespace, name)| namespace.eq_ignore_ascii_case("html").then_some(name))
402402
.unwrap_or(self.tag_name);
403+
let formatted_tag_name = if matches!(
404+
ctx.language,
405+
Language::Html | Language::Jinja | Language::Vento
406+
) && css_dataset::tags::STANDARD_HTML_TAGS
407+
.iter()
408+
.any(|tag| tag.eq_ignore_ascii_case(self.tag_name))
409+
{
410+
Cow::from(self.tag_name.to_ascii_lowercase())
411+
} else {
412+
Cow::from(self.tag_name)
413+
};
403414
let is_root = state.is_root;
404415
let mut state = State {
405416
current_tag_name: Some(tag_name),
406417
is_root: false,
407418
in_svg: tag_name.eq_ignore_ascii_case("svg"),
408419
indent_level: state.indent_level,
409420
};
410-
let should_lower_cased = matches!(
411-
ctx.language,
412-
Language::Html | Language::Jinja | Language::Vento
413-
) && css_dataset::tags::STANDARD_HTML_TAGS
414-
.iter()
415-
.any(|tag| tag.eq_ignore_ascii_case(self.tag_name));
416421

417422
let self_closing = if helpers::is_void_element(tag_name, ctx.language) {
418423
ctx.options
@@ -447,115 +452,128 @@ impl<'s> DocGen<'s> for Element<'s> {
447452
let mut docs = Vec::with_capacity(5);
448453

449454
docs.push(Doc::text("<"));
450-
docs.push(Doc::text(if should_lower_cased {
451-
Cow::from(self.tag_name.to_ascii_lowercase())
452-
} else {
453-
Cow::from(self.tag_name)
454-
}));
455-
456-
let attrs_sep = if !self.first_attr_same_line
457-
&& !ctx.options.prefer_attrs_single_line
458-
&& self.attrs.len() > 1
459-
&& !ctx
460-
.options
461-
.max_attrs_per_line
462-
.map(|value| value.get() > 1)
463-
.unwrap_or_default()
464-
{
465-
Doc::hard_line()
466-
} else {
467-
Doc::line_or_space()
468-
};
469-
let attrs = if let Some(max) = ctx.options.max_attrs_per_line {
470-
// fix #2
471-
if self.attrs.is_empty() {
472-
Doc::line_or_nil()
473-
} else {
474-
Doc::line_or_space()
455+
docs.push(Doc::text(formatted_tag_name.clone()));
456+
457+
match self.attrs.as_slice() {
458+
[attr] if !is_whitespace_sensitive && !is_multi_line_attr(attr) => {
459+
docs.push(Doc::space());
460+
docs.push(attr.doc(ctx, &state));
461+
if self_closing && is_empty {
462+
docs.push(Doc::text(" />"));
463+
return Doc::list(docs);
464+
} else {
465+
docs.push(Doc::text(">"));
466+
};
467+
if self.void_element {
468+
return Doc::list(docs);
469+
}
475470
}
476-
.concat(itertools::intersperse(
477-
self.attrs.chunks(max.into()).map(|chunk| {
471+
_ => {
472+
let attrs_sep = if !self.first_attr_same_line
473+
&& !ctx.options.prefer_attrs_single_line
474+
&& self.attrs.len() > 1
475+
&& !ctx
476+
.options
477+
.max_attrs_per_line
478+
.map(|value| value.get() > 1)
479+
.unwrap_or_default()
480+
{
481+
Doc::hard_line()
482+
} else {
483+
Doc::line_or_space()
484+
};
485+
let attrs = if let Some(max) = ctx.options.max_attrs_per_line {
486+
// fix #2
487+
if self.attrs.is_empty() {
488+
Doc::line_or_nil()
489+
} else {
490+
Doc::line_or_space()
491+
}
492+
.concat(itertools::intersperse(
493+
self.attrs.chunks(max.into()).map(|chunk| {
494+
Doc::list(
495+
itertools::intersperse(
496+
chunk.iter().map(|attr| attr.doc(ctx, &state)),
497+
attrs_sep.clone(),
498+
)
499+
.collect(),
500+
)
501+
.group()
502+
}),
503+
Doc::hard_line(),
504+
))
505+
.nest(ctx.indent_width)
506+
} else {
478507
Doc::list(
479-
itertools::intersperse(
480-
chunk.iter().map(|attr| attr.doc(ctx, &state)),
481-
attrs_sep.clone(),
482-
)
483-
.collect(),
508+
self.attrs
509+
.iter()
510+
.flat_map(|attr| [attrs_sep.clone(), attr.doc(ctx, &state)].into_iter())
511+
.collect(),
484512
)
485-
.group()
486-
}),
487-
Doc::hard_line(),
488-
))
489-
.nest(ctx.indent_width)
490-
} else {
491-
Doc::list(
492-
self.attrs
493-
.iter()
494-
.flat_map(|attr| [attrs_sep.clone(), attr.doc(ctx, &state)].into_iter())
495-
.collect(),
496-
)
497-
.nest(ctx.indent_width)
498-
};
513+
.nest(ctx.indent_width)
514+
};
499515

500-
if self.void_element {
501-
docs.push(attrs);
502-
if self_closing {
503-
docs.push(Doc::line_or_space());
504-
docs.push(Doc::text("/>"));
505-
} else {
506-
if !ctx.options.closing_bracket_same_line {
507-
docs.push(Doc::line_or_nil());
508-
}
509-
docs.push(Doc::text(">"));
510-
}
511-
return Doc::list(docs).group();
512-
}
513-
if self_closing && is_empty {
514-
docs.push(attrs);
515-
docs.push(Doc::line_or_space());
516-
docs.push(Doc::text("/>"));
517-
return Doc::list(docs).group();
518-
}
519-
if ctx.options.closing_bracket_same_line {
520-
docs.push(attrs.append(Doc::text(">")).group());
521-
} else {
522-
// for #16
523-
if is_whitespace_sensitive
524-
&& !self.attrs.is_empty() // there're no attributes, so don't insert line break
525-
&& self
526-
.children
527-
.first()
528-
.is_some_and(|child| {
529-
if let NodeKind::Text(text_node) = &child.kind {
530-
!text_node.raw.starts_with(|c: char| c.is_ascii_whitespace())
531-
} else {
532-
false
533-
}
534-
})
535-
&& self
536-
.children
537-
.last()
538-
.is_some_and(|child| {
539-
if let NodeKind::Text(text_node) = &child.kind {
540-
!text_node.raw.ends_with(|c: char| c.is_ascii_whitespace())
541-
} else {
542-
false
516+
if self.void_element {
517+
docs.push(attrs);
518+
if self_closing {
519+
docs.push(Doc::line_or_space());
520+
docs.push(Doc::text("/>"));
521+
} else {
522+
if !ctx.options.closing_bracket_same_line {
523+
docs.push(Doc::line_or_nil());
543524
}
544-
})
545-
{
546-
docs.push(
547-
attrs
548-
.group()
549-
.append(Doc::line_or_nil())
550-
.append(Doc::text(">")),
551-
);
552-
} else {
553-
docs.push(
554-
attrs
555-
.append(Doc::line_or_nil())
556-
.append(Doc::text(">"))
557-
.group(),
558-
);
525+
docs.push(Doc::text(">"));
526+
}
527+
return Doc::list(docs).group();
528+
}
529+
if self_closing && is_empty {
530+
docs.push(attrs);
531+
docs.push(Doc::line_or_space());
532+
docs.push(Doc::text("/>"));
533+
return Doc::list(docs).group();
534+
}
535+
if ctx.options.closing_bracket_same_line {
536+
docs.push(attrs.append(Doc::text(">")).group());
537+
} else {
538+
// for #16
539+
if is_whitespace_sensitive
540+
&& !self.attrs.is_empty() // there're no attributes, so don't insert line break
541+
&& self
542+
.children
543+
.first()
544+
.is_some_and(|child| {
545+
if let NodeKind::Text(text_node) = &child.kind {
546+
!text_node.raw.starts_with(|c: char| c.is_ascii_whitespace())
547+
} else {
548+
false
549+
}
550+
})
551+
&& self
552+
.children
553+
.last()
554+
.is_some_and(|child| {
555+
if let NodeKind::Text(text_node) = &child.kind {
556+
!text_node.raw.ends_with(|c: char| c.is_ascii_whitespace())
557+
} else {
558+
false
559+
}
560+
})
561+
{
562+
docs.push(
563+
attrs
564+
.group()
565+
.append(Doc::line_or_nil())
566+
.append(Doc::text(">")),
567+
);
568+
} else {
569+
docs.push(
570+
attrs
571+
.append(Doc::line_or_nil())
572+
.append(Doc::text(">"))
573+
.group(),
574+
);
575+
}
576+
}
559577
}
560578
}
561579

@@ -756,11 +774,7 @@ impl<'s> DocGen<'s> for Element<'s> {
756774

757775
docs.push(
758776
Doc::text("</")
759-
.append(Doc::text(if should_lower_cased {
760-
Cow::from(self.tag_name.to_ascii_lowercase())
761-
} else {
762-
Cow::from(self.tag_name)
763-
}))
777+
.append(Doc::text(formatted_tag_name))
764778
.append(Doc::line_or_nil())
765779
.append(Doc::text(">"))
766780
.group(),
@@ -1966,6 +1980,25 @@ fn is_all_ascii_whitespace(s: &str) -> bool {
19661980
!s.is_empty() && s.as_bytes().iter().all(|byte| byte.is_ascii_whitespace())
19671981
}
19681982

1983+
fn is_multi_line_attr(attr: &Attribute) -> bool {
1984+
match attr {
1985+
Attribute::Native(attr) => attr
1986+
.value
1987+
.map(|(value, _)| value.trim().contains('\n'))
1988+
.unwrap_or(false),
1989+
Attribute::VueDirective(attr) => attr
1990+
.value
1991+
.map(|(value, _)| value.contains('\n'))
1992+
.unwrap_or(false),
1993+
Attribute::Astro(attr) => attr.expr.0.contains('\n'),
1994+
Attribute::Svelte(attr) => attr.expr.0.contains('\n'),
1995+
Attribute::JinjaComment(comment) => comment.raw.contains('\n'),
1996+
Attribute::JinjaTag(tag) => tag.content.contains('\n'),
1997+
// Templating blocks usually span across multiple lines so let's just assume true.
1998+
Attribute::JinjaBlock(..) | Attribute::VentoTagOrBlock(..) => true,
1999+
}
2000+
}
2001+
19692002
fn should_ignore_node<'s, E, F>(index: usize, nodes: &[Node], ctx: &Ctx<'s, E, F>) -> bool
19702003
where
19712004
F: for<'a> FnMut(&'a str, Hints) -> Result<Cow<'a, str>, E>,

markup_fmt/tests/fmt/html/attributes/class-bem1.snap

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ source: markup_fmt/tests/fmt.rs
1414
</div>
1515

1616
<div class="a-bem-block a-bem-block--with-modifier">
17-
<div
18-
class="a-bem-block__element a-bem-block__element--with-modifier also-another-block"
19-
>
20-
<div
21-
class="a-bem-block__element a-bem-block__element--with-modifier also-another-block__element"
22-
>
17+
<div class="a-bem-block__element a-bem-block__element--with-modifier also-another-block">
18+
<div class="a-bem-block__element a-bem-block__element--with-modifier also-another-block__element">
2319
</div>
2420
</div>
2521
</div>

markup_fmt/tests/fmt/html/attributes/class-names.snap

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ source: markup_fmt/tests/fmt.rs
5656
Scrolling_Bxsh($headerShadow)
5757
"
5858
>
59-
<div
60-
class="Bgc(#fff) M(a) Maw(1301px) Miw(1000px) Pb(12px) Pt(22px) Pos(r) TranslateZ(0) Z(6)"
61-
>
59+
<div class="Bgc(#fff) M(a) Maw(1301px) Miw(1000px) Pb(12px) Pt(22px) Pos(r) TranslateZ(0) Z(6)">
6260
<h1 class="Fz(0) Pstart(15px) Pos(a)">
6361
<a
6462
id="header-logo"

0 commit comments

Comments
 (0)