#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ThemeMetadata {
pub name: Option<String>,
pub author: Option<String>,
pub license: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
}
pub fn parse_theme_metadata(css: &str) -> ThemeMetadata {
ThemeMetadata {
name: extract_token(css, "--theme-name"),
author: extract_token(css, "--theme-author"),
license: extract_token(css, "--theme-license"),
version: extract_token(css, "--theme-version"),
description: extract_token(css, "--theme-description"),
}
}
fn extract_token(css: &str, property: &str) -> Option<String> {
let mut search_from = 0;
while let Some(offset) = css[search_from..].find(property) {
let start = search_from + offset;
let after_name = &css[start + property.len()..];
let next_non_space = after_name.trim_start();
if let Some(value) = next_non_space.strip_prefix(':') {
let semicolon = value.find(';')?;
let raw = value[..semicolon].trim();
let quote = raw.chars().next()?;
if quote == '\'' || quote == '"' {
if let Some(rest) = raw.strip_prefix(quote) {
if let Some(unquoted) = rest.strip_suffix(quote) {
return Some(unquoted.trim().to_string());
}
}
}
return None;
}
search_from = start + property.len();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_all_fields_single_quotes() {
let css = r#"
:root {
--theme-name: 'GitHub';
--theme-author: 'Kim Skov Rasmussen';
--theme-license: 'MIT';
--theme-version: '0.24.0';
--theme-description: 'GitHub-inspired theme';
--text-color: #1f2328;
}
"#;
let meta = parse_theme_metadata(css);
assert_eq!(meta.name.as_deref(), Some("GitHub"));
assert_eq!(meta.author.as_deref(), Some("Kim Skov Rasmussen"));
assert_eq!(meta.license.as_deref(), Some("MIT"));
assert_eq!(meta.version.as_deref(), Some("0.24.0"));
assert_eq!(meta.description.as_deref(), Some("GitHub-inspired theme"));
}
#[test]
fn parses_double_quotes() {
let css = r#"--theme-name: "Nord";"#;
assert_eq!(parse_theme_metadata(css).name.as_deref(), Some("Nord"));
}
#[test]
fn missing_fields_are_none() {
let css = ":root { --text-color: #333; }";
let meta = parse_theme_metadata(css);
assert_eq!(meta, ThemeMetadata::default());
}
#[test]
fn does_not_match_longer_property_names_with_same_prefix() {
let css = "--theme-name-extra: 'not-a-name'; --theme-name: 'Real';";
let meta = parse_theme_metadata(css);
assert_eq!(meta.name.as_deref(), Some("Real"));
}
}