Skip to content

Commit

Permalink
feat(css_formatter): automatically downcase all keywords and regular …
Browse files Browse the repository at this point in the history
…identifiers (biomejs#1354)
  • Loading branch information
faultyserver authored Dec 28, 2023
1 parent c212dd6 commit b99e785
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ impl FormatNodeRule<CssAttributeMatcherValue> for FormatCssAttributeMatcherValue
write!(f, [string.format()])
}
AnyCssAttributeMatcherValue::CssIdentifier(ident) => {
let value = ident.value_token()?;

if f.comments().is_suppressed(ident.syntax()) {
return write!(f, [ident.format()]);
}

// Unlike almost all other usages of regular identifiers,
// attribute values are case-sensitive, so the identifier here
// does not get converted to lowercase. Once it's quoted, it
// will be parsed as a CssString on the next pass, at which
// point casing is preserved no matter what.
let value = ident.value_token()?;
let quoted = std::format!("\"{}\"", value.text_trimmed());

write!(
Expand Down
14 changes: 12 additions & 2 deletions crates/biome_css_formatter/src/css/auxiliary/identifier.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::prelude::*;
use crate::{prelude::*, utils::string_utils::FormatTokenAsLowercase};
use biome_css_syntax::{CssIdentifier, CssIdentifierFields};
use biome_formatter::write;

Expand All @@ -9,6 +9,16 @@ impl FormatNodeRule<CssIdentifier> for FormatCssIdentifier {
fn fmt_fields(&self, node: &CssIdentifier, f: &mut CssFormatter) -> FormatResult<()> {
let CssIdentifierFields { value_token } = node.as_fields();

write!(f, [value_token.format()])
// Identifiers in CSS are used all over the place. Type selectors,
// declaration names, value definitions, and plenty more. For the most
// part, these identifiers are case-insensitive, meaning they can
// safely be re-written in any casing, and for formatting we want them
// to always be in lowercase.
//
// Other kinds of identifiers (custom identifiers and dashed
// identifiers) are defined to be case-sensitive, which is why they
// have their own types to be parsed and formatted separately, ensuring
// that only identifiers which _can_ be re-written this way are.
write!(f, [FormatTokenAsLowercase::from(value_token?)])
}
}
42 changes: 37 additions & 5 deletions crates/biome_css_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ mod prelude;
mod separated;
mod utils;

use std::borrow::Cow;

use crate::comments::CssCommentStyle;
pub(crate) use crate::context::CssFormatContext;
use crate::context::CssFormatOptions;
use crate::cst::FormatCssSyntaxNode;
use biome_css_syntax::{AnyCssValue, CssLanguage, CssSyntaxNode, CssSyntaxToken};
use biome_formatter::comments::Comments;
use biome_formatter::prelude::*;
use biome_formatter::token::string::ToAsciiLowercaseCow;
use biome_formatter::trivia::format_skipped_token_trivia;
use biome_formatter::{
write, CstFormatContext, FormatContext, FormatLanguage, FormatOwnedWithRule, FormatRefWithRule,
FormatToken, TransformSourceMap,
TransformSourceMap,
};
use biome_formatter::{Formatted, Printed};
use biome_rowan::{AstNode, SyntaxNode, TextRange};
Expand Down Expand Up @@ -258,22 +262,50 @@ impl FormatLanguage for CssFormatLanguage {
}
}

/// Format implementation specific to JavaScript tokens.
pub(crate) type FormatCssSyntaxToken = FormatToken<CssFormatContext>;
/// Format implementation specific to CSS tokens.
///
/// This re-implementation of FormatToken allows the formatter to automatically
/// rewrite all keywords in lowercase, since they are case-insensitive. Other
/// tokens like identifiers handle lowercasing themselves.
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct FormatCssSyntaxToken;

impl FormatRule<CssSyntaxToken> for FormatCssSyntaxToken {
type Context = CssFormatContext;

fn fmt(&self, token: &CssSyntaxToken, f: &mut Formatter<Self::Context>) -> FormatResult<()> {
f.state_mut().track_token(token);

write!(f, [format_skipped_token_trivia(token)])?;

if token.kind().is_contextual_keyword() {
let original = token.text_trimmed();
match original.to_ascii_lowercase_cow() {
Cow::Borrowed(_) => write!(f, [format_trimmed_token(token)]),
Cow::Owned(lowercase) => write!(
f,
[dynamic_text(&lowercase, token.text_trimmed_range().start())]
),
}
} else {
write!(f, [format_trimmed_token(token)])
}
}
}

impl AsFormat<CssFormatContext> for CssSyntaxToken {
type Format<'a> = FormatRefWithRule<'a, CssSyntaxToken, FormatCssSyntaxToken>;

fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatCssSyntaxToken::default())
FormatRefWithRule::new(self, FormatCssSyntaxToken)
}
}

impl IntoFormat<CssFormatContext> for CssSyntaxToken {
type Format = FormatOwnedWithRule<CssSyntaxToken, FormatCssSyntaxToken>;

fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatCssSyntaxToken::default())
FormatOwnedWithRule::new(self, FormatCssSyntaxToken)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ Line width: 80
@container (inline-size >= calc(200px)) {
}

@container (WIDTH <= 150px) {
@container (width <= 150px) {
}
@container (150px <= WIDTH) {
@container (150px <= width) {
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Line width: 80
}

@layer framework {
@media ONLY screen AND (color) {
@media only screen and (color) {
article {
padding: 1rem 3rem;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ Line width: 80
margin: 20px;
}

@page :FIRST {
@page :first {
}
@page :LEFT {
@page :left {
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Line width: 80
}
}

@scope TO (.content > *) {
@scope to (.content > *) {
img {
border-radius: 50%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Line width: 80
}
@supports not (display: flex) {
}
@SUPPORTS not (display: flex) {
@supports not (display: flex) {
}
@supports (box-shadow: 0 0 2px black inset) or
(-moz-box-shadow: 0 0 2px black inset) or
Expand All @@ -245,15 +245,15 @@ Line width: 80
}
@supports (display: flex !important) {
}
@supports NOT (display: flex) {
@supports not (display: flex) {
}
@supports ((transition-property: color) OR (animation-name: foo)) AND
@supports ((transition-property: color) or (animation-name: foo)) and
(transform: rotate(10deg)) {
}
@supports (transition-property: color) OR
((animation-name: foo) AND (transform: rotate(10deg))) {
@supports (transition-property: color) or
((animation-name: foo) and (transform: rotate(10deg))) {
}
@supports (NOT (display: flex)) {
@supports (not (display: flex)) {
}

@supports selector(col || td) {
Expand Down
29 changes: 29 additions & 0 deletions crates/biome_css_formatter/tests/specs/css/casing.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* All values in CSS are case-insensitive except for Custom and Dashed
* identifiers. Everything else can and will be re-written in lowercase.
*/

DIV { COLOR: BLUE; }

DIV.classNames#AND_Ids.ArePreserved {}

[attr=IdentifierValuesPreserveWhenStringified] {}

@MEDiA NoT SCReEN AND ( CoLOR ), PRINT AND (COLOR) { }

DIV {
--Preserved-Casing: BLUE;
ColOR: VAR(--Preserved-Casing);
}

@font-PALETTE-values --AnyCASInG-works { }

/*
* The only exception (at least that I've found so far in the spec), is @page
* using a _regular_ identifier for the page name, but where that identifier is
* considered case-sensitive. Biome uses a CssCustomIdentifier here instead to
* automatically preserve casing rather than creating a special exception.
*/
@PAGE ThisIsPreserved:FIRST {

}
91 changes: 91 additions & 0 deletions crates/biome_css_formatter/tests/specs/css/casing.css.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: css/casing.css
---

# Input

```css
/*
* All values in CSS are case-insensitive except for Custom and Dashed
* identifiers. Everything else can and will be re-written in lowercase.
*/

DIV { COLOR: BLUE; }

DIV.classNames#AND_Ids.ArePreserved {}

[attr=IdentifierValuesPreserveWhenStringified] {}

@MEDiA NoT SCReEN AND ( CoLOR ), PRINT AND (COLOR) { }

DIV {
--Preserved-Casing: BLUE;
ColOR: VAR(--Preserved-Casing);
}

@font-PALETTE-values --AnyCASInG-works { }

/*
* The only exception (at least that I've found so far in the spec), is @page
* using a _regular_ identifier for the page name, but where that identifier is
* considered case-sensitive. Biome uses a CssCustomIdentifier here instead to
* automatically preserve casing rather than creating a special exception.
*/
@PAGE ThisIsPreserved:FIRST {

}
```


=============================

# Outputs

## Output 1

-----
Indent style: Tab
Indent width: 2
Line ending: LF
Line width: 80
-----

```css
/*
* All values in CSS are case-insensitive except for Custom and Dashed
* identifiers. Everything else can and will be re-written in lowercase.
*/

div {
color: blue;
}

div.classNames#AND_Ids.ArePreserved {
}

[attr="IdentifierValuesPreserveWhenStringified"] {
}

@media not screen and (color), print and (color) {
}

div {
--preserved-casing: blue;
color: var(--Preserved-Casing);
}

@font-palette-values --AnyCASInG-works {
}

/*
* The only exception (at least that I've found so far in the spec), is @page
* using a _regular_ identifier for the page name, but where that identifier is
* considered case-sensitive. Biome uses a CssCustomIdentifier here instead to
* automatically preserve casing rather than creating a special exception.
*/
@page ThisIsPreserved:first {
}
```


2 changes: 1 addition & 1 deletion crates/biome_json_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ impl FormatLanguage for JsonFormatLanguage {
}
}

/// Format implementation specific to JavaScript tokens.
/// Format implementation specific to JSON tokens.
pub(crate) type FormatJsonSyntaxToken = FormatToken<JsonFormatContext>;

impl AsFormat<JsonFormatContext> for JsonSyntaxToken {
Expand Down

0 comments on commit b99e785

Please sign in to comment.