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

fix(css): align CSS value list format with Prettier #5334

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

wanghaoPolar
Copy link
Contributor

@wanghaoPolar wanghaoPolar commented Mar 12, 2025

Summary

Related issue: #5307

Currently biome's formatting of CSS values is different from Prettier

example link

Align biome with Prettier

Related code:
crates/biome_css_formatter/src/utils/component_value_list.rs

Align with Prettier logic. (For detail see below section)

Added a layout called OneGroupPerLine

When a value list satisfy below conditions, will format using OneGroupPerLine

  1. Is direct child of a CSS property declaration
  2. Values are separated into multiple groups by comma
  3. At least one of groups contains >= 2 values

Render OneGroupPerLine

  1. Init variable at_group_boundary to false, and iterate through the value list:
  2. Prepend new hard line before a value if at_group_boundary is true
  3. If current value is comma, set at_group_boundary to true, else, set to false

Prettier logic

Here is a brief description of how Prettier handle it: ### Process AST

Related code:
https://github.com/prettier/prettier/blob/3.5.3/src/language-css/parse/parse-value.js

Prettier first use PostcssValuesParser to parse CSS values, the AST is similar to what we have in biome

Then, Prettier calls parseNestedValue to process the AST as follows:

  1. Wrap all nodes to a hierarchy of paren_group and comma_group
  2. By default everything is inside a root paren_group, any new parenthesis (like in url()) will create a new layer of paren_group
  3. Inside a paren_group, by default everything is wrapped in a comma_group. When find a comma, push in the current comma_group, rest items are added to a new comma_group.
  4. Flatten paren_group and comma_group: if the group contains only 1 item, remove the group wrapper and expose the item directly.
body {
    /*
Original AST: "red"
After step 2,3: 
    paren_group(
        comma_group(
            "red"
        )
    )
After step 4: "red"
    */
    color: red;
}

body {
    /*
paren_group(
    comma_group(
        func(
            "url",
            ["image.jpg"]
        ),
        "no-repeat",
        "center",
        "center"
    ),
    func("rgba", [0,0,0,0.5])
)
    */
    background: url('image.jpg') no-repeat center center, rgba(0, 0, 0, 0.5);
}

Print

Related code:
https://github.com/prettier/prettier/blob/3.5.3/src/language-css/print/parenthesized-value-group.js#L62

Related to reported bug, one of the cases that Prettier will print each value to a single line is:

  1. The value is directly under a rule declaration whose property name doesn't start with '--'; and
  2. The values are wrapped in a paren_group; and
  3. One of the child of paren_group is a comma group

Condition 2 implies that there are >= 2 values. (Otherwise the paren_group wrapper will be removed).
Condition 3 implies that at least one of the values has >= 2 parts. (Otherwise the comma_group wrapper will be removed).

body {
    /* OnePerLine because 
        1. `url("image.jpg") no-repeat center center` contains >= 2 parts, forms a comma group
        2. there are 2 values (url... and rgba...), form a paren_group
     */
	background:
		url("image.jpg") no-repeat center center,
		rgba(0, 0, 0, 0.5);

    /* Fill because
        1. property starts with "--"
     */
	--custom-background: url("image.jpg") no-repeat center center, rgba(0, 0, 0, 0.5);

    /* Fill because
        1. only 1 comma group
     */
	background: url("image.jpg") no-repeat center center;
    
    /* Fill because
        1. no comma group exist
    */
    unicode-range: U+0025-00FF, U+4??;
}

Test Plan

Snapshot updated

  • font-face.css updated to aligned with Prettier
  • all.css.snap, border.css.snap, generic.css.snap , the changes are as expected
  • indent.css.snap updated to mostly aligned with Prettier. Still remain 1 indentation diff not related to this change.
  • url.css.snap updated. Currently result is still different from Prettier. But it's because the value is invalid. Prettier fail to parse the value and choose to keep original format.

Add new test

crates/biome_css_formatter/tests/specs/css/value_one_group_per_line.css

  1. Almost all aligned with Prettier.
  2. the font-family part still use Fill. Because each group separated by comma only contain 1 value, not trigger OneGroupPerLine. And the value count <= 12, not trigger OnePerLine.
  3. Prettier print each value in one line because it recognize -apple-system as 2 values: - + apple-system, thus trigger its OneGroupPerLine logic.

@github-actions github-actions bot added A-Formatter Area: formatter L-CSS Language: CSS labels Mar 12, 2025
@wanghaoPolar wanghaoPolar marked this pull request as draft March 12, 2025 13:00
Copy link

codspeed-hq bot commented Mar 12, 2025

CodSpeed Performance Report

Merging #5334 will not alter performance

Comparing wanghaoPolar:css-value-format (f30af76) with main (e9e8267)

Summary

✅ 95 untouched benchmarks

@wanghaoPolar wanghaoPolar changed the title WIP Align CSS value format with Prettier fix(css): Align CSS value format with Prettier Mar 20, 2025
@wanghaoPolar wanghaoPolar marked this pull request as ready for review March 20, 2025 06:46
@wanghaoPolar wanghaoPolar changed the title fix(css): Align CSS value format with Prettier fix(css): align CSS value list format with Prettier Mar 20, 2025
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great fix! A couple of things left:

  • add more comments to document the new layout variant
  • add a changeset

Comment on lines +70 to +72
at_group_boundary =
is_comma && matches!(layout, ValueListLayout::OneGroupPerLine);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to add a comment that explains why we change the value of at_group_boundary after the first iteration of the loop.

@@ -160,6 +168,16 @@ pub(crate) enum ValueListLayout {
/// sans-serif;
/// ```
OnePerLine,

/// Separate values by comma into multiple groups, and print each group on a single line
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PR description, you explained when this layout was applied. That is essential knowledge and we want to keep it around, so please add that information here :)

}
group_size += 1;
}
group_count += 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add another item after the loop? This means that if the list is empty, group_count is always one here, which doesn't add up.

group_size += 1;
}
group_count += 1;
max_group_size = cmp::max(group_size, max_group_size);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we calculate max_group_size again? You might want to consider adding a comment explaining why there's this logic because it's unclear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWESOME!!! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Formatter Area: formatter L-CSS Language: CSS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants