use std::borrow::Cow;
use super::{CssParser, is_boolean_operator};
use crate::lexer::TokenKind;
use crate::url::trim_url_raw;
use crate::whitespace::{Comments, Edge, holds_boundary_member, spell_run};
use tsv_lang::printing::format_string_literal;
use tsv_lang::{Error, Span};
pub(super) fn parse_raw_prelude_content<'arena>(
parser: &mut CssParser<'_, 'arena>,
normalize_whitespace: bool,
normalize_quotes: bool,
) -> Result<(&'arena str, Span), Error> {
let prelude_start_raw = parser.current_start();
let prelude_start = parser.span_pos(prelude_start_raw);
let mut prelude = String::new();
let mut trailing_spaces: usize = 0;
let mut prev_token_kind: Option<TokenKind> = None;
let mut last_non_whitespace_kind: Option<TokenKind> = None;
let mut paren_depth: u32 = 0;
let mut feature_colon_seen = false;
let mut after_splitting_colon = false;
let mut ends_in_verbatim_run = false;
let mut ends = PreludeEnds::default();
while !parser.at_prelude_end() {
ends.observe(parser, prelude.len(), trailing_spaces);
if parser.check(TokenKind::Whitespace) {
if !normalize_whitespace {
prelude.push_str(parser.current_value());
parser.advance()?;
prev_token_kind = Some(TokenKind::Whitespace);
continue;
}
let at_expression_open =
matches!(prev_token_kind, Some(TokenKind::LeftParen)) && paren_depth == 1;
let at_expression_close = matches!(parser.peek_kind(), Ok(TokenKind::RightParen))
&& paren_depth == 1
&& !after_splitting_colon;
let skip_whitespace = at_expression_open || at_expression_close;
let raw_run = parser.current_value();
let verbatim_run = paren_depth >= 1
&& feature_colon_seen
&& !after_splitting_colon
&& !raw_run.contains(['\n', '\r']);
let raw_run: Option<String> = verbatim_run.then(|| raw_run.to_owned());
parser.advance()?;
if skip_whitespace {
continue;
}
match raw_run {
Some(run) => {
prelude.push_str(&run);
trailing_spaces = 0;
ends_in_verbatim_run = true;
}
None => {
prelude.push(' ');
trailing_spaces += 1;
ends_in_verbatim_run = false;
after_splitting_colon = false;
}
}
prev_token_kind = Some(TokenKind::Whitespace);
continue;
}
if matches!(parser.current_kind(), TokenKind::Url) {
let raw = parser.current_value();
if raw.starts_with("url(") {
match trim_url_raw(raw) {
Some(trimmed) => prelude.push_str(&trimmed),
None => prelude.push_str(raw),
}
} else {
prelude.push_str(raw);
}
trailing_spaces = 0;
ends_in_verbatim_run = false;
after_splitting_colon = false;
prev_token_kind = Some(TokenKind::Url);
last_non_whitespace_kind = Some(TokenKind::Url);
parser.advance()?;
continue;
}
if matches!(parser.current_kind(), TokenKind::Identifier)
&& parser.current_value().eq_ignore_ascii_case("url")
&& matches!(parser.peek_kind(), Ok(TokenKind::LeftParen))
{
let is_lowercase_url = parser.current_value() == "url";
let url_start = parser.current_start();
parser.advance()?; let mut depth: u32 = 0;
let mut url_end;
loop {
match parser.current_kind() {
TokenKind::LeftParen => depth += 1,
TokenKind::RightParen => depth = depth.saturating_sub(1),
TokenKind::Eof => {
url_end = parser.current_start();
break;
}
_ => {}
}
let is_close = depth == 0 && matches!(parser.current_kind(), TokenKind::RightParen);
url_end = parser.current_end();
parser.advance()?;
if is_close {
break;
}
}
let raw = &parser.source()[url_start..url_end];
let part: Cow<'_, str> = if is_lowercase_url {
let trimmed = trim_url_raw(raw).unwrap_or(Cow::Borrowed(raw));
if normalize_quotes {
Cow::Owned(normalize_url_string_quote(&trimmed))
} else {
trimmed
}
} else {
Cow::Borrowed(raw)
};
prelude.push_str(&part);
trailing_spaces = 0;
ends_in_verbatim_run = false;
after_splitting_colon = false;
prev_token_kind = Some(TokenKind::RightParen);
last_non_whitespace_kind = Some(TokenKind::RightParen);
continue;
}
let is_bool_op = is_boolean_operator(parser);
let is_comment = matches!(parser.current_kind(), TokenKind::Comment);
let pads_boolean_operator = is_bool_op
&& last_non_whitespace_kind != Some(TokenKind::LeftParen)
&& paren_depth == 0;
let pads_comment = is_comment && paren_depth == 0;
if normalize_whitespace {
let has_trailing_space = trailing_spaces > 0 || ends_in_verbatim_run;
if (pads_comment || pads_boolean_operator) && !has_trailing_space {
prelude.push(' ');
trailing_spaces += 1;
}
let strips_leading_space = match parser.current_kind() {
TokenKind::Colon => paren_depth == 0 || !feature_colon_seen,
TokenKind::Comma => paren_depth == 0,
_ => false,
};
if strips_leading_space {
prelude.truncate(prelude.len() - trailing_spaces);
}
}
match parser.current_kind() {
TokenKind::String { quote } => {
let content =
&parser.source()[parser.current_start() + 1..parser.current_end() - 1];
if normalize_quotes {
prelude.push_str(&format_string_literal(content, quote));
} else {
prelude.push(quote);
prelude.push_str(content);
prelude.push(quote);
}
}
_ => prelude.push_str(parser.current_value()),
}
trailing_spaces = 0;
ends_in_verbatim_run = false;
let current_kind = parser.current_kind();
parser.advance()?;
if normalize_whitespace && !parser.check(TokenKind::Whitespace) {
if pads_boolean_operator {
prelude.push(' ');
trailing_spaces += 1;
} else if pads_comment {
if !matches!(
parser.current_kind(),
TokenKind::Comma | TokenKind::RightParen | TokenKind::Semicolon
) {
prelude.push(' ');
trailing_spaces += 1;
}
} else if matches!(current_kind, TokenKind::Comma) && paren_depth == 0 {
prelude.push(' ');
trailing_spaces += 1;
} else if matches!(current_kind, TokenKind::Colon)
&& (paren_depth == 0 || !feature_colon_seen)
{
let should_add_space = matches!(
last_non_whitespace_kind,
Some(TokenKind::Identifier)
| Some(TokenKind::Number)
| Some(TokenKind::Dimension { .. })
| Some(TokenKind::Percentage)
);
if should_add_space {
prelude.push(' ');
trailing_spaces += 1;
}
}
}
prev_token_kind = Some(current_kind);
if !matches!(current_kind, TokenKind::Whitespace) {
last_non_whitespace_kind = Some(current_kind);
}
after_splitting_colon =
matches!(current_kind, TokenKind::Colon) && paren_depth >= 1 && !feature_colon_seen;
match current_kind {
TokenKind::LeftParen => paren_depth += 1,
TokenKind::RightParen => {
paren_depth = paren_depth.saturating_sub(1);
if paren_depth == 0 {
feature_colon_seen = false;
}
}
TokenKind::Colon if paren_depth >= 1 => feature_colon_seen = true,
_ => {}
}
}
let prelude = ends
.respell(
parser.source(),
prelude_start_raw,
parser.current_start(),
&prelude,
)
.unwrap_or(prelude);
let content = parser.alloc_str_in(crate::escapes::trim_css(&prelude));
let prelude_end = parser.span_pos(parser.current_start());
let span = Span {
start: prelude_start,
end: prelude_end,
};
Ok((content, span))
}
#[derive(Default)]
pub(super) struct PreludeEnds {
head: Option<(usize, usize)>,
tail: Option<(usize, usize)>,
}
impl PreludeEnds {
pub(super) fn observe(
&mut self,
parser: &CssParser<'_, '_>,
out_len: usize,
trailing_spaces: usize,
) {
let is_gap = match parser.current_kind() {
TokenKind::Whitespace | TokenKind::Comment => true,
TokenKind::Identifier => {
parser.boundary_run_len() == parser.current_end() - parser.current_start()
}
_ => false,
};
if !is_gap {
self.head
.get_or_insert_with(|| (parser.current_start(), out_len));
self.tail = None;
} else if self.head.is_some() && self.tail.is_none() {
self.tail = Some((parser.current_start(), out_len - trailing_spaces));
}
}
pub(super) fn respell(
&self,
source: &str,
start: usize,
end: usize,
out: &str,
) -> Option<String> {
let spell = |from: usize, to: usize, edges: (Edge, Edge)| {
spell_run(source, from, to, false, edges, Comments::Carried)
};
let Some((head_raw, head_len)) = self.head else {
return holds_boundary_member(source, start, end)
.then(|| spell(start, end, (Edge::Flush, Edge::Flush)));
};
let (tail_raw, tail_len) = self.tail.unwrap_or((end, out.len()));
let head_holds = holds_boundary_member(source, start, head_raw);
let tail_holds = holds_boundary_member(source, tail_raw, end);
if !head_holds && !tail_holds {
return None;
}
let mut respelled = String::with_capacity(out.len());
if head_holds {
respelled.push_str(&spell(start, head_raw, (Edge::Flush, Edge::Presence)));
} else {
respelled.push_str(&out[..head_len]);
}
respelled.push_str(&out[head_len..tail_len]);
if tail_holds {
respelled.push_str(&spell(tail_raw, end, (Edge::Presence, Edge::Flush)));
} else {
respelled.push_str(&out[tail_len..]);
}
Some(respelled)
}
}
fn normalize_url_string_quote(url: &str) -> String {
if !url.starts_with("url(") || !url.ends_with(')') {
return url.to_string();
}
let inner = &url[4..url.len() - 1];
let b = inner.as_bytes();
if b.len() >= 2 && (b[0] == b'"' || b[0] == b'\'') && b[b.len() - 1] == b[0] {
format!(
"url({})",
format_string_literal(&inner[1..inner.len() - 1], b[0] as char)
)
} else {
url.to_string()
}
}