Skip to content

Commit

Permalink
feat(format/html): implement whitespace sensitivity
Browse files Browse the repository at this point in the history
  • Loading branch information
dyc3 committed Jan 2, 2025
1 parent f342128 commit 8828c2a
Show file tree
Hide file tree
Showing 21 changed files with 542 additions and 43 deletions.
6 changes: 6 additions & 0 deletions crates/biome_html_formatter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ impl FromStr for WhitespaceSensitivity {
}
}

impl WhitespaceSensitivity {
pub fn is_strict(&self) -> bool {
matches!(self, Self::Strict)
}
}

/// Whether to indent the content of `<script>` and `<style>` tags for HTML-ish templating languages (Vue, Svelte, etc.).
///
/// When true, the content of `<script>` and `<style>` tags will be indented one level.
Expand Down
37 changes: 26 additions & 11 deletions crates/biome_html_formatter/src/html/auxiliary/closing_element.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
use crate::prelude::*;
use biome_formatter::write;
use biome_formatter::{write, FormatRuleWithOptions};
use biome_html_syntax::{HtmlClosingElement, HtmlClosingElementFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatHtmlClosingElement;
pub(crate) struct FormatHtmlClosingElement {
/// Is the `</tag` part being borrowed by the children of the element?
tag_borrowed: bool,
}
pub(crate) struct FormatHtmlClosingElementOptions {
pub tag_borrowed: bool,
}

impl FormatRuleWithOptions<HtmlClosingElement> for FormatHtmlClosingElement {
type Options = FormatHtmlClosingElementOptions;

fn with_options(mut self, options: Self::Options) -> Self {
self.tag_borrowed = options.tag_borrowed;
self
}
}

impl FormatNodeRule<HtmlClosingElement> for FormatHtmlClosingElement {
fn fmt_fields(&self, node: &HtmlClosingElement, f: &mut HtmlFormatter) -> FormatResult<()> {
let HtmlClosingElementFields {
Expand All @@ -12,15 +28,14 @@ impl FormatNodeRule<HtmlClosingElement> for FormatHtmlClosingElement {
r_angle_token,
} = node.as_fields();

write!(
f,
[
l_angle_token.format(),
slash_token.format(),
name.format(),
r_angle_token.format(),
]
)?;
if !self.tag_borrowed {
write!(
f,
[l_angle_token.format(), slash_token.format(), name.format()]
)?;
}

write!(f, [r_angle_token.format()])?;

Ok(())
}
Expand Down
116 changes: 110 additions & 6 deletions crates/biome_html_formatter/src/html/auxiliary/element.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
use crate::prelude::*;
use biome_formatter::write;
use crate::html::lists::element_list::{FormatHtmlElementListOptions, HtmlChildListLayout};
use crate::utils::metadata::HTML_INLINE_TAGS;
use crate::{
html::lists::element_list::{FormatChildrenResult, FormatHtmlElementList},
prelude::*,
};
use biome_formatter::{format_args, write, FormatRuleWithOptions};
use biome_html_syntax::{HtmlElement, HtmlElementFields};

use super::{
closing_element::{FormatHtmlClosingElement, FormatHtmlClosingElementOptions},
opening_element::{FormatHtmlOpeningElement, FormatHtmlOpeningElementOptions},
};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatHtmlElement;

impl FormatNodeRule<HtmlElement> for FormatHtmlElement {
fn fmt_fields(&self, node: &HtmlElement, f: &mut HtmlFormatter) -> FormatResult<()> {
let HtmlElementFields {
Expand All @@ -20,16 +31,109 @@ impl FormatNodeRule<HtmlElement> for FormatHtmlElement {
// We ignore the `script` and `style` tags as well, since embedded language parsing/formatting is not yet implemented.
let should_be_verbatim = ["script", "style", "pre"]
.iter()
.any(|tag| tag_name.eq_ignore_ascii_case(tag));
.any(|tag| tag_name.trim().eq_ignore_ascii_case(tag));
let is_inline_tag = HTML_INLINE_TAGS
.iter()
.any(|tag| tag_name.trim().eq_ignore_ascii_case(tag)); // FIXME: trim is here because parser seems to sometimes include whitespace in tag names?

let content_has_leading_whitespace = children
.syntax()
.first_token()
.map(|tok| tok.has_leading_whitespace_or_newline())
.unwrap_or_default()
|| opening_element
.as_ref()
.ok()
.and_then(|elem| {
elem.r_angle_token()
.ok()
.map(|tok| tok.has_trailing_whitespace())
})
.unwrap_or_default();
let content_has_trailing_whitespace = children
.syntax()
.last_token()
.map(|tok| tok.has_trailing_whitespace())
.unwrap_or_default()
|| closing_element
.as_ref()
.ok()
.and_then(|elem| {
elem.l_angle_token()
.ok()
.map(|tok| tok.has_leading_whitespace_or_newline())
})
.unwrap_or_default();

let should_borrow_opening_r_angle =
is_inline_tag && !children.is_empty() && !content_has_leading_whitespace;
let should_borrow_closing_tag =
is_inline_tag && !children.is_empty() && !content_has_trailing_whitespace;

let borrowed_r_angle = if should_borrow_opening_r_angle {
opening_element
.as_ref()
.ok()
.and_then(|elem| elem.r_angle_token().ok())
} else {
None
};
let borrowed_closing_tag = if should_borrow_closing_tag {
closing_element.clone().ok()
} else {
None
};

dbg!(
content_has_leading_whitespace,
content_has_trailing_whitespace,
should_borrow_opening_r_angle,
should_borrow_closing_tag,
);

write!(f, [opening_element.format()])?;
FormatNodeRule::fmt(
&FormatHtmlOpeningElement::default().with_options(FormatHtmlOpeningElementOptions {
r_angle_is_borrowed: borrowed_r_angle.is_some(),
}),
&opening_element?,
f,
)?;
if should_be_verbatim {
format_verbatim_skipped(children.syntax()).fmt(f)?;
write!(f, [hard_line_break()])?;
} else {
write!(f, [children.format()])?;
let format_children = FormatHtmlElementList::default()
.with_options(FormatHtmlElementListOptions {
layout: HtmlChildListLayout::BestFitting,
borrowed_r_angle,
borrowed_closing_tag,
})
.fmt_children(&children, f)?;
match format_children {
FormatChildrenResult::ForceMultiline(multiline) => {
write!(f, [multiline])?;
}
FormatChildrenResult::BestFitting {
flat_children,
expanded_children,
} => {
write!(
f,
[best_fitting![
format_args![flat_children],
format_args![expanded_children]
]]
)?;
}
}
}
write!(f, [closing_element.format()])?;
FormatNodeRule::fmt(
&FormatHtmlClosingElement::default().with_options(FormatHtmlClosingElementOptions {
tag_borrowed: should_borrow_closing_tag,
}),
&closing_element?,
f,
)?;

Ok(())
}
Expand Down
30 changes: 26 additions & 4 deletions crates/biome_html_formatter/src/html/auxiliary/opening_element.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
use crate::prelude::*;
use biome_formatter::write;
use biome_formatter::{write, FormatRuleWithOptions};
use biome_html_syntax::{HtmlOpeningElement, HtmlOpeningElementFields};
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatHtmlOpeningElement;
pub(crate) struct FormatHtmlOpeningElement {
/// Is the r_angle being borrowed by the children?
r_angle_is_borrowed: bool,
}

pub(crate) struct FormatHtmlOpeningElementOptions {
/// Is the r_angle being borrowed by the children?
pub r_angle_is_borrowed: bool,
}

impl FormatRuleWithOptions<HtmlOpeningElement> for FormatHtmlOpeningElement {
type Options = FormatHtmlOpeningElementOptions;

fn with_options(mut self, options: Self::Options) -> Self {
self.r_angle_is_borrowed = options.r_angle_is_borrowed;
self
}
}

impl FormatNodeRule<HtmlOpeningElement> for FormatHtmlOpeningElement {
fn fmt_fields(&self, node: &HtmlOpeningElement, f: &mut HtmlFormatter) -> FormatResult<()> {
let HtmlOpeningElementFields {
Expand All @@ -12,11 +30,15 @@ impl FormatNodeRule<HtmlOpeningElement> for FormatHtmlOpeningElement {
r_angle_token,
} = node.as_fields();

write!(f, [l_angle_token.format(), name.format(),])?;
write!(f, [l_angle_token.format(), name.format()])?;
if attributes.len() > 0 {
write!(f, [space(), attributes.format()])?
}
write!(f, [r_angle_token.format()])?;
if !self.r_angle_is_borrowed {
write!(f, [r_angle_token.format()])?;
} else {
write!(f, [format_removed(&r_angle_token?)])?;
}

Ok(())
}
Expand Down
Loading

0 comments on commit 8828c2a

Please sign in to comment.