use crate::ast::{CssValue, StringCooked};
use crate::escapes;
use bumpalo::Bump;
use std::borrow::Cow;
use tsv_lang::Span;
pub(crate) fn parse_string_literal<'arena>(
s: &str,
span: Span,
arena: &'arena Bump,
) -> Option<CssValue<'arena>> {
let bytes = s.as_bytes();
if let Some("e) = bytes.first()
&& (quote == b'"' || quote == b'\'')
&& quoted_string_spans_all(bytes)
{
let raw_content = &s[1..s.len() - 1];
let content = match escapes::decode_escape_sequences_in(raw_content, arena) {
None => StringCooked::Verbatim,
Some(decoded) => StringCooked::Decoded(decoded),
};
debug_assert!(
match (&content, escapes::decode_escape_sequences(raw_content)) {
(StringCooked::Verbatim, Cow::Borrowed(_)) => true,
(StringCooked::Decoded(decoded), Cow::Owned(reference)) => **decoded == reference,
_ => false,
},
"the arena decoder left the reference decoder on {raw_content:?}"
);
return Some(CssValue::String { content, span });
}
None
}
fn quoted_string_spans_all(bytes: &[u8]) -> bool {
matches!(crate::lexer::string_end(bytes, 0), Ok(end) if end == bytes.len())
}
#[cfg(test)]
mod tests {
use super::{parse_string_literal, quoted_string_spans_all};
use crate::ast::{CssValue, StringCooked};
use crate::escapes::decode_escape_sequences;
use bumpalo::Bump;
use std::borrow::Cow;
use tsv_lang::Span;
#[test]
fn a_literal_decodes_as_the_reference_decoder_does() {
let bodies = [
"",
"abc",
"é",
r"\41",
r"\41 b",
"\\41\r\nb",
r"\110000",
r"\D800x",
r"\0",
"a\\\nb",
r"a\'b",
r#"a\"b"#,
r"\\",
r"é\é",
];
let arena = Bump::new();
for body in bodies {
for quote in ['"', '\''] {
let literal = format!("{quote}{body}{quote}");
let span = Span::new(0, literal.len() as u32);
let Some(CssValue::String { content, .. }) =
parse_string_literal(&literal, span, &arena)
else {
panic!("{literal:?} is one string");
};
match (content, decode_escape_sequences(body)) {
(StringCooked::Verbatim, Cow::Borrowed(_)) => {}
(StringCooked::Decoded(decoded), Cow::Owned(reference)) => {
assert_eq!(decoded, reference, "{literal:?}");
}
(content, reference) => panic!("{literal:?}: {content:?} vs {reference:?}"),
}
for open in [
format!("{quote}{body}"),
format!("{quote}{body}\\{quote}"),
format!("{quote}{body}{quote}x{quote}"),
] {
let span = Span::new(0, open.len() as u32);
assert!(
parse_string_literal(&open, span, &arena).is_none(),
"{open:?} is not one closed string"
);
}
}
}
}
fn spans_all(s: &str) -> bool {
quoted_string_spans_all(s.as_bytes())
}
#[test]
fn single_complete_strings_span_all() {
assert!(spans_all("'abc'"));
assert!(spans_all("\"abc\""));
assert!(spans_all("''")); assert!(spans_all("'a\"b'")); assert!(spans_all("'a\\'b'")); assert!(spans_all("'é\"café'")); }
#[test]
fn glued_runs_do_not_span_all() {
assert!(!spans_all("'a'x'b'"));
assert!(!spans_all("'a''b'"));
assert!(!spans_all("\"a\"x\"b\""));
assert!(!spans_all("'a\\\\'b'"));
assert!(!spans_all("'a\\'"));
}
}