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

feat(css_formatter): Formatting for border property #1453

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions crates/biome_css_formatter/src/css/auxiliary/border.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
use crate::prelude::*;
use biome_css_syntax::CssBorder;
use biome_rowan::AstNode;
use crate::{prelude::*, utils::properties::FormatPropertyValueFields};
use biome_css_syntax::{CssBorder, CssBorderFields};
use biome_formatter::{format_args, write};

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssBorder;
impl FormatNodeRule<CssBorder> for FormatCssBorder {
fn fmt_fields(&self, node: &CssBorder, f: &mut CssFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let CssBorderFields {
line_width,
line_style,
color,
} = node.as_fields();

write!(
f,
[FormatPropertyValueFields::new(&format_args![
line_width.format(),
line_style.format(),
color.format(),
])
.with_slot_map(node.concrete_order_slot_map())]
)
}
}
9 changes: 6 additions & 3 deletions crates/biome_css_formatter/src/css/auxiliary/line_style.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::prelude::*;
use biome_css_syntax::CssLineStyle;
use biome_rowan::AstNode;
use biome_css_syntax::{CssLineStyle, CssLineStyleFields};
use biome_formatter::write;

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssLineStyle;
impl FormatNodeRule<CssLineStyle> for FormatCssLineStyle {
fn fmt_fields(&self, node: &CssLineStyle, f: &mut CssFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let CssLineStyleFields { keyword } = node.as_fields();

write!(f, [keyword.format()])
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::prelude::*;
use biome_css_syntax::CssLineWidthKeyword;
use biome_rowan::AstNode;
use biome_css_syntax::{CssLineWidthKeyword, CssLineWidthKeywordFields};
use biome_formatter::write;

#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssLineWidthKeyword;
impl FormatNodeRule<CssLineWidthKeyword> for FormatCssLineWidthKeyword {
fn fmt_fields(&self, node: &CssLineWidthKeyword, f: &mut CssFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let CssLineWidthKeywordFields { keyword } = node.as_fields();

write!(f, [keyword.format()])
}
}
14 changes: 11 additions & 3 deletions crates/biome_css_formatter/src/css/properties/border_property.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
use crate::prelude::*;
use biome_css_syntax::CssBorderProperty;
use biome_rowan::AstNode;
use biome_css_syntax::{CssBorderProperty, CssBorderPropertyFields};
use biome_formatter::write;
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssBorderProperty;
impl FormatNodeRule<CssBorderProperty> for FormatCssBorderProperty {
fn fmt_fields(&self, node: &CssBorderProperty, f: &mut CssFormatter) -> FormatResult<()> {
format_verbatim_node(node.syntax()).fmt(f)
let CssBorderPropertyFields {
name,
colon_token,
value,
} = node.as_fields();
write!(
f,
[name.format(), colon_token.format(), space(), value.format()]
)
}
}
4 changes: 3 additions & 1 deletion crates/biome_css_formatter/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub(crate) use crate::{
};
pub(crate) use biome_formatter::prelude::*;
#[allow(unused_imports)]
pub(crate) use biome_rowan::{AstNode as _, AstNodeList as _, AstSeparatedList as _};
pub(crate) use biome_rowan::{
AstNode as _, AstNodeList as _, AstNodeSlotMap as _, AstSeparatedList as _,
};

pub(crate) use crate::separated::FormatAstSeparatedListExtension;
1 change: 1 addition & 0 deletions crates/biome_css_formatter/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub(crate) mod component_value_list;
pub(crate) mod properties;
pub(crate) mod string_utils;
172 changes: 172 additions & 0 deletions crates/biome_css_formatter/src/utils/properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use crate::prelude::*;
use biome_formatter::{write, Arguments};

/// Format all of the fields of a `PropertyValue` node in an arbitrary order,
/// given by `slot_map`.
///
/// Because the CSS grammar allows rules to specify fields that can appear
/// in any order, there isn't always a linear mapping between the _declared_
/// order (how they appear in the grammar) and the _concrete_ order (how they
/// appear in the source text) of the fields. The parser supports this by
/// building a `slot_map` to map the declared order to the concrete order.
///
/// When formatting, by default we want to preserve the ordering of fields as
/// they were written in the source, but just using the `AstNode` alone will
/// naturally re-write the value in the _declared_ order. To preserve the
/// _concrete_ order, we can invert the `slot_map` and sort it to re-determine
/// the ordering of fields and then iterate that list to format each field
/// individually.
///
/// ## Fields
///
/// The caller provides a list of _pre-formatted_ fields, using the
/// [`biome_formatter::format_args!`] macro. This way, it can either pass
/// through a field as-is with default formatting, or it can apply any other
/// formatting it once for that field:
///
/// ```rust,ignore
/// let formatted = format!(CssFormatContext::default(), [
/// FormatPropertyValueFields::new(&format_args![
/// text("a"),
/// text("b"),
/// group(&block_indent(&format_args![text("c"), hard_line_break(), text("d")]))
/// ])
/// .with_slot_map([1, 2, 0])
/// ])?;
///
/// assert_eq!("b
/// \tc
/// \td
/// a", formatted.print()?.as_code());
/// ```
///
/// ## Concrete Ordering
///
/// By default, using this struct will format the fields of the node in order.
/// This is sufficient for nodes that don't have any dynamically-ordered
/// fields, but for dynamic nodes that want to preserve the order of fields as
/// they were given in the input, or for any node that wants to change the
/// ordering of the fields, the caller will need to provide a `slot_map` that
/// this struct can use to re-order the fields.
///
/// To preserve the field order as it was written in the original source, use
/// [biome_rowan::AstNodeSlotMap::concrete_order_slot_map], which will ensure
/// the ordering matches what was given. This should be the default for most
/// if not all dynamic nodes.
///
/// ```rust,ignore
/// .with_slot_map(node.concrete_order_slot_map())
/// ```
///
/// Any other method of building a slot map is also valid, but should generally
/// be avoided, as ensuring consistency across formats is difficult without a
/// strong heuristic.
///
/// ## Grouping Fields (Future)
///
/// In some cases, a property may want to group certain fields together in
/// order to apply special formatting. As an example, consider a grammar like:
///
/// ```ebnf
/// font =
/// (style: CssFontStyle ||
/// variant: CssFontVariant ||
/// weight: CssFontWeight)?
/// size: CssNumber ( '/' line_height: CssLineHeight)?
/// ```
///
/// Here, the `style`, `variant`, and `weight` fields can appear conditionally
/// and in any order, but if `line_height` is present, it (and the slash token)
/// must appear immediately adjacent to the `size` field. While it would be
/// valid to just have the fields fill and wrap over lines as needed, the
/// formatter might want to preserve the adjacency and ensure that `size` and
/// `line_height` always get written on the same line.
///
/// To do this, the value formatter can write both fields in a single group,
/// and then use an `empty_field_slot` value in the slots where the other
/// fields have been taken from:
///
/// ```rust,ignore
/// FormatPropertyValueFields::new(&format_args![
/// style.format(),
/// variant.format(),
/// weight.format(),
/// group(&format_args![
/// size.format(), slash_token.format(), line_height.format()
/// ]),
/// empty_field_slot(),
/// empty_field_slot()
/// ])
/// .with_slot_map(node.concrete_order_slot_map())
/// ```
///
/// The `empty_field_slot()` values will tell this struct to skip formatting
/// for that field, with the assumption that another field includes its value.
pub struct FormatPropertyValueFields<'fmt, const N: usize> {
slot_map: Option<[u8; N]>,
fields: &'fmt Arguments<'fmt, CssFormatContext>,
}

impl<'fmt, const N: usize> FormatPropertyValueFields<'fmt, N> {
pub fn new(fields: &'fmt Arguments<'fmt, CssFormatContext>) -> Self {
Self {
slot_map: None,
fields,
}
}

pub fn with_slot_map(mut self, slot_map: [u8; N]) -> Self {
debug_assert!(
self.fields.items().len() == N,
"slot_map must specify the same number of fields as this struct contains"
);
self.slot_map = Some(slot_map);
self
}
}

impl<'fmt, const N: usize> Format<CssFormatContext> for FormatPropertyValueFields<'fmt, N> {
fn fmt(&self, f: &mut CssFormatter) -> FormatResult<()> {
let values = format_with(|f: &mut Formatter<'_, CssFormatContext>| {
let mut filler = f.fill();

// First, determine the ordering of fields to use. If no slot_map is
// provided along with the fields, then they can just be used in the
// same order, but if a `slot_map` is present, then the fields are
// re-ordered to match the concrete ordering from the source syntax.
//
// The fields are wrapped with `Option` for two reasons: for nodes
// with slot maps, it simplifies how the re-ordered slice is built, and
// it also allows empty/missing fields to be removed in the next step.
match self.slot_map {
None => {
for field in self.fields.items() {
filler.entry(&soft_line_break_or_space(), field);
}
}
Some(slot_map) => {
for slot in slot_map {
// This condition ensures that missing values are _not_ included in the
// fill. The generated `slot_map` for an AstNode guarantees that all
// present fields have a tangible value here, while all absent fields
// have this sentinel value ([biome_css_syntax::SLOT_MAP_EMPTY_VALUE]).
//
// This check is important to ensure that we don't add empty values to
// the fill, since that would add double separators when we don't want
// them.
if slot == u8::MAX {
continue;
}

let field = &self.fields.items()[slot as usize];
filler.entry(&soft_line_break_or_space(), field);
}
}
};

filler.finish()
});

write!(f, [group(&indent(&values))])
}
}
8 changes: 5 additions & 3 deletions crates/biome_css_formatter/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ mod language {
fn quick_test() {
let src = r#"
div {
prod: fn(100px);
prod: --fn(100px);
prod: --fn--fn(100px);
border: #fff solid

2px;
border: THICK #000;
border: medium;
}

"#;
Expand Down
39 changes: 39 additions & 0 deletions crates/biome_css_formatter/tests/specs/css/properties/border.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
div {
/* Generic property tests */
border: InItial;
border
:
inherit
;

border : zzz-unknown-value ;
border : a,
value list ;


/* <line-style> */
border : SOLID;
border: none
;

/* <line-width> */
border : ThIn;
border:
medium
;
border: 100px;

/* <color> */
border:
#fff;

/* combinations */
border: 2px
dotted;
border : outset #f33;
border:#000 medium

dashed

;
}
Loading
Loading