use merman::{
OperationControl,
RenderOutput,
RenderRequest,
Renderer,
SvgRequest,
svg::SvgPipeline,
};
const THEME: &[(&str, &str)] = &[
("&", "fill:var(--fg);"),
(
".flowchartTitleText,.gitTitleText,.statediagramTitleText,.classTitleText",
"fill:var(--fg);",
),
(
".node rect,.node circle,.node ellipse,.node polygon,.node path",
"fill:var(--code-bg);stroke:var(--accent);",
),
(".node .katex path", "fill:var(--fg);stroke:var(--fg);"),
(
".nodeLabel,.nodeLabel p,.label:not([class*=\"branch-label\"]), \
.label:not([class*=\"branch-label\"]) text,.label span,.labelText",
"fill:var(--fg);color:var(--fg);",
),
(
".edgePaths .path,.edgePath .path,.flowchart-link,.transition,.relation",
"stroke:var(--muted);",
),
(
".edgeLabel,.edgeLabel p,.labelBkg",
"background-color:var(--bg);color:var(--fg);",
),
(".edgeLabel rect,.edgeLabel .label rect", "fill:var(--bg);"),
(
".label div .edgeLabel,.edgeLabel .label text",
"fill:var(--fg);color:var(--fg);",
),
(
".cluster rect,.statediagram-cluster rect,.statediagram-cluster.statediagram-cluster \
.inner",
"fill:var(--sidebar-bg);stroke:var(--rule);",
),
(
".cluster text,.cluster span,.cluster-label text,.cluster-label span",
"fill:var(--fg);color:var(--fg);",
),
(
".arrowheadPath,.marker,marker[id] path,marker[id] circle",
"fill:var(--muted);stroke:var(--muted);",
),
(
".composition,.dependency",
"fill:var(--muted)!important;stroke:var(--muted)!important;",
),
(".extension,.aggregation", "stroke:var(--muted)!important;"),
(
".lollipop",
"fill:var(--code-bg)!important;stroke:var(--accent)!important;",
),
(".sequenceNumber", "fill:var(--bg);stroke:none;"),
(
"rect.actor,.actor-man circle,.actor-man line,.labelBox",
"fill:var(--code-bg);stroke:var(--accent);",
),
("text.actor,text.actor>tspan", "fill:var(--fg);stroke:none;"),
(".actor-line", "stroke:var(--rule);"),
(
".messageText,.loopText,.loopText>tspan,.noteText,.noteText>tspan,.sectionTitle,.\
sectionTitle>tspan",
"fill:var(--fg);stroke:none;",
),
(".messageLine0,.messageLine1", "stroke:var(--muted);"),
(
".note,g rect.rect",
"fill:var(--sidebar-bg);stroke:var(--rule);",
),
(".loopLine", "stroke:var(--rule);"),
(
".activation0,.activation1,.activation2",
"fill:var(--sidebar-bg);stroke:var(--rule);",
),
(
"g.stateGroup rect,g.classGroup rect,.stateLabel .box,.classLabel .box,.stateGroup \
.composit,.stateGroup .alt-composit,.statediagram-state rect.divider,.end-state-inner",
"fill:var(--code-bg);stroke:var(--accent);",
),
("g.stateGroup text", "fill:var(--fg);stroke:none;"),
("g.stateGroup line", "stroke:var(--muted);"),
(
".node circle.state-start,.node .fork-join",
"fill:var(--fg);stroke:var(--fg);",
),
(
".state-note,.statediagram-note rect",
"fill:var(--sidebar-bg);stroke:var(--rule);",
),
(
".state-note text,.statediagram-note text,.statediagram-note .nodeLabel,.noteLabel \
.nodeLabel,.noteLabel .edgeLabel",
"fill:var(--fg);color:var(--fg);",
),
(".pieTitleText,.legend text", "fill:var(--fg);"),
(".pieOuterCircle", "stroke:var(--rule);"),
(".pieCircle", "stroke:var(--bg);"),
(".branch", "stroke:var(--muted);"),
(".tag-hole", "fill:var(--bg);"),
(".commit-label", "fill:var(--fg);"),
(
".commit-label-bkg,.tag-label-bkg",
"fill:var(--sidebar-bg);opacity:0.9;",
),
];
fn theme(id: &str) -> String {
let mut css = String::new();
for (selectors, declarations) in THEME {
for (nth, selector) in selectors.split(',').enumerate() {
if nth > 0 {
css.push(',');
}
css.push('#');
css.push_str(id);
match selector.trim() {
"&" => {}
selector => {
css.push(' ');
css.push_str(selector);
}
}
}
css.push('{');
css.push_str(declarations);
css.push('}');
}
css
}
pub fn svg(source: &str, id: &str) -> Option<String> {
Some(adapt(&draw(source, SvgRequest::default())?, id))
}
pub fn printable(source: &str) -> Option<String> {
draw(
source,
SvgRequest {
pipeline: Some(SvgPipeline::resvg_safe()),
..SvgRequest::default()
},
)
}
fn draw(source: &str, request: SvgRequest) -> Option<String> {
let output = Renderer::new()
.render(RenderRequest::svg(source, OperationControl::new(), request))
.ok()?;
let RenderOutput::Svg(Some(rendered)) = output else {
return None;
};
Some(rendered.svg().to_string())
}
fn adapt(svg: &str, id: &str) -> String {
let out = svg.replace("merman", id);
let out = out.replace("background-color: white", "background-color: transparent");
let theme = format!("<style>{}</style>", theme(id));
match out.find("</style>") {
Some(at) => {
let at = at + "</style>".len();
format!("{}{theme}{}", &out[..at], &out[at..])
}
None => out,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draws_a_flowchart() {
let svg = svg("flowchart LR\n A[one] --> B[two]", "d1").expect("renders");
assert!(svg.starts_with("<svg"), "{}", &svg[..60.min(svg.len())]);
assert!(svg.contains("one") && svg.contains("two"));
}
#[test]
fn gives_each_diagram_its_own_name() {
let svg = svg("flowchart LR\n A --> B", "diagram-7").expect("renders");
assert!(svg.contains("id=\"diagram-7\""));
assert!(
!svg.contains("merman"),
"every use of the generated name should be replaced"
);
assert!(svg.contains("diagram-7-drop-shadow") || svg.contains("diagram-7_flowchart"));
}
#[test]
fn follows_the_page_rather_than_mermaid_s_palette() {
let svg = svg("flowchart LR\n A[one] --> B[two]", "d1").expect("renders");
assert!(
svg.contains("var(--fg)"),
"text should take the page's colour"
);
assert!(
!svg.contains("background-color: white"),
"a light background would be wrong on a dark page"
);
}
#[test]
fn sets_a_printed_diagram_s_labels_in_svg() {
let svg = printable("flowchart LR\n A[one] --> B[two]").expect("renders");
assert!(!svg.contains("foreignObject"), "labels should be SVG text");
assert!(svg.contains("one") && svg.contains("two"));
}
#[test]
fn colours_a_sequence_diagram_s_labels_and_arrows() {
let svg = svg(
"sequenceDiagram\n A->>B: hello\n Note over A,B: a note",
"d1",
)
.expect("renders");
assert!(
svg.contains("#d1 text.actor"),
"an actor's label should be scoped by element, not by class"
);
assert!(
!svg.contains("text#d1"),
"the scope goes before the selector"
);
assert!(
svg.contains("#d1 marker[id] path"),
"arrowheads should follow the page"
);
}
#[test]
fn leaves_the_declarations_alone() {
let svg = svg("gitGraph\n commit", "d1").expect("renders");
assert!(
svg.contains("opacity:0.9"),
"a decimal point is not a class"
);
}
#[test]
fn declines_what_it_cannot_read() {
assert!(svg("this is not a diagram at all", "d1").is_none());
}
}