///|
/// The effective configuration of one diagram: mermaid's defaults, the
/// site configuration and the diagram's own frontmatter and directives,
/// merged the way `mermaid.initialize` and `mermaidAPI.render` do.
pub struct Config {
priv data : Map[String, Json]
priv variables : @theme.Theme
/// What the site configuration and the diagram's directives set, without
/// the defaults: mermaid's `getUserDefinedConfig`.
priv user : Json
}
///|
/// A configuration error.
pub(all) suberror ConfigError {
/// A theme variable is not a color mermaid can compute with.
InvalidColor(String)
} derive(Eq, Debug)
///|
/// Map a diagram type to the configuration key holding its section.
pub fn diagram_config_key(diagram_type : String) -> String {
match diagram_type {
"flowchart-v2" | "flowchart-elk" => "flowchart"
"classDiagram" => "class"
"stateDiagram" => "state"
"xychart" => "xyChart"
"railroadAbnf" | "railroadEbnf" | "railroadPeg" => "railroad"
other => other
}
}
///|
fn theme_variables(name : String, overrides : Json) -> Json? raise ConfigError {
let overrides = match overrides {
Object(fields) => Some(fields)
_ => None
}
let theme = try {
match overrides {
Some(overrides) => @theme.Theme::new(name, overrides~)
None => @theme.Theme::new(name)
}
} catch {
@color.Unsupported(text) => raise InvalidColor(text)
@color.MixedChannels => raise InvalidColor(name)
}
theme.map(theme => Json(theme))
}
///|
fn field(object : Json, key : String) -> Json {
match object {
Object(fields) => fields.get(key).unwrap_or(Json::null())
_ => Json::null()
}
}
///|
/// JavaScript truthiness.
fn truthy(value : Json) -> Bool {
match value {
Null | False => false
True | Array(_) | Object(_) => true
Number(n, ..) => n != 0.0 && n == n
String(s) => s != ""
}
}
///|
/// Whether `value` may be used for the appearance key `key`: an unknown
/// theme or look is passed over rather than applied.
fn is_usable_appearance(key : String, value : Json) -> Bool {
match (key, value) {
(_, Null) => false
("theme", String(name)) => name == "null" || @theme.is_theme_name(name)
("theme", _) => false
("look", String(name)) => ["classic", "handDrawn", "neo"].contains(name)
("look", _) => false
_ => true
}
}
///|
/// The value `layer` gives the appearance key: the diagram's own section
/// wins over the top level.
fn read_appearance(layer : Json, section : String, key : String) -> Json {
for candidate in [field(field(layer, section), key), field(layer, key)] {
if is_usable_appearance(key, candidate) {
return candidate
}
}
Json::null()
}
///|
/// Build the configuration for a diagram.
///
/// - `site` is the configuration an application would pass to
/// `mermaid.initialize`.
/// - `directive` is the diagram's own configuration: its frontmatter
/// `config` merged with its `%%{init}%%` directive.
/// - `diagram_type` scopes `theme`, `look` and `layout`, which a diagram
/// section may set for that diagram type only.
pub fn Config::new(
site~ : Json = Json::object(Map([])),
directive~ : Json = Json::object(Map([])),
diagram_type? : String,
) -> Config raise ConfigError {
let defaults = Json::object(default_data)
// The legacy place of the font family: `initialize` copies it into the
// theme variables unless they name one.
let site = if site is Object(fields) &&
truthy(field(site, "fontFamily")) &&
!truthy(field(field(site, "themeVariables"), "fontFamily")) {
let fields = fields.copy()
let variables = match fields.get("themeVariables") {
Some(Object(variables)) => variables.copy()
_ => Map([])
}
variables["fontFamily"] = field(site, "fontFamily")
fields["themeVariables"] = Json::object(variables)
Json::object(fields)
} else {
site
}
let mut site_config = assign_with_depth(clone(defaults), site)
if field(site, "theme") is String(name) &&
theme_variables(name, field(site, "themeVariables")) is Some(variables) &&
site_config is Object(fields) {
fields["themeVariables"] = variables
site_config = Json::object(fields)
}
// A directive may only touch known keys, never the secure ones.
let mut directive = sanitize_directive(clone(directive))
if directive is Object(fields) &&
truthy(field(directive, "fontFamily")) &&
!truthy(field(field(directive, "themeVariables"), "fontFamily")) {
let variables = match fields.get("themeVariables") {
Some(Object(variables)) => variables.copy()
_ => Map([])
}
variables["fontFamily"] = field(directive, "fontFamily")
fields["themeVariables"] = Json::object(variables)
}
let secure = match field(site_config, "secure") {
Array(keys) =>
keys.filter_map(key => if key is String(s) { Some(s) } else { None })
_ => []
}
directive = sanitize(directive, secure)
let sum = assign_with_depth(Json::object(Map([])), directive)
let user = assign_with_depth(
assign_with_depth(Json::object(Map([])), clone(site)),
sum,
)
guard assign_with_depth(clone(site_config), sum) is Object(config) else {
{ data: Map([]), variables: @theme.Theme::from_variables(Map([])), user, }
}
// Resolve theme, look and layout onto the top level, where renderers
// read them: most specific layer first.
if diagram_type is Some(diagram_type) {
let section = diagram_config_key(diagram_type)
for key in ["theme", "look", "layout"] {
for layer in [sum, directive, site, defaults] {
let value = read_appearance(layer, section, key)
if value is Null {
continue
}
config[key] = value
if config.get(section) is Some(Object(fields)) && fields.contains(key) {
fields[key] = value
}
break
}
}
}
let theme = config.get("theme").unwrap_or(Json::null())
let overridden = truthy(field(sum, "theme")) ||
theme != field(site_config, "theme")
if overridden && theme is String(name) {
let overrides = assign_with_depth(
match field(site, "themeVariables") {
Null => Json::object(Map([]))
variables => clone(variables)
},
field(sum, "themeVariables"),
)
if theme_variables(name, overrides) is Some(variables) {
config["themeVariables"] = variables
}
}
let variables = match config.get("themeVariables") {
Some(Object(variables)) => @theme.Theme::from_variables(variables)
_ => @theme.Theme::from_variables(Map([]))
}
{ data: config, variables, user, }
}
///|
/// The value at a dotted path such as `layout` as the site configuration
/// and the diagram's directives set it, `Json::null()` where they leave
/// it to the defaults: mermaid's `getUserDefinedConfig`.
pub fn Config::user_defined(self : Config, path : String) -> Json {
let mut value = self.user
for key in path.split(".") {
value = field(value, key.to_owned())
}
value
}
///|
/// The theme variables in effect.
pub fn Config::theme(self : Config) -> @theme.Theme {
self.variables
}
///|
/// The value at a dotted path such as `pie.textPosition`, or
/// `Json::null()` when unset.
pub fn Config::get(self : Config, path : String) -> Json {
let mut value = Json::object(self.data)
for key in path.split(".") {
value = field(value, key.to_owned())
}
value
}
///|
/// The number at `path`, if it is one.
pub fn Config::number(self : Config, path : String) -> Double? {
match self.get(path) {
Number(n, ..) => Some(n)
_ => None
}
}
///|
/// The string at `path`, if it is one.
pub fn Config::text(self : Config, path : String) -> String? {
match self.get(path) {
String(s) => Some(s)
_ => None
}
}
///|
/// Whether the value at `path` is truthy in the JavaScript sense.
pub fn Config::flag(self : Config, path : String) -> Bool {
truthy(self.get(path))
}
///|
/// mermaid's `evaluate`: everything is true except `false` and the strings
/// `false`, `null` and `0`.
pub fn evaluate(value : Json) -> Bool {
match value {
False => false
String(s) =>
!["false", "null", "0"].contains(s.trim().to_owned().to_lower())
Null => false
Number(n, ..) => n != 0.0
_ => true
}
}
///|
pub impl ToJson for Config with fn to_json(self) {
clone(Json::object(self.data))
}