makeover 5.0.0

Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast.
Documentation
//! Writing caller-supplied text into CSS.
//!
//! A theme file is user-writable, so a family name, a source file or a theme id
//! can hold anything. Each reaches the stylesheet through one of these, which is
//! the only place that decides how.

/// `text` as the body of a double-quoted CSS string.
///
/// `"` and `\` are escaped, `<` is escaped as `\3c ` so caller text cannot close
/// an inline stylesheet tag, and every control character, newline included, is
/// written as a hex escape: a raw newline ends a CSS string, which is a way out.
pub(crate) fn string_body(text: &str) -> String {
    use std::fmt::Write as _;

    let mut out = String::with_capacity(text.len());
    for c in text.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '<' => out.push_str("\\3c "),
            c if c.is_control() => {
                // The trailing space ends the escape, so a hex digit after it
                // is read as itself.
                let _ = write!(out, "\\{:x} ", u32::from(c));
            }
            c => out.push(c),
        }
    }
    out
}

/// `value` if it is a bare CSS keyword or number list (`700`, `100 900`,
/// `italic`, `oblique 10deg`), and `None` for anything that could end the
/// declaration or open another.
pub(crate) fn keyword(value: &str) -> Option<&str> {
    let bare = !value.trim().is_empty()
        && value
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '-' | '.' | '%'));
    bare.then_some(value)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_string_body_cannot_close_its_string() {
        assert_eq!(string_body(r#"Quasi "Mono""#), r#"Quasi \"Mono\""#);
        assert_eq!(string_body(r"a\b"), r"a\\b");
        assert_eq!(string_body("a\nb"), "a\\a b");
        assert_eq!(string_body("plain name"), "plain name");
        assert_eq!(string_body("</style>"), r"\3c /style>");
        assert_eq!(string_body("</style><script>"), r"\3c /style>\3c script>");
    }

    #[test]
    fn only_a_bare_keyword_is_a_keyword() {
        assert_eq!(keyword("700"), Some("700"));
        assert_eq!(keyword("100 900"), Some("100 900"));
        assert_eq!(keyword("oblique 10deg"), Some("oblique 10deg"));
        for hostile in ["700; } body { x: y", "normal}", "", "  ", "a/*"] {
            assert_eq!(keyword(hostile), None, "{hostile:?}");
        }
    }
}