//! # Userstyles 🎨 //! //! > **Bauke's collection of userstyles.** #![forbid(unsafe_code)] #![warn(missing_docs, clippy::missing_docs_in_private_items)] use serde::Deserialize; /// All potential errors that could occur. #[derive(Debug, thiserror::Error)] pub enum Error { /// [`std::io`] errors. #[error(transparent)] Io(#[from] std::io::Error), /// [`serde_json`] errors. #[error(transparent)] Json(#[from] serde_json::Error), } /// All available userstyles as a const, to easily iterate over them. pub const ALL_USERSTYLES: &[Userstyles] = &[Userstyles::TildesBaukula, Userstyles::TildesCompact]; /// All available userstyles. #[derive(Debug)] pub enum Userstyles { /// Tildes Baukula TildesBaukula, /// Tildes Compact TildesCompact, } /// Userstyle data. #[derive(Debug, Deserialize)] pub struct Userstyle { /// Compiled CSS. pub css: String, /// An optional image. pub image: Option>, /// Metadata of the userstyle. pub metadata: UserstyleMetadata, } /// Userstyle metadata. #[derive(Debug, Deserialize)] pub struct UserstyleMetadata { /// The name. pub name: String, /// The namespace. pub namespace: String, /// The version. pub version: String, /// The author. pub author: String, /// The description. pub description: String, /// The homepage URL. #[serde(rename = "homepageURL")] pub homepage_url: String, /// The update URL. #[serde(rename = "updateURL")] pub update_url: String, /// The license. pub license: String, /// The domain for `@-moz-document domain`. pub domain: String, } impl Userstyle { /// Formats a [`Userstyle`] in the expected `.user.css` format. pub fn format(&self) -> String { let mut css = String::new(); for line in self.css.lines() { if line.is_empty() { continue; } css += &format!(" {}\n", line); } format!( r#"/* ==UserStyle== @name {name} @namespace {namespace} @version {version} @author {author} @description {description} @homepageURL {homepage_url} @updateURL {update_url} @license {license} ==/UserStyle== */ @-moz-document domain("{domain}") {{ {css} }} "#, name = self.metadata.name, namespace = self.metadata.namespace, version = self.metadata.version, author = self.metadata.author, description = self.metadata.description, homepage_url = self.metadata.homepage_url, update_url = self.metadata.update_url, license = self.metadata.license, domain = self.metadata.domain, css = css.trim_end(), ) } /// Returns a [`Userstyle`] from a given `target` [`Userstyles`]. pub fn load(target: &Userstyles) -> Result { let (css, image, metadata) = match target { Userstyles::TildesBaukula => ( include_str!("../target/userstyles/tildes-baukula.css"), Some( include_bytes!("tildes-baukula/images/tildes-baukula-logo.png") .to_vec(), ), include_str!("tildes-baukula/metadata.json"), ), Userstyles::TildesCompact => ( include_str!("../target/userstyles/tildes-compact.css"), Some( include_bytes!("tildes-compact/images/tildes-compact-logo.png") .to_vec(), ), include_str!("tildes-compact/metadata.json"), ), }; Ok(Self { css: css.to_string(), image, metadata: serde_json::from_str(metadata)?, }) } } /// Assert that all metadata.json files parse successfully. #[test] fn test_metadata_parsing() { for target in ALL_USERSTYLES { assert!(Userstyle::load(target).is_ok()); } }