use crate::css_vars::CssVariableProcessor;
use crate::error::{Result, WeChatError};
use askama::Template;
use comrak::{
ComrakOptions, ComrakPlugins, markdown_to_html_with_plugins, plugins::syntect::SyntectAdapter,
};
use std::collections::HashMap;
use tracing::warn;
const DEFAULT_CSS: &str = include_str!("../themes/default.css");
const LAPIS_CSS: &str = include_str!("../themes/lapis.css");
const MAIZE_CSS: &str = include_str!("../themes/maize.css");
const ORANGEHEART_CSS: &str = include_str!("../themes/orangeheart.css");
const PHYCAT_CSS: &str = include_str!("../themes/phycat.css");
const PIE_CSS: &str = include_str!("../themes/pie.css");
const PURPLE_CSS: &str = include_str!("../themes/purple.css");
const RAINBOW_CSS: &str = include_str!("../themes/rainbow.css");
const ATOM_ONE_DARK_CSS: &str = include_str!("../themes/highlight/atom-one-dark.min.css");
const ATOM_ONE_LIGHT_CSS: &str = include_str!("../themes/highlight/atom-one-light.min.css");
const DRACULA_CSS: &str = include_str!("../themes/highlight/dracula.min.css");
const GITHUB_DARK_CSS: &str = include_str!("../themes/highlight/github-dark.min.css");
const GITHUB_CSS: &str = include_str!("../themes/highlight/github.min.css");
const MONOKAI_CSS: &str = include_str!("../themes/highlight/monokai.min.css");
const SOLARIZED_DARK_CSS: &str = include_str!("../themes/highlight/solarized-dark.min.css");
const SOLARIZED_LIGHT_CSS: &str = include_str!("../themes/highlight/solarized-light.min.css");
const XCODE_CSS: &str = include_str!("../themes/highlight/xcode.min.css");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinTheme {
Default,
Lapis,
Maize,
OrangeHeart,
PhyCat,
Pie,
Purple,
Rainbow,
}
impl BuiltinTheme {
pub fn as_str(&self) -> &'static str {
match self {
BuiltinTheme::Default => "default",
BuiltinTheme::Lapis => "lapis",
BuiltinTheme::Maize => "maize",
BuiltinTheme::OrangeHeart => "orangeheart",
BuiltinTheme::PhyCat => "phycat",
BuiltinTheme::Pie => "pie",
BuiltinTheme::Purple => "purple",
BuiltinTheme::Rainbow => "rainbow",
}
}
pub fn all() -> Vec<BuiltinTheme> {
vec![
BuiltinTheme::Default,
BuiltinTheme::Lapis,
BuiltinTheme::Maize,
BuiltinTheme::OrangeHeart,
BuiltinTheme::PhyCat,
BuiltinTheme::Pie,
BuiltinTheme::Purple,
BuiltinTheme::Rainbow,
]
}
}
impl std::str::FromStr for BuiltinTheme {
type Err = WeChatError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"default" => Ok(BuiltinTheme::Default),
"lapis" => Ok(BuiltinTheme::Lapis),
"maize" => Ok(BuiltinTheme::Maize),
"orangeheart" => Ok(BuiltinTheme::OrangeHeart),
"phycat" => Ok(BuiltinTheme::PhyCat),
"pie" => Ok(BuiltinTheme::Pie),
"purple" => Ok(BuiltinTheme::Purple),
"rainbow" => Ok(BuiltinTheme::Rainbow),
_ => Err(WeChatError::ThemeNotFound {
theme: s.to_string(),
}),
}
}
}
#[derive(Template)]
#[template(path = "article.html")]
pub struct ArticleTemplate {
pub title: String,
pub description: String,
pub author: String,
pub content: String,
pub theme_css: String,
pub highlight_css: String,
}
#[derive(Debug, Clone)]
pub struct ThemeTemplate {
pub theme_css: String,
pub code_css: String,
pub name: String,
}
impl ThemeTemplate {
pub fn new(theme_css: String, code_css: String, name: String) -> Self {
Self {
theme_css,
code_css,
name,
}
}
pub fn from_static(theme_css: &'static str, code_css: &'static str, name: String) -> Self {
Self {
theme_css: theme_css.to_string(),
code_css: code_css.to_string(),
name,
}
}
pub fn render(&self, content: &str, metadata: &HashMap<String, String>) -> Result<String> {
let css_processor = CssVariableProcessor::new();
let processed_theme_css =
css_processor
.process_css(&self.theme_css)
.map_err(|e| WeChatError::Internal {
message: format!("CSS variable processing failed for theme CSS: {e}"),
})?;
let processed_highlight_css =
css_processor
.process_css(&self.code_css)
.map_err(|e| WeChatError::Internal {
message: format!("CSS variable processing failed for highlight CSS: {e}"),
})?;
let template = ArticleTemplate {
title: metadata.get("title").cloned().unwrap_or_default(),
description: metadata.get("description").cloned().unwrap_or_default(),
author: metadata.get("author").cloned().unwrap_or_default(),
content: content.to_string(),
theme_css: processed_theme_css,
highlight_css: processed_highlight_css,
};
let html_with_css = template.render().map_err(|e| WeChatError::Internal {
message: format!("Template rendering failed: {e}"),
})?;
let html_with_protected_code = self.post_process_code_blocks(html_with_css);
let inlined_html =
css_inline::inline(&html_with_protected_code).map_err(|e| WeChatError::Internal {
message: format!("CSS inlining failed: {e}"),
})?;
let html_without_newlines = inlined_html.replace("\n", "");
Ok(html_without_newlines)
}
fn post_process_code_blocks(&self, html: String) -> String {
use regex::Regex;
let pre_code_regex =
Regex::new(r#"(?s)(<pre[^>]*>)(<code[^>]*>)(.*?)</code></pre>"#).unwrap();
let result = pre_code_regex.replace_all(&html, |caps: ®ex::Captures| {
let pre_tag = &caps[1];
let code_tag = &caps[2];
let content = &caps[3];
let processed_content = self.process_code_content(content);
format!("{pre_tag}{code_tag}{processed_content}</code></pre>")
});
result.to_string()
}
fn process_code_content(&self, html_content: &str) -> String {
let mut result = html_content.to_string();
result = result.replace(">\n", "><br/>");
let mut processed = String::new();
let mut in_tag = false;
for ch in result.chars() {
match ch {
'<' => {
in_tag = true;
processed.push(ch);
}
'>' => {
in_tag = false;
processed.push(ch);
}
'\n' if !in_tag => {
processed.push_str("<br/>");
}
_ => {
processed.push(ch);
}
}
}
processed = processed.replace("<br/><br/>", "<br/>");
processed = processed.replace(" ", " ");
processed = processed.replace('\t', " ");
processed
}
}
#[derive(Debug)]
pub struct ThemeManager {
templates: HashMap<String, ThemeTemplate>,
highlight_css: HashMap<String, String>,
markdown_options: ComrakOptions<'static>,
}
impl ThemeManager {
pub fn new() -> Self {
let mut manager = Self {
templates: HashMap::new(),
highlight_css: HashMap::new(),
markdown_options: Self::create_markdown_options(),
};
manager.load_builtin_themes();
manager.load_highlight_themes();
manager
}
fn create_markdown_options() -> ComrakOptions<'static> {
let mut options = ComrakOptions::default();
options.extension.strikethrough = true;
options.extension.table = true;
options.extension.footnotes = true;
options.extension.tasklist = true;
options.parse.smart = true;
options
}
fn load_builtin_themes(&mut self) {
for theme in BuiltinTheme::all() {
let template = self.create_builtin_theme(theme);
self.templates.insert(theme.as_str().to_string(), template);
}
}
fn load_highlight_themes(&mut self) {
let highlight_themes = [
("atom-one-dark", ATOM_ONE_DARK_CSS),
("atom-one-light", ATOM_ONE_LIGHT_CSS),
("dracula", DRACULA_CSS),
("github-dark", GITHUB_DARK_CSS),
("github", GITHUB_CSS),
("monokai", MONOKAI_CSS),
("solarized-dark", SOLARIZED_DARK_CSS),
("solarized-light", SOLARIZED_LIGHT_CSS),
("xcode", XCODE_CSS),
("vscode", GITHUB_CSS), ];
for (name, css) in highlight_themes {
self.highlight_css.insert(name.to_string(), css.to_string());
}
}
fn create_builtin_theme(&self, theme: BuiltinTheme) -> ThemeTemplate {
let css = self.get_embedded_theme_css(theme);
ThemeTemplate::from_static(css, "", theme.as_str().to_string())
}
fn get_embedded_theme_css(&self, theme: BuiltinTheme) -> &'static str {
match theme {
BuiltinTheme::Default => DEFAULT_CSS,
BuiltinTheme::Lapis => LAPIS_CSS,
BuiltinTheme::Maize => MAIZE_CSS,
BuiltinTheme::OrangeHeart => ORANGEHEART_CSS,
BuiltinTheme::PhyCat => PHYCAT_CSS,
BuiltinTheme::Pie => PIE_CSS,
BuiltinTheme::Purple => PURPLE_CSS,
BuiltinTheme::Rainbow => RAINBOW_CSS,
}
}
pub fn render(
&self,
markdown_content: &str,
theme_name: &str,
code_theme: &str,
metadata: &HashMap<String, String>,
) -> Result<String> {
let template =
self.templates
.get(theme_name)
.ok_or_else(|| WeChatError::ThemeNotFound {
theme: theme_name.to_string(),
})?;
let highlight_css = self.get_highlight_css(code_theme);
let syntect_theme_name = match code_theme {
"solarized-light" => Some("Solarized (light)"),
"solarized-dark" => Some("Solarized (dark)"),
"monokai" => Some("Monokai"),
"github" | "vscode" => Some("InspiredGitHub"),
"github-dark" => Some("base16-ocean.dark"),
"atom-one-dark" => Some("base16-ocean.dark"),
"atom-one-light" => Some("InspiredGitHub"),
"dracula" => Some("base16-ocean.dark"),
"xcode" => Some("InspiredGitHub"),
_ => None, };
let adapter = SyntectAdapter::new(syntect_theme_name);
let mut plugins = ComrakPlugins::default();
plugins.render.codefence_syntax_highlighter = Some(&adapter);
let html_content =
markdown_to_html_with_plugins(markdown_content, &self.markdown_options, &plugins);
let template_with_highlight = ThemeTemplate {
theme_css: template.theme_css.clone(),
code_css: highlight_css,
name: template.name.clone(),
};
template_with_highlight.render(&html_content, metadata)
}
pub fn add_theme(&mut self, name: String, template: ThemeTemplate) {
self.templates.insert(name, template);
}
pub fn available_themes(&self) -> Vec<&String> {
self.templates.keys().collect()
}
pub fn has_theme(&self, name: &str) -> bool {
self.templates.contains_key(name)
}
fn get_highlight_css(&self, theme: &str) -> String {
self.highlight_css.get(theme).cloned().unwrap_or_else(|| {
warn!("Highlight theme '{theme}' not found, falling back to 'github'");
self.highlight_css
.get("github")
.cloned()
.unwrap_or_default()
})
}
}
impl Default for ThemeManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builtin_theme_parsing() {
assert_eq!(
"default".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::Default
);
assert_eq!(
"lapis".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::Lapis
);
assert_eq!(
"maize".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::Maize
);
assert_eq!(
"orangeheart".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::OrangeHeart
);
assert_eq!(
"phycat".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::PhyCat
);
assert_eq!("pie".parse::<BuiltinTheme>().unwrap(), BuiltinTheme::Pie);
assert_eq!(
"purple".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::Purple
);
assert_eq!(
"rainbow".parse::<BuiltinTheme>().unwrap(),
BuiltinTheme::Rainbow
);
assert!("nonexistent".parse::<BuiltinTheme>().is_err());
}
#[test]
fn test_theme_manager_creation() {
let manager = ThemeManager::new();
for theme in BuiltinTheme::all() {
assert!(manager.has_theme(theme.as_str()));
}
let themes = manager.available_themes();
assert!(themes.len() >= 4);
}
#[test]
fn test_theme_rendering() {
let manager = ThemeManager::new();
let markdown = "# Test Title\n\nThis is a test paragraph with **bold** text.";
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test Article".to_string());
metadata.insert("author".to_string(), "Test Author".to_string());
let result = manager.render(markdown, "default", "vscode", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1"));
assert!(html.contains("Test Title"));
assert!(html.contains("<strong"));
assert!(html.contains("bold"));
assert!(html.contains("id=\"wepub\""));
}
#[test]
fn test_nonexistent_theme() {
let manager = ThemeManager::new();
let result = manager.render("# Test", "nonexistent", "vscode", &HashMap::new());
assert!(result.is_err());
if let Err(WeChatError::ThemeNotFound { theme }) = result {
assert_eq!(theme, "nonexistent");
} else {
panic!("Expected ThemeNotFound error");
}
}
#[test]
fn test_custom_theme() {
let mut manager = ThemeManager::new();
let custom_template = ThemeTemplate::new(
"#wepub { color: red; }".to_string(),
String::new(),
"custom".to_string(),
);
manager.add_theme("custom".to_string(), custom_template);
assert!(manager.has_theme("custom"));
let result = manager.render("# Test", "custom", "vscode", &HashMap::new());
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("style="));
assert!(html.contains("Test"));
assert!(html.contains("id=\"wepub\""));
}
#[test]
fn test_highlight_theme_rendering() {
let manager = ThemeManager::new();
let markdown = r#"# Test
```rust
fn main() {
println!("Hello, world!");
}
```"#;
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test Article".to_string());
metadata.insert("author".to_string(), "Test Author".to_string());
let result = manager.render(markdown, "default", "solarized-light", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1"));
assert!(html.contains("Test"));
assert!(html.contains("<code"));
let result = manager.render(markdown, "default", "vscode", &metadata);
assert!(result.is_ok());
let result = manager.render(markdown, "default", "nonexistent", &metadata);
assert!(result.is_ok());
}
#[test]
fn test_template_css_inlining() {
let css = "#wepub h1 { color: red; font-size: 2em; }";
let template = ThemeTemplate::new(css.to_string(), String::new(), "theme".to_string());
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "My Title".to_string());
metadata.insert("author".to_string(), "John Doe".to_string());
let result = template.render("<h1>Content</h1>", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("id=\"wepub\""));
assert!(html.contains("<h1"));
assert!(html.contains("Content"));
assert!(html.contains("style="));
}
#[test]
fn test_css_variable_processing_in_theme() {
let css_with_vars = r#"
:root {
--primary-color: #4870ac;
--text-color: #40464f;
--header-color: var(--primary-color);
}
#wepub { color: var(--text-color); }
#wepub h1 { color: var(--header-color); }
"#;
let template =
ThemeTemplate::new(css_with_vars.to_string(), String::new(), "test".to_string());
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test".to_string());
metadata.insert("author".to_string(), "Test Author".to_string());
let result = template.render("<h1>Test Header</h1><p>Test content</p>", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(!html.contains("var(")); assert!(html.contains("#40464f")); assert!(html.contains("#4870ac")); }
#[test]
fn test_nested_css_variables_in_theme() {
let css_with_nested_vars = r#"
:root {
--base-color: #4870ac;
--primary-color: var(--base-color);
--header-span-color: var(--primary-color);
--shadow-color: #eee;
--shadow: 3px 3px 10px var(--shadow-color);
}
#wepub h1 span { color: var(--header-span-color); }
#wepub .box { box-shadow: var(--shadow); }
"#;
let template = ThemeTemplate::new(
css_with_nested_vars.to_string(),
String::new(),
"nested".to_string(),
);
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Nested Test".to_string());
let result = template.render("<h1><span>Nested</span></h1>", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(!html.contains("var("));
assert!(html.contains("#4870ac")); assert!(html.contains("3px 3px 10px #eee")); }
#[test]
fn test_real_theme_css_variable_processing() {
let manager = ThemeManager::new();
let markdown = "# Test Title\n\nThis is a **test** paragraph.";
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Variable Test".to_string());
metadata.insert("author".to_string(), "Test Author".to_string());
let result = manager.render(markdown, "purple", "github", &metadata);
assert!(result.is_ok());
let html = result.unwrap();
assert!(!html.contains("var(--title-color"));
assert!(!html.contains("var(--text-color"));
assert!(!html.contains("var(--shadow-color"));
assert!(
html.contains("#8064a9")
|| html.contains("color:#8064a9")
|| html.contains("color: #8064a9")
);
assert!(
html.contains("#444444")
|| html.contains("color:#444444")
|| html.contains("color: #444444")
);
assert!(html.contains("Test Title"));
assert!(html.contains("<strong"));
assert!(html.contains("test"));
assert!(html.contains("id=\"wepub\""));
}
#[test]
fn test_all_themes_css_variable_processing() {
let manager = ThemeManager::new();
let markdown = "# Test\n\nCSS variables test.";
let metadata = HashMap::new();
for theme in BuiltinTheme::all() {
let result = manager.render(markdown, theme.as_str(), "github", &metadata);
assert!(
result.is_ok(),
"Theme {} should render successfully",
theme.as_str()
);
let html = result.unwrap();
assert!(html.contains("Test"));
assert!(html.contains("id=\"wepub\""));
let theme_css = get_embedded_theme_css(theme);
let var_count_before = theme_css_var_count(theme_css);
let var_count_after = html.matches("var(--").count();
if var_count_before > 0 {
println!(
"Theme {}: {} vars before, {} vars after",
theme.as_str(),
var_count_before,
var_count_after
);
}
}
}
#[test]
fn test_post_process_code_blocks_issues_analysis() {
let manager = ThemeManager::new();
let markdown = "```rust\nfn main() {}\n```";
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test".to_string());
let final_html = manager
.render(markdown, "default", "github", &metadata)
.unwrap();
assert!(final_html.contains("<pre"), "Pre tags should be preserved");
assert!(
final_html.contains("<code"),
"Code tags should be preserved"
);
assert!(
final_html.contains("style="),
"Code blocks should have inline styles"
);
assert!(
final_html.contains("fn") && final_html.contains("main"),
"Code content is preserved"
);
}
#[test]
fn test_post_process_code_blocks_edge_cases() {
let manager = ThemeManager::new();
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test".to_string());
let markdown_samples = [
"This is `inline code` text.",
"```rust\nfn main() {\n println!(\"Hello\");\n}\n```",
"```javascript\nconst regex = /<code>([^<]*)</code>/;\n```",
"First `code` and second `code`\n\n```\nblock code\n```",
];
for (i, markdown_input) in markdown_samples.iter().enumerate() {
println!("Testing Markdown sample {}: {}", i + 1, markdown_input);
let rendered = manager
.render(markdown_input, "default", "github", &metadata)
.unwrap();
println!("Rendered output length: {} chars", rendered.len());
if markdown_input.contains("```") {
assert!(
rendered.contains("style="),
"Sample {}: Block code should have inline styles",
i + 1
);
assert!(
rendered.contains("font-family"),
"Sample {}: Code should have font-family style",
i + 1
);
}
if markdown_input.contains("`") && !markdown_input.contains("```") {
assert!(
rendered.contains("style="),
"Sample {}: Inline code should have inline styles",
i + 1
);
}
}
}
#[test]
fn test_wechat_code_block_newline_fix() {
let manager = ThemeManager::new();
let complex_markdown = r#"# Code Block Test
Here's a complex Rust function:
```rust
fn calculate_fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
```
And some inline `code` as well."#;
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Code Test".to_string());
metadata.insert("author".to_string(), "Test Author".to_string());
let final_html = manager
.render(complex_markdown, "default", "github", &metadata)
.unwrap();
assert!(
final_html.contains("<br>") || final_html.contains("white-space: pre"),
"Code should have line breaks via <br> tags or white-space: pre CSS"
);
assert!(
final_html.contains("fibonacci"),
"Code content should be preserved"
);
assert!(
final_html.contains("temp"),
"Multi-line code should be preserved"
);
assert!(
final_html.contains("span"),
"Syntax highlighting spans should be preserved"
);
assert!(
final_html.contains("style="),
"Inline styles should be applied"
);
let fibonacci_context = if let Some(start) = final_html.find("fibonacci") {
&final_html[start..start + 500.min(final_html.len() - start)]
} else {
""
};
assert!(
fibonacci_context.contains("<br>") || fibonacci_context.contains("white-space: pre"),
"Function should have proper line breaks, not be all on one line"
);
}
#[test]
fn test_css_inlining_effect_on_code_blocks() {
let manager = ThemeManager::new();
let simple_markdown = "```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```";
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), "Test".to_string());
use comrak::{
ComrakOptions, ComrakPlugins, markdown_to_html_with_plugins,
plugins::syntect::SyntectAdapter,
};
let mut options = ComrakOptions::default();
options.extension.strikethrough = true;
options.extension.table = true;
options.extension.footnotes = true;
options.extension.tasklist = true;
options.parse.smart = true;
let adapter = SyntectAdapter::new(Some("InspiredGitHub"));
let mut plugins = ComrakPlugins::default();
plugins.render.codefence_syntax_highlighter = Some(&adapter);
let html_before_processing =
markdown_to_html_with_plugins(simple_markdown, &options, &plugins);
println!("HTML before post-processing: {html_before_processing}");
let final_html = manager
.render(simple_markdown, "default", "github", &metadata)
.unwrap();
println!(
"Final HTML after CSS inlining: contains <pre>: {}",
final_html.contains("<pre>")
);
println!(
"Final HTML after CSS inlining: contains <code>: {}",
final_html.contains("<code>")
);
if let Some(code_start) = final_html.find("fn main") {
let context_start = code_start.saturating_sub(200);
let context_end = (code_start + 400).min(final_html.len());
println!(
"Code context in final HTML: {}",
&final_html[context_start..context_end]
);
}
assert!(
final_html.contains("<br>") || final_html.contains("white-space: pre"),
"Code should have line breaks via <br> tags or white-space: pre CSS"
);
assert!(
final_html.contains("println"),
"Code content should be preserved"
);
assert!(
final_html.contains("main"),
"Code content should be preserved"
);
let println_context = if let Some(start) = final_html.find("println") {
&final_html[start.saturating_sub(100)..start + 200.min(final_html.len() - start)]
} else {
""
};
assert!(
println_context.contains("<br>")
|| println_context.contains("white-space: pre")
|| final_html.contains("<br>"),
"Code should have line breaks. Context: {println_context}"
);
}
fn theme_css_var_count(css: &str) -> usize {
css.matches("var(--").count()
}
fn get_embedded_theme_css(theme: BuiltinTheme) -> &'static str {
match theme {
BuiltinTheme::Default => DEFAULT_CSS,
BuiltinTheme::Lapis => LAPIS_CSS,
BuiltinTheme::Maize => MAIZE_CSS,
BuiltinTheme::OrangeHeart => ORANGEHEART_CSS,
BuiltinTheme::PhyCat => PHYCAT_CSS,
BuiltinTheme::Pie => PIE_CSS,
BuiltinTheme::Purple => PURPLE_CSS,
BuiltinTheme::Rainbow => RAINBOW_CSS,
}
}
}