///|
/// Built-in theme identifiers for `render_mermaid_with_theme`.
pub enum ThemeName {
ZincLight
ZincDark
TokyoNight
TokyoNightStorm
TokyoNightLight
CatppuccinMocha
CatppuccinLatte
Nord
NordLight
Dracula
GithubLight
GithubDark
SolarizedLight
SolarizedDark
OneDark
} derive(Eq, Show, ToJson)
///|
/// Minimal Shiki token color model used by `from_shiki_theme`.
pub(all) struct ShikiTokenColor {
scopes : Array[String]
foreground : String?
} derive(Eq, Show, ToJson)
///|
/// Helper to construct a token-color entry for one scope.
pub fn shiki_token_color(
scope : String,
foreground : String?,
) -> ShikiTokenColor {
{ scopes: [scope], foreground }
}
///|
/// Helper to construct a token-color entry for multiple scopes.
pub fn shiki_token_color_many(
scopes : Array[String],
foreground : String?,
) -> ShikiTokenColor {
{ scopes, foreground }
}
///|
/// Simplified Shiki-like theme model used to derive `DiagramColors`.
pub(all) struct ShikiTheme {
theme_type : String?
colors : Map[String, String]?
token_colors : Array[ShikiTokenColor]?
} derive(Eq, Show, ToJson)
///|
/// Build a `ShikiTheme` value.
pub fn shiki_theme(
theme_type : String?,
colors : Map[String, String]?,
token_colors : Array[ShikiTokenColor]?,
) -> ShikiTheme {
{ theme_type, colors, token_colors }
}
///|
/// Build a dark `ShikiTheme`.
pub fn shiki_dark_theme(
colors : Map[String, String]?,
token_colors : Array[ShikiTokenColor]?,
) -> ShikiTheme {
shiki_theme(Some("dark"), colors, token_colors)
}
///|
/// Build a light `ShikiTheme`.
pub fn shiki_light_theme(
colors : Map[String, String]?,
token_colors : Array[ShikiTokenColor]?,
) -> ShikiTheme {
shiki_theme(Some("light"), colors, token_colors)
}
///|
fn shiki_theme_color(theme : ShikiTheme, key : String) -> String? {
match theme.colors {
Some(colors) => colors.get(key)
None => None
}
}
///|
fn shiki_token_scope_color(theme : ShikiTheme, scope : String) -> String? {
match theme.token_colors {
Some(token_colors) => {
let mut found : String? = None
for token in token_colors {
if found is Some(_) {
continue
}
if token.scopes.contains(scope) {
match token.foreground {
Some(color) => found = Some(color)
None => ()
}
}
}
found
}
None => None
}
}
///|
/// Convert editor-like theme data to diagram colors.
/// Falls back to sensible light/dark defaults when fields are missing.
pub fn from_shiki_theme(theme : ShikiTheme) -> DiagramColors {
let theme_kind = match theme.theme_type {
Some(kind) => kind.trim().to_string().to_lower()
None => ""
}
let is_dark = theme_kind == "dark"
let bg = match shiki_theme_color(theme, "editor.background") {
Some(color) => color
None => if is_dark { "#1e1e1e" } else { "#ffffff" }
}
let fg = match shiki_theme_color(theme, "editor.foreground") {
Some(color) => color
None => if is_dark { "#d4d4d4" } else { "#333333" }
}
let line = shiki_theme_color(theme, "editorLineNumber.foreground")
let accent = match shiki_theme_color(theme, "focusBorder") {
Some(color) => Some(color)
None => shiki_token_scope_color(theme, "keyword")
}
let muted = match shiki_token_scope_color(theme, "comment") {
Some(color) => Some(color)
None => line
}
let surface = shiki_theme_color(theme, "editor.selectionBackground")
let border = shiki_theme_color(theme, "editorWidget.border")
{ bg, fg, line, accent, muted, surface, border }
}
///|
/// Return the color palette for a built-in theme.
pub fn theme_colors(theme : ThemeName) -> DiagramColors {
match theme {
ZincLight =>
{
bg: "#FFFFFF",
fg: "#27272A",
line: None,
accent: None,
muted: None,
surface: None,
border: None,
}
ZincDark =>
{
bg: "#18181B",
fg: "#FAFAFA",
line: None,
accent: None,
muted: None,
surface: None,
border: None,
}
TokyoNight =>
{
bg: "#1a1b26",
fg: "#a9b1d6",
line: Some("#3d59a1"),
accent: Some("#7aa2f7"),
muted: Some("#565f89"),
surface: None,
border: None,
}
TokyoNightStorm =>
{
bg: "#24283b",
fg: "#a9b1d6",
line: Some("#3d59a1"),
accent: Some("#7aa2f7"),
muted: Some("#565f89"),
surface: None,
border: None,
}
TokyoNightLight =>
{
bg: "#d5d6db",
fg: "#343b58",
line: Some("#34548a"),
accent: Some("#34548a"),
muted: Some("#9699a3"),
surface: None,
border: None,
}
CatppuccinMocha =>
{
bg: "#1e1e2e",
fg: "#cdd6f4",
line: Some("#585b70"),
accent: Some("#cba6f7"),
muted: Some("#6c7086"),
surface: None,
border: None,
}
CatppuccinLatte =>
{
bg: "#eff1f5",
fg: "#4c4f69",
line: Some("#9ca0b0"),
accent: Some("#8839ef"),
muted: Some("#9ca0b0"),
surface: None,
border: None,
}
Nord =>
{
bg: "#2e3440",
fg: "#d8dee9",
line: Some("#4c566a"),
accent: Some("#88c0d0"),
muted: Some("#616e88"),
surface: None,
border: None,
}
NordLight =>
{
bg: "#eceff4",
fg: "#2e3440",
line: Some("#aab1c0"),
accent: Some("#5e81ac"),
muted: Some("#7b88a1"),
surface: None,
border: None,
}
Dracula =>
{
bg: "#282a36",
fg: "#f8f8f2",
line: Some("#6272a4"),
accent: Some("#bd93f9"),
muted: Some("#6272a4"),
surface: None,
border: None,
}
GithubLight =>
{
bg: "#ffffff",
fg: "#1f2328",
line: Some("#d1d9e0"),
accent: Some("#0969da"),
muted: Some("#59636e"),
surface: None,
border: None,
}
GithubDark =>
{
bg: "#0d1117",
fg: "#e6edf3",
line: Some("#3d444d"),
accent: Some("#4493f8"),
muted: Some("#9198a1"),
surface: None,
border: None,
}
SolarizedLight =>
{
bg: "#fdf6e3",
fg: "#657b83",
line: Some("#93a1a1"),
accent: Some("#268bd2"),
muted: Some("#93a1a1"),
surface: None,
border: None,
}
SolarizedDark =>
{
bg: "#002b36",
fg: "#839496",
line: Some("#586e75"),
accent: Some("#268bd2"),
muted: Some("#586e75"),
surface: None,
border: None,
}
OneDark =>
{
bg: "#282c34",
fg: "#abb2bf",
line: Some("#4b5263"),
accent: Some("#c678dd"),
muted: Some("#5c6370"),
surface: None,
border: None,
}
}
}
///|
/// Return the canonical slug for a built-in theme.
pub fn theme_slug(theme : ThemeName) -> String {
match theme {
ZincLight => "zinc-light"
ZincDark => "zinc-dark"
TokyoNight => "tokyo-night"
TokyoNightStorm => "tokyo-night-storm"
TokyoNightLight => "tokyo-night-light"
CatppuccinMocha => "catppuccin-mocha"
CatppuccinLatte => "catppuccin-latte"
Nord => "nord"
NordLight => "nord-light"
Dracula => "dracula"
GithubLight => "github-light"
GithubDark => "github-dark"
SolarizedLight => "solarized-light"
SolarizedDark => "solarized-dark"
OneDark => "one-dark"
}
}
///|
fn normalize_theme_name_input(name : String) -> String {
let normalized = name
.trim()
.to_string()
.to_lower()
.replace_all(old="_", new="-")
.replace_all(old=" ", new="-")
.replace_all(old="\t", new="-")
.replace_all(old="\n", new="-")
.replace_all(old="\r", new="-")
let mut collapsed = normalized
while collapsed.contains("--") {
collapsed = collapsed.replace_all(old="--", new="-")
}
let mut trimmed = collapsed
while trimmed.has_prefix("-") && trimmed.length() > 0 {
trimmed = (try! trimmed[1:]).to_string()
}
while trimmed.has_suffix("-") && trimmed.length() > 0 {
trimmed = (try! trimmed[:trimmed.length() - 1]).to_string()
}
trimmed
}
///|
/// Normalize user theme input:
/// lowercase, spaces/underscores to `-`, collapse repeated separators.
pub fn normalize_theme_name(name : String) -> String {
normalize_theme_name_input(name)
}
///|
/// Parse a user-supplied theme name to a `ThemeName`.
/// Accepts `default` as an alias for `zinc-light`.
///
/// # Example
/// ```mbt check
/// test {
/// match parse_theme_name("TOKYO NIGHT") {
/// Some(theme) => assert_eq(theme_slug(theme), "tokyo-night")
/// None => fail("theme should parse")
/// }
/// match parse_theme_name("default") {
/// Some(theme) => assert_eq(theme_slug(theme), "zinc-light")
/// None => fail("default should parse")
/// }
/// assert_true(parse_theme_name("not-a-theme") is None)
/// }
/// ```
pub fn parse_theme_name(name : String) -> ThemeName? {
match normalize_theme_name_input(name) {
"default" => Some(ZincLight)
"zinc-light" => Some(ZincLight)
"zinc-dark" => Some(ZincDark)
"tokyo-night" => Some(TokyoNight)
"tokyo-night-storm" => Some(TokyoNightStorm)
"tokyo-night-light" => Some(TokyoNightLight)
"catppuccin-mocha" => Some(CatppuccinMocha)
"catppuccin-latte" => Some(CatppuccinLatte)
"nord" => Some(Nord)
"nord-light" => Some(NordLight)
"dracula" => Some(Dracula)
"github-light" => Some(GithubLight)
"github-dark" => Some(GithubDark)
"solarized-light" => Some(SolarizedLight)
"solarized-dark" => Some(SolarizedDark)
"one-dark" => Some(OneDark)
_ => None
}
}
///|
/// Resolve a user theme name directly to colors.
///
/// # Example
/// ```mbt check
/// test {
/// match theme_by_name("github-dark") {
/// Some(colors) => assert_eq(colors.bg, "#0d1117")
/// None => fail("theme should resolve")
/// }
/// }
/// ```
pub fn theme_by_name(name : String) -> DiagramColors? {
match parse_theme_name(name) {
Some(theme) => Some(theme_colors(theme))
None => None
}
}
///|
/// Return whether a user theme name resolves to a known built-in theme.
pub fn theme_exists(name : String) -> Bool {
parse_theme_name(name) is Some(_)
}
///|
/// Return the canonical slug for a user theme name, if it resolves.
pub fn canonical_theme_slug(name : String) -> String? {
match parse_theme_name(name) {
Some(theme) => Some(theme_slug(theme))
None => None
}
}
///|
/// Return all built-in themes in stable display order.
pub fn built_in_themes() -> Array[ThemeName] {
[
ZincLight,
ZincDark,
TokyoNight,
TokyoNightStorm,
TokyoNightLight,
CatppuccinMocha,
CatppuccinLatte,
Nord,
NordLight,
Dracula,
GithubLight,
GithubDark,
SolarizedLight,
SolarizedDark,
OneDark,
]
}
///|
/// Return canonical slugs for all built-in themes.
pub fn built_in_theme_slugs() -> Array[String] {
built_in_themes().map(theme_slug)
}
///|
/// Return built-in theme slugs as a comma-separated string.
pub fn built_in_theme_slugs_csv() -> String {
built_in_theme_slugs().join(", ")
}
///|
/// Return a slug-to-colors map for all built-in themes.
pub fn built_in_theme_colors() -> Map[String, DiagramColors] {
let themes : Map[String, DiagramColors] = {}
for theme in built_in_themes() {
themes[theme_slug(theme)] = theme_colors(theme)
}
themes
}