use crate::ast;
use std::borrow::Cow;
use tsv_lang::Span;
use tsv_lang::is_js_whitespace;
use tsv_lang::{trim_end_js_whitespace, trim_js_whitespace, trim_start_js_whitespace};
mod write;
pub(crate) use write::write_stylesheet_file_bytes;
pub use write::{write_css_children, write_css_comments};
#[inline]
fn trim_wire(s: &str) -> &str {
trim_js_whitespace(s)
}
#[inline]
pub(super) fn trim_wire_start(s: &str) -> &str {
trim_start_js_whitespace(s)
}
#[inline]
pub(super) fn trim_wire_end(s: &str) -> &str {
trim_end_js_whitespace(s)
}
pub(super) fn split_declaration_svelte_compat(decl_source: &str, colon_pos: usize) -> (&str, &str) {
let before_colon = &decl_source[..colon_pos];
if let Some(comment_idx) = before_colon.find("/*") {
let before_comment = &before_colon[..comment_idx];
if !trim_wire(before_comment).is_empty() {
let property = trim_wire(before_comment);
let value = &decl_source[comment_idx..];
return (property, value);
}
}
let property = &decl_source[..colon_pos];
let value = trim_wire_start(&decl_source[colon_pos + 1..]);
(property, value)
}
fn strip_css_comments(input: &str) -> Cow<'_, str> {
strip_css_comments_inner(input, 0, None)
}
pub(super) fn strip_css_comments_collecting<'a>(
input: &'a str,
base: u32,
sink: &mut Vec<WireComment>,
) -> Cow<'a, str> {
strip_css_comments_inner(input, base, Some(sink))
}
#[derive(Clone, Copy)]
pub(super) struct WireComment {
pub(super) span: Span,
pub(super) position: Option<u32>,
}
fn strip_css_comments_inner<'a>(
input: &'a str,
base: u32,
sink: Option<&mut Vec<WireComment>>,
) -> Cow<'a, str> {
if !input.contains("/*") {
return Cow::Borrowed(trim_wire(input));
}
let mut out = String::with_capacity(input.len());
let mut stripped: Vec<(usize, Span)> = Vec::new();
let mut rest = input;
while let Some(ch) = rest.chars().next() {
if crate::comments::is_comment_start(rest.as_bytes(), 0) {
let Some(end) = crate::comments::comment_end_checked(rest.as_bytes(), 0) else {
out.push_str(rest);
break;
};
if sink.is_some() {
let start = base as usize + (input.len() - rest.len());
stripped.push((out.len(), Span::new(start as u32, (start + end) as u32)));
}
rest = &rest[end..];
continue;
}
if ch == '"' || ch == '\'' {
emit(&mut out, &mut rest, ch);
copy_quoted(&mut out, &mut rest, ch);
continue;
}
if starts_with_url_open(rest) {
out.push_str(&rest[..4]);
rest = &rest[4..];
copy_balanced_parens(&mut out, &mut rest);
continue;
}
emit(&mut out, &mut rest, ch);
}
let end = trim_wire_end(&out).len();
let leading = out.len() - trim_wire_start(&out).len();
if let Some(sink) = sink {
sink.reserve(stripped.len());
let (mut cursor, mut position) = (leading, 0usize);
for (offset, span) in stripped {
let offset = offset.max(leading);
position += out[cursor..offset]
.chars()
.map(char::len_utf16)
.sum::<usize>();
cursor = offset;
sink.push(WireComment {
span,
position: Some(position as u32),
});
}
}
out.truncate(end);
out.drain(..leading.min(end));
Cow::Owned(out)
}
fn emit(out: &mut String, rest: &mut &str, ch: char) {
out.push(ch);
*rest = &rest[ch.len_utf8()..];
}
fn copy_quoted(out: &mut String, rest: &mut &str, quote: char) {
while let Some(ch) = rest.chars().next() {
emit(out, rest, ch);
if ch == '\\' {
if let Some(esc) = rest.chars().next() {
emit(out, rest, esc);
}
} else if ch == quote {
break;
}
}
}
fn copy_balanced_parens(out: &mut String, rest: &mut &str) {
let mut depth: u32 = 1;
while depth > 0 {
let Some(ch) = rest.chars().next() else { break };
emit(out, rest, ch);
match ch {
'(' => depth += 1,
')' => depth -= 1,
'"' | '\'' => copy_quoted(out, rest, ch),
_ => {}
}
}
}
fn starts_with_url_open(s: &str) -> bool {
let bytes = s.as_bytes();
bytes.len() >= 4
&& bytes[0].eq_ignore_ascii_case(&b'u')
&& bytes[1].eq_ignore_ascii_case(&b'r')
&& bytes[2].eq_ignore_ascii_case(&b'l')
&& bytes[3] == b'('
}
pub(super) fn convert_prelude_to_string<'src>(
prelude: &ast::PreludeValue<'_>,
source: &'src str,
) -> Cow<'src, str> {
match prelude {
ast::PreludeValue::Values { span, .. } => {
strip_css_comments(span.extract(source))
}
ast::PreludeValue::Raw { span, .. } => strip_css_comments(span.extract(source)),
ast::PreludeValue::Selectors { span, .. } => strip_css_comments(span.extract(source)),
ast::PreludeValue::CustomSelector { span, .. } => strip_css_comments(span.extract(source)),
ast::PreludeValue::Supports { span, .. } => strip_css_comments(span.extract(source)),
ast::PreludeValue::Container { span, .. } => strip_css_comments(span.extract(source)),
ast::PreludeValue::Media { span, .. } => strip_css_comments(span.extract(source)),
}
}
pub(super) fn selector_contains_invalid(complex: &ast::ComplexSelector<'_>) -> bool {
for relative in complex.children {
for simple in relative.selectors {
if matches!(simple, ast::SimpleSelector::Invalid { .. }) {
return true;
}
}
}
false
}
pub(super) fn raw_selector_name(source: &str, span: Span, prefix_len: usize) -> Cow<'_, str> {
let (start, end) = (span.start as usize + prefix_len, span.end as usize);
if end <= source.len()
&& tsv_lang::swar::next_byte_below_or_of(source.as_bytes(), start, end, 0, [b'\\']) == end
{
return Cow::Borrowed(&source[start..end]);
}
raw_selector_name_decode(source, span, prefix_len)
}
#[cold]
#[inline(never)]
fn raw_selector_name_decode(source: &str, span: Span, prefix_len: usize) -> Cow<'_, str> {
let raw = &source[span.start as usize + prefix_len..span.end as usize];
if !raw.contains('\\') {
return Cow::Borrowed(raw);
}
let mut out = String::with_capacity(raw.len());
let mut chars = raw.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '\\' {
out.push(ch);
continue;
}
if chars.peek().is_some_and(char::is_ascii_hexdigit) {
let mut hex = String::new();
for _ in 0..6 {
match chars.peek() {
Some(&d) if d.is_ascii_hexdigit() => {
hex.push(d);
chars.next();
}
_ => break,
}
}
if chars.peek() == Some(&'\r') {
chars.next();
if chars.peek() == Some(&'\n') {
chars.next();
}
} else if chars.peek().copied().is_some_and(is_js_whitespace) {
chars.next();
}
if let Ok(cp) = u32::from_str_radix(&hex, 16)
&& let Some(c) = char::from_u32(cp)
{
if c == '\\' {
out.push_str("\\\\");
} else {
out.push(c);
}
}
} else if let Some(next) = chars.next() {
out.push('\\');
out.push(next);
} else {
out.push('\\');
}
}
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn strip(s: &str) -> String {
strip_css_comments(s).into_owned()
}
#[test]
fn strip_css_comments_basic_removal_and_trim() {
assert_eq!(strip("/* c */ 12px"), "12px");
assert_eq!(strip("blue /* c */"), "blue");
assert_eq!(strip("/* a */ red"), "red");
}
#[test]
fn strip_css_comments_interior_whitespace_preserved() {
assert_eq!(strip("var(--a, /* c */ red)"), "var(--a, red)",);
assert_eq!(
strip("sidebar /* x */ (min-width: 100px)"),
"sidebar (min-width: 100px)",
);
}
#[test]
fn strip_css_comments_inside_strings_are_preserved() {
assert_eq!(strip("\"/* not a comment */\""), "\"/* not a comment */\"",);
assert_eq!(strip("'/* keep */'"), "'/* keep */'");
}
#[test]
fn strip_css_comments_inside_url_are_preserved() {
assert_eq!(
strip("url(\"data:image/svg+xml,/* x */\")"),
"url(\"data:image/svg+xml,/* x */\")",
);
}
#[test]
fn strip_css_comments_inside_other_functions_are_stripped() {
assert_eq!(strip("calc(/* x */ 1px + 2px)"), "calc( 1px + 2px)",);
assert_eq!(strip("URL(/* keep */)"), "URL(/* keep */)");
}
#[test]
fn strip_css_comments_unterminated_kept_verbatim() {
assert_eq!(strip("red /* oops"), "red /* oops");
}
#[test]
fn strip_css_comments_escaped_quote_does_not_close_string() {
assert_eq!(
strip("\"a\\\" /* in str */ b\" /* real */ c"),
"\"a\\\" /* in str */ b\" c",
);
}
type Recorded = (u32, u32, Option<u32>);
fn collect(input: &str, base: u32) -> (String, Vec<Recorded>) {
let mut sink = Vec::new();
let value = strip_css_comments_collecting(input, base, &mut sink).into_owned();
let recorded = sink
.iter()
.map(|c| (c.span.start, c.span.end, c.position))
.collect();
(value, recorded)
}
#[test]
fn strip_css_comments_collecting_records_spans_and_positions() {
assert_eq!(
collect(" red /* c */ blue", 100),
("red blue".to_owned(), vec![(105, 112, Some(4))]),
);
assert_eq!(
collect("a /* c1 */ b /* c2 */ c", 0),
(
"a b c".to_owned(),
vec![(2, 10, Some(2)), (13, 21, Some(5))],
),
);
}
#[test]
fn strip_css_comments_collecting_position_edges() {
assert_eq!(
collect("/* c */ red", 0),
("red".to_owned(), vec![(0, 7, Some(0))])
);
assert_eq!(
collect("red /* c */", 0),
("red".to_owned(), vec![(4, 11, Some(4))])
);
assert_eq!(
collect("'😀' /* c */", 0),
("'😀'".to_owned(), vec![(7, 14, Some(5))]),
);
}
#[test]
fn strip_css_comments_collecting_skips_unterminated() {
assert_eq!(
collect("red /* oops", 0),
("red /* oops".to_owned(), vec![])
);
}
}