dioxus-code 0.0.1

Syntax-highlighted code blocks for Dioxus.
use std::env;
use std::fs;
use std::path::PathBuf;

use arborium::theme::builtin;

struct ThemeDef {
    const_name: &'static str,
    theme: arborium::theme::Theme,
}

fn main() {
    println!("cargo:rerun-if-changed=build.rs");

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let asset_dir = manifest_dir.join("assets/generated/arborium-themes");
    fs::create_dir_all(&asset_dir).unwrap();

    let themes = themes();
    let mut generated = String::from(
        r#"impl Theme {
"#,
    );

    for theme in themes {
        let name = slug(&theme.theme.name);
        let css_file = format!("{name}.css");
        let class = format!("dxc-{name}");
        let selector = format!(".{class}");
        let css = flatten_theme_css(&selector, &theme.theme.to_css(&selector));
        fs::write(asset_dir.join(&css_file), css).unwrap();

        generated.push_str(&format!(
            "    /// Stylesheet asset for the `{name}` theme.\n    pub const {const_name}_CSS: Asset = asset!(\"/assets/generated/arborium-themes/{css_file}\");\n",
            const_name = theme.const_name,
            css_file = css_file,
            name = name,
        ));

        generated.push_str(&format!(
            "    /// The `{name}` syntax theme.\n    pub const {const_name}: Self = Self {{ name: \"{name}\", class: \"{class}\", asset: Self::{const_name}_CSS }};\n",
            const_name = theme.const_name,
            name = name,
            class = class,
        ));
    }

    generated.push_str(
        r#"}
"#,
    );

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    fs::write(out_dir.join("theme_assets.rs"), generated).unwrap();
}

fn flatten_theme_css(selector: &str, css: &str) -> String {
    let mut base = Vec::new();
    let mut nested = Vec::new();

    for line in css.lines().skip(1) {
        let trimmed = line.trim();
        if trimmed == "}" || trimmed.is_empty() {
            continue;
        }

        if trimmed.starts_with("a-") {
            let (tag, rest) = trimmed.split_once(' ').unwrap();
            nested.push(format!("{selector} .{tag} {rest}"));
        } else {
            base.push(format!("  {trimmed}"));
        }
    }

    let mut flattened = String::new();
    flattened.push_str(selector);
    flattened.push_str(" {\n");
    for line in base {
        flattened.push_str(&line);
        flattened.push('\n');
    }
    flattened.push_str("}\n");

    for line in nested {
        flattened.push_str(&line);
        flattened.push('\n');
    }

    flattened
}

fn themes() -> Vec<ThemeDef> {
    vec![
        theme("ALABASTER", builtin::alabaster()),
        theme("AYU_DARK", builtin::ayu_dark()),
        theme("AYU_LIGHT", builtin::ayu_light()),
        theme("CATPPUCCIN_FRAPPE", builtin::catppuccin_frappe()),
        theme("CATPPUCCIN_LATTE", builtin::catppuccin_latte()),
        theme("CATPPUCCIN_MACCHIATO", builtin::catppuccin_macchiato()),
        theme("CATPPUCCIN_MOCHA", builtin::catppuccin_mocha()),
        theme("COBALT2", builtin::cobalt2()),
        theme("DAYFOX", builtin::dayfox()),
        theme("DESERT256", builtin::desert256()),
        theme("DRACULA", builtin::dracula()),
        theme("EF_MELISSA_DARK", builtin::ef_melissa_dark()),
        theme("GITHUB_DARK", builtin::github_dark()),
        theme("GITHUB_LIGHT", builtin::github_light()),
        theme("GRUVBOX_DARK", builtin::gruvbox_dark()),
        theme("GRUVBOX_LIGHT", builtin::gruvbox_light()),
        theme("KANAGAWA_DRAGON", builtin::kanagawa_dragon()),
        theme("LIGHT_OWL", builtin::light_owl()),
        theme("LUCIUS_LIGHT", builtin::lucius_light()),
        theme("MELANGE_DARK", builtin::melange_dark()),
        theme("MELANGE_LIGHT", builtin::melange_light()),
        theme("MONOKAI", builtin::monokai()),
        theme("NORD", builtin::nord()),
        theme("ONE_DARK", builtin::one_dark()),
        theme("ROSE_PINE_MOON", builtin::rose_pine_moon()),
        theme("RUSTDOC_AYU", builtin::rustdoc_ayu()),
        theme("RUSTDOC_DARK", builtin::rustdoc_dark()),
        theme("RUSTDOC_LIGHT", builtin::rustdoc_light()),
        theme("SOLARIZED_DARK", builtin::solarized_dark()),
        theme("SOLARIZED_LIGHT", builtin::solarized_light()),
        theme("TOKYO_NIGHT", builtin::tokyo_night()),
        theme("ZENBURN", builtin::zenburn()),
    ]
}

fn theme(const_name: &'static str, theme: arborium::theme::Theme) -> ThemeDef {
    ThemeDef { const_name, theme }
}

fn slug(name: &str) -> String {
    let mut slug = String::new();

    for ch in name.chars() {
        match ch {
            'a'..='z' | '0'..='9' => slug.push(ch),
            'A'..='Z' => slug.push(ch.to_ascii_lowercase()),
            ' ' | '_' | '-' if !slug.ends_with('-') => slug.push('-'),
            'é' | 'É' => slug.push('e'),
            _ => {}
        }
    }

    slug.trim_matches('-').to_string()
}