use ammonia::Builder; use once_cell::sync::Lazy; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; mod bbcode; mod color; mod email; mod handle; mod render; mod url; pub use bbcode::Tag; pub use render::NodeView; fn allow_styles<'u>(allowed: &[&str], value: &'u str) -> Option> { let mut altered = false; let rules: Vec<_> = value .split(';') .filter_map(|rule| { let name = rule.split(':').next()?.trim(); log::debug!("Checking '{}' against {:?}", name, allowed); if allowed.contains(&name) { Some(rule) } else { altered = true; None } }) .collect(); if altered { if rules.is_empty() { None } else { Some(Cow::Owned(rules.join(";"))) } } else { Some(Cow::Borrowed(value)) } } fn attribute_filter<'u>(element: &str, attribute: &str, value: &'u str) -> Option> { match (element, attribute) { ("ul", "style") | ("ol", "style") => allow_styles(&["list-style-type", "type"], value), ("span", "style") => allow_styles(&["color", "opacity"], value), ("div", "class") | ("span", "class") | ("pre", "class") | ("span", "data-symbol") | ("blockquote", "data-author") | ("a", "rel") | ("a", "title") | ("a", "href") | ("img", "src") | ("img", "title") | ("img", "alt") => Some(Cow::Borrowed(value)), _ => None, } } static STRIP_CONFIG: Lazy = Lazy::new(|| { let mut builder = Builder::new(); builder.allowed_classes(HashMap::new()).tags(HashSet::new()); builder }); static AMMONIA_CONFIG: Lazy = Lazy::new(|| { let mut classes = HashMap::new(); let div_hs = classes.entry("div").or_insert(HashSet::new()); div_hs.insert("center"); div_hs.insert("right"); div_hs.insert("toolkit-code"); let span_hs = classes.entry("span").or_insert(HashSet::new()); span_hs.insert("underline"); span_hs.insert("smallcaps"); span_hs.insert("monospace"); span_hs.insert("spoiler"); let pre_hs = classes.entry("pre").or_insert(HashSet::new()); pre_hs.insert("codeblock"); pre_hs.insert("toolkit-code--pre"); let mut schemes = HashSet::new(); schemes.insert("http"); schemes.insert("https"); schemes.insert("mailto"); let mut tags = HashSet::new(); tags.insert("div"); tags.insert("span"); tags.insert("pre"); tags.insert("code"); tags.insert("i"); tags.insert("em"); tags.insert("b"); tags.insert("strong"); tags.insert("s"); tags.insert("sub"); tags.insert("sup"); tags.insert("blockquote"); tags.insert("a"); tags.insert("img"); tags.insert("br"); tags.insert("hr"); let mut builder = Builder::new(); builder .tags(tags) .allowed_classes(classes) .url_schemes(schemes) .link_rel(Some("nofollow noopener noreferer")) .attribute_filter(attribute_filter) .add_tag_attributes("span", &["style"]) .add_tag_attributes("div", &["style"]); builder }); pub fn html(source: &str) -> String { let h = AMMONIA_CONFIG.clean(source).to_string(); log::debug!("{}", h); h } pub fn bbcode(source: &str, mapper: F) -> String where for<'a> F: Fn(NodeView<'a>) -> NodeView<'a> + Copy, { let stripped = STRIP_CONFIG.clean(source).to_string(); let preprocessed = render::preprocessor(&stripped, mapper); log::debug!("{}", preprocessed); preprocessed }