From b99e7856e99d656675d688d75a875570382f9c71 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 28 Dec 2023 11:07:32 -0800 Subject: [PATCH] feat(css_formatter): automatically downcase all keywords and regular identifiers (#1354) --- .../css/auxiliary/attribute_matcher_value.rs | 8 +- .../src/css/auxiliary/identifier.rs | 14 ++- crates/biome_css_formatter/src/lib.rs | 42 ++++++++- .../tests/specs/css/atrule/container.css.snap | 4 +- .../tests/specs/css/atrule/layer.css.snap | 2 +- .../tests/specs/css/atrule/page.css.snap | 4 +- .../tests/specs/css/atrule/scope.css.snap | 2 +- .../tests/specs/css/atrule/supports.css.snap | 12 +-- .../tests/specs/css/casing.css | 29 ++++++ .../tests/specs/css/casing.css.snap | 91 +++++++++++++++++++ crates/biome_json_formatter/src/lib.rs | 2 +- 11 files changed, 188 insertions(+), 22 deletions(-) create mode 100644 crates/biome_css_formatter/tests/specs/css/casing.css create mode 100644 crates/biome_css_formatter/tests/specs/css/casing.css.snap diff --git a/crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs b/crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs index b96aebc20a43..429f7a74799e 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/attribute_matcher_value.rs @@ -24,12 +24,16 @@ impl FormatNodeRule 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!( diff --git a/crates/biome_css_formatter/src/css/auxiliary/identifier.rs b/crates/biome_css_formatter/src/css/auxiliary/identifier.rs index 17d0b9e758ff..40699a133bbd 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/identifier.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/identifier.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{prelude::*, utils::string_utils::FormatTokenAsLowercase}; use biome_css_syntax::{CssIdentifier, CssIdentifierFields}; use biome_formatter::write; @@ -9,6 +9,16 @@ impl FormatNodeRule 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?)]) } } diff --git a/crates/biome_css_formatter/src/lib.rs b/crates/biome_css_formatter/src/lib.rs index 22a6285fc3d9..873790d6533d 100644 --- a/crates/biome_css_formatter/src/lib.rs +++ b/crates/biome_css_formatter/src/lib.rs @@ -7,6 +7,8 @@ mod prelude; mod separated; mod utils; +use std::borrow::Cow; + use crate::comments::CssCommentStyle; pub(crate) use crate::context::CssFormatContext; use crate::context::CssFormatOptions; @@ -14,9 +16,11 @@ 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}; @@ -258,14 +262,42 @@ impl FormatLanguage for CssFormatLanguage { } } -/// Format implementation specific to JavaScript tokens. -pub(crate) type FormatCssSyntaxToken = FormatToken; +/// 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 for FormatCssSyntaxToken { + type Context = CssFormatContext; + + fn fmt(&self, token: &CssSyntaxToken, f: &mut Formatter) -> 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 for CssSyntaxToken { type Format<'a> = FormatRefWithRule<'a, CssSyntaxToken, FormatCssSyntaxToken>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatCssSyntaxToken::default()) + FormatRefWithRule::new(self, FormatCssSyntaxToken) } } @@ -273,7 +305,7 @@ impl IntoFormat for CssSyntaxToken { type Format = FormatOwnedWithRule; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, FormatCssSyntaxToken::default()) + FormatOwnedWithRule::new(self, FormatCssSyntaxToken) } } diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/container.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/container.css.snap index 58b87684a671..67bddb1f589e 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/container.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/container.css.snap @@ -142,9 +142,9 @@ Line width: 80 @container (inline-size >= calc(200px)) { } -@container (WIDTH <= 150px) { +@container (width <= 150px) { } -@container (150px <= WIDTH) { +@container (150px <= width) { } ``` diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/layer.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/layer.css.snap index ed0b816ee7dd..062473306a8d 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/layer.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/layer.css.snap @@ -168,7 +168,7 @@ Line width: 80 } @layer framework { - @media ONLY screen AND (color) { + @media only screen and (color) { article { padding: 1rem 3rem; } diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/page.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/page.css.snap index 88d723819821..5574188377a3 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/page.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/page.css.snap @@ -182,9 +182,9 @@ Line width: 80 margin: 20px; } -@page :FIRST { +@page :first { } -@page :LEFT { +@page :left { } ``` diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/scope.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/scope.css.snap index 09a1a8897411..9179264f8ced 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/scope.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/scope.css.snap @@ -125,7 +125,7 @@ Line width: 80 } } -@scope TO (.content > *) { +@scope to (.content > *) { img { border-radius: 50%; } diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/supports.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/supports.css.snap index 31a099941c1c..27b7abba6ae0 100644 --- a/crates/biome_css_formatter/tests/specs/css/atrule/supports.css.snap +++ b/crates/biome_css_formatter/tests/specs/css/atrule/supports.css.snap @@ -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 @@ -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) { diff --git a/crates/biome_css_formatter/tests/specs/css/casing.css b/crates/biome_css_formatter/tests/specs/css/casing.css new file mode 100644 index 000000000000..3fbf6b3ee458 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/casing.css @@ -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 { + +} \ No newline at end of file diff --git a/crates/biome_css_formatter/tests/specs/css/casing.css.snap b/crates/biome_css_formatter/tests/specs/css/casing.css.snap new file mode 100644 index 000000000000..9486a2f8f153 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/casing.css.snap @@ -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 { +} +``` + + diff --git a/crates/biome_json_formatter/src/lib.rs b/crates/biome_json_formatter/src/lib.rs index 0daa0de5caba..aef32730f184 100644 --- a/crates/biome_json_formatter/src/lib.rs +++ b/crates/biome_json_formatter/src/lib.rs @@ -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; impl AsFormat for JsonSyntaxToken {