use std::borrow::Cow;
use crate::context::CssFormatOptions;
use crate::prelude::*;
use biome_css_syntax::CssLanguage;
use biome_css_syntax::CssSyntaxKind::{CSS_STRING_LITERAL, CSS_URL_VALUE_RAW_LITERAL, IDENT};
use biome_css_syntax::CssSyntaxToken;
use biome_formatter::token::string::normalize_string;
use biome_formatter::QuoteStyle;
use biome_formatter::{
prelude::{dynamic_text, write},
token::string::ToAsciiLowercaseCow,
trivia::format_replaced,
Format, FormatResult,
};
use biome_rowan::SyntaxToken;
use crate::{prelude::CssFormatContext, AsFormat, CssFormatter};
pub(crate) struct FormatTokenAsLowercase {
token: SyntaxToken<CssLanguage>,
}
impl From<SyntaxToken<CssLanguage>> for FormatTokenAsLowercase {
fn from(value: SyntaxToken<CssLanguage>) -> Self {
Self { token: value }
}
}
impl Format<CssFormatContext> for FormatTokenAsLowercase {
fn fmt(&self, f: &mut CssFormatter) -> FormatResult<()> {
let original = self.token.text_trimmed();
match original.to_ascii_lowercase_cow() {
Cow::Borrowed(_) => write!(f, [self.token.format()]),
Cow::Owned(lowercase) => write!(
f,
[format_replaced(
&self.token,
&dynamic_text(&lowercase, self.token.text_trimmed_range().start()),
)]
),
}
}
}
pub(crate) struct FormatLiteralStringToken<'token> {
token: &'token CssSyntaxToken,
}
impl<'token> FormatLiteralStringToken<'token> {
pub fn new(token: &'token CssSyntaxToken) -> Self {
Self { token }
}
fn token(&self) -> &'token CssSyntaxToken {
self.token
}
pub fn clean_text(&self, options: &CssFormatOptions) -> CleanedStringLiteralText {
let token = self.token();
debug_assert!(
matches!(
token.kind(),
CSS_STRING_LITERAL | CSS_URL_VALUE_RAW_LITERAL | IDENT
),
"Found kind {:?}",
token.kind()
);
let chosen_quote_style = options.quote_style();
let mut string_cleaner = LiteralStringNormaliser::new(self, chosen_quote_style);
let content = string_cleaner.normalise_text();
CleanedStringLiteralText {
text: content,
token,
}
}
}
pub(crate) struct CleanedStringLiteralText<'a> {
token: &'a CssSyntaxToken,
text: Cow<'a, str>,
}
impl Format<CssFormatContext> for CleanedStringLiteralText<'_> {
fn fmt(&self, f: &mut Formatter<CssFormatContext>) -> FormatResult<()> {
format_replaced(
self.token,
&syntax_token_cow_slice(
self.text.clone(),
self.token,
self.token.text_trimmed_range().start(),
),
)
.fmt(f)
}
}
impl Format<CssFormatContext> for FormatLiteralStringToken<'_> {
fn fmt(&self, f: &mut CssFormatter) -> FormatResult<()> {
let cleaned = self.clean_text(f.options());
cleaned.fmt(f)
}
}
struct StringInformation {
preferred_quote: QuoteStyle,
raw_content_has_quotes: bool,
}
impl FormatLiteralStringToken<'_> {
fn compute_string_information(&self, chosen_quote: QuoteStyle) -> StringInformation {
if !matches!(self.token().kind(), CSS_STRING_LITERAL) {
return StringInformation {
raw_content_has_quotes: false,
preferred_quote: chosen_quote,
};
}
let literal = self.token().text_trimmed();
let alternate = chosen_quote.other();
let char_count = literal.chars().count();
let (preferred_quotes_count, alternate_quotes_count) = literal.chars().enumerate().fold(
(0, 0),
|(preferred_quotes_counter, alternate_quotes_counter), (index, current_character)| {
if index == 0 || index == char_count - 1 {
(preferred_quotes_counter, alternate_quotes_counter)
} else if current_character == chosen_quote.as_char() {
(preferred_quotes_counter + 1, alternate_quotes_counter)
} else if current_character == alternate.as_char() {
(preferred_quotes_counter, alternate_quotes_counter + 1)
} else {
(preferred_quotes_counter, alternate_quotes_counter)
}
},
);
StringInformation {
raw_content_has_quotes: preferred_quotes_count > 0 || alternate_quotes_count > 0,
preferred_quote: if preferred_quotes_count > alternate_quotes_count {
alternate
} else {
chosen_quote
},
}
}
}
struct LiteralStringNormaliser<'token> {
token: &'token FormatLiteralStringToken<'token>,
chosen_quote_style: QuoteStyle,
}
impl<'token> LiteralStringNormaliser<'token> {
pub fn new(
token: &'token FormatLiteralStringToken<'_>,
chosen_quote_style: QuoteStyle,
) -> Self {
Self {
token,
chosen_quote_style,
}
}
fn normalise_text(&mut self) -> Cow<'token, str> {
let string_information = self
.token
.compute_string_information(self.chosen_quote_style);
match self.token.token.kind() {
CSS_STRING_LITERAL => self.normalise_string_literal(string_information),
_ => self.normalise_non_string_token(string_information),
}
}
fn get_token(&self) -> &'token CssSyntaxToken {
self.token.token()
}
fn normalise_string_literal(&self, string_information: StringInformation) -> Cow<'token, str> {
let preferred_quote = string_information.preferred_quote;
let polished_raw_content = self.normalize_string(&string_information);
match polished_raw_content {
Cow::Borrowed(raw_content) => {
let final_content = self.swap_quotes(raw_content, &string_information);
match final_content {
Cow::Borrowed(final_content) => Cow::Borrowed(final_content),
Cow::Owned(final_content) => Cow::Owned(final_content),
}
}
Cow::Owned(s) => {
let final_content = std::format!(
"{}{}{}",
preferred_quote.as_char(),
s.as_str(),
preferred_quote.as_char()
);
Cow::Owned(final_content)
}
}
}
fn normalise_non_string_token(
&self,
string_information: StringInformation,
) -> Cow<'token, str> {
let preferred_quote = string_information.preferred_quote;
let polished_raw_content = self.normalize_string(&string_information);
match polished_raw_content {
Cow::Borrowed(raw_content) => {
let final_content = self.swap_quotes(raw_content, &string_information);
match final_content {
Cow::Borrowed(final_content) => Cow::Borrowed(final_content),
Cow::Owned(final_content) => Cow::Owned(final_content),
}
}
Cow::Owned(s) => {
let final_content = std::format!(
"{}{}{}",
preferred_quote.as_char(),
s.as_str(),
preferred_quote.as_char()
);
Cow::Owned(final_content)
}
}
}
fn normalize_string(&self, string_information: &StringInformation) -> Cow<'token, str> {
let raw_content = self.raw_content();
normalize_string(raw_content, string_information.preferred_quote.into(), true)
}
fn raw_content(&self) -> &'token str {
let token = self.get_token();
match token.kind() {
CSS_STRING_LITERAL => {
let content = token.text_trimmed();
&content[1..content.len() - 1]
}
_ => token.text_trimmed(),
}
}
fn swap_quotes(
&self,
content_to_use: &'token str,
string_information: &StringInformation,
) -> Cow<'token, str> {
let original_content = self.get_token().text_trimmed();
let preferred_quote = string_information.preferred_quote;
let raw_content_has_quotes = string_information.raw_content_has_quotes;
if raw_content_has_quotes {
Cow::Borrowed(original_content)
} else if !original_content.starts_with(preferred_quote.as_char()) {
Cow::Owned(std::format!(
"{}{}{}",
preferred_quote.as_char(),
content_to_use,
preferred_quote.as_char()
))
} else {
Cow::Borrowed(original_content)
}
}
}