use crate::MermaidConfig;
use crate::theme_color::{self, ColorAdjustment, ColorError};
use serde::Deserialize;
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use std::sync::OnceLock;
pub(crate) const SUPPORTED_THEME_NAMES: &[&str] = &[
"default",
"base",
"dark",
"forest",
"neutral",
"neo",
"neo-dark",
"redux",
"redux-dark",
"redux-color",
"redux-dark-color",
];
const THEME_ARTIFACT_SCHEMA_VERSION: u32 = 1;
static GENERATED_THEME_ARTIFACT: OnceLock<GeneratedThemeArtifact> = OnceLock::new();
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct GeneratedThemeArtifact {
schema_version: u32,
provenance: GeneratedThemeProvenance,
themes: Map<String, Value>,
dark_mode_true: Map<String, Value>,
oracle_cases: Vec<Value>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct GeneratedThemeProvenance {
generator: String,
mermaid_version: String,
mermaid_package_sha256: String,
mermaid_source_tag: String,
mermaid_source_commit: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemeProgramKind {
Default,
Base,
Dark,
Forest,
Neutral,
Extended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemeDependencyGraph {
None,
Default,
Base,
Dark,
Forest,
Neutral,
DarkenedScale,
DarkenedScaleAndGit,
DynamicGit,
}
#[derive(Debug, Clone, Copy)]
struct ThemeProgram {
name: &'static str,
kind: ThemeProgramKind,
dependencies: ThemeDependencyGraph,
evaluated_color_inputs: &'static [&'static str],
}
const DEFAULT_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
];
const DARK_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"background",
"cScale0",
"cScale1",
"quadrant1Fill",
];
const FOREST_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
];
const NEUTRAL_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"border1",
"cScale0",
"cScale1",
"quadrant1Fill",
];
const BASE_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
];
const REDUX_COLOR_INPUTS: &[&str] = &[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"git0",
"git1",
"quadrant1Fill",
];
const THEME_PROGRAMS: &[ThemeProgram] = &[
ThemeProgram::new(
"default",
ThemeProgramKind::Default,
ThemeDependencyGraph::Default,
DEFAULT_COLOR_INPUTS,
),
ThemeProgram::new(
"base",
ThemeProgramKind::Base,
ThemeDependencyGraph::Base,
BASE_COLOR_INPUTS,
),
ThemeProgram::new(
"dark",
ThemeProgramKind::Dark,
ThemeDependencyGraph::Dark,
DARK_COLOR_INPUTS,
),
ThemeProgram::new(
"forest",
ThemeProgramKind::Forest,
ThemeDependencyGraph::Forest,
FOREST_COLOR_INPUTS,
),
ThemeProgram::new(
"neutral",
ThemeProgramKind::Neutral,
ThemeDependencyGraph::Neutral,
NEUTRAL_COLOR_INPUTS,
),
ThemeProgram::new(
"neo",
ThemeProgramKind::Extended,
ThemeDependencyGraph::None,
BASE_COLOR_INPUTS,
),
ThemeProgram::new(
"neo-dark",
ThemeProgramKind::Extended,
ThemeDependencyGraph::DarkenedScale,
BASE_COLOR_INPUTS,
),
ThemeProgram::new(
"redux",
ThemeProgramKind::Extended,
ThemeDependencyGraph::None,
REDUX_COLOR_INPUTS,
),
ThemeProgram::new(
"redux-dark",
ThemeProgramKind::Extended,
ThemeDependencyGraph::DarkenedScaleAndGit,
BASE_COLOR_INPUTS,
),
ThemeProgram::new(
"redux-color",
ThemeProgramKind::Extended,
ThemeDependencyGraph::None,
BASE_COLOR_INPUTS,
),
ThemeProgram::new(
"redux-dark-color",
ThemeProgramKind::Extended,
ThemeDependencyGraph::DynamicGit,
BASE_COLOR_INPUTS,
),
];
impl ThemeProgram {
const fn new(
name: &'static str,
kind: ThemeProgramKind,
dependencies: ThemeDependencyGraph,
evaluated_color_inputs: &'static [&'static str],
) -> Self {
Self {
name,
kind,
dependencies,
evaluated_color_inputs,
}
}
fn resolve(requested: &str) -> &'static Self {
THEME_PROGRAMS
.iter()
.find(|program| program.name == requested)
.unwrap_or(&THEME_PROGRAMS[0])
}
fn default_snapshot(self) -> &'static Map<String, Value> {
generated_theme_artifact()
.themes
.get(self.name)
.and_then(Value::as_object)
.unwrap_or_else(|| panic!("generated theme artifact is missing `{}`", self.name))
}
fn dark_mode_snapshot(self) -> &'static Map<String, Value> {
generated_theme_artifact()
.dark_mode_true
.get(self.name)
.and_then(Value::as_object)
.unwrap_or_else(|| {
panic!(
"generated theme artifact is missing darkMode=true `{}`",
self.name
)
})
}
fn calculation_snapshot(self, explicit: &Map<String, Value>) -> &'static Map<String, Value> {
if explicit.get("darkMode").is_some_and(is_js_truthy) {
self.dark_mode_snapshot()
} else {
self.default_snapshot()
}
}
fn exact_snapshot(self, explicit: &Map<String, Value>) -> Option<&'static Map<String, Value>> {
if explicit
.keys()
.all(|key| matches!(key.as_str(), "darkMode" | "fontFamily" | "fontSize"))
{
Some(self.calculation_snapshot(explicit))
} else {
None
}
}
fn normalize_overrides(self, raw: Map<String, Value>) -> Map<String, Value> {
let defaults = self.default_snapshot();
raw.into_iter()
.filter(|(key, value)| assign_with_depth_accepts_theme_value(defaults.get(key), value))
.collect()
}
fn validate_evaluated_inputs(self, explicit: &Map<String, Value>) -> Result<(), ColorError> {
for key in self.evaluated_color_inputs {
let Some(value) = explicit.get(*key) else {
continue;
};
if value.is_null() {
continue;
}
let Value::String(color) = value else {
return Err(ColorError::UnsupportedFormat {
input: value.to_string(),
});
};
theme_color::ThemeColor::parse(color)?;
}
Ok(())
}
fn execute(self, config: &mut MermaidConfig) -> Result<(), ColorError> {
match self.kind {
ThemeProgramKind::Default => apply_default_theme_defaults(config),
ThemeProgramKind::Base => apply_base_theme_defaults(config),
ThemeProgramKind::Dark => apply_dark_theme_defaults(config),
ThemeProgramKind::Forest => apply_forest_theme_defaults(config),
ThemeProgramKind::Neutral => apply_neutral_theme_defaults(config),
ThemeProgramKind::Extended => apply_snapshot_theme_defaults(config, self.name),
}
}
fn apply_dependency_graph(
self,
explicit: &Map<String, Value>,
calculated: &mut Map<String, Value>,
) -> Result<(), ColorError> {
match self.dependencies {
ThemeDependencyGraph::None => Ok(()),
ThemeDependencyGraph::Default => apply_default_theme_dependencies(explicit, calculated),
ThemeDependencyGraph::Base => apply_base_theme_dependencies(explicit, calculated),
ThemeDependencyGraph::Dark => apply_dark_theme_dependencies(explicit, calculated),
ThemeDependencyGraph::Forest => apply_forest_theme_dependencies(explicit, calculated),
ThemeDependencyGraph::Neutral => apply_neutral_theme_dependencies(explicit, calculated),
ThemeDependencyGraph::DarkenedScale => {
apply_darkened_scale_dependencies(explicit, calculated)
}
ThemeDependencyGraph::DarkenedScaleAndGit => {
apply_darkened_scale_dependencies(explicit, calculated)?;
apply_dynamic_git_dependencies(explicit, calculated)
}
ThemeDependencyGraph::DynamicGit => {
apply_dynamic_git_dependencies(explicit, calculated)
}
}
}
}
fn get_truthy_string(map: &Map<String, Value>, key: &str) -> Option<String> {
map.get(key)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
fn is_js_truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(value) => *value,
Value::Number(value) => value.as_f64().is_some_and(|value| value != 0.0),
Value::String(value) => !value.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
fn assign_with_depth_accepts_theme_value(default: Option<&Value>, source: &Value) -> bool {
let source_is_non_null_object = matches!(source, Value::Array(_) | Value::Object(_));
let source_is_object = matches!(source, Value::Null | Value::Array(_) | Value::Object(_));
let default_is_object = default
.is_some_and(|value| matches!(value, Value::Null | Value::Array(_) | Value::Object(_)));
source_is_non_null_object && (default.is_none() || default_is_object)
|| !source_is_object && !default_is_object
}
fn required_color(map: &Map<String, Value>, key: &str) -> Result<String, ColorError> {
get_truthy_string(map, key).ok_or_else(|| ColorError::UnsupportedFormat {
input: map
.get(key)
.map(Value::to_string)
.unwrap_or_else(|| format!("missing theme color `{key}`")),
})
}
fn value_is_missing(map: &Map<String, Value>, key: &str) -> bool {
map.get(key).is_none_or(|value| !is_js_truthy(value))
}
fn set_if_missing(map: &mut Map<String, Value>, key: &str, value: Value) {
if value_is_missing(map, key) {
map.insert(key.to_string(), value);
}
}
fn set_string_if_missing(map: &mut Map<String, Value>, key: &str, value: impl Into<String>) {
set_if_missing(map, key, Value::String(value.into()));
}
fn set_string_if_missing_with(
map: &mut Map<String, Value>,
key: &str,
value: impl FnOnce() -> Result<String, ColorError>,
) -> Result<(), ColorError> {
if value_is_missing(map, key) {
map.insert(key.to_string(), Value::String(value()?));
}
Ok(())
}
fn set_finite_number_if_missing(map: &mut Map<String, Value>, key: &str, value: f64) {
if let Some(number) = serde_json::Number::from_f64(value) {
set_if_missing(map, key, Value::Number(number));
}
}
fn set_derived_string_unless_explicit(
map: &mut Map<String, Value>,
explicit: &Map<String, Value>,
key: &str,
value: impl Into<String>,
) {
if !explicit.contains_key(key) {
map.insert(key.to_string(), Value::String(value.into()));
}
}
fn theme_variables_map(config: &MermaidConfig) -> Map<String, Value> {
match config.as_value().get("themeVariables") {
Some(Value::Object(m)) => m.clone(),
_ => Map::new(),
}
}
fn generated_theme_artifact() -> &'static GeneratedThemeArtifact {
GENERATED_THEME_ARTIFACT.get_or_init(|| {
let artifact: GeneratedThemeArtifact =
serde_json::from_str(include_str!("generated/theme_variables_11_16_1.json"))
.expect("generated Mermaid theme artifact JSON is valid");
assert_eq!(artifact.schema_version, THEME_ARTIFACT_SCHEMA_VERSION);
assert_eq!(
artifact.provenance.mermaid_version,
crate::baseline::PINNED_MERMAID_BASELINE_VERSION
);
assert_eq!(
artifact.provenance.mermaid_source_tag,
crate::baseline::PINNED_MERMAID_BASELINE_TAG
);
assert_eq!(
artifact.provenance.generator,
"cargo run -p xtask -- gen-theme-snapshot"
);
assert_eq!(artifact.provenance.mermaid_source_commit.len(), 40);
assert_eq!(artifact.provenance.mermaid_package_sha256.len(), 64);
assert_eq!(artifact.oracle_cases.len(), THEME_PROGRAMS.len() * 5 + 11);
for program in THEME_PROGRAMS {
assert!(
artifact
.themes
.get(program.name)
.is_some_and(Value::is_object)
);
assert!(
artifact
.dark_mode_true
.get(program.name)
.is_some_and(Value::is_object)
);
}
artifact
})
}
fn merge_theme_variable_defaults(target: &mut Map<String, Value>, defaults: &Map<String, Value>) {
for (key, default_value) in defaults {
match (target.get_mut(key), default_value) {
(Some(Value::Object(target_map)), Value::Object(default_map)) => {
merge_theme_variable_defaults(target_map, default_map);
}
(Some(Value::Null), _) => {
target.insert(key.clone(), default_value.clone());
}
(Some(Value::String(current)), _) if current.trim().is_empty() => {
target.insert(key.clone(), default_value.clone());
}
(None, _) => {
target.insert(key.clone(), default_value.clone());
}
_ => {}
}
}
}
fn finish_theme_defaults(
config: &mut MermaidConfig,
theme: &str,
tv: Map<String, Value>,
) -> Result<(), ColorError> {
let explicit = theme_variables_map(config);
let resolution = ThemeResolution::new(theme, explicit, tv)?;
config.set_value(
"themeVariables",
Value::Object(resolution.into_resolved_variables()),
);
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemeResolutionStage {
DefaultSnapshot,
OverridesApplied,
Calculated,
ExplicitReplay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemeValueOrigin {
DefaultSnapshot,
Calculated,
ExplicitOverride,
}
#[derive(Debug, Clone)]
struct ThemeStageSnapshot {
stage: ThemeResolutionStage,
variables: Map<String, Value>,
origins: BTreeMap<String, ThemeValueOrigin>,
}
impl ThemeStageSnapshot {
fn from_variables(
stage: ThemeResolutionStage,
variables: Map<String, Value>,
origin: ThemeValueOrigin,
) -> Self {
let origins = variables.keys().map(|key| (key.clone(), origin)).collect();
Self {
stage,
variables,
origins,
}
}
fn overlay(&mut self, values: &Map<String, Value>, origin: ThemeValueOrigin) {
for (key, value) in values {
self.variables.insert(key.clone(), value.clone());
self.origins.insert(key.clone(), origin);
}
}
}
#[derive(Debug, Clone)]
struct ThemeResolution {
default_snapshot: ThemeStageSnapshot,
overrides_applied: ThemeStageSnapshot,
calculated: ThemeStageSnapshot,
explicit_replay: ThemeStageSnapshot,
}
impl ThemeResolution {
fn new(
theme: &str,
explicit: Map<String, Value>,
calculated: Map<String, Value>,
) -> Result<Self, ColorError> {
let program = ThemeProgram::resolve(theme);
let has_user_theme_variables = !explicit.is_empty();
let default_variables = program.default_snapshot().clone();
let default_snapshot = ThemeStageSnapshot::from_variables(
ThemeResolutionStage::DefaultSnapshot,
default_variables,
ThemeValueOrigin::DefaultSnapshot,
);
let mut overrides_applied = default_snapshot.clone();
overrides_applied.stage = ThemeResolutionStage::OverridesApplied;
overrides_applied.overlay(&explicit, ThemeValueOrigin::ExplicitOverride);
let mut calculated_snapshot = ThemeStageSnapshot::from_variables(
ThemeResolutionStage::Calculated,
calculated,
ThemeValueOrigin::Calculated,
);
if let Some(snapshot) = program.exact_snapshot(&explicit) {
calculated_snapshot = ThemeStageSnapshot::from_variables(
ThemeResolutionStage::Calculated,
snapshot.clone(),
ThemeValueOrigin::Calculated,
);
} else {
let snapshot = program.calculation_snapshot(&explicit);
merge_theme_variable_defaults(&mut calculated_snapshot.variables, snapshot);
for key in snapshot.keys() {
calculated_snapshot
.origins
.entry(key.clone())
.or_insert(ThemeValueOrigin::DefaultSnapshot);
}
let before_dependencies = calculated_snapshot.variables.clone();
program.apply_dependency_graph(&explicit, &mut calculated_snapshot.variables)?;
for (key, value) in &calculated_snapshot.variables {
if before_dependencies.get(key) != Some(value) {
calculated_snapshot
.origins
.insert(key.clone(), ThemeValueOrigin::Calculated);
}
}
}
let mut explicit_replay = calculated_snapshot.clone();
explicit_replay.stage = ThemeResolutionStage::ExplicitReplay;
if has_user_theme_variables && theme == "default" {
restore_default_baseline_palette(
&mut explicit_replay.variables,
&mut explicit_replay.origins,
&default_snapshot.variables,
);
}
explicit_replay.overlay(&explicit, ThemeValueOrigin::ExplicitOverride);
Ok(Self {
default_snapshot,
overrides_applied,
calculated: calculated_snapshot,
explicit_replay,
})
}
fn into_resolved_variables(self) -> Map<String, Value> {
debug_assert_eq!(
self.default_snapshot.stage,
ThemeResolutionStage::DefaultSnapshot
);
debug_assert_eq!(
self.overrides_applied.stage,
ThemeResolutionStage::OverridesApplied
);
debug_assert_eq!(self.calculated.stage, ThemeResolutionStage::Calculated);
debug_assert_eq!(
self.explicit_replay.stage,
ThemeResolutionStage::ExplicitReplay
);
self.explicit_replay.variables
}
}
fn restore_default_baseline_palette(
target: &mut Map<String, Value>,
origins: &mut BTreeMap<String, ThemeValueOrigin>,
baseline: &Map<String, Value>,
) {
for prefix in [
"cScale",
"cScalePeer",
"cScaleInv",
"cScaleLabel",
"surface",
"surfacePeer",
] {
for index in 0..12 {
let key = format!("{prefix}{index}");
if let Some(value) = baseline.get(&key) {
target.insert(key.clone(), value.clone());
origins.insert(key, ThemeValueOrigin::DefaultSnapshot);
}
}
}
for index in 1..=12 {
let key = format!("pie{index}");
if let Some(value) = baseline.get(&key) {
target.insert(key.clone(), value.clone());
origins.insert(key, ThemeValueOrigin::DefaultSnapshot);
}
}
if let Some(value) = baseline.get("scaleLabelColor") {
target.insert("scaleLabelColor".to_string(), value.clone());
origins.insert(
"scaleLabelColor".to_string(),
ThemeValueOrigin::DefaultSnapshot,
);
}
}
fn mermaid_default_font_family() -> Value {
Value::String("\"trebuchet ms\", verdana, arial, sans-serif".to_string())
}
fn mk_border(color: &str, dark_mode: bool) -> Result<String, ColorError> {
theme_color::adjust(
color,
ColorAdjustment::hsl(0.0, -40.0, if dark_mode { 10.0 } else { -10.0 }),
)
}
fn ensure_gradient_theme_defaults(tv: &mut Map<String, Value>) {
let primary_border_color =
get_truthy_string(tv, "primaryBorderColor").unwrap_or_else(|| "#9370DB".to_string());
let secondary_border_color = get_truthy_string(tv, "secondaryBorderColor")
.unwrap_or_else(|| primary_border_color.clone());
set_if_missing(tv, "useGradient", Value::Bool(true));
set_if_missing(tv, "gradientStart", Value::String(primary_border_color));
set_if_missing(tv, "gradientStop", Value::String(secondary_border_color));
}
fn ensure_xychart_theme_defaults(tv: &mut Map<String, Value>, default_palette: &str) {
let background = get_truthy_string(tv, "background").unwrap_or_else(|| "white".to_string());
let primary_text = get_truthy_string(tv, "primaryTextColor")
.or_else(|| get_truthy_string(tv, "textColor"))
.unwrap_or_else(|| "#333".to_string());
let mut xy = match tv.get("xyChart") {
Some(Value::Object(m)) => m.clone(),
_ => Map::new(),
};
set_if_missing(
&mut xy,
"backgroundColor",
Value::String(background.clone()),
);
for key in [
"titleColor",
"dataLabelColor",
"xAxisTitleColor",
"xAxisLabelColor",
"xAxisTickColor",
"xAxisLineColor",
"yAxisTitleColor",
"yAxisLabelColor",
"yAxisTickColor",
"yAxisLineColor",
] {
set_if_missing(&mut xy, key, Value::String(primary_text.clone()));
}
set_if_missing(
&mut xy,
"plotColorPalette",
Value::String(default_palette.to_string()),
);
tv.insert("xyChart".to_string(), Value::Object(xy));
}
fn apply_quadrant_theme_defaults(
tv: &mut Map<String, Value>,
fill_base: &str,
text_base: &str,
border_base: &str,
) -> Result<(), ColorError> {
set_string_if_missing(tv, "quadrant1Fill", fill_base);
for (key, adjustment) in [
("quadrant2Fill", 5.0),
("quadrant3Fill", 10.0),
("quadrant4Fill", 15.0),
] {
set_string_if_missing(
tv,
key,
theme_color::adjust(
fill_base,
ColorAdjustment::rgb(adjustment, adjustment, adjustment),
)?,
);
}
set_string_if_missing(tv, "quadrant1TextFill", text_base);
for (key, adjustment) in [
("quadrant2TextFill", -5.0),
("quadrant3TextFill", -10.0),
("quadrant4TextFill", -15.0),
] {
set_string_if_missing(
tv,
key,
theme_color::adjust(
text_base,
ColorAdjustment::rgb(adjustment, adjustment, adjustment),
)?,
);
}
let quadrant1_fill = required_color(tv, "quadrant1Fill")?;
let point_fill = if theme_color::is_dark(&quadrant1_fill)? {
theme_color::lighten(&quadrant1_fill, f64::NAN)?
} else {
theme_color::darken(&quadrant1_fill, f64::NAN)?
};
set_string_if_missing(tv, "quadrantPointFill", point_fill);
for key in [
"quadrantPointTextFill",
"quadrantXAxisTextFill",
"quadrantYAxisTextFill",
"quadrantTitleFill",
] {
set_string_if_missing(tv, key, text_base);
}
for key in [
"quadrantInternalBorderStrokeFill",
"quadrantExternalBorderStrokeFill",
] {
set_string_if_missing(tv, key, border_base);
}
Ok(())
}
fn apply_current_quadrant_theme_defaults(tv: &mut Map<String, Value>) -> Result<(), ColorError> {
let fill_base = required_color(tv, "primaryColor")?;
let text_base = required_color(tv, "primaryTextColor")?;
let border_base = required_color(tv, "primaryBorderColor")?;
apply_quadrant_theme_defaults(tv, &fill_base, &text_base, &border_base)
}
fn discard_non_explicit_quadrant_snapshot_values(
tv: &mut Map<String, Value>,
explicit: &Map<String, Value>,
) {
for key in [
"quadrant1Fill",
"quadrant2Fill",
"quadrant3Fill",
"quadrant4Fill",
"quadrant1TextFill",
"quadrant2TextFill",
"quadrant3TextFill",
"quadrant4TextFill",
"quadrantPointFill",
"quadrantPointTextFill",
"quadrantXAxisTextFill",
"quadrantYAxisTextFill",
"quadrantInternalBorderStrokeFill",
"quadrantExternalBorderStrokeFill",
"quadrantTitleFill",
] {
if !explicit.contains_key(key) {
tv.remove(key);
}
}
}
#[derive(Debug, Clone, Copy)]
enum ColorTransform {
Darken(f64),
Lighten(f64),
}
impl ColorTransform {
fn apply(self, color: &str) -> Result<String, ColorError> {
match self {
Self::Darken(amount) => theme_color::darken(color, amount),
Self::Lighten(amount) => theme_color::lighten(color, amount),
}
}
}
fn validate_explicit_git_transforms(
tv: &Map<String, Value>,
explicit: &Map<String, Value>,
transform: ColorTransform,
) -> Result<(), ColorError> {
for index in 0..8 {
let key = format!("git{index}");
if explicit.contains_key(&key) {
transform.apply(&required_color(tv, &key)?)?;
}
}
Ok(())
}
fn apply_single_pass_git_palette(
tv: &mut Map<String, Value>,
explicit: &Map<String, Value>,
bases: [String; 8],
transform: ColorTransform,
) -> Result<(), ColorError> {
for (index, base) in bases.into_iter().enumerate() {
let git_key = format!("git{index}");
let source = if explicit.contains_key(&git_key) {
required_color(tv, &git_key)?
} else {
base
};
let transformed = transform.apply(&source)?;
if !explicit.contains_key(&git_key) {
tv.insert(git_key, Value::String(transformed.clone()));
}
let inverse_key = format!("gitInv{index}");
if !explicit.contains_key(&inverse_key) {
tv.insert(
inverse_key,
Value::String(theme_color::invert(&transformed)?),
);
}
}
Ok(())
}
fn calculated_or_fallback_value(
explicit: &Map<String, Value>,
key: &str,
fallback: Value,
) -> Value {
explicit
.get(key)
.filter(|value| is_js_truthy(value))
.cloned()
.unwrap_or(fallback)
}
fn apply_scale_label_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
fallback: Value,
) {
let scale_label = calculated_or_fallback_value(explicit, "scaleLabelColor", fallback);
tv.insert("scaleLabelColor".to_string(), scale_label.clone());
for index in 0..12 {
let key = format!("cScaleLabel{index}");
if !explicit.contains_key(&key) {
tv.insert(key, scale_label.clone());
}
}
}
fn apply_default_theme_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let primary = required_color(tv, "primaryColor")?;
set_derived_string_unless_explicit(
tv,
explicit,
"rowOdd",
theme_color::lighten(&primary, 75.0)?,
);
set_derived_string_unless_explicit(
tv,
explicit,
"rowEven",
theme_color::lighten(&primary, 1.0)?,
);
Ok(())
}
fn apply_base_theme_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
let main_bkg = required_color(tv, "mainBkg")?;
let (row_odd, row_even) = if dark_mode {
(
theme_color::darken(&main_bkg, 5.0)?,
theme_color::darken(&main_bkg, 10.0)?,
)
} else {
(
theme_color::lighten(&main_bkg, 75.0)?,
theme_color::lighten(&main_bkg, 5.0)?,
)
};
set_derived_string_unless_explicit(tv, explicit, "rowOdd", row_odd);
set_derived_string_unless_explicit(tv, explicit, "rowEven", row_even);
let multiplier = if dark_mode { -4.0 } else { -1.0 };
for index in 0..5 {
set_derived_string_unless_explicit(
tv,
explicit,
&format!("surface{index}"),
theme_color::adjust(
&main_bkg,
ColorAdjustment::hsl(180.0, -15.0, multiplier * (5 + index * 3) as f64),
)?,
);
set_derived_string_unless_explicit(
tv,
explicit,
&format!("surfacePeer{index}"),
theme_color::adjust(
&main_bkg,
ColorAdjustment::hsl(180.0, -15.0, multiplier * (8 + index * 3) as f64),
)?,
);
}
let label_text = tv
.get("labelTextColor")
.cloned()
.unwrap_or_else(|| Value::String(if dark_mode { "#eee" } else { "#333" }.to_string()));
apply_scale_label_dependencies(explicit, tv, label_text);
Ok(())
}
fn apply_dark_theme_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let fallback = if tv.get("darkMode").is_some_and(is_js_truthy) {
Value::String("black".to_string())
} else {
tv.get("labelTextColor")
.cloned()
.unwrap_or_else(|| Value::String("lightgrey".to_string()))
};
apply_scale_label_dependencies(explicit, tv, fallback);
Ok(())
}
fn git_palette_bases(tv: &Map<String, Value>) -> Result<[String; 8], ColorError> {
let primary = required_color(tv, "primaryColor")?;
Ok([
primary.clone(),
required_color(tv, "secondaryColor")?,
required_color(tv, "tertiaryColor")?,
theme_color::adjust(&primary, ColorAdjustment::hsl(-30.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(-60.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(-90.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
])
}
fn apply_dynamic_git_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let transform = if tv.get("darkMode").is_some_and(is_js_truthy) {
ColorTransform::Lighten(25.0)
} else {
ColorTransform::Darken(25.0)
};
apply_single_pass_git_palette(tv, explicit, git_palette_bases(tv)?, transform)
}
fn apply_forest_theme_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let main_bkg = if explicit.contains_key("mainBkg") {
required_color(tv, "mainBkg")?
} else {
required_color(
ThemeProgram::resolve("forest").default_snapshot(),
"mainBkg",
)?
};
set_derived_string_unless_explicit(
tv,
explicit,
"rowOdd",
theme_color::lighten(&main_bkg, 75.0)?,
);
set_derived_string_unless_explicit(
tv,
explicit,
"rowEven",
theme_color::lighten(&main_bkg, 20.0)?,
);
apply_dynamic_git_dependencies(explicit, tv)
}
fn apply_neutral_theme_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
for index in 0..12 {
let scale = required_color(tv, &format!("cScale{index}"))?;
let peer = if dark_mode {
theme_color::lighten(&scale, 10.0)?
} else {
theme_color::darken(&scale, 10.0)?
};
set_derived_string_unless_explicit(tv, explicit, &format!("cScalePeer{index}"), peer);
set_derived_string_unless_explicit(
tv,
explicit,
&format!("cScaleInv{index}"),
theme_color::invert(&scale)?,
);
}
let fallback = if dark_mode {
Value::String("black".to_string())
} else {
tv.get("labelTextColor")
.cloned()
.unwrap_or_else(|| Value::String("#333".to_string()))
};
let scale_label = calculated_or_fallback_value(explicit, "scaleLabelColor", fallback);
tv.insert("scaleLabelColor".to_string(), scale_label.clone());
let scale_one = tv
.get("cScale1")
.cloned()
.unwrap_or_else(|| Value::String("#F4F4F4".to_string()));
for index in 0..12 {
let key = format!("cScaleLabel{index}");
if !explicit.contains_key(&key) {
tv.insert(
key,
if matches!(index, 0 | 2) {
scale_one.clone()
} else {
scale_label.clone()
},
);
}
}
Ok(())
}
fn darkened_scale_bases(tv: &Map<String, Value>) -> Result<[String; 12], ColorError> {
let primary = required_color(tv, "primaryColor")?;
Ok([
primary.clone(),
required_color(tv, "secondaryColor")?,
required_color(tv, "tertiaryColor")?,
theme_color::adjust(&primary, ColorAdjustment::hsl(30.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(90.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(150.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(210.0, 0.0, 150.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(270.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(300.0, 0.0, 0.0))?,
theme_color::adjust(&primary, ColorAdjustment::hsl(330.0, 0.0, 0.0))?,
])
}
fn apply_darkened_scale_dependencies(
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
let transform = ColorTransform::Darken(if dark_mode { 75.0 } else { 25.0 });
let bases = darkened_scale_bases(tv)?;
for (index, base) in bases.into_iter().enumerate() {
let scale_key = format!("cScale{index}");
let source = get_truthy_string(explicit, &scale_key).unwrap_or(base);
let scale = transform.apply(&source)?;
if !explicit.contains_key(&scale_key) {
tv.insert(scale_key, Value::String(scale.clone()));
}
set_derived_string_unless_explicit(
tv,
explicit,
&format!("cScaleInv{index}"),
theme_color::invert(&scale)?,
);
let peer = if dark_mode {
theme_color::lighten(&scale, 10.0)?
} else {
theme_color::darken(&scale, 10.0)?
};
set_derived_string_unless_explicit(tv, explicit, &format!("cScalePeer{index}"), peer);
}
let label_text = tv
.get("labelTextColor")
.cloned()
.unwrap_or_else(|| Value::String("#e0dfdf".to_string()));
apply_scale_label_dependencies(explicit, tv, label_text);
Ok(())
}
fn validate_explicit_operation(
tv: &Map<String, Value>,
explicit: &Map<String, Value>,
key: &str,
operation: impl FnOnce(&str) -> Result<String, ColorError>,
) -> Result<(), ColorError> {
if explicit.contains_key(key) {
operation(&required_color(tv, key)?)?;
}
Ok(())
}
fn replay_extended_theme_khroma_operations(
theme: &str,
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
match theme {
"neo" | "redux" | "redux-color" => {
for key in ["secondaryColor", "tertiaryColor"] {
validate_explicit_operation(tv, explicit, key, |color| {
mk_border(color, dark_mode)
})?;
}
}
"neo-dark" => {
validate_explicit_operation(tv, explicit, "secondaryColor", |color| {
theme_color::darken(color, 10.0)
})?;
validate_explicit_operation(tv, explicit, "tertiaryColor", |color| {
theme_color::darken(color, if dark_mode { 75.0 } else { 25.0 })
})?;
}
_ => {}
}
let scale_policy = match theme {
"neo" | "neo-dark" | "redux-dark" => Some(Some(ColorTransform::Darken(if dark_mode {
75.0
} else {
25.0
}))),
"redux-color" | "redux-dark-color" => Some(None),
_ => None,
};
if let Some(transform) = scale_policy {
for index in 0..2 {
let scale_key = format!("cScale{index}");
if !explicit.contains_key(&scale_key) {
continue;
}
let source = required_color(tv, &scale_key)?;
let calculated = match transform {
Some(transform) => transform.apply(&source)?,
None => source,
};
let inverse_key = format!("cScaleInv{index}");
if !explicit.contains_key(&inverse_key) {
tv.insert(
inverse_key,
Value::String(theme_color::invert(&calculated)?),
);
}
let peer_key = format!("cScalePeer{index}");
if !explicit.contains_key(&peer_key) {
let peer = if dark_mode {
theme_color::lighten(&calculated, 10.0)?
} else {
theme_color::darken(&calculated, 10.0)?
};
tv.insert(peer_key, Value::String(peer));
}
if theme == "redux-dark-color" {
let label_key = format!("cScaleLabel{index}");
if !explicit.contains_key(&label_key) {
tv.insert(
label_key,
Value::String(theme_color::darken(&calculated, 75.0)?),
);
}
}
}
}
Ok(())
}
pub(crate) fn apply_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let requested = config.get_str("theme").unwrap_or("default").to_string();
let program = *ThemeProgram::resolve(&requested);
let raw = theme_variables_map(config);
program.validate_evaluated_inputs(&raw)?;
let explicit = program.normalize_overrides(raw);
config.set_value("themeVariables", Value::Object(explicit));
program.execute(config)
}
fn apply_snapshot_theme_defaults(
config: &mut MermaidConfig,
theme: &str,
) -> Result<(), ColorError> {
let tv = theme_variables_map(config);
if tv.is_empty() {
return finish_theme_defaults(config, theme, tv);
}
let explicit = tv.clone();
let mut resolved = tv;
let program = ThemeProgram::resolve(theme);
merge_theme_variable_defaults(&mut resolved, program.calculation_snapshot(&explicit));
apply_extended_theme_visible_derivations(theme, &explicit, &mut resolved)?;
finish_theme_defaults(config, theme, resolved)
}
fn apply_extended_theme_visible_derivations(
theme: &str,
explicit: &Map<String, Value>,
tv: &mut Map<String, Value>,
) -> Result<(), ColorError> {
if !matches!(
theme,
"neo" | "neo-dark" | "redux" | "redux-dark" | "redux-color" | "redux-dark-color"
) {
return Ok(());
}
replay_extended_theme_khroma_operations(theme, explicit, tv)?;
if explicit.contains_key("primaryColor") {
let primary = required_color(tv, "primaryColor")?;
set_derived_string_unless_explicit(tv, explicit, "nodeBkg", primary.clone());
set_derived_string_unless_explicit(tv, explicit, "tagLabelBackground", primary.clone());
if matches!(theme, "neo" | "redux" | "redux-color")
&& !explicit.contains_key("secondaryColor")
{
let secondary = theme_color::adjust(&primary, ColorAdjustment::hsl(-120.0, 0.0, 0.0))?;
tv.insert("secondaryColor".to_string(), Value::String(secondary));
}
}
if explicit.contains_key("background") {
let background = required_color(tv, "background")?;
if !explicit.contains_key("lineColor") {
let line_color = theme_color::invert(&background)?;
tv.insert("lineColor".to_string(), Value::String(line_color));
}
if !explicit.contains_key("arrowheadColor") {
let arrowhead_color = theme_color::invert(&background)?;
tv.insert("arrowheadColor".to_string(), Value::String(arrowhead_color));
}
}
if explicit.contains_key("lineColor") || explicit.contains_key("background") {
let line_color = required_color(tv, "lineColor")?;
for key in [
"defaultLinkColor",
"archEdgeColor",
"archEdgeArrowColor",
"relationColor",
"transitionColor",
"specialStateColor",
] {
set_derived_string_unless_explicit(tv, explicit, key, line_color.clone());
}
}
if explicit.contains_key("secondaryColor") || explicit.contains_key("primaryColor") {
let secondary = required_color(tv, "secondaryColor")?;
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
let label_background = if dark_mode {
theme_color::darken(&secondary, 30.0)?
} else {
secondary.clone()
};
for key in [
"edgeLabelBackground",
"activationBkgColor",
"commitLabelBackground",
"relationLabelBackground",
] {
set_derived_string_unless_explicit(tv, explicit, key, label_background.clone());
}
}
if explicit.contains_key("mainBkg") {
let main_bkg = required_color(tv, "mainBkg")?;
for key in [
"actorBkg",
"labelBoxBkgColor",
"personBkg",
"stateBkg",
"labelBackgroundColor",
] {
set_derived_string_unless_explicit(tv, explicit, key, main_bkg.clone());
}
}
if explicit.contains_key("primaryColor")
&& matches!(theme, "neo-dark" | "redux-dark" | "redux-dark-color")
{
let primary = required_color(tv, "primaryColor")?;
for key in ["requirementBackground", "pie1"] {
set_derived_string_unless_explicit(tv, explicit, key, primary.clone());
}
}
for i in 0..8 {
let git_key = format!("git{i}");
let git_inv_key = format!("gitInv{i}");
if explicit.contains_key(&git_key) && !explicit.contains_key(&git_inv_key) {
let git = required_color(tv, &git_key)?;
let inv = theme_color::invert(&git)?;
tv.insert(git_inv_key, Value::String(inv));
}
}
discard_non_explicit_quadrant_snapshot_values(tv, explicit);
let quadrant_fill_base = if matches!(theme, "neo" | "redux" | "redux-color") {
"#ECECFE".to_string()
} else {
required_color(tv, "primaryColor")?
};
let quadrant_text_base = required_color(tv, "primaryTextColor")?;
let quadrant_border_base = required_color(tv, "primaryBorderColor")?;
apply_quadrant_theme_defaults(
tv,
&quadrant_fill_base,
&quadrant_text_base,
&quadrant_border_base,
)?;
Ok(())
}
fn apply_default_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let mut tv = theme_variables_map(config);
let explicit_theme_variables = tv.clone();
let default_primary = "#ECECFF";
let default_secondary = "#ffffde";
let default_tertiary =
theme_color::adjust(default_primary, ColorAdjustment::hsl(-160.0, 0.0, 0.0))?;
let default_primary_border = mk_border(default_primary, false)?;
set_if_missing(&mut tv, "background", Value::String("white".to_string()));
set_if_missing(
&mut tv,
"primaryColor",
Value::String(default_primary.to_string()),
);
set_if_missing(
&mut tv,
"secondaryColor",
Value::String(default_secondary.to_string()),
);
set_if_missing(
&mut tv,
"tertiaryColor",
Value::String(default_tertiary.clone()),
);
set_if_missing(
&mut tv,
"primaryBorderColor",
Value::String(default_primary_border.clone()),
);
set_if_missing(
&mut tv,
"secondaryBorderColor",
Value::String(mk_border(default_secondary, false)?),
);
set_if_missing(
&mut tv,
"tertiaryBorderColor",
Value::String(mk_border(&default_tertiary, false)?),
);
set_if_missing(
&mut tv,
"primaryTextColor",
Value::String("#131300".to_string()),
);
set_if_missing(
&mut tv,
"secondaryTextColor",
Value::String("#000021".to_string()),
);
set_if_missing(
&mut tv,
"tertiaryTextColor",
Value::String(theme_color::invert(&default_tertiary)?),
);
set_if_missing(
&mut tv,
"mainBkg",
Value::String(default_primary.to_string()),
);
set_if_missing(
&mut tv,
"secondBkg",
Value::String(default_secondary.to_string()),
);
set_if_missing(&mut tv, "lineColor", Value::String("#333333".to_string()));
set_if_missing(&mut tv, "border1", Value::String("#9370DB".to_string()));
set_if_missing(&mut tv, "border2", Value::String("#aaaa33".to_string()));
set_if_missing(
&mut tv,
"arrowheadColor",
Value::String("#333333".to_string()),
);
set_if_missing(&mut tv, "fontFamily", mermaid_default_font_family());
set_if_missing(&mut tv, "fontSize", Value::String("16px".to_string()));
set_if_missing(
&mut tv,
"labelBackground",
Value::String("rgba(232,232,232, 0.8)".to_string()),
);
set_if_missing(&mut tv, "textColor", Value::String("#333".to_string()));
set_if_missing(&mut tv, "THEME_COLOR_LIMIT", Value::Number(12.into()));
set_if_missing(&mut tv, "radius", Value::Number(5.into()));
set_if_missing(&mut tv, "strokeWidth", Value::Number(1.into()));
let main_bkg = get_truthy_string(&tv, "mainBkg").unwrap_or_else(|| default_primary.to_string());
let second_bkg =
get_truthy_string(&tv, "secondBkg").unwrap_or_else(|| default_secondary.to_string());
let line_color = get_truthy_string(&tv, "lineColor").unwrap_or_else(|| "#333333".to_string());
let text_color = get_truthy_string(&tv, "textColor").unwrap_or_else(|| "#333".to_string());
let border1 = get_truthy_string(&tv, "border1").unwrap_or_else(|| "#9370DB".to_string());
let border2 = get_truthy_string(&tv, "border2").unwrap_or_else(|| "#aaaa33".to_string());
let label_background = get_truthy_string(&tv, "labelBackground")
.unwrap_or_else(|| "rgba(232,232,232, 0.8)".to_string());
let primary_text_color =
get_truthy_string(&tv, "primaryTextColor").unwrap_or_else(|| "#131300".to_string());
set_if_missing(&mut tv, "nodeBkg", Value::String(main_bkg.clone()));
set_if_missing(&mut tv, "nodeBorder", Value::String(border1.clone()));
set_if_missing(&mut tv, "clusterBkg", Value::String(second_bkg.clone()));
set_if_missing(&mut tv, "clusterBorder", Value::String(border2.clone()));
set_if_missing(
&mut tv,
"defaultLinkColor",
Value::String(line_color.clone()),
);
set_if_missing(&mut tv, "titleColor", Value::String(text_color.clone()));
set_if_missing(
&mut tv,
"edgeLabelBackground",
Value::String(label_background.clone()),
);
set_if_missing(
&mut tv,
"nodeTextColor",
Value::String(primary_text_color.clone()),
);
set_if_missing(&mut tv, "actorBorder", Value::String(border1.clone()));
set_if_missing(&mut tv, "actorBkg", Value::String(main_bkg.clone()));
set_if_missing(
&mut tv,
"actorTextColor",
Value::String("black".to_string()),
);
let actor_text_color =
get_truthy_string(&tv, "actorTextColor").unwrap_or_else(|| "black".to_string());
set_if_missing(&mut tv, "actorLineColor", Value::String(border1.clone()));
set_if_missing(&mut tv, "labelBoxBkgColor", Value::String(main_bkg.clone()));
set_if_missing(&mut tv, "signalColor", Value::String(text_color.clone()));
set_if_missing(
&mut tv,
"signalTextColor",
Value::String(text_color.clone()),
);
set_if_missing(
&mut tv,
"labelBoxBorderColor",
Value::String(border1.clone()),
);
set_if_missing(
&mut tv,
"labelTextColor",
Value::String(actor_text_color.clone()),
);
set_if_missing(
&mut tv,
"loopTextColor",
Value::String(actor_text_color.clone()),
);
set_if_missing(&mut tv, "noteBorderColor", Value::String(border2.clone()));
set_if_missing(
&mut tv,
"noteBkgColor",
Value::String("#fff5ad".to_string()),
);
set_if_missing(
&mut tv,
"noteTextColor",
Value::String(actor_text_color.clone()),
);
set_if_missing(
&mut tv,
"activationBorderColor",
Value::String("#666".to_string()),
);
set_if_missing(
&mut tv,
"activationBkgColor",
Value::String("#f4f4f4".to_string()),
);
set_if_missing(
&mut tv,
"sequenceNumberColor",
Value::String("white".to_string()),
);
set_if_missing(
&mut tv,
"rectBkgColor",
Value::String(default_tertiary.clone()),
);
for (key, value) in [
("sectionBkgColor", "rgba(102, 102, 255, 0.49)"),
("altSectionBkgColor", "white"),
("sectionBkgColor2", "#fff400"),
("excludeBkgColor", "#eeeeee"),
("taskBorderColor", "#534fbc"),
("taskBkgColor", "#8a90dd"),
("taskTextLightColor", "white"),
("taskTextColor", "white"),
("taskTextDarkColor", "black"),
("taskTextOutsideColor", "black"),
("taskTextClickableColor", "#003163"),
("activeTaskBorderColor", "#534fbc"),
("activeTaskBkgColor", "#bfc7ff"),
("gridColor", "lightgrey"),
("doneTaskBkgColor", "lightgrey"),
("doneTaskBorderColor", "grey"),
("critBorderColor", "#ff8888"),
("critBkgColor", "red"),
("todayLineColor", "red"),
("vertLineColor", "navy"),
("noteFontWeight", "normal"),
("fontWeight", "normal"),
] {
set_if_missing(&mut tv, key, Value::String(value.to_string()));
}
let primary_border_color = match get_truthy_string(&tv, "primaryBorderColor") {
Some(color) => color,
None => mk_border(default_primary, false)?,
};
let secondary_border_color = match get_truthy_string(&tv, "secondaryBorderColor") {
Some(color) => color,
None => mk_border(default_secondary, false)?,
};
set_if_missing(
&mut tv,
"personBorder",
Value::String(primary_border_color.clone()),
);
set_if_missing(&mut tv, "personBkg", Value::String(main_bkg.clone()));
set_if_missing(&mut tv, "archEdgeColor", Value::String(line_color.clone()));
set_if_missing(
&mut tv,
"archEdgeArrowColor",
Value::String(line_color.clone()),
);
set_if_missing(&mut tv, "archEdgeWidth", Value::String("3".to_string()));
set_if_missing(
&mut tv,
"archGroupBorderColor",
Value::String(primary_border_color.clone()),
);
set_if_missing(
&mut tv,
"archGroupBorderWidth",
Value::String("2px".to_string()),
);
set_if_missing(
&mut tv,
"rowOdd",
Value::String(theme_color::lighten(default_primary, 75.0)?),
);
set_if_missing(
&mut tv,
"rowEven",
Value::String(theme_color::lighten(default_primary, 1.0)?),
);
set_if_missing(
&mut tv,
"attributeBackgroundColorOdd",
Value::String("#ffffff".to_string()),
);
set_if_missing(
&mut tv,
"attributeBackgroundColorEven",
Value::String("#f2f2f2".to_string()),
);
set_if_missing(&mut tv, "labelColor", Value::String("black".to_string()));
set_if_missing(
&mut tv,
"errorBkgColor",
Value::String("#552222".to_string()),
);
set_if_missing(
&mut tv,
"errorTextColor",
Value::String("#552222".to_string()),
);
set_if_missing(
&mut tv,
"transitionColor",
Value::String(line_color.clone()),
);
set_if_missing(
&mut tv,
"transitionLabelColor",
Value::String(text_color.clone()),
);
let state_label_color = get_truthy_string(&tv, "stateBkg")
.map(Value::String)
.unwrap_or_else(|| Value::String(primary_text_color.clone()));
set_if_missing(&mut tv, "stateLabelColor", state_label_color);
set_if_missing(&mut tv, "stateBkg", Value::String(main_bkg.clone()));
let state_bkg = get_truthy_string(&tv, "stateBkg").unwrap_or_else(|| main_bkg.clone());
set_if_missing(&mut tv, "labelBackgroundColor", Value::String(state_bkg));
let composite_background = get_truthy_string(&tv, "background")
.map(Value::String)
.unwrap_or_else(|| Value::String("white".to_string()));
set_if_missing(&mut tv, "compositeBackground", composite_background);
set_if_missing(
&mut tv,
"altBackground",
Value::String("#f0f0f0".to_string()),
);
set_if_missing(
&mut tv,
"compositeTitleBackground",
Value::String(main_bkg.clone()),
);
let node_border = get_truthy_string(&tv, "nodeBorder").unwrap_or_else(|| border1.clone());
set_if_missing(
&mut tv,
"compositeBorder",
Value::String(node_border.clone()),
);
set_if_missing(&mut tv, "innerEndBackground", Value::String(node_border));
set_if_missing(
&mut tv,
"specialStateColor",
Value::String(line_color.clone()),
);
set_if_missing(
&mut tv,
"classText",
Value::String(primary_text_color.clone()),
);
let primary_color =
get_truthy_string(&tv, "primaryColor").unwrap_or_else(|| default_primary.to_string());
let secondary_color =
get_truthy_string(&tv, "secondaryColor").unwrap_or_else(|| default_secondary.to_string());
let tertiary_color = default_tertiary.clone();
let c_scale_bases = [
primary_color.clone(),
secondary_color.clone(),
tertiary_color.clone(),
theme_color::adjust(&primary_color, ColorAdjustment::hsl(30.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(90.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(150.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(210.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(270.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(300.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(330.0, 0.0, 0.0))?,
];
let mut c_scales = Vec::with_capacity(c_scale_bases.len());
for base in c_scale_bases {
c_scales.push(theme_color::darken(&base, 10.0)?);
}
for (i, v) in c_scales.iter().enumerate() {
set_if_missing(&mut tv, &format!("cScale{i}"), Value::String(v.clone()));
}
set_if_missing(
&mut tv,
"cScalePeer1",
Value::String(theme_color::darken(&secondary_color, 45.0)?),
);
set_if_missing(
&mut tv,
"cScalePeer2",
Value::String(theme_color::darken(&tertiary_color, 40.0)?),
);
for (i, fallback_color) in c_scales.iter().enumerate() {
let color =
get_truthy_string(&tv, &format!("cScale{i}")).unwrap_or_else(|| fallback_color.clone());
set_if_missing(
&mut tv,
&format!("cScalePeer{i}"),
Value::String(theme_color::darken(&color, 25.0)?),
);
set_if_missing(
&mut tv,
&format!("cScaleInv{i}"),
Value::String(theme_color::adjust(
&color,
ColorAdjustment::hsl(180.0, 0.0, 0.0),
)?),
);
if i == 0 || i == 3 {
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String("#ffffff".to_string()),
);
}
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(actor_text_color.clone()),
);
}
set_if_missing(
&mut tv,
"scaleLabelColor",
Value::String(actor_text_color.clone()),
);
for (key, value) in [
("fillType0", primary_color.clone()),
("fillType1", secondary_color.clone()),
(
"fillType2",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType3",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType4",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType5",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType6",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
(
"fillType7",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
("pie1", primary_color.clone()),
("pie2", secondary_color.clone()),
("pie3", theme_color::darken(&tertiary_color, 40.0)?),
("pie4", theme_color::darken(&primary_color, 10.0)?),
("pie5", theme_color::darken(&secondary_color, 30.0)?),
("pie6", theme_color::darken(&tertiary_color, 20.0)?),
(
"pie7",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, -20.0))?,
),
(
"pie8",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-60.0, 0.0, -40.0))?,
),
(
"pie9",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, -40.0))?,
),
(
"pie10",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, -40.0))?,
),
(
"pie11",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-90.0, 0.0, -40.0))?,
),
(
"pie12",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, -30.0))?,
),
] {
set_if_missing(&mut tv, key, Value::String(value));
}
for (key, value) in [
("pieTitleTextSize", "25px"),
("pieTitleTextColor", "black"),
("pieSectionTextSize", "17px"),
("pieSectionTextColor", text_color.as_str()),
("pieLegendTextSize", "17px"),
("pieLegendTextColor", "black"),
("pieStrokeColor", "black"),
("pieStrokeWidth", "2px"),
("pieOuterStrokeWidth", "2px"),
("pieOuterStrokeColor", "black"),
("pieOpacity", "0.7"),
] {
set_if_missing(&mut tv, key, Value::String(value.to_string()));
}
set_if_missing(
&mut tv,
"requirementBackground",
Value::String(default_primary.to_string()),
);
set_if_missing(
&mut tv,
"requirementBorderColor",
Value::String(primary_border_color.clone()),
);
set_if_missing(
&mut tv,
"requirementBorderSize",
Value::String("1".to_string()),
);
set_if_missing(
&mut tv,
"requirementTextColor",
Value::String(primary_text_color.clone()),
);
set_if_missing(&mut tv, "relationColor", Value::String(line_color.clone()));
set_if_missing(
&mut tv,
"relationLabelBackground",
Value::String(label_background),
);
set_if_missing(
&mut tv,
"relationLabelColor",
Value::String(actor_text_color.clone()),
);
set_if_missing(&mut tv, "tagLabelColor", Value::String(primary_text_color));
set_if_missing(
&mut tv,
"tagLabelBackground",
Value::String(default_primary.to_string()),
);
set_if_missing(
&mut tv,
"tagLabelBorder",
Value::String(primary_border_color.clone()),
);
set_if_missing(
&mut tv,
"tagLabelFontSize",
Value::String("10px".to_string()),
);
set_if_missing(
&mut tv,
"commitLabelColor",
Value::String("#000021".to_string()),
);
set_if_missing(
&mut tv,
"commitLabelBackground",
Value::String(default_secondary.to_string()),
);
set_if_missing(
&mut tv,
"commitLabelFontSize",
Value::String("10px".to_string()),
);
set_if_missing(&mut tv, "useGradient", Value::Bool(false));
set_if_missing(
&mut tv,
"gradientStart",
Value::String(primary_border_color),
);
set_if_missing(
&mut tv,
"gradientStop",
Value::String(secondary_border_color),
);
set_if_missing(
&mut tv,
"dropShadow",
Value::String("drop-shadow(1px 2px 2px rgba(185, 185, 185, 1))".to_string()),
);
ensure_xychart_theme_defaults(
&mut tv,
"#ECECFF,#8493A6,#FFC3A0,#DCDDE1,#B8E994,#D1A36F,#C3CDE6,#FFB6C1,#496078,#F8F3E3",
);
apply_quadrant_theme_defaults(&mut tv, default_primary, "#131300", &default_primary_border)?;
validate_explicit_git_transforms(&tv, &explicit_theme_variables, ColorTransform::Darken(25.0))?;
finish_theme_defaults(config, "default", tv)
}
fn apply_dark_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let mut tv = theme_variables_map(config);
let c_scales_hex: [&str; 12] = [
"#1f2020", "#0b0000", "#4d1037", "#3f5258", "#4f2f1b", "#6e0a0a", "#3b0048", "#995a01", "#154706",
"#161722", "#00296f", "#01629c",
];
set_string_if_missing(&mut tv, "background", "#333");
set_string_if_missing(&mut tv, "primaryColor", "#1f2020");
if get_truthy_string(&tv, "primaryTextColor").is_none()
&& let Some(primary_color) = get_truthy_string(&tv, "primaryColor")
{
tv.insert(
"primaryTextColor".to_string(),
Value::String(theme_color::invert(&primary_color)?),
);
}
set_string_if_missing(&mut tv, "textColor", "#ccc");
set_if_missing(&mut tv, "fontFamily", mermaid_default_font_family());
set_string_if_missing(&mut tv, "fontSize", "16px");
set_string_if_missing(&mut tv, "border1", "#ccc");
set_string_if_missing(&mut tv, "border2", "rgba(255, 255, 255, 0.25)");
set_string_if_missing(&mut tv, "labelBackground", "#181818");
set_string_if_missing(&mut tv, "titleColor", "#F9FFFE");
set_if_missing(&mut tv, "THEME_COLOR_LIMIT", Value::Number(12.into()));
set_if_missing(&mut tv, "radius", Value::Number(5.into()));
set_if_missing(&mut tv, "strokeWidth", Value::Number(1.into()));
set_string_if_missing(&mut tv, "errorBkgColor", "#a44141");
set_string_if_missing(&mut tv, "errorTextColor", "#ddd");
let primary_color =
get_truthy_string(&tv, "primaryColor").unwrap_or_else(|| "#1f2020".to_string());
let default_secondary_color = theme_color::lighten(&primary_color, 16.0)?;
set_if_missing(
&mut tv,
"secondaryColor",
Value::String(default_secondary_color.clone()),
);
set_if_missing(
&mut tv,
"primaryBorderColor",
Value::String("#cccccc".to_string()),
);
set_if_missing(
&mut tv,
"secondaryBorderColor",
Value::String(mk_border(&default_secondary_color, false)?),
);
let default_tertiary_color =
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-160.0, 0.0, 0.0))?;
set_string_if_missing(&mut tv, "tertiaryColor", default_tertiary_color.clone());
set_string_if_missing(
&mut tv,
"tertiaryBorderColor",
mk_border(&default_tertiary_color, false)?,
);
set_string_if_missing(
&mut tv,
"secondaryTextColor",
theme_color::invert(&default_secondary_color)?,
);
set_string_if_missing(
&mut tv,
"tertiaryTextColor",
theme_color::invert(&default_tertiary_color)?,
);
ensure_gradient_theme_defaults(&mut tv);
let secondary_color =
get_truthy_string(&tv, "secondaryColor").unwrap_or_else(|| default_secondary_color.clone());
let secondary_text_color = match get_truthy_string(&tv, "secondaryTextColor") {
Some(color) => color,
None => theme_color::invert(&secondary_color)?,
};
let tertiary_color =
get_truthy_string(&tv, "tertiaryColor").unwrap_or_else(|| default_tertiary_color.clone());
let background = get_truthy_string(&tv, "background").unwrap_or_else(|| "#333".to_string());
let primary_text_color =
get_truthy_string(&tv, "primaryTextColor").unwrap_or_else(|| "#e0dfdf".to_string());
let text_color = get_truthy_string(&tv, "textColor").unwrap_or_else(|| "#ccc".to_string());
let line_color = get_truthy_string(&tv, "lineColor").unwrap_or_else(|| "lightgrey".to_string());
let border1 = get_truthy_string(&tv, "border1").unwrap_or_else(|| "#ccc".to_string());
let border2 = get_truthy_string(&tv, "border2")
.unwrap_or_else(|| "rgba(255, 255, 255, 0.25)".to_string());
let primary_border_color =
get_truthy_string(&tv, "primaryBorderColor").unwrap_or_else(|| "#cccccc".to_string());
let secondary_border_color = match get_truthy_string(&tv, "secondaryBorderColor") {
Some(color) => color,
None => mk_border(&secondary_color, false)?,
};
for (key, color) in [
("fillType0", primary_color.clone()),
("fillType1", secondary_color.clone()),
(
"fillType2",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType3",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType4",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType5",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType6",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
(
"fillType7",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
] {
tv.insert(key.to_string(), Value::String(color));
}
set_string_if_missing(&mut tv, "mainBkg", primary_color.clone());
set_string_if_missing(&mut tv, "secondBkg", secondary_color.clone());
set_string_if_missing(&mut tv, "mainContrastColor", "lightgrey");
set_string_if_missing(
&mut tv,
"darkTextColor",
"hsl(28.5714285714, 17.3553719008%, 86.2745098039%)",
);
set_string_if_missing(&mut tv, "lineColor", "lightgrey");
set_string_if_missing(&mut tv, "arrowheadColor", "lightgrey");
set_string_if_missing(&mut tv, "nodeBkg", primary_color.clone());
set_string_if_missing(&mut tv, "mainBkg", primary_color.clone());
set_string_if_missing(&mut tv, "nodeBorder", border1.clone());
set_string_if_missing(&mut tv, "clusterBkg", secondary_color.clone());
set_string_if_missing(&mut tv, "clusterBorder", border2.clone());
set_string_if_missing(&mut tv, "defaultLinkColor", line_color.clone());
set_string_if_missing(&mut tv, "edgeLabelBackground", "hsl(0, 0%, 34.4117647059%)");
set_string_if_missing(&mut tv, "classText", primary_text_color.clone());
set_string_if_missing(&mut tv, "actorBorder", border1.clone());
set_string_if_missing(&mut tv, "actorBkg", primary_color.clone());
set_string_if_missing(&mut tv, "actorTextColor", "lightgrey");
set_string_if_missing(&mut tv, "actorLineColor", border1.clone());
set_string_if_missing(&mut tv, "signalColor", "lightgrey");
set_string_if_missing(&mut tv, "signalTextColor", "lightgrey");
set_string_if_missing(&mut tv, "labelBoxBkgColor", primary_color.clone());
set_string_if_missing(&mut tv, "labelBoxBorderColor", border1.clone());
set_string_if_missing(&mut tv, "labelTextColor", "lightgrey");
set_string_if_missing(&mut tv, "loopTextColor", "lightgrey");
set_string_if_missing(&mut tv, "noteBorderColor", secondary_border_color.clone());
set_string_if_missing(&mut tv, "noteBkgColor", secondary_color.clone());
set_string_if_missing(&mut tv, "noteTextColor", secondary_text_color.clone());
set_string_if_missing(&mut tv, "activationBorderColor", border1);
set_string_if_missing(&mut tv, "activationBkgColor", secondary_color.clone());
set_string_if_missing(&mut tv, "sequenceNumberColor", "black");
set_string_if_missing(&mut tv, "rectBkgColor", tertiary_color.clone());
set_string_if_missing(
&mut tv,
"sectionBkgColor",
"hsl(50, 26.087%, 48.2352941176%)",
);
set_string_if_missing(&mut tv, "altSectionBkgColor", background.clone());
set_string_if_missing(&mut tv, "sectionBkgColor2", "#EAE8D9");
set_string_if_missing(
&mut tv,
"excludeBkgColor",
"hsl(50, 26.087%, 38.2352941176%)",
);
set_string_if_missing(
&mut tv,
"taskBorderColor",
theme_color::rgba(255.0, 255.0, 255.0, 70.0)?,
);
set_string_if_missing(
&mut tv,
"taskBkgColor",
"hsl(180, 1.5873015873%, 35.3529411765%)",
);
set_string_if_missing(
&mut tv,
"taskTextColor",
"hsl(28.5714285714, 17.3553719008%, 86.2745098039%)",
);
set_string_if_missing(&mut tv, "taskTextLightColor", "lightgrey");
set_string_if_missing(&mut tv, "taskTextOutsideColor", "lightgrey");
set_string_if_missing(&mut tv, "taskTextClickableColor", "#003163");
set_string_if_missing(
&mut tv,
"activeTaskBorderColor",
theme_color::rgba(255.0, 255.0, 255.0, 50.0)?,
);
set_string_if_missing(&mut tv, "activeTaskBkgColor", "#81B1DB");
set_string_if_missing(&mut tv, "gridColor", "lightgrey");
set_string_if_missing(&mut tv, "doneTaskBkgColor", "lightgrey");
set_string_if_missing(&mut tv, "doneTaskBorderColor", "grey");
set_string_if_missing(&mut tv, "critBorderColor", "#E83737");
set_string_if_missing(&mut tv, "critBkgColor", "#E83737");
set_string_if_missing(&mut tv, "taskTextDarkColor", "#2c2c2c");
set_string_if_missing(&mut tv, "todayLineColor", "#DB5757");
set_string_if_missing(&mut tv, "vertLineColor", "#00BFFF");
set_string_if_missing(&mut tv, "personBorder", primary_border_color.clone());
set_string_if_missing(&mut tv, "personBkg", primary_color.clone());
set_string_if_missing(&mut tv, "archEdgeColor", line_color.clone());
set_string_if_missing(&mut tv, "archEdgeArrowColor", line_color.clone());
set_string_if_missing(&mut tv, "archEdgeWidth", "3");
set_string_if_missing(
&mut tv,
"archGroupBorderColor",
primary_border_color.clone(),
);
set_string_if_missing(&mut tv, "archGroupBorderWidth", "2px");
set_string_if_missing(&mut tv, "rowOdd", "hsl(180, 1.5873015873%, 17.3529411765%)");
set_string_if_missing(&mut tv, "rowEven", "hsl(180, 1.5873015873%, 2.3529411765%)");
set_string_if_missing(&mut tv, "transitionColor", line_color.clone());
set_string_if_missing(&mut tv, "transitionLabelColor", text_color.clone());
set_string_if_missing(&mut tv, "stateLabelColor", primary_text_color.clone());
set_string_if_missing(&mut tv, "stateBkg", primary_color.clone());
set_string_if_missing(&mut tv, "labelBackgroundColor", primary_color.clone());
set_string_if_missing(&mut tv, "compositeBackground", background.clone());
set_string_if_missing(&mut tv, "altBackground", "#555");
set_string_if_missing(&mut tv, "compositeTitleBackground", primary_color.clone());
let composite_border =
get_truthy_string(&tv, "nodeBorder").unwrap_or_else(|| "#ccc".to_string());
set_string_if_missing(&mut tv, "compositeBorder", composite_border);
set_string_if_missing(&mut tv, "innerEndBackground", primary_border_color.clone());
set_string_if_missing(&mut tv, "specialStateColor", "#f4f4f4");
set_string_if_missing_with(&mut tv, "emSwimlaneBackgroundOdd", || {
theme_color::lighten(&background, 5.0)
})?;
set_string_if_missing_with(&mut tv, "emSwimlaneBackgroundStroke", || {
theme_color::lighten(&background, 12.0)
})?;
set_string_if_missing_with(&mut tv, "attributeBackgroundColorOdd", || {
theme_color::lighten(&background, 12.0)
})?;
set_string_if_missing_with(&mut tv, "attributeBackgroundColorEven", || {
theme_color::lighten(&background, 2.0)
})?;
set_string_if_missing(&mut tv, "noteFontWeight", "normal");
set_string_if_missing(&mut tv, "fontWeight", "normal");
set_string_if_missing(
&mut tv,
"dropShadow",
"drop-shadow( 1px 2px 2px rgba(185,185,185,1))",
);
let label_text_color =
get_truthy_string(&tv, "labelTextColor").unwrap_or_else(|| "lightgrey".to_string());
set_if_missing(
&mut tv,
"scaleLabelColor",
Value::String(label_text_color.clone()),
);
let scale_label_color =
get_truthy_string(&tv, "scaleLabelColor").unwrap_or_else(|| label_text_color.clone());
for (i, c_hex) in c_scales_hex.iter().enumerate() {
let c_scale_key = format!("cScale{i}");
let default_scale = if i == 0 {
primary_color.clone()
} else {
(*c_hex).to_string()
};
set_if_missing(&mut tv, &c_scale_key, Value::String(default_scale));
let c_scale = required_color(&tv, &c_scale_key)?;
set_if_missing(
&mut tv,
&format!("cScalePeer{i}"),
Value::String(theme_color::lighten(&c_scale, 10.0)?),
);
set_if_missing(
&mut tv,
&format!("cScaleInv{i}"),
Value::String(theme_color::invert(&c_scale)?),
);
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(scale_label_color.clone()),
);
}
set_string_if_missing(&mut tv, "pieTitleTextColor", line_color.clone());
set_string_if_missing(&mut tv, "pieSectionTextColor", text_color);
set_string_if_missing(&mut tv, "pieLegendTextColor", line_color.clone());
set_string_if_missing(&mut tv, "branchLabelColor", "#2c2c2c");
set_string_if_missing(&mut tv, "gitBranchLabel0", "#2c2c2c");
set_string_if_missing(&mut tv, "gitBranchLabel1", "lightgrey");
set_string_if_missing(&mut tv, "gitBranchLabel2", "lightgrey");
set_string_if_missing(&mut tv, "gitBranchLabel3", "#2c2c2c");
for i in 4..8 {
set_string_if_missing(&mut tv, &format!("gitBranchLabel{i}"), "lightgrey");
}
set_string_if_missing(&mut tv, "tagLabelColor", primary_text_color);
set_string_if_missing(&mut tv, "tagLabelBackground", primary_color);
set_string_if_missing(&mut tv, "tagLabelBorder", primary_border_color);
set_string_if_missing(&mut tv, "tagLabelFontSize", "10px");
set_string_if_missing(&mut tv, "commitLabelColor", secondary_text_color);
set_string_if_missing(&mut tv, "commitLabelBackground", secondary_color);
set_string_if_missing(&mut tv, "commitLabelFontSize", "10px");
ensure_xychart_theme_defaults(
&mut tv,
"#3498db,#2ecc71,#e74c3c,#f1c40f,#bdc3c7,#ffffff,#34495e,#9b59b6,#1abc9c,#e67e22",
);
apply_current_quadrant_theme_defaults(&mut tv)?;
finish_theme_defaults(config, "dark", tv)
}
fn apply_forest_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let mut tv = theme_variables_map(config);
let explicit_theme_variables = tv.clone();
set_if_missing(
&mut tv,
"primaryColor",
Value::String("#cde498".to_string()),
);
set_if_missing(
&mut tv,
"secondaryColor",
Value::String("#cdffb2".to_string()),
);
set_if_missing(&mut tv, "background", Value::String("white".to_string()));
set_if_missing(&mut tv, "border1", Value::String("#13540c".to_string()));
set_if_missing(&mut tv, "border2", Value::String("#6eaa49".to_string()));
set_if_missing(
&mut tv,
"arrowheadColor",
Value::String("green".to_string()),
);
set_if_missing(&mut tv, "fontFamily", mermaid_default_font_family());
set_if_missing(&mut tv, "fontSize", Value::String("16px".to_string()));
set_if_missing(&mut tv, "titleColor", Value::String("#333".to_string()));
set_if_missing(
&mut tv,
"edgeLabelBackground",
Value::String("#e8e8e8".to_string()),
);
set_if_missing(
&mut tv,
"errorBkgColor",
Value::String("#552222".to_string()),
);
set_if_missing(
&mut tv,
"errorTextColor",
Value::String("#552222".to_string()),
);
let primary_color = required_color(&tv, "primaryColor")?;
if get_truthy_string(&tv, "primaryTextColor").is_none() {
tv.insert(
"primaryTextColor".to_string(),
Value::String(theme_color::invert(&primary_color)?),
);
}
let secondary_color =
get_truthy_string(&tv, "secondaryColor").unwrap_or_else(|| "#cdffb2".to_string());
set_if_missing(&mut tv, "mainBkg", Value::String(primary_color.clone()));
set_if_missing(&mut tv, "secondBkg", Value::String(secondary_color.clone()));
set_if_missing(
&mut tv,
"rowOdd",
Value::String(theme_color::lighten(&primary_color, 75.0)?),
);
set_if_missing(
&mut tv,
"rowEven",
Value::String(theme_color::lighten(&primary_color, 20.0)?),
);
set_if_missing(&mut tv, "lineColor", Value::String("#000000".to_string()));
set_if_missing(&mut tv, "textColor", Value::String("#000000".to_string()));
set_if_missing(&mut tv, "nodeBkg", Value::String(primary_color.clone()));
set_if_missing(&mut tv, "nodeBorder", Value::String("#13540c".to_string()));
set_if_missing(
&mut tv,
"clusterBkg",
Value::String(secondary_color.clone()),
);
set_if_missing(
&mut tv,
"clusterBorder",
Value::String("#6eaa49".to_string()),
);
set_if_missing(
&mut tv,
"defaultLinkColor",
Value::String("#000000".to_string()),
);
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
set_if_missing(
&mut tv,
"primaryBorderColor",
Value::String(mk_border(&primary_color, dark_mode)?),
);
set_if_missing(
&mut tv,
"secondaryBorderColor",
Value::String(mk_border(&secondary_color, dark_mode)?),
);
ensure_gradient_theme_defaults(&mut tv);
let tertiary_color = if let Some(color) = get_truthy_string(&tv, "tertiaryColor") {
color
} else {
theme_color::lighten(&primary_color, 10.0)?
};
set_if_missing(
&mut tv,
"tertiaryColor",
Value::String(tertiary_color.clone()),
);
set_if_missing(
&mut tv,
"tertiaryBorderColor",
Value::String(mk_border(&tertiary_color, dark_mode)?),
);
set_if_missing(
&mut tv,
"labelTextColor",
Value::String("black".to_string()),
);
set_if_missing(
&mut tv,
"scaleLabelColor",
Value::String("black".to_string()),
);
let scale_label_color =
get_truthy_string(&tv, "scaleLabelColor").unwrap_or_else(|| "black".to_string());
let c_scale_bases = [
primary_color.clone(),
secondary_color.clone(),
tertiary_color.clone(),
theme_color::adjust(&primary_color, ColorAdjustment::hsl(30.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(90.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(150.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(210.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(270.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(300.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(330.0, 0.0, 0.0))?,
];
let mut c_scales = Vec::with_capacity(c_scale_bases.len());
for base in c_scale_bases {
c_scales.push(theme_color::darken(&base, 10.0)?);
}
for (i, v) in c_scales.iter().enumerate() {
set_if_missing(&mut tv, &format!("cScale{i}"), Value::String(v.clone()));
}
set_if_missing(
&mut tv,
"cScalePeer1",
Value::String(theme_color::darken(&secondary_color, 45.0)?),
);
set_if_missing(
&mut tv,
"cScalePeer2",
Value::String(theme_color::darken(&tertiary_color, 40.0)?),
);
for (i, fallback_color) in c_scales.iter().enumerate() {
let c_scale_key = format!("cScale{i}");
let mut color =
get_truthy_string(&tv, &c_scale_key).unwrap_or_else(|| fallback_color.clone());
if explicit_theme_variables.contains_key(&c_scale_key) {
color = theme_color::darken(&color, 10.0)?;
}
set_if_missing(
&mut tv,
&format!("cScalePeer{i}"),
Value::String(theme_color::darken(&color, 25.0)?),
);
set_if_missing(
&mut tv,
&format!("cScaleInv{i}"),
Value::String(theme_color::adjust(
&color,
ColorAdjustment::hsl(180.0, 0.0, 0.0),
)?),
);
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(scale_label_color.clone()),
);
}
ensure_xychart_theme_defaults(
&mut tv,
"#CDE498,#FF6B6B,#A0D2DB,#D7BDE2,#F0F0F0,#FFC3A0,#7FD8BE,#FF9A8B,#FAF3E0,#FFF176",
);
apply_current_quadrant_theme_defaults(&mut tv)?;
finish_theme_defaults(config, "forest", tv)
}
fn apply_neutral_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let mut tv = theme_variables_map(config);
set_string_if_missing(&mut tv, "background", "#ffffff");
set_string_if_missing(&mut tv, "primaryColor", "#eee");
set_if_missing(&mut tv, "fontFamily", mermaid_default_font_family());
set_string_if_missing(&mut tv, "fontSize", "16px");
if get_truthy_string(&tv, "primaryTextColor").is_none()
&& let Some(primary_color) = get_truthy_string(&tv, "primaryColor")
{
tv.insert(
"primaryTextColor".to_string(),
Value::String(theme_color::invert(&primary_color)?),
);
}
let c_scales_hex: [&str; 12] = [
"#555", "#F4F4F4", "#555", "#BBB", "#777", "#999", "#DDD", "#FFF", "#DDD", "#BBB", "#999",
"#777",
];
let primary_color =
get_truthy_string(&tv, "primaryColor").unwrap_or_else(|| "#eee".to_string());
let contrast = get_truthy_string(&tv, "contrast").unwrap_or_else(|| "#707070".to_string());
let default_secondary_color = theme_color::lighten(&contrast, 55.0)?;
let default_tertiary_color =
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-160.0, 0.0, 0.0))?;
set_if_missing(
&mut tv,
"secondaryColor",
Value::String(default_secondary_color.clone()),
);
set_string_if_missing(&mut tv, "tertiaryColor", default_tertiary_color.clone());
set_if_missing(
&mut tv,
"primaryBorderColor",
Value::String(mk_border(&primary_color, false)?),
);
set_if_missing(
&mut tv,
"secondaryBorderColor",
Value::String(mk_border(&default_secondary_color, false)?),
);
set_string_if_missing(
&mut tv,
"tertiaryBorderColor",
mk_border(&default_tertiary_color, false)?,
);
set_string_if_missing(
&mut tv,
"secondaryTextColor",
theme_color::invert(&default_secondary_color)?,
);
set_string_if_missing(
&mut tv,
"tertiaryTextColor",
theme_color::invert(&default_tertiary_color)?,
);
ensure_gradient_theme_defaults(&mut tv);
let secondary_color =
get_truthy_string(&tv, "secondaryColor").unwrap_or_else(|| default_secondary_color.clone());
let tertiary_color =
get_truthy_string(&tv, "tertiaryColor").unwrap_or_else(|| default_tertiary_color.clone());
let secondary_text_color = match get_truthy_string(&tv, "secondaryTextColor") {
Some(color) => color,
None => theme_color::invert(&secondary_color)?,
};
let tertiary_text_color = match get_truthy_string(&tv, "tertiaryTextColor") {
Some(color) => color,
None => theme_color::invert(&tertiary_color)?,
};
let background = get_truthy_string(&tv, "background").unwrap_or_else(|| "#ffffff".to_string());
let primary_text_color =
get_truthy_string(&tv, "primaryTextColor").unwrap_or_else(|| "#111111".to_string());
let text_color = get_truthy_string(&tv, "textColor").unwrap_or_else(|| "#000000".to_string());
let primary_border_color = match get_truthy_string(&tv, "primaryBorderColor") {
Some(color) => color,
None => mk_border(&primary_color, false)?,
};
let contrast = get_truthy_string(&tv, "contrast").unwrap_or_else(|| "#707070".to_string());
set_string_if_missing(&mut tv, "textColor", "#000000");
set_string_if_missing(&mut tv, "mainBkg", primary_color.clone());
set_string_if_missing(&mut tv, "secondBkg", secondary_color.clone());
set_string_if_missing(&mut tv, "lineColor", "#666");
set_string_if_missing(&mut tv, "border1", "#999");
let border1 = required_color(&tv, "border1")?;
set_string_if_missing(&mut tv, "border2", contrast.clone());
set_string_if_missing(&mut tv, "note", "#ffa");
set_string_if_missing(&mut tv, "text", "#333");
set_string_if_missing(&mut tv, "critical", "#d42");
set_string_if_missing(&mut tv, "done", "#bbb");
set_string_if_missing(&mut tv, "arrowheadColor", "#333333");
set_if_missing(&mut tv, "THEME_COLOR_LIMIT", Value::Number(12.into()));
set_if_missing(&mut tv, "radius", Value::Number(5.into()));
set_if_missing(&mut tv, "strokeWidth", Value::Number(1.into()));
set_string_if_missing(&mut tv, "nodeBkg", primary_color.clone());
set_string_if_missing(&mut tv, "nodeBorder", "#999");
set_string_if_missing(&mut tv, "clusterBkg", secondary_color.clone());
set_string_if_missing(&mut tv, "clusterBorder", contrast);
set_string_if_missing(&mut tv, "defaultLinkColor", "#666");
set_string_if_missing(&mut tv, "titleColor", "#333");
set_string_if_missing(&mut tv, "edgeLabelBackground", "white");
set_string_if_missing(&mut tv, "classText", primary_text_color.clone());
let actor_border = theme_color::lighten(&border1, 23.0)?;
tv.insert(
"actorBorder".to_string(),
Value::String(actor_border.clone()),
);
set_string_if_missing(&mut tv, "actorBkg", primary_color.clone());
set_string_if_missing(&mut tv, "actorTextColor", "#333");
tv.insert(
"actorLineColor".to_string(),
Value::String(actor_border.clone()),
);
set_string_if_missing(&mut tv, "signalColor", "#333");
set_string_if_missing(&mut tv, "signalTextColor", "#333");
set_string_if_missing(&mut tv, "labelBoxBkgColor", primary_color.clone());
tv.insert(
"labelBoxBorderColor".to_string(),
Value::String(actor_border),
);
set_string_if_missing(&mut tv, "labelTextColor", "#333");
set_string_if_missing(&mut tv, "loopTextColor", "#333");
set_string_if_missing(&mut tv, "noteBorderColor", "#999");
set_string_if_missing(&mut tv, "noteBkgColor", "#666");
set_string_if_missing(&mut tv, "noteTextColor", "#fff");
set_string_if_missing(&mut tv, "activationBorderColor", "#666");
set_string_if_missing(&mut tv, "activationBkgColor", "#f4f4f4");
set_string_if_missing(&mut tv, "sequenceNumberColor", "white");
set_string_if_missing(&mut tv, "sectionBkgColor", "hsl(0, 0%, 73.9215686275%)");
set_string_if_missing(&mut tv, "altSectionBkgColor", "white");
set_string_if_missing(&mut tv, "sectionBkgColor2", "hsl(0, 0%, 73.9215686275%)");
set_string_if_missing(&mut tv, "excludeBkgColor", "#eeeeee");
set_string_if_missing(&mut tv, "taskBorderColor", "hsl(0, 0%, 34.1176470588%)");
set_string_if_missing(&mut tv, "taskBkgColor", "#707070");
set_string_if_missing(&mut tv, "taskTextLightColor", "white");
set_string_if_missing(&mut tv, "taskTextColor", "white");
set_string_if_missing(&mut tv, "taskTextDarkColor", "#333");
set_string_if_missing(&mut tv, "taskTextOutsideColor", "#333");
set_string_if_missing(&mut tv, "taskTextClickableColor", "#003163");
set_string_if_missing(
&mut tv,
"activeTaskBorderColor",
"hsl(0, 0%, 34.1176470588%)",
);
set_string_if_missing(&mut tv, "activeTaskBkgColor", primary_color.clone());
tv.insert(
"gridColor".to_string(),
Value::String(theme_color::lighten(&border1, 30.0)?),
);
set_string_if_missing(&mut tv, "doneTaskBkgColor", "#bbb");
set_string_if_missing(&mut tv, "doneTaskBorderColor", "#666");
set_string_if_missing(&mut tv, "critBkgColor", "#d42");
set_string_if_missing(
&mut tv,
"critBorderColor",
"hsl(9.4736842105, 72.1518987342%, 44.5098039216%)",
);
set_string_if_missing(&mut tv, "todayLineColor", "#d42");
set_string_if_missing(&mut tv, "vertLineColor", "#d42");
set_string_if_missing(&mut tv, "personBorder", primary_border_color.clone());
set_string_if_missing(&mut tv, "personBkg", primary_color.clone());
set_string_if_missing(&mut tv, "archEdgeColor", "#666");
set_string_if_missing(&mut tv, "archEdgeArrowColor", "#666");
set_string_if_missing(&mut tv, "archEdgeWidth", "3");
set_string_if_missing(
&mut tv,
"archGroupBorderColor",
primary_border_color.clone(),
);
set_string_if_missing(&mut tv, "archGroupBorderWidth", "2px");
set_string_if_missing(&mut tv, "rowOdd", "hsl(0, 0%, 100%)");
set_string_if_missing(&mut tv, "rowEven", "#f4f4f4");
set_string_if_missing(&mut tv, "transitionColor", "#000");
set_string_if_missing(&mut tv, "transitionLabelColor", text_color.clone());
set_string_if_missing(&mut tv, "stateLabelColor", primary_text_color.clone());
set_string_if_missing(&mut tv, "stateBkg", primary_color.clone());
set_string_if_missing(&mut tv, "labelBackgroundColor", primary_color.clone());
set_string_if_missing(&mut tv, "compositeBackground", background);
set_string_if_missing(&mut tv, "altBackground", "#f4f4f4");
set_string_if_missing(&mut tv, "compositeTitleBackground", primary_color.clone());
set_string_if_missing(&mut tv, "stateBorder", "#000");
set_string_if_missing(&mut tv, "innerEndBackground", primary_border_color.clone());
set_string_if_missing(&mut tv, "specialStateColor", "#222");
set_string_if_missing(&mut tv, "errorBkgColor", tertiary_color);
set_string_if_missing(&mut tv, "errorTextColor", tertiary_text_color);
set_string_if_missing(&mut tv, "attributeBackgroundColorOdd", "#ffffff");
set_string_if_missing(&mut tv, "attributeBackgroundColorEven", "#f2f2f2");
set_string_if_missing(&mut tv, "noteFontWeight", "normal");
set_string_if_missing(&mut tv, "fontWeight", "normal");
set_string_if_missing(
&mut tv,
"dropShadow",
"drop-shadow( 1px 2px 2px rgba(185,185,185,1))",
);
for (key, color) in [
("fillType0", primary_color.clone()),
("fillType1", secondary_color.clone()),
(
"fillType2",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType3",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(64.0, 0.0, 0.0))?,
),
(
"fillType4",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType5",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(-64.0, 0.0, 0.0))?,
),
(
"fillType6",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
(
"fillType7",
theme_color::adjust(&secondary_color, ColorAdjustment::hsl(128.0, 0.0, 0.0))?,
),
] {
tv.insert(key.to_string(), Value::String(color));
}
set_string_if_missing(&mut tv, "scaleLabelColor", "#333");
let scale_label_color =
get_truthy_string(&tv, "scaleLabelColor").unwrap_or_else(|| "#333".to_string());
for (i, c_hex) in c_scales_hex.iter().enumerate() {
let c_scale_key = format!("cScale{i}");
set_if_missing(&mut tv, &c_scale_key, Value::String((*c_hex).to_string()));
let c_scale = required_color(&tv, &c_scale_key)?;
set_if_missing(
&mut tv,
&format!("cScalePeer{i}"),
Value::String(theme_color::darken(&c_scale, 10.0)?),
);
set_if_missing(
&mut tv,
&format!("cScaleInv{i}"),
Value::String(theme_color::invert(&c_scale)?),
);
if i == 0 || i == 2 {
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(c_scales_hex[1].to_string()),
);
}
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(scale_label_color.clone()),
);
}
set_string_if_missing(&mut tv, "pieTitleTextColor", "#333");
set_string_if_missing(&mut tv, "pieSectionTextColor", text_color);
set_string_if_missing(&mut tv, "pieLegendTextColor", "#333");
set_string_if_missing(&mut tv, "branchLabelColor", "#333");
set_string_if_missing(&mut tv, "gitBranchLabel0", "#333");
set_string_if_missing(&mut tv, "gitBranchLabel1", "white");
set_string_if_missing(&mut tv, "gitBranchLabel2", "#333");
set_string_if_missing(&mut tv, "gitBranchLabel3", "white");
for i in 4..8 {
set_string_if_missing(&mut tv, &format!("gitBranchLabel{i}"), "#333");
}
set_string_if_missing(&mut tv, "tagLabelColor", primary_text_color);
set_string_if_missing(&mut tv, "tagLabelBackground", primary_color);
set_string_if_missing(&mut tv, "tagLabelBorder", primary_border_color);
set_string_if_missing(&mut tv, "tagLabelFontSize", "10px");
set_string_if_missing(&mut tv, "commitLabelColor", secondary_text_color);
set_string_if_missing(&mut tv, "commitLabelBackground", secondary_color);
set_string_if_missing(&mut tv, "commitLabelFontSize", "10px");
ensure_xychart_theme_defaults(
&mut tv,
"#EEE,#6BB8E4,#8ACB88,#C7ACD6,#E8DCC2,#FFB2A8,#FFF380,#7E8D91,#FFD8B1,#FAF3E0",
);
apply_current_quadrant_theme_defaults(&mut tv)?;
finish_theme_defaults(config, "neutral", tv)
}
fn apply_base_theme_defaults(config: &mut MermaidConfig) -> Result<(), ColorError> {
let mut tv = theme_variables_map(config);
let explicit_theme_variables = tv.clone();
let dark_mode = tv.get("darkMode").is_some_and(is_js_truthy);
let background = get_truthy_string(&tv, "background").unwrap_or_else(|| "#f4f4f4".to_string());
let primary_color =
get_truthy_string(&tv, "primaryColor").unwrap_or_else(|| "#fff4dd".to_string());
set_if_missing(&mut tv, "background", Value::String(background.clone()));
set_if_missing(
&mut tv,
"primaryColor",
Value::String(primary_color.clone()),
);
set_if_missing(
&mut tv,
"primaryTextColor",
Value::String(if dark_mode { "#eee" } else { "#333" }.to_string()),
);
set_if_missing(&mut tv, "fontFamily", mermaid_default_font_family());
set_if_missing(&mut tv, "fontSize", Value::String("16px".to_string()));
let primary_text_color = get_truthy_string(&tv, "primaryTextColor")
.unwrap_or_else(|| if dark_mode { "#eee" } else { "#333" }.to_string());
let secondary_color = if let Some(color) = get_truthy_string(&tv, "secondaryColor") {
color
} else {
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-120.0, 0.0, 0.0))?
};
set_if_missing(
&mut tv,
"secondaryColor",
Value::String(secondary_color.clone()),
);
let tertiary_color = if let Some(color) = get_truthy_string(&tv, "tertiaryColor") {
color
} else {
theme_color::adjust(&primary_color, ColorAdjustment::hsl(180.0, 0.0, 5.0))?
};
set_if_missing(
&mut tv,
"tertiaryColor",
Value::String(tertiary_color.clone()),
);
if get_truthy_string(&tv, "primaryBorderColor").is_none() {
let color = mk_border(&primary_color, dark_mode)?;
tv.insert("primaryBorderColor".to_string(), Value::String(color));
}
if get_truthy_string(&tv, "secondaryBorderColor").is_none() {
let color = mk_border(&secondary_color, dark_mode)?;
tv.insert("secondaryBorderColor".to_string(), Value::String(color));
}
if get_truthy_string(&tv, "tertiaryBorderColor").is_none() {
let color = mk_border(&tertiary_color, dark_mode)?;
tv.insert("tertiaryBorderColor".to_string(), Value::String(color));
}
if get_truthy_string(&tv, "lineColor").is_none() {
tv.insert(
"lineColor".to_string(),
Value::String(theme_color::invert(&background)?),
);
}
let line_color = get_truthy_string(&tv, "lineColor").unwrap_or_else(|| "#333333".to_string());
set_if_missing(&mut tv, "arrowheadColor", Value::String(line_color));
set_if_missing(
&mut tv,
"textColor",
Value::String(primary_text_color.clone()),
);
let primary_border_color =
get_truthy_string(&tv, "primaryBorderColor").unwrap_or_else(|| "#9370DB".to_string());
let tertiary_border_color =
get_truthy_string(&tv, "tertiaryBorderColor").unwrap_or_else(|| "#aaaa33".to_string());
ensure_gradient_theme_defaults(&mut tv);
set_if_missing(&mut tv, "nodeBkg", Value::String(primary_color.clone()));
set_if_missing(&mut tv, "mainBkg", Value::String(primary_color.clone()));
set_if_missing(&mut tv, "nodeBorder", Value::String(primary_border_color));
set_if_missing(&mut tv, "clusterBkg", Value::String(tertiary_color.clone()));
set_if_missing(
&mut tv,
"clusterBorder",
Value::String(tertiary_border_color),
);
set_if_missing(&mut tv, "nodeTextColor", Value::String(primary_text_color));
if get_truthy_string(&tv, "tertiaryTextColor").is_none() {
tv.insert(
"tertiaryTextColor".to_string(),
Value::String(theme_color::invert(&tertiary_color)?),
);
}
let tertiary_text_color =
get_truthy_string(&tv, "tertiaryTextColor").unwrap_or_else(|| "#333".to_string());
set_if_missing(
&mut tv,
"titleColor",
Value::String(tertiary_text_color.clone()),
);
if get_truthy_string(&tv, "edgeLabelBackground").is_none() {
let color = if dark_mode {
theme_color::darken(&secondary_color, 30.0)?
} else {
secondary_color.clone()
};
tv.insert("edgeLabelBackground".to_string(), Value::String(color));
}
set_if_missing(
&mut tv,
"errorBkgColor",
Value::String(tertiary_color.clone()),
);
set_if_missing(
&mut tv,
"errorTextColor",
Value::String(tertiary_text_color),
);
let darken_amount = if dark_mode { 75.0 } else { 25.0 };
for (key, base) in [
("cScale0", primary_color.clone()),
("cScale1", secondary_color.clone()),
("cScale2", tertiary_color.clone()),
(
"cScale3",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(30.0, 0.0, 0.0))?,
),
(
"cScale4",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
),
(
"cScale5",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(90.0, 0.0, 0.0))?,
),
(
"cScale6",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
),
(
"cScale7",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(150.0, 0.0, 0.0))?,
),
(
"cScale8",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(210.0, 0.0, 150.0))?,
),
(
"cScale9",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(270.0, 0.0, 0.0))?,
),
(
"cScale10",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(300.0, 0.0, 0.0))?,
),
(
"cScale11",
theme_color::adjust(&primary_color, ColorAdjustment::hsl(330.0, 0.0, 0.0))?,
),
] {
let color = theme_color::darken(&base, darken_amount)?;
set_if_missing(&mut tv, key, Value::String(color));
}
let scale_label_color = get_truthy_string(&tv, "labelTextColor")
.unwrap_or_else(|| if dark_mode { "black" } else { "#333" }.to_string());
for i in 0..12 {
let key = format!("cScale{i}");
let mut color = required_color(&tv, &key)?;
if explicit_theme_variables.contains_key(&key) {
color = theme_color::darken(&color, darken_amount)?;
}
let peer = if dark_mode {
theme_color::lighten(&color, 10.0)?
} else {
theme_color::darken(&color, 10.0)?
};
set_if_missing(&mut tv, &format!("cScalePeer{i}"), Value::String(peer));
set_if_missing(
&mut tv,
&format!("cScaleInv{i}"),
Value::String(theme_color::invert(&color)?),
);
set_if_missing(
&mut tv,
&format!("cScaleLabel{i}"),
Value::String(scale_label_color.clone()),
);
}
let mut radar = match tv.get("radar") {
Some(Value::Object(m)) => m.clone(),
_ => Map::new(),
};
let line_color = get_truthy_string(&tv, "lineColor").unwrap_or_else(|| "#333333".to_string());
set_if_missing(&mut radar, "axisColor", Value::String(line_color));
set_if_missing(&mut radar, "axisStrokeWidth", Value::Number(2.into()));
set_if_missing(&mut radar, "axisLabelFontSize", Value::Number(12.into()));
set_finite_number_if_missing(&mut radar, "curveOpacity", 0.5);
set_if_missing(&mut radar, "curveStrokeWidth", Value::Number(2.into()));
set_if_missing(
&mut radar,
"graticuleColor",
Value::String("#DEDEDE".to_string()),
);
set_if_missing(&mut radar, "graticuleStrokeWidth", Value::Number(1.into()));
set_finite_number_if_missing(&mut radar, "graticuleOpacity", 0.3);
set_if_missing(&mut radar, "legendBoxSize", Value::Number(12.into()));
set_if_missing(&mut radar, "legendFontSize", Value::Number(12.into()));
tv.insert("radar".to_string(), Value::Object(radar));
ensure_xychart_theme_defaults(
&mut tv,
"#FFF4DD,#FFD8B1,#FFA07A,#ECEFF1,#D6DBDF,#C3E0A8,#FFB6A4,#FFD74D,#738FA7,#FFFFF0",
);
apply_single_pass_git_palette(
&mut tv,
&explicit_theme_variables,
[
primary_color.clone(),
secondary_color.clone(),
tertiary_color.clone(),
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-30.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-60.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(-90.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(60.0, 0.0, 0.0))?,
theme_color::adjust(&primary_color, ColorAdjustment::hsl(120.0, 0.0, 0.0))?,
],
if dark_mode {
ColorTransform::Lighten(25.0)
} else {
ColorTransform::Darken(25.0)
},
)?;
apply_current_quadrant_theme_defaults(&mut tv)?;
finish_theme_defaults(config, "base", tv)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn supported_theme_names_match_core_expansion_surface() {
assert_eq!(
crate::supported_themes(),
&[
"default",
"base",
"dark",
"forest",
"neutral",
"neo",
"neo-dark",
"redux",
"redux-dark",
"redux-color",
"redux-dark-color"
]
);
}
#[test]
fn supported_theme_defaults_match_upstream_snapshot() {
for &theme in SUPPORTED_THEME_NAMES {
let mut cfg = MermaidConfig::from_value(json!({
"theme": theme
}));
apply_theme_defaults(&mut cfg).unwrap();
let actual = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
let expected = ThemeProgram::resolve(theme).default_snapshot();
assert_eq!(actual, expected, "theme {theme}");
}
}
#[test]
fn dark_mode_branch_matches_generated_mermaid_oracle_for_every_theme() {
for &theme in SUPPORTED_THEME_NAMES {
let mut config = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": { "darkMode": true }
}));
apply_theme_defaults(&mut config).unwrap();
let actual = config
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
let expected = ThemeProgram::resolve(theme).dark_mode_snapshot();
assert_eq!(actual, expected, "theme {theme}");
}
}
#[test]
fn generated_mermaid_oracle_locks_override_value_semantics() {
fn value_at_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
path.split('.')
.try_fold(root, |value, key| value.as_object()?.get(key))
}
let mut mismatches = Vec::new();
for case in &generated_theme_artifact().oracle_cases {
let id = case.get("id").and_then(Value::as_str).unwrap();
let theme = case.get("theme").and_then(Value::as_str).unwrap();
let overrides = case.get("overrides").cloned().unwrap();
let expected_status = case.get("status").and_then(Value::as_str).unwrap();
let mut config = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": overrides
}));
let result = apply_theme_defaults(&mut config);
if expected_status == "error" {
if result.is_ok() {
mismatches.push(format!("{theme}/{id}: expected an error"));
}
continue;
}
if let Err(error) = result {
mismatches.push(format!("{theme}/{id}: unexpected error: {error}"));
continue;
}
let actual = config.as_value().get("themeVariables").unwrap();
let selected = case.get("selected").and_then(Value::as_object).unwrap();
for (path, expected) in selected {
let state = expected.get("state").and_then(Value::as_str).unwrap();
let actual_value = value_at_path(actual, path);
match state {
"missing" if actual_value.is_some() => mismatches.push(format!(
"{theme}/{id}/{path}: expected missing, found {}",
actual_value.unwrap()
)),
"value" if actual_value != expected.get("value") => mismatches.push(format!(
"{theme}/{id}/{path}: expected {}, found {}",
expected.get("value").unwrap(),
actual_value
.map(Value::to_string)
.unwrap_or_else(|| "missing".to_string())
)),
"missing" | "value" => {}
other => mismatches
.push(format!("{theme}/{id}/{path}: unknown oracle state {other}")),
}
}
}
assert!(
mismatches.is_empty(),
"Mermaid theme oracle mismatches:\n{}",
mismatches.join("\n")
);
}
#[test]
fn font_only_override_preserves_upstream_derived_palette_for_public_themes() {
for &theme in SUPPORTED_THEME_NAMES {
let mut cfg = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": {
"fontFamily": "Inter, sans-serif"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let actual = cfg
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
let expected = ThemeProgram::resolve(theme).default_snapshot();
for key in [
"cScale0",
"cScale1",
"cScalePeer0",
"cScaleInv0",
"cScaleLabel0",
] {
assert_eq!(
actual.get(key),
expected.get(key),
"theme {theme} has derived palette drift at {key}"
);
}
for (key, expected_value) in expected {
if key == "fontFamily" {
continue;
}
assert_eq!(
actual.get(key),
Some(expected_value),
"theme {theme} has a non-font snapshot drift at {key}"
);
}
assert_eq!(
actual.get("fontFamily").and_then(Value::as_str),
Some("Inter, sans-serif"),
"theme {theme} should replay the explicit font override"
);
}
}
#[test]
fn explicit_scale_override_recomputes_peer_and_inverse_from_override_stage() {
let cases = [
(
"default",
"hsl(240, 100%, 61.2745098039%)",
"hsl(60, 100%, 86.2745098039%)",
),
("dark", "hsl(210, 68%, 90.3921568627%)", "#543210"),
(
"forest",
"hsl(210, 68%, 45.3921568627%)",
"hsl(30, 68%, 70.3921568627%)",
),
("neutral", "hsl(210, 68%, 70.3921568627%)", "#543210"),
(
"base",
"hsl(210, 68%, 45.3921568627%)",
"rgb(191.1000000002, 113.7500000001, 36.4)",
),
];
for (theme, expected_peer, expected_inverse) in cases {
let mut cfg = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": {
"primaryColor": "#123456",
"cScale0": "#abcdef"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let actual = cfg
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
assert_eq!(
actual.get("cScale0").and_then(Value::as_str),
Some("#abcdef"),
"theme {theme} must replay explicit cScale0"
);
assert_eq!(
actual.get("cScalePeer0").and_then(Value::as_str),
Some(expected_peer),
"theme {theme} must derive cScalePeer0 from the override stage"
);
assert_eq!(
actual.get("cScaleInv0").and_then(Value::as_str),
Some(expected_inverse),
"theme {theme} must derive cScaleInv0 from the override stage"
);
}
}
#[test]
fn extended_theme_scale_override_replays_after_source_ordered_derivations() {
let cases = [
(
"neo",
"hsl(210, 68%, 45.3921568627%)",
"rgb(191.1000000002, 113.7500000001, 36.4)",
"#333",
),
(
"neo-dark",
"hsl(210, 68%, 45.3921568627%)",
"rgb(191.1000000002, 113.7500000001, 36.4)",
"#e0dfdf",
),
(
"redux",
"hsl(0, 0%, 65%)",
"rgb(63.75, 63.75, 63.75)",
"#28253D",
),
(
"redux-dark",
"hsl(210, 68%, 45.3921568627%)",
"rgb(191.1000000002, 113.7500000001, 36.4)",
"#e0dfdf",
),
(
"redux-color",
"hsl(210, 68%, 70.3921568627%)",
"#543210",
"#28253D",
),
(
"redux-dark-color",
"hsl(210, 68%, 70.3921568627%)",
"#543210",
"hsl(210, 68%, 5.3921568627%)",
),
];
for (theme, expected_peer, expected_inverse, expected_label) in cases {
let mut config = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": { "cScale0": "#abcdef" }
}));
apply_theme_defaults(&mut config).unwrap();
let variables = config
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
for (key, expected) in [
("cScale0", "#abcdef"),
("cScalePeer0", expected_peer),
("cScaleInv0", expected_inverse),
("cScaleLabel0", expected_label),
] {
assert_eq!(
variables.get(key).and_then(Value::as_str),
Some(expected),
"theme {theme} has incorrect {key}"
);
}
}
}
#[test]
fn primary_color_override_follows_each_upstream_theme_scale_contract() {
let cases = [
(
"default",
"hsl(240, 100%, 76.2745098039%)",
"hsl(240, 100%, 61.2745098039%)",
"hsl(60, 100%, 86.2745098039%)",
),
(
"dark",
"#123456",
"hsl(210, 65.3846153846%, 30.3921568627%)",
"#edcba9",
),
(
"forest",
"hsl(210, 65.3846153846%, 10.3921568627%)",
"hsl(210, 65.3846153846%, 0%)",
"hsl(30, 65.3846153846%, 10.3921568627%)",
),
("neutral", "#555", "hsl(0, 0%, 23.3333333333%)", "#aaaaaa"),
(
"base",
"hsl(210, 65.3846153846%, 0%)",
"hsl(210, 65.3846153846%, 0%)",
"#ffffff",
),
];
for (theme, expected_scale, expected_peer, expected_inverse) in cases {
let mut cfg = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": {
"primaryColor": "#123456"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let actual = cfg
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
for (key, expected) in [
("cScale0", expected_scale),
("cScalePeer0", expected_peer),
("cScaleInv0", expected_inverse),
] {
assert_eq!(
actual.get(key).and_then(Value::as_str),
Some(expected),
"theme {theme} has incorrect {key} after primaryColor override"
);
}
}
}
#[test]
fn quadrant_primary_override_matches_mermaid_11_16_theme_lifecycles() {
let cases = [
("default", "#ECECFF", "#f1f1ff", "hsl(240, 100%, NaN%)"),
(
"dark",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
(
"forest",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
(
"neutral",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
(
"base",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
("neo", "#ECECFE", "#f1f1ff", "hsl(240, 90%, NaN%)"),
(
"neo-dark",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
("redux", "#ECECFE", "#f1f1ff", "hsl(240, 90%, NaN%)"),
(
"redux-dark",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
("redux-color", "#ECECFE", "#f1f1ff", "hsl(240, 90%, NaN%)"),
(
"redux-dark-color",
"#123456",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
),
];
for (theme, expected_q1, expected_q2, expected_point) in cases {
let mut config = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": { "primaryColor": "#123456" }
}));
apply_theme_defaults(&mut config).unwrap();
let variables = config
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
for (key, expected) in [
("quadrant1Fill", expected_q1),
("quadrant2Fill", expected_q2),
("quadrantPointFill", expected_point),
] {
assert_eq!(
variables.get(key).and_then(Value::as_str),
Some(expected),
"theme {theme} has incorrect {key}"
);
}
}
}
#[test]
fn quadrant_partial_and_text_overrides_match_mermaid_11_16_replay_order() {
let mut config = MermaidConfig::from_value(json!({
"theme": "base",
"themeVariables": {
"primaryTextColor": "#123456",
"quadrant1Fill": "rgba(18, 52, 86, 0.5)",
"quadrant2Fill": "#abcdef"
}
}));
apply_theme_defaults(&mut config).unwrap();
let variables = config
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
for (key, expected) in [
("quadrant1Fill", "rgba(18, 52, 86, 0.5)"),
("quadrant2Fill", "#abcdef"),
("quadrant3Fill", "#fffee7"),
("quadrant1TextFill", "#123456"),
("quadrant2TextFill", "#0d2f51"),
("quadrantPointFill", "hsla(210, 65.3846153846%, NaN%, 0.5)"),
] {
assert_eq!(variables.get(key).and_then(Value::as_str), Some(expected));
}
let mut default_config = MermaidConfig::from_value(json!({
"theme": "default",
"themeVariables": { "primaryTextColor": "#123456" }
}));
apply_theme_defaults(&mut default_config).unwrap();
assert_eq!(
default_config.get_str("themeVariables.quadrant1TextFill"),
Some("#131300")
);
}
#[test]
fn quadrant_accepts_khroma_named_rgb_and_alpha_colors() {
let cases = [
(
"rebeccapurple",
"#6b389e",
"hsl(270, 50%, NaN%)",
"hsl(270, 10%, 30%)",
),
(
"rgb(18, 52, 86)",
"#17395b",
"hsl(210, 65.3846153846%, NaN%)",
"hsl(210, 25.3846153846%, 10.3921568627%)",
),
(
"rgba(18, 52, 86, 0.5)",
"rgba(23, 57, 91, 0.5)",
"hsla(210, 65.3846153846%, NaN%, 0.5)",
"hsla(210, 25.3846153846%, 10.3921568627%, 0.5)",
),
];
for (primary, expected_q2, expected_point, expected_border) in cases {
let mut config = MermaidConfig::from_value(json!({
"theme": "base",
"themeVariables": { "primaryColor": primary }
}));
apply_theme_defaults(&mut config).unwrap();
let variables = config
.as_value()
.get("themeVariables")
.and_then(Value::as_object)
.unwrap();
for (key, expected) in [
("quadrant2Fill", expected_q2),
("quadrantPointFill", expected_point),
("quadrantInternalBorderStrokeFill", expected_border),
] {
assert_eq!(
variables.get(key).and_then(Value::as_str),
Some(expected),
"primaryColor {primary} has incorrect {key}"
);
}
}
}
#[test]
fn invalid_color_timing_matches_mermaid_11_16_initialize_matrix() {
let keys = [
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"primaryBorderColor",
"secondaryBorderColor",
"tertiaryBorderColor",
"border1",
"cScale0",
"cScale1",
"git0",
"git1",
"gitInv0",
"pie1",
"pie3",
"quadrant1Fill",
];
let cases: [(&str, &[&str]); 11] = [
(
"default",
&[
"primaryColor",
"secondaryColor",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"dark",
&[
"primaryColor",
"secondaryColor",
"background",
"cScale0",
"cScale1",
"quadrant1Fill",
],
),
(
"forest",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"neutral",
&[
"primaryColor",
"secondaryColor",
"border1",
"cScale0",
"cScale1",
"quadrant1Fill",
],
),
(
"base",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"neo",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"neo-dark",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"redux",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"redux-dark",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"redux-color",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
(
"redux-dark-color",
&[
"primaryColor",
"secondaryColor",
"tertiaryColor",
"background",
"cScale0",
"cScale1",
"git0",
"git1",
"quadrant1Fill",
],
),
];
let mut mismatches = Vec::new();
for (theme, error_keys) in cases {
for key in keys {
let mut config = MermaidConfig::from_value(json!({
"theme": theme,
"themeVariables": { (key): "not-a-color" }
}));
let actual_error = apply_theme_defaults(&mut config).is_err();
let expected_error = error_keys.contains(&key);
if actual_error != expected_error {
mismatches.push(format!(
"{theme}.{key}: expected error={expected_error}, actual={actual_error}"
));
}
}
}
assert!(
mismatches.is_empty(),
"invalid-color timing mismatches:\n{}",
mismatches.join("\n")
);
}
#[test]
fn invalid_theme_colors_fail_at_direct_and_site_config_operation_boundaries() {
let mut config = MermaidConfig::from_value(json!({
"theme": "base",
"themeVariables": { "primaryColor": "not-a-color" }
}));
assert!(matches!(
apply_theme_defaults(&mut config),
Err(ColorError::UnsupportedFormat { .. })
));
let engine = crate::Engine::new().with_site_config(MermaidConfig::from_value(json!({
"theme": "base",
"themeVariables": { "primaryColor": "not-a-color" }
})));
assert!(matches!(
engine.parse_metadata_sync("flowchart TD\n A"),
Err(crate::Error::ThemeColor(
ColorError::UnsupportedFormat { .. }
))
));
}
#[test]
fn invalid_frontmatter_theme_color_fails_at_parse_operation_boundary() {
let engine = crate::Engine::new().with_site_config(MermaidConfig::from_value(json!({
"secure": [
"secure",
"securityLevel",
"startOnLoad",
"maxTextSize",
"suppressErrorRendering",
"maxEdges"
]
})));
let source = r#"---
config:
theme: base
themeVariables:
primaryColor: not-a-color
---
flowchart TD
A
"#;
assert!(matches!(
engine.parse_metadata_sync(source),
Err(crate::Error::ThemeColor(
ColorError::UnsupportedFormat { .. }
))
));
}
#[test]
fn theme_resolution_records_stage_and_final_value_provenance() {
let explicit = json!({
"fontFamily": "Inter, sans-serif",
"cScale0": "#abcdef"
})
.as_object()
.unwrap()
.clone();
let calculated = json!({
"fontFamily": "Inter, sans-serif",
"cScale0": "hsl(210, 68%, 70.3921568627%)",
"cScalePeer0": "hsl(210, 68%, 55.3921568627%)"
})
.as_object()
.unwrap()
.clone();
let resolution = ThemeResolution::new("default", explicit, calculated).unwrap();
assert_eq!(
resolution.default_snapshot.stage,
ThemeResolutionStage::DefaultSnapshot
);
assert_eq!(
resolution.overrides_applied.stage,
ThemeResolutionStage::OverridesApplied
);
assert_eq!(
resolution.calculated.stage,
ThemeResolutionStage::Calculated
);
assert_eq!(
resolution.explicit_replay.stage,
ThemeResolutionStage::ExplicitReplay
);
assert_eq!(
resolution.explicit_replay.origins.get("fontFamily"),
Some(&ThemeValueOrigin::ExplicitOverride)
);
assert_eq!(
resolution.explicit_replay.origins.get("cScale0"),
Some(&ThemeValueOrigin::ExplicitOverride)
);
assert_eq!(
resolution.explicit_replay.origins.get("cScalePeer0"),
Some(&ThemeValueOrigin::DefaultSnapshot)
);
}
#[test]
fn default_theme_populates_mermaid_theme_variables() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "default"
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("background").and_then(|v| v.as_str()), Some("white"));
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some("#ECECFF")
);
assert_eq!(
tv.get("secondaryColor").and_then(|v| v.as_str()),
Some("#ffffde")
);
assert_eq!(tv.get("pie1").and_then(|v| v.as_str()), Some("#ECECFF"));
assert_eq!(tv.get("pie2").and_then(|v| v.as_str()), Some("#ffffde"));
assert_eq!(tv.get("mainBkg").and_then(|v| v.as_str()), Some("#ECECFF"));
assert_eq!(
tv.get("nodeBorder").and_then(|v| v.as_str()),
Some("#9370DB")
);
assert_eq!(
tv.get("edgeLabelBackground").and_then(|v| v.as_str()),
Some("rgba(232,232,232, 0.8)")
);
assert_eq!(
tv.get("classText").and_then(|v| v.as_str()),
Some("#131300")
);
assert_eq!(
tv.get("noteTextColor").and_then(|v| v.as_str()),
Some("black")
);
assert_eq!(tv.get("useGradient").and_then(|v| v.as_bool()), Some(false));
assert_eq!(
tv.get("gradientStart").and_then(|v| v.as_str()),
Some("hsl(240, 60%, 86.2745098039%)")
);
let xy = tv.get("xyChart").and_then(|v| v.as_object()).unwrap();
assert_eq!(
xy.get("backgroundColor").and_then(|v| v.as_str()),
Some("white")
);
assert_eq!(
xy.get("dataLabelColor").and_then(|v| v.as_str()),
Some("#131300")
);
}
#[test]
fn default_theme_preserves_user_overrides_after_derivation() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "default",
"themeVariables": {
"primaryColor": "#111111",
"mainBkg": "#101010",
"classText": "#abcdef",
"xyChart": {
"titleColor": "red"
}
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some("#111111")
);
assert_eq!(tv.get("pie1").and_then(|v| v.as_str()), Some("#ECECFF"));
assert_eq!(tv.get("pie2").and_then(|v| v.as_str()), Some("#ffffde"));
assert_eq!(tv.get("mainBkg").and_then(|v| v.as_str()), Some("#101010"));
assert_eq!(tv.get("nodeBkg").and_then(|v| v.as_str()), Some("#101010"));
assert_eq!(
tv.get("classText").and_then(|v| v.as_str()),
Some("#abcdef")
);
assert_eq!(
tv.get("primaryTextColor").and_then(|v| v.as_str()),
Some("#131300")
);
let xy = tv.get("xyChart").and_then(|v| v.as_object()).unwrap();
assert_eq!(xy.get("titleColor").and_then(|v| v.as_str()), Some("red"));
assert_eq!(xy.get("dataLabelColor"), None);
}
#[test]
fn default_theme_merges_unrelated_theme_variable_overrides_without_hsl_rewriting_pie_base() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "default",
"themeVariables": {
"pieOuterStrokeWidth": "5px"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("pie1").and_then(|v| v.as_str()), Some("#ECECFF"));
assert_eq!(tv.get("pie2").and_then(|v| v.as_str()), Some("#ffffde"));
assert_eq!(
tv.get("pieOuterStrokeWidth").and_then(|v| v.as_str()),
Some("5px")
);
}
#[test]
fn unknown_theme_falls_back_to_default_theme_variables() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "unknown"
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some("#ECECFF")
);
assert_eq!(
tv.get("classText").and_then(|v| v.as_str()),
Some("#131300")
);
}
#[test]
fn mermaid_11_16_extended_theme_names_use_their_snapshots() {
let cases = [
("neo", "#cccccc", "#000000"),
("neo-dark", "#1f2020", "#ccc"),
("redux", "#cccccc", "#28253D"),
("redux-dark", "#1f2020", "#FFFFFF"),
("redux-color", "#cccccc", "#28253D"),
("redux-dark-color", "#1f2020", "#FFFFFF"),
];
for (theme, primary, node_border) in cases {
let mut cfg = MermaidConfig::from_value(json!({
"theme": theme
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some(primary),
"theme {theme}"
);
assert_eq!(
tv.get("nodeBorder").and_then(|v| v.as_str()),
Some(node_border),
"theme {theme}"
);
}
}
#[test]
fn extended_theme_preserves_explicit_theme_variable_overrides() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux",
"themeVariables": {
"primaryColor": "#123456",
"nodeBorder": "#abcdef"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some("#123456")
);
assert_eq!(
tv.get("nodeBorder").and_then(|v| v.as_str()),
Some("#abcdef")
);
assert_eq!(
tv.get("fontFamily").and_then(|v| v.as_str()),
Some("\"Recursive Variable\", arial, sans-serif")
);
}
#[test]
fn extended_theme_recomputes_visible_derivations_from_base_overrides() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux",
"themeVariables": {
"primaryColor": "#123456"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("primaryColor").and_then(|v| v.as_str()),
Some("#123456")
);
assert_eq!(tv.get("nodeBkg").and_then(|v| v.as_str()), Some("#123456"));
assert_eq!(
tv.get("secondaryColor").and_then(|v| v.as_str()),
Some("hsl(90, 65.3846153846%, 20.3921568627%)")
);
assert_eq!(
tv.get("edgeLabelBackground").and_then(|v| v.as_str()),
Some("hsl(90, 65.3846153846%, 20.3921568627%)")
);
assert_eq!(
tv.get("tagLabelBackground").and_then(|v| v.as_str()),
Some("#123456")
);
assert_eq!(
tv.get("fontFamily").and_then(|v| v.as_str()),
Some("\"Recursive Variable\", arial, sans-serif")
);
assert_eq!(
tv.get("git0").and_then(|v| v.as_str()),
Some("hsl(240, 90%, 71.0784313725%)")
);
}
#[test]
fn extended_theme_explicit_derived_overrides_still_win() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux",
"themeVariables": {
"primaryColor": "#123456",
"nodeBkg": "#abcdef",
"edgeLabelBackground": "#fedcba"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("nodeBkg").and_then(|v| v.as_str()), Some("#abcdef"));
assert_eq!(
tv.get("edgeLabelBackground").and_then(|v| v.as_str()),
Some("#fedcba")
);
assert_eq!(
tv.get("secondaryColor").and_then(|v| v.as_str()),
Some("hsl(90, 65.3846153846%, 20.3921568627%)")
);
}
#[test]
fn extended_theme_recomputes_background_and_main_background_derivations() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux",
"themeVariables": {
"background": "#010203",
"mainBkg": "#101112"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
for key in [
"lineColor",
"arrowheadColor",
"defaultLinkColor",
"archEdgeColor",
"archEdgeArrowColor",
"relationColor",
"transitionColor",
"specialStateColor",
] {
assert_eq!(
tv.get(key).and_then(|v| v.as_str()),
Some("#fefdfc"),
"key {key}"
);
}
for key in [
"actorBkg",
"labelBoxBkgColor",
"personBkg",
"stateBkg",
"labelBackgroundColor",
] {
assert_eq!(
tv.get(key).and_then(|v| v.as_str()),
Some("#101112"),
"key {key}"
);
}
assert_eq!(tv.get("nodeBkg").and_then(|v| v.as_str()), Some("#cccccc"));
}
#[test]
fn dark_extended_theme_recomputes_primary_visible_derivations() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux-dark",
"themeVariables": {
"primaryColor": "#123456"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
for key in ["requirementBackground", "pie1", "quadrant1Fill"] {
assert_eq!(
tv.get(key).and_then(|v| v.as_str()),
Some("#123456"),
"key {key}"
);
}
assert_eq!(
tv.get("git0").and_then(|v| v.as_str()),
Some("hsl(210, 65.3846153846%, 0%)")
);
assert_eq!(
tv.get("git1").and_then(|v| v.as_str()),
Some("hsl(180, 1.5873015873%, 3.3529411765%)")
);
assert_eq!(
tv.get("git3").and_then(|v| v.as_str()),
Some("hsl(180, 65.3846153846%, 0%)")
);
assert_eq!(tv.get("gitInv0").and_then(|v| v.as_str()), Some("#ffffff"));
assert_eq!(
tv.get("gitInv1").and_then(|v| v.as_str()),
Some("rgb(246.5857142856, 246.3142857142, 246.3142857142)")
);
}
#[test]
fn extended_theme_explicit_git_color_derives_git_inverse_unless_explicit() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "redux",
"themeVariables": {
"git0": "#000000",
"git1": "#111111",
"gitInv1": "#222222"
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("git0").and_then(|v| v.as_str()), Some("#000000"));
assert_eq!(tv.get("gitInv0").and_then(|v| v.as_str()), Some("#ffffff"));
assert_eq!(tv.get("git1").and_then(|v| v.as_str()), Some("#111111"));
assert_eq!(tv.get("gitInv1").and_then(|v| v.as_str()), Some("#222222"));
}
#[test]
fn base_theme_derivation_matches_upstream_fixture_values() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "base",
"themeVariables": {
"primaryColor": "#411d4e",
"titleColor": "white",
"darkMode": true
}
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("textColor").and_then(|v| v.as_str()), Some("#eee"));
assert_eq!(
tv.get("lineColor").and_then(|v| v.as_str()),
Some("#0b0b0b")
);
assert_eq!(
tv.get("nodeBorder").and_then(|v| v.as_str()),
Some("hsl(284.0816326531, 5.7943925234%, 30.9803921569%)")
);
assert_eq!(
tv.get("secondaryBorderColor").and_then(|v| v.as_str()),
Some("hsl(164.0816326531, 5.7943925234%, 30.9803921569%)")
);
assert_eq!(tv.get("useGradient").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
tv.get("gradientStart").and_then(|v| v.as_str()),
Some("hsl(284.0816326531, 5.7943925234%, 30.9803921569%)")
);
assert_eq!(
tv.get("gradientStop").and_then(|v| v.as_str()),
Some("hsl(164.0816326531, 5.7943925234%, 30.9803921569%)")
);
assert_eq!(tv.get("mainBkg").and_then(|v| v.as_str()), Some("#411d4e"));
assert_eq!(
tv.get("clusterBkg").and_then(|v| v.as_str()),
Some("hsl(104.0816326531, 45.7943925234%, 25.9803921569%)")
);
assert_eq!(
tv.get("clusterBorder").and_then(|v| v.as_str()),
Some("hsl(104.0816326531, 5.7943925234%, 35.9803921569%)")
);
assert_eq!(
tv.get("edgeLabelBackground").and_then(|v| v.as_str()),
Some("hsl(164.0816326531, 45.7943925234%, 0%)")
);
assert_eq!(
tv.get("errorBkgColor").and_then(|v| v.as_str()),
Some("hsl(104.0816326531, 45.7943925234%, 25.9803921569%)")
);
assert_eq!(
tv.get("errorTextColor").and_then(|v| v.as_str()),
Some("rgb(202.9906542056, 158.4112149531, 219.0887850467)")
);
assert_eq!(tv.get("titleColor").and_then(|v| v.as_str()), Some("white"));
}
#[test]
fn forest_theme_derives_cscale_palette_like_upstream() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "forest"
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(
tv.get("cScale0").and_then(|v| v.as_str()),
Some("hsl(78.1578947368, 58.4615384615%, 64.5098039216%)")
);
assert_eq!(
tv.get("cScalePeer0").and_then(|v| v.as_str()),
Some("hsl(78.1578947368, 58.4615384615%, 39.5098039216%)")
);
assert_eq!(
tv.get("cScalePeer1").and_then(|v| v.as_str()),
Some("hsl(98.961038961, 100%, 39.9019607843%)")
);
assert_eq!(
tv.get("cScalePeer2").and_then(|v| v.as_str()),
Some("hsl(78.1578947368, 58.4615384615%, 44.5098039216%)")
);
assert_eq!(tv.get("useGradient").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
tv.get("gradientStart").and_then(|v| v.as_str()),
Some("hsl(78.1578947368, 18.4615384615%, 64.5098039216%)")
);
assert_eq!(
tv.get("gradientStop").and_then(|v| v.as_str()),
Some("hsl(98.961038961, 60%, 74.9019607843%)")
);
}
#[test]
fn dark_theme_derives_peer_and_inverted_scales_like_upstream() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "dark"
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("cScale1").and_then(|v| v.as_str()), Some("#0b0000"));
assert_eq!(
tv.get("cScalePeer1").and_then(|v| v.as_str()),
Some("hsl(0, 100%, 12.1568627451%)")
);
assert_eq!(
tv.get("cScaleInv1").and_then(|v| v.as_str()),
Some("#f4ffff")
);
assert_eq!(
tv.get("cScaleLabel1").and_then(|v| v.as_str()),
Some("lightgrey")
);
assert_eq!(tv.get("useGradient").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
tv.get("gradientStart").and_then(|v| v.as_str()),
Some("#cccccc")
);
assert_eq!(tv.get("mainBkg").and_then(|v| v.as_str()), Some("#1f2020"));
assert_eq!(
tv.get("lineColor").and_then(|v| v.as_str()),
Some("lightgrey")
);
assert_eq!(
tv.get("actorTextColor").and_then(|v| v.as_str()),
Some("lightgrey")
);
assert_eq!(
tv.get("classText").and_then(|v| v.as_str()),
Some("#e0dfdf")
);
assert_eq!(
tv.get("noteTextColor").and_then(|v| v.as_str()),
Some("rgb(183.8476190475, 181.5523809523, 181.5523809523)")
);
assert_eq!(
tv.get("taskTextDarkColor").and_then(|v| v.as_str()),
Some("#2c2c2c")
);
assert_eq!(
tv.get("attributeBackgroundColorOdd")
.and_then(|v| v.as_str()),
Some("hsl(0, 0%, 32%)")
);
}
#[test]
fn neutral_theme_derives_peer_and_label_scales_like_upstream() {
let mut cfg = MermaidConfig::from_value(json!({
"theme": "neutral"
}));
apply_theme_defaults(&mut cfg).unwrap();
let tv = cfg
.as_value()
.get("themeVariables")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(tv.get("cScale0").and_then(|v| v.as_str()), Some("#555"));
assert_eq!(
tv.get("cScalePeer0").and_then(|v| v.as_str()),
Some("hsl(0, 0%, 23.3333333333%)")
);
assert_eq!(
tv.get("cScaleInv0").and_then(|v| v.as_str()),
Some("#aaaaaa")
);
assert_eq!(
tv.get("cScaleLabel0").and_then(|v| v.as_str()),
Some("#F4F4F4")
);
assert_eq!(tv.get("useGradient").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
tv.get("gradientStart").and_then(|v| v.as_str()),
Some("hsl(0, 0%, 83.3333333333%)")
);
assert_eq!(
tv.get("gradientStop").and_then(|v| v.as_str()),
Some("hsl(0, 0%, 88.9215686275%)")
);
assert_eq!(tv.get("mainBkg").and_then(|v| v.as_str()), Some("#eee"));
assert_eq!(
tv.get("textColor").and_then(|v| v.as_str()),
Some("#000000")
);
assert_eq!(
tv.get("actorTextColor").and_then(|v| v.as_str()),
Some("#333")
);
assert_eq!(
tv.get("classText").and_then(|v| v.as_str()),
Some("#111111")
);
assert_eq!(
tv.get("noteBkgColor").and_then(|v| v.as_str()),
Some("#666")
);
assert_eq!(
tv.get("noteTextColor").and_then(|v| v.as_str()),
Some("#fff")
);
assert_eq!(
tv.get("taskTextOutsideColor").and_then(|v| v.as_str()),
Some("#333")
);
}
}