#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Theme {
#[cfg(feature = "theme-zorto")]
Zorto,
#[cfg(feature = "theme-dkdc")]
Dkdc,
#[cfg(feature = "theme-default")]
Default,
#[cfg(feature = "theme-ember")]
Ember,
#[cfg(feature = "theme-forest")]
Forest,
#[cfg(feature = "theme-ocean")]
Ocean,
#[cfg(feature = "theme-rose")]
Rose,
#[cfg(feature = "theme-slate")]
Slate,
#[cfg(feature = "theme-midnight")]
Midnight,
#[cfg(feature = "theme-sunset")]
Sunset,
#[cfg(feature = "theme-mint")]
Mint,
#[cfg(feature = "theme-plum")]
Plum,
#[cfg(feature = "theme-sand")]
Sand,
#[cfg(feature = "theme-arctic")]
Arctic,
#[cfg(feature = "theme-lime")]
Lime,
#[cfg(feature = "theme-charcoal")]
Charcoal,
}
impl Theme {
pub fn from_name(name: &str) -> Option<Self> {
match name {
#[cfg(feature = "theme-zorto")]
"zorto" => Some(Self::Zorto),
#[cfg(feature = "theme-dkdc")]
"dkdc" => Some(Self::Dkdc),
#[cfg(feature = "theme-default")]
"default" => Some(Self::Default),
#[cfg(feature = "theme-ember")]
"ember" => Some(Self::Ember),
#[cfg(feature = "theme-forest")]
"forest" => Some(Self::Forest),
#[cfg(feature = "theme-ocean")]
"ocean" => Some(Self::Ocean),
#[cfg(feature = "theme-rose")]
"rose" => Some(Self::Rose),
#[cfg(feature = "theme-slate")]
"slate" => Some(Self::Slate),
#[cfg(feature = "theme-midnight")]
"midnight" => Some(Self::Midnight),
#[cfg(feature = "theme-sunset")]
"sunset" => Some(Self::Sunset),
#[cfg(feature = "theme-mint")]
"mint" => Some(Self::Mint),
#[cfg(feature = "theme-plum")]
"plum" => Some(Self::Plum),
#[cfg(feature = "theme-sand")]
"sand" => Some(Self::Sand),
#[cfg(feature = "theme-arctic")]
"arctic" => Some(Self::Arctic),
#[cfg(feature = "theme-lime")]
"lime" => Some(Self::Lime),
#[cfg(feature = "theme-charcoal")]
"charcoal" => Some(Self::Charcoal),
_ => None,
}
}
#[allow(unused_mut, clippy::vec_init_then_push)]
pub fn available() -> Vec<&'static str> {
let mut names = Vec::new();
#[cfg(feature = "theme-zorto")]
names.push("zorto");
#[cfg(feature = "theme-dkdc")]
names.push("dkdc");
#[cfg(feature = "theme-default")]
names.push("default");
#[cfg(feature = "theme-ember")]
names.push("ember");
#[cfg(feature = "theme-forest")]
names.push("forest");
#[cfg(feature = "theme-ocean")]
names.push("ocean");
#[cfg(feature = "theme-rose")]
names.push("rose");
#[cfg(feature = "theme-slate")]
names.push("slate");
#[cfg(feature = "theme-midnight")]
names.push("midnight");
#[cfg(feature = "theme-sunset")]
names.push("sunset");
#[cfg(feature = "theme-mint")]
names.push("mint");
#[cfg(feature = "theme-plum")]
names.push("plum");
#[cfg(feature = "theme-sand")]
names.push("sand");
#[cfg(feature = "theme-arctic")]
names.push("arctic");
#[cfg(feature = "theme-lime")]
names.push("lime");
#[cfg(feature = "theme-charcoal")]
names.push("charcoal");
names
}
const BASE_HTML: (&'static str, &'static str) = (
"base.html",
include_str!("../themes/zorto/templates/base.html"),
);
const PAGE_HTML: (&'static str, &'static str) = (
"page.html",
include_str!("../themes/zorto/templates/page.html"),
);
const SECTION_HTML: (&'static str, &'static str) = (
"section.html",
include_str!("../themes/zorto/templates/section.html"),
);
const INDEX_HTML: (&'static str, &'static str) = (
"index.html",
include_str!("../themes/zorto/templates/index.html"),
);
const NOT_FOUND_HTML: (&'static str, &'static str) = (
"404.html",
include_str!("../themes/zorto/templates/404.html"),
);
const POST_MACRO_HTML: (&'static str, &'static str) = (
"macros/post.html",
include_str!("../themes/zorto/templates/macros/post.html"),
);
const TAGS_LIST_HTML: (&'static str, &'static str) = (
"tags/list.html",
include_str!("../themes/zorto/templates/tags/list.html"),
);
const TAGS_SINGLE_HTML: (&'static str, &'static str) = (
"tags/single.html",
include_str!("../themes/zorto/templates/tags/single.html"),
);
#[allow(unreachable_patterns)]
pub fn templates(&self) -> Vec<(&'static str, &'static str)> {
vec![
Self::BASE_HTML,
Self::PAGE_HTML,
Self::SECTION_HTML,
Self::INDEX_HTML,
Self::NOT_FOUND_HTML,
Self::POST_MACRO_HTML,
Self::TAGS_LIST_HTML,
Self::TAGS_SINGLE_HTML,
]
}
const SHARED_STRUCTURE: &'static str = include_str!("../themes/shared/_structure.scss");
const SHARED_COMPONENTS: &'static str = include_str!("../themes/shared/_components.scss");
#[allow(unreachable_patterns)]
pub fn scss(&self) -> Vec<(&'static str, &'static str)> {
let mut files = vec![
("_structure.scss", Self::SHARED_STRUCTURE),
("_components.scss", Self::SHARED_COMPONENTS),
];
match self {
#[cfg(feature = "theme-zorto")]
Self::Zorto => files.push((
"style.scss",
include_str!("../themes/zorto/sass/style.scss"),
)),
#[cfg(feature = "theme-dkdc")]
Self::Dkdc => {
files.push(("style.scss", include_str!("../themes/dkdc/sass/style.scss")))
}
#[cfg(feature = "theme-default")]
Self::Default => files.push((
"style.scss",
include_str!("../themes/default/sass/style.scss"),
)),
#[cfg(feature = "theme-ember")]
Self::Ember => files.push((
"style.scss",
include_str!("../themes/ember/sass/style.scss"),
)),
#[cfg(feature = "theme-forest")]
Self::Forest => files.push((
"style.scss",
include_str!("../themes/forest/sass/style.scss"),
)),
#[cfg(feature = "theme-ocean")]
Self::Ocean => files.push((
"style.scss",
include_str!("../themes/ocean/sass/style.scss"),
)),
#[cfg(feature = "theme-rose")]
Self::Rose => {
files.push(("style.scss", include_str!("../themes/rose/sass/style.scss")))
}
#[cfg(feature = "theme-slate")]
Self::Slate => files.push((
"style.scss",
include_str!("../themes/slate/sass/style.scss"),
)),
#[cfg(feature = "theme-midnight")]
Self::Midnight => files.push((
"style.scss",
include_str!("../themes/midnight/sass/style.scss"),
)),
#[cfg(feature = "theme-sunset")]
Self::Sunset => files.push((
"style.scss",
include_str!("../themes/sunset/sass/style.scss"),
)),
#[cfg(feature = "theme-mint")]
Self::Mint => {
files.push(("style.scss", include_str!("../themes/mint/sass/style.scss")))
}
#[cfg(feature = "theme-plum")]
Self::Plum => {
files.push(("style.scss", include_str!("../themes/plum/sass/style.scss")))
}
#[cfg(feature = "theme-sand")]
Self::Sand => {
files.push(("style.scss", include_str!("../themes/sand/sass/style.scss")))
}
#[cfg(feature = "theme-arctic")]
Self::Arctic => files.push((
"style.scss",
include_str!("../themes/arctic/sass/style.scss"),
)),
#[cfg(feature = "theme-lime")]
Self::Lime => {
files.push(("style.scss", include_str!("../themes/lime/sass/style.scss")))
}
#[cfg(feature = "theme-charcoal")]
Self::Charcoal => files.push((
"style.scss",
include_str!("../themes/charcoal/sass/style.scss"),
)),
_ => {}
}
files
}
pub fn name(&self) -> &'static str {
match self {
#[cfg(feature = "theme-zorto")]
Self::Zorto => "zorto",
#[cfg(feature = "theme-dkdc")]
Self::Dkdc => "dkdc",
#[cfg(feature = "theme-default")]
Self::Default => "default",
#[cfg(feature = "theme-ember")]
Self::Ember => "ember",
#[cfg(feature = "theme-forest")]
Self::Forest => "forest",
#[cfg(feature = "theme-ocean")]
Self::Ocean => "ocean",
#[cfg(feature = "theme-rose")]
Self::Rose => "rose",
#[cfg(feature = "theme-slate")]
Self::Slate => "slate",
#[cfg(feature = "theme-midnight")]
Self::Midnight => "midnight",
#[cfg(feature = "theme-sunset")]
Self::Sunset => "sunset",
#[cfg(feature = "theme-mint")]
Self::Mint => "mint",
#[cfg(feature = "theme-plum")]
Self::Plum => "plum",
#[cfg(feature = "theme-sand")]
Self::Sand => "sand",
#[cfg(feature = "theme-arctic")]
Self::Arctic => "arctic",
#[cfg(feature = "theme-lime")]
Self::Lime => "lime",
#[cfg(feature = "theme-charcoal")]
Self::Charcoal => "charcoal",
#[allow(unreachable_patterns)]
_ => "unknown",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_every_available_theme_round_trips() {
let names = Theme::available();
assert!(
!names.is_empty(),
"Theme::available() must list at least one theme under default features"
);
for name in names {
let theme = Theme::from_name(name)
.unwrap_or_else(|| panic!("Theme::from_name({name:?}) returned None"));
assert_eq!(
theme.name(),
name,
"Theme::{theme:?}.name() must round-trip with from_name input"
);
let templates = theme.templates();
assert!(
!templates.is_empty(),
"theme {name} must expose at least one template"
);
for (tmpl_name, content) in &templates {
assert!(
!tmpl_name.is_empty(),
"theme {name} template name must be non-empty"
);
assert!(
!content.is_empty(),
"theme {name} template {tmpl_name} content must be non-empty"
);
}
let scss = theme.scss();
assert!(
!scss.is_empty(),
"theme {name} must expose at least one SCSS file"
);
let scss_names: Vec<&str> = scss.iter().map(|(n, _)| *n).collect();
assert!(
scss_names.contains(&"_structure.scss"),
"theme {name} missing shared _structure.scss"
);
assert!(
scss_names.contains(&"_components.scss"),
"theme {name} missing shared _components.scss"
);
assert!(
scss_names.contains(&"style.scss"),
"theme {name} missing its own style.scss (likely an unreachable_patterns drift)"
);
for (scss_name, content) in &scss {
assert!(
!content.is_empty(),
"theme {name} SCSS file {scss_name} content must be non-empty"
);
}
}
}
#[test]
fn test_from_name_unknown_returns_none() {
assert!(Theme::from_name("nonexistent-theme-name").is_none());
assert!(Theme::from_name("").is_none());
assert!(Theme::from_name("Zorto").is_none() || cfg!(feature = "theme-zorto"));
}
#[test]
fn test_templates_include_required_files() {
let names = Theme::available();
if names.is_empty() {
return;
}
let theme = Theme::from_name(names[0]).expect("first available theme parses");
let names: Vec<&str> = theme.templates().iter().map(|(n, _)| *n).collect();
for required in [
"base.html",
"page.html",
"section.html",
"index.html",
"404.html",
] {
assert!(
names.contains(&required),
"theme {} missing required template {required}",
theme.name()
);
}
}
#[test]
fn test_no_root_relative_asset_paths_in_theme_templates() {
let bad_patterns: &[&str] = &[
"href=\"/",
"href='/",
"src=\"/",
"src='/",
"fetch(\"/",
"fetch('/",
"replace(from=config.base_url",
];
for name in Theme::available() {
let theme = Theme::from_name(name).expect("available theme parses");
for (tmpl_name, content) in theme.templates() {
for pat in bad_patterns {
if let Some(idx) = content.find(pat) {
let start = idx.saturating_sub(20);
let end = (idx + pat.len() + 40).min(content.len());
panic!(
"theme {name} / template {tmpl_name} contains forbidden \
pattern {pat:?} — use get_url(path=...) or keep the \
base_url prefix. Context: {ctx:?}",
ctx = &content[start..end]
);
}
}
}
}
}
}