use std::collections::HashSet;
use cssparser::{BasicParseErrorKind, DeclarationParser, ParseError, ParseErrorKind, Parser, ParserInput, ParserState, ToCss, Token};
pub fn filter_style_attribute(
style: &str,
names: &HashSet<&str>,
) -> String {
let mut out = String::with_capacity(style.len() + 1);
let mut input = ParserInput::new(style);
let mut p = Parser::new(&mut input);
loop {
match parse_one_declaration(&mut p, names) {
Ok((name, value)) => {
if !name.is_empty() {
out.push_str(&name);
out.push(':');
out.push_str(&value);
out.push(';');
}
},
Err(e) => match e.kind {
ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break,
ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(Token::Semicolon)) => (),
ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(_)) => {
advance(&mut p);
},
_ => unreachable!(
"parse_one_declaration should only attempt to parse an ident, a colon, \
or a Declaration, so its only errors should be EOF or an unexpected token"
),
},
}
}
if !out.is_empty() {
out.pop();
}
out
}
pub fn parse_one_declaration<'i, 't>(
input: &mut Parser<'i, 't>,
valid_properties: &HashSet<&str>,
) -> Result<(cssparser::CowRcStr<'i>, String), ParseError<'i, ()>>
{
let name = input.expect_ident()?.clone();
if !valid_properties.contains(&*name) {
advance(input);
return Ok(("".into(), String::new()));
}
input.expect_colon()?;
Declarations.parse_value(name, input, &input.state())
}
struct Declarations;
impl <'i> DeclarationParser<'i> for Declarations {
type Declaration = (cssparser::CowRcStr<'i>, String);
type Error = ();
fn parse_value<'t>(
&mut self,
name: cssparser::CowRcStr<'i>,
input: &mut Parser<'i, 't>,
_declaration_start: &ParserState,
) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
let mut value = String::new();
loop {
let t = match input.next() {
Err(e) if e.kind == cssparser::BasicParseErrorKind::EndOfInput => {
&Token::Semicolon
}
t => t?,
};
use Token::*;
match t {
Semicolon => {
if value.chars().all(char::is_whitespace) {
return Ok(("".into(), String::new()));
}
break
}
BadString(_) | BadUrl(_) => {
let err = cssparser::BasicParseErrorKind::UnexpectedToken(t.clone());
return Err(input.new_error(err));
}
Function(_) => {
if !value.is_empty() && value.chars().last() != Some(' ') {
value.push(' ');
}
let Ok(_) = t.to_css(&mut value) else {
let err = cssparser::BasicParseErrorKind::UnexpectedToken(t.clone());
return Err(input.new_error::<()>(err));
};
input.parse_nested_block(|p| {
let mut first = true;
loop {
match p.next() {
Ok(t) => {
if t.is_parse_error() {
let err = cssparser::BasicParseErrorKind::UnexpectedToken(t.clone());
return Err(p.new_error(err));
}
if !first && t != &Comma {
value.push(' ');
}
let Ok(_) = t.to_css(&mut value) else {
let err = cssparser::BasicParseErrorKind::UnexpectedToken(t.clone());
return Err(p.new_error::<()>(err));
};
first = false;
}
Err(e) if e.kind == BasicParseErrorKind::EndOfInput => break Ok(()),
Err(e) => return Err(e.into()),
}
}
})?;
value.push(')');
continue;
}
_ => (),
}
if !value.is_empty() && value.chars().last() != Some(' ') {
value.push(' ');
}
let Ok(_) = t.to_css(&mut value) else {
let err = cssparser::BasicParseErrorKind::UnexpectedToken(t.clone());
return Err(input.new_error(err));
};
}
if value.chars().all(char::is_whitespace) {
Err(input.new_error(cssparser::BasicParseErrorKind::EndOfInput))
} else {
Ok((name, value))
}
}
}
fn advance<'i, 't>(p: &mut Parser<'i, 't>) {
loop {
match p.next() {
Ok(Token::Semicolon) => { return }
Ok(Token::CurlyBracketBlock) => { return },
Err(e) if e.kind == cssparser::BasicParseErrorKind::EndOfInput => { return }
_ => ()
}
}
}
#[cfg(test)]
mod tests {
use super::filter_style_attribute;
use std::{collections::HashSet, sync::LazyLock};
#[test]
fn single_declaration() {
assert_eq!(
filter_style_attribute("font-style: italic", &HashSet::from(["font-style"])),
"font-style:italic",
);
}
#[test]
fn terminated_declaration() {
assert_eq!(
filter_style_attribute("font-style: italic;", &HashSet::from(["font-style"])),
"font-style:italic",
);
}
#[test]
fn complex() {
assert_eq!(
filter_style_attribute(
"background: no-repeat center/80% url(\"../img/image.png\");",
&HashSet::from(["background"]),
),
"background:no-repeat center / 80% url(\"../img/image.png\")",
)
}
#[test]
fn at_rule() {
assert_eq!(
filter_style_attribute(
"@unsupported { splines: reticulating } color: green",
&HashSet::from(["color", "splines"]),
),
"color:green",
);
}
#[test]
fn invalid_at_rules() {
assert_eq!(
filter_style_attribute("@charset 'utf-8'; color: green", &HashSet::from(["color"])),
"color:green",
);
assert_eq!(
filter_style_attribute("@foo url(https://example.org); color: green", &HashSet::from(["color"])),
"color:green",
);
assert_eq!(
filter_style_attribute("@media screen { color: red }; color: green", &HashSet::from(["color"])),
"color:green",
);
assert_eq!(
filter_style_attribute("@scope (main) { div { color: red } }; color: green", &HashSet::from(["color"])),
"color:green",
);
}
#[test]
fn empty_value() {
assert_eq!(
filter_style_attribute("content: ''", &HashSet::from(["content"])),
"content:\"\"",
)
}
static ALLOWED: LazyLock<HashSet<&str>> = LazyLock::new(|| HashSet::from(["color", "foo"]));
#[test]
fn multiple() {
assert_eq!(filter_style_attribute("foo: 1; color: green", &ALLOWED), "foo:1;color:green");
}
#[test]
fn malformed_declarations() {
let h = &HashSet::from(["color"]);
for decl in [
"color:green",
"color:green; color",
"color:green; color:",
"color:green; color{;color:maroon}",
] {
assert_eq!(
filter_style_attribute(decl, h),
"color:green",
"{}", decl,
);
}
for decl in [
"color:red; color; color:green",
"color:red; color:; color:green",
"color:red; color{;color:maroon}; color:green",
] {
assert_eq!(
filter_style_attribute(decl, h),
"color:red;color:green",
"{}", decl,
);
}
}
#[ignore = "can't recover from such a BadString (servo/rust-cssparser#393)"]
#[test]
fn badstring_escaped_newline() {
assert_eq!(filter_style_attribute("foo: '\n'; color: green", &ALLOWED), "color:green");
}
#[ignore = "can't recover from such a BadString (servo/rust-cssparser#393)"]
#[test]
fn badstring_literal_newline() {
assert_eq!(filter_style_attribute("foo: '
'; color: green", &ALLOWED), "color:green");
}
#[test]
fn bad_url() {
assert_eq!(filter_style_attribute("foo: url(x'y); color: green", &ALLOWED), "color:green");
}
}