use std::collections::BTreeMap;
use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use arborium::theme::builtin;
struct ThemeDef {
const_name: &'static str,
theme: arborium::theme::Theme,
}
struct ParsedTheme {
const_name: &'static str,
slug: String,
css_file: String,
system_light_css_file: String,
system_dark_css_file: String,
class: String,
system_light_class: String,
system_dark_class: String,
rules: ThemeRules,
}
#[derive(Default)]
struct ThemeRules {
base: Vec<Declaration>,
tags: BTreeMap<String, Vec<Declaration>>,
}
#[derive(Clone)]
struct Declaration {
property: String,
value: String,
}
#[derive(Default)]
struct SharedRules {
base: Vec<String>,
tags: BTreeMap<String, Vec<String>>,
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rustc-check-cfg=cfg(docsrs)");
let asset_dir = if env::var_os("DOCS_RS").is_some() {
None
} else {
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();
Some(asset_dir)
};
let themes: Vec<_> = themes().into_iter().map(parse_theme).collect();
let mut shared_rules = SharedRules::default();
let mut generated = String::from(
r#"impl Theme {
const THEME_CSS: Asset = asset!("/assets/generated/arborium-themes/dioxus-code-theme.css");
"#,
);
for theme in &themes {
shared_rules.insert(&theme.rules);
if let Some(asset_dir) = &asset_dir {
fs::write(asset_dir.join(&theme.css_file), fixed_theme_css(theme)).unwrap();
fs::write(
asset_dir.join(&theme.system_light_css_file),
system_slot_css(&theme.system_light_class, "light", &theme.rules),
)
.unwrap();
fs::write(
asset_dir.join(&theme.system_dark_css_file),
system_slot_css(&theme.system_dark_class, "dark", &theme.rules),
)
.unwrap();
}
generated.push_str(&format!(
" const {const_name}_CSS: Asset = asset!(\"/assets/generated/arborium-themes/{css_file}\");\n",
const_name = theme.const_name,
css_file = theme.css_file,
));
generated.push_str(&format!(
" const {const_name}_SYSTEM_LIGHT_CSS: Asset = asset!(\"/assets/generated/arborium-themes/{css_file}\");\n",
const_name = theme.const_name,
css_file = theme.system_light_css_file,
));
generated.push_str(&format!(
" const {const_name}_SYSTEM_DARK_CSS: Asset = asset!(\"/assets/generated/arborium-themes/{css_file}\");\n",
const_name = theme.const_name,
css_file = theme.system_dark_css_file,
));
generated.push_str(&format!(
" /// The `{slug}` syntax theme.\n ///\n /// ```rust\n /// use dioxus_code::Theme;\n /// let _theme = Theme::{const_name};\n /// ```\n pub const {const_name}: Self = Self {{ stylesheet: ThemeStylesheet {{ class: \"{class}\", asset: Self::{const_name}_CSS }}, system_light: ThemeStylesheet {{ class: \"{system_light_class}\", asset: Self::{const_name}_SYSTEM_LIGHT_CSS }}, system_dark: ThemeStylesheet {{ class: \"{system_dark_class}\", asset: Self::{const_name}_SYSTEM_DARK_CSS }} }};\n",
const_name = theme.const_name,
slug = theme.slug,
class = theme.class,
system_light_class = theme.system_light_class,
system_dark_class = theme.system_dark_class,
));
}
generated.push_str(
r#" /// Every syntax theme, in declaration order.
///
/// ```rust
/// use dioxus_code::Theme;
/// assert!(Theme::ALL.contains(&Theme::TOKYO_NIGHT));
/// ```
pub const ALL: &'static [Theme] = &[
"#,
);
for theme in &themes {
generated.push_str(&format!(" Self::{},\n", theme.const_name));
}
generated.push_str(
r#" ];
"#,
);
generated.push_str(
r#"}
"#,
);
if let Some(asset_dir) = &asset_dir {
fs::write(
asset_dir.join("dioxus-code-theme.css"),
shared_theme_css(&shared_rules),
)
.unwrap();
}
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
fs::write(out_dir.join("theme_assets.rs"), generated).unwrap();
}
fn parse_theme(theme: ThemeDef) -> ParsedTheme {
let slug = slug(&theme.theme.name);
let css_file = format!("{slug}.css");
let system_light_css_file = format!("{slug}-system-light.css");
let system_dark_css_file = format!("{slug}-system-dark.css");
let class = format!("dxc-{slug}");
let system_light_class = format!("dxc-system-light-{slug}");
let system_dark_class = format!("dxc-system-dark-{slug}");
let selector = format!(".{class}");
let rules = parse_theme_rules(&theme.theme.to_css(&selector));
ParsedTheme {
const_name: theme.const_name,
slug,
css_file,
system_light_css_file,
system_dark_css_file,
class,
system_light_class,
system_dark_class,
rules,
}
}
fn parse_theme_rules(css: &str) -> ThemeRules {
let mut rules = ThemeRules::default();
for line in css.lines().skip(1) {
let trimmed = line.trim();
if trimmed == "}" || trimmed.is_empty() {
continue;
}
if trimmed.starts_with("a-") {
let (tag, body) = trimmed.split_once('{').unwrap();
let body = body.trim().trim_end_matches('}').trim();
rules
.tags
.entry(tag.trim().to_string())
.or_default()
.extend(parse_declarations(body));
} else {
rules.base.extend(parse_declarations(trimmed));
}
}
rules
}
fn parse_declarations(input: &str) -> Vec<Declaration> {
input
.split(';')
.filter_map(|declaration| {
let declaration = declaration.trim();
if declaration.is_empty() {
return None;
}
let (property, value) = declaration.split_once(':')?;
Some(Declaration {
property: property.trim().to_string(),
value: value.trim().to_string(),
})
})
.collect()
}
fn fixed_theme_css(theme: &ParsedTheme) -> String {
let mut css = String::new();
writeln!(css, ".{} {{", theme.class).unwrap();
write_slot_variables(&mut css, "light", &theme.rules);
css.push_str("}\n");
css
}
fn system_slot_css(selector: &str, slot: &str, rules: &ThemeRules) -> String {
let mut css = String::new();
writeln!(css, ".{selector} {{").unwrap();
write_slot_variables(&mut css, slot, rules);
css.push_str("}\n");
css
}
fn write_slot_variables(css: &mut String, slot: &str, rules: &ThemeRules) {
for declaration in &rules.base {
let variable = base_variable_suffix(&declaration.property);
writeln!(css, " --dxc-{slot}-{variable}: {};", declaration.value).unwrap();
}
for (tag, declarations) in &rules.tags {
for declaration in declarations {
let variable = tag_variable_suffix(tag, &declaration.property);
writeln!(css, " --dxc-{slot}-{variable}: {};", declaration.value).unwrap();
}
}
}
impl SharedRules {
fn insert(&mut self, rules: &ThemeRules) {
for declaration in &rules.base {
insert_unique(&mut self.base, &declaration.property);
}
for (tag, declarations) in &rules.tags {
let properties = self.tags.entry(tag.clone()).or_default();
for declaration in declarations {
insert_unique(properties, &declaration.property);
}
}
}
}
fn insert_unique(values: &mut Vec<String>, value: &str) {
if !values.iter().any(|existing| existing == value) {
values.push(value.to_string());
}
}
fn shared_theme_css(rules: &SharedRules) -> String {
let mut css = String::from(
r#".dxc-system {
--dxc-light-on: initial;
--dxc-dark-on: ;
}
@media (prefers-color-scheme: dark) {
.dxc-system {
--dxc-light-on: ;
--dxc-dark-on: initial;
}
}
"#,
);
css.push_str(".dxc,\n.dxc-editor {\n");
for property in &rules.base {
let variable = base_variable_suffix(property);
writeln!(css, " {property}: {};", active_value(&variable)).unwrap();
}
css.push_str("}\n");
for (tag, properties) in &rules.tags {
writeln!(css, ".dxc .{tag},\n.dxc-editor .{tag} {{").unwrap();
for property in properties {
let variable = tag_variable_suffix(tag, property);
writeln!(css, " {property}: {};", active_value(&variable)).unwrap();
}
css.push_str("}\n");
}
css
}
fn active_value(variable: &str) -> String {
format!(
"var(--dxc-light-on, var(--dxc-light-{variable},)) var(--dxc-dark-on, var(--dxc-dark-{variable},))"
)
}
fn base_variable_suffix(property: &str) -> String {
if let Some(custom_property) = property.strip_prefix("--") {
format!("var-{}", css_identifier(custom_property))
} else {
css_identifier(property)
}
}
fn tag_variable_suffix(tag: &str, property: &str) -> String {
format!("{}-{}", css_identifier(tag), base_variable_suffix(property))
}
fn css_identifier(input: &str) -> String {
let mut output = String::new();
for ch in input.chars() {
match ch {
'a'..='z' | '0'..='9' => output.push(ch),
'A'..='Z' => output.push(ch.to_ascii_lowercase()),
'-' | '_' if !output.ends_with('-') => output.push('-'),
_ => {}
}
}
output.trim_matches('-').to_string()
}
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()
}