use super::*;
fn gitgraph_css(diagram_id: &str, effective_config: &serde_json::Value) -> String {
let id = escape_xml(diagram_id);
let parts = info_css_parts_with_config(diagram_id, effective_config);
let mut out = parts.css_prefix;
let _ = write!(
&mut out,
r#"#{} .branch{{stroke-width:1;stroke:{};stroke-dasharray:2;}}#{} .arrow{{stroke-width:8;stroke-linecap:round;fill:none;}}#{} .commit-label{{font-size:10px;}}#{} .commit-label-bkg{{font-size:10px;opacity:0.5;}}"#,
id, parts.line_color, id, id, id
);
out.push_str(&parts.root_rule);
out
}
pub(super) fn render_gitgraph_diagram_svg(
layout: &crate::model::GitGraphDiagramLayout,
semantic: &serde_json::Value,
effective_config: &serde_json::Value,
diagram_title: Option<&str>,
measurer: &dyn TextMeasurer,
options: &SvgRenderOptions,
) -> Result<String> {
const THEME_COLOR_LIMIT: i64 = 8;
const PX: f64 = 4.0;
const PY: f64 = 2.0;
const VIEWBOX_PLACEHOLDER: &str = "__MERMAID_VIEWBOX__";
const MAX_WIDTH_PLACEHOLDER: &str = "__MERMAID_MAX_WIDTH__";
const TITLE_X_PLACEHOLDER: &str = "__MERMAID_GITGRAPH_TITLE_X__";
const VIEWBOX_PADDING_PX: f64 = 8.0;
fn gitgraph_simple_text_bbox_width_px(
measurer: &dyn TextMeasurer,
text: &str,
style: &crate::text::TextStyle,
apply_corrections: bool,
) -> f64 {
let base = measurer
.measure_svg_simple_text_bbox_width_px(text, style)
.max(0.0);
let extra = if apply_corrections {
gitgraph_simple_text_bbox_width_correction_px(text)
} else {
0.0
};
(base + extra).max(0.0)
}
fn gitgraph_simple_text_bbox_width_correction_px(text: &str) -> f64 {
let extra = crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_extra_px(text);
let Some(first) = text.chars().next() else {
return extra;
};
let Some(last) = text.chars().next_back() else {
return extra;
};
extra
+ gitgraph_simple_text_bbox_width_correction_left_px(first)
+ gitgraph_simple_text_bbox_width_correction_right_px(last)
}
fn gitgraph_simple_text_bbox_width_correction_left_px(ch: char) -> f64 {
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_left_px(ch)
}
fn gitgraph_simple_text_bbox_width_correction_right_px(ch: char) -> f64 {
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_right_px(ch)
}
let diagram_id = options.diagram_id.as_deref().unwrap_or("merman");
let bounds = layout.bounds.clone().unwrap_or(Bounds {
min_x: 0.0,
min_y: 0.0,
max_x: 100.0,
max_y: 100.0,
});
let vb_min_x = bounds.min_x;
let vb_min_y = bounds.min_y;
let vb_w = (bounds.max_x - bounds.min_x).max(1.0);
let vb_h = (bounds.max_y - bounds.min_y).max(1.0);
let acc_title = semantic
.get("accTitle")
.and_then(|v| v.as_str())
.map(|s| s.trim())
.filter(|s| !s.is_empty());
let acc_descr = semantic
.get("accDescr")
.and_then(|v| v.as_str())
.map(|s| s.trim_end_matches('\n'))
.filter(|s| !s.is_empty());
let aria_title_id = format!("chart-title-{diagram_id}");
let aria_desc_id = format!("chart-desc-{diagram_id}");
let mut out = String::new();
let aria_describedby = acc_descr.is_some().then(|| escape_attr(&aria_desc_id));
let aria_labelledby = acc_title.is_some().then(|| escape_attr(&aria_title_id));
let style_attr = format!("max-width: {MAX_WIDTH_PLACEHOLDER}px; background-color: white;");
root_svg::push_svg_root_open_ex(
&mut out,
diagram_id,
None,
root_svg::SvgRootWidth::Percent100,
None,
Some(style_attr.as_str()),
Some(VIEWBOX_PLACEHOLDER),
root_svg::SvgRootStyleViewBoxOrder::StyleThenViewBox,
&[],
"gitGraph",
aria_labelledby.as_deref(),
aria_describedby.as_deref(),
false,
);
if let Some(t) = acc_title {
let _ = write!(
&mut out,
r#"<title id="{}">{}</title>"#,
escape_attr(&aria_title_id),
escape_xml(t)
);
}
if let Some(d) = acc_descr {
let _ = write!(
&mut out,
r#"<desc id="{}">{}</desc>"#,
escape_attr(&aria_desc_id),
escape_xml(d)
);
}
let css = gitgraph_css(diagram_id, effective_config);
let _ = write!(&mut out, r#"<style>{}</style>"#, css);
out.push_str(r#"<g/>"#);
out.push_str(r#"<g class="commit-bullets"/>"#);
out.push_str(r#"<g class="commit-labels"/>"#);
let mut branch_idx: std::collections::HashMap<&str, i64> = std::collections::HashMap::new();
for b in &layout.branches {
branch_idx.insert(b.name.as_str(), b.index);
}
let direction = layout.direction.as_str();
if layout.show_branches {
out.push_str("<g>");
for b in &layout.branches {
let idx = b.index % THEME_COLOR_LIMIT;
let pos = b.pos;
if direction == "TB" {
let _ = write!(
&mut out,
r#"<line x1="{x1}" y1="30" x2="{x2}" y2="{y2}" class="branch branch{idx}"/>"#,
x1 = fmt(pos),
x2 = fmt(pos),
y2 = fmt(layout.max_pos),
idx = idx
);
} else if direction == "BT" {
let _ = write!(
&mut out,
r#"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="30" class="branch branch{idx}"/>"#,
x1 = fmt(pos),
y1 = fmt(layout.max_pos),
x2 = fmt(pos),
idx = idx
);
} else {
let _ = write!(
&mut out,
r#"<line x1="0" y1="{y1}" x2="{x2}" y2="{y2}" class="branch branch{idx}"/>"#,
y1 = fmt(pos),
x2 = fmt(layout.max_pos),
y2 = fmt(pos),
idx = idx
);
}
let name = escape_xml(&b.name);
let bbox_w = b.bbox_width.max(0.0);
let bbox_h = b.bbox_height.max(0.0);
let bkg_class = format!(r#"branchLabelBkg label{idx}"#);
let label_class = format!(r#"label branch-label{idx}"#);
if direction == "TB" {
let x = pos - bbox_w / 2.0 - 10.0;
let _ = write!(
&mut out,
r#"<rect class="{cls}" rx="4" ry="4" x="{x}" y="0" width="{w}" height="{h}"/>"#,
cls = bkg_class,
x = fmt(x),
w = fmt(bbox_w + 18.0),
h = fmt(bbox_h + 4.0),
);
let tx = pos - bbox_w / 2.0 - 5.0;
let _ = write!(
&mut out,
r#"<g class="branchLabel"><g class="{cls}" transform="translate({x}, 0)"><text><tspan xml:space="preserve" dy="1em" x="0" class="row">{name}</tspan></text></g></g>"#,
cls = label_class,
x = fmt(tx),
name = name
);
} else if direction == "BT" {
let x = pos - bbox_w / 2.0 - 10.0;
let _ = write!(
&mut out,
r#"<rect class="{cls}" rx="4" ry="4" x="{x}" y="{y}" width="{w}" height="{h}"/>"#,
cls = bkg_class,
x = fmt(x),
y = fmt(layout.max_pos),
w = fmt(bbox_w + 18.0),
h = fmt(bbox_h + 4.0),
);
let tx = pos - bbox_w / 2.0 - 5.0;
let _ = write!(
&mut out,
r#"<g class="branchLabel"><g class="{cls}" transform="translate({x}, {y})"><text><tspan xml:space="preserve" dy="1em" x="0" class="row">{name}</tspan></text></g></g>"#,
cls = label_class,
x = fmt(tx),
y = fmt(layout.max_pos),
name = name
);
} else {
let rotate_pad = if layout.rotate_commit_label {
30.0
} else {
0.0
};
let x = -bbox_w - 4.0 - rotate_pad;
let y = -bbox_h / 2.0 + 8.0;
let _ = write!(
&mut out,
r#"<rect class="{cls}" rx="4" ry="4" x="{x}" y="{y}" width="{w}" height="{h}" transform="translate(-19, {ty})"/>"#,
cls = bkg_class,
x = fmt(x),
y = fmt(y),
w = fmt(bbox_w + 18.0),
h = fmt(bbox_h + 4.0),
ty = fmt(pos - bbox_h / 2.0),
);
let tx = -bbox_w - 14.0 - rotate_pad;
let _ = write!(
&mut out,
r#"<g class="branchLabel"><g class="{cls}" transform="translate({x}, {y})"><text><tspan xml:space="preserve" dy="1em" x="0" class="row">{name}</tspan></text></g></g>"#,
cls = label_class,
x = fmt(tx),
y = fmt(pos - bbox_h / 2.0 - 1.0),
name = name
);
}
}
out.push_str("</g>");
}
out.push_str(r#"<g class="commit-arrows">"#);
for a in &layout.arrows {
let _ = write!(
&mut out,
r#"<path d="{d}" class="arrow arrow{idx}"/>"#,
d = escape_attr(&a.d),
idx = a.class_index % THEME_COLOR_LIMIT
);
}
out.push_str("</g>");
fn commit_class_type(symbol_type: i64) -> &'static str {
match symbol_type {
0 => "commit-normal",
1 => "commit-reverse",
2 => "commit-highlight",
3 => "commit-merge",
4 => "commit-cherry-pick",
_ => "commit-normal",
}
}
fn commit_symbol_type(commit: &crate::model::GitGraphCommitLayout) -> i64 {
commit.custom_type.unwrap_or(commit.commit_type)
}
out.push_str(r#"<g class="commit-bullets">"#);
for c in &layout.commits {
let branch_i = branch_idx.get(c.branch.as_str()).copied().unwrap_or(0);
let symbol_type = commit_symbol_type(c);
let type_class = commit_class_type(symbol_type);
let idx = branch_i % THEME_COLOR_LIMIT;
let id = escape_attr(&c.id);
if symbol_type == 2 {
let _ = write!(
&mut out,
r#"<rect x="{x}" y="{y}" width="20" height="20" class="commit {id} commit-highlight{idx} {type_class}-outer"/>"#,
x = fmt(c.x - 10.0),
y = fmt(c.y - 10.0),
id = id,
idx = idx,
type_class = type_class
);
let _ = write!(
&mut out,
r#"<rect x="{x}" y="{y}" width="12" height="12" class="commit {id} commit{idx} {type_class}-inner"/>"#,
x = fmt(c.x - 6.0),
y = fmt(c.y - 6.0),
id = id,
idx = idx,
type_class = type_class
);
} else if symbol_type == 4 {
let _ = write!(
&mut out,
r#"<circle cx="{x}" cy="{y}" r="10" class="commit {id} {type_class}"/>"#,
x = fmt(c.x),
y = fmt(c.y),
id = id,
type_class = type_class
);
let _ = write!(
&mut out,
r##"<circle cx="{x}" cy="{y}" r="2.75" fill="#fff" class="commit {id} {type_class}"/>"##,
x = fmt(c.x - 3.0),
y = fmt(c.y + 2.0),
id = id,
type_class = type_class
);
let _ = write!(
&mut out,
r##"<circle cx="{x}" cy="{y}" r="2.75" fill="#fff" class="commit {id} {type_class}"/>"##,
x = fmt(c.x + 3.0),
y = fmt(c.y + 2.0),
id = id,
type_class = type_class
);
let _ = write!(
&mut out,
r##"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="#fff" class="commit {id} {type_class}"/>"##,
x1 = fmt(c.x + 3.0),
y1 = fmt(c.y + 1.0),
x2 = fmt(c.x),
y2 = fmt(c.y - 5.0),
id = id,
type_class = type_class
);
let _ = write!(
&mut out,
r##"<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="#fff" class="commit {id} {type_class}"/>"##,
x1 = fmt(c.x - 3.0),
y1 = fmt(c.y + 1.0),
x2 = fmt(c.x),
y2 = fmt(c.y - 5.0),
id = id,
type_class = type_class
);
} else {
let r = if c.commit_type == 3 { 9.0 } else { 10.0 };
let _ = write!(
&mut out,
r#"<circle cx="{x}" cy="{y}" r="{r}" class="commit {id} commit{idx}"/>"#,
x = fmt(c.x),
y = fmt(c.y),
r = fmt(r),
id = id,
idx = idx
);
if symbol_type == 3 {
let _ = write!(
&mut out,
r#"<circle cx="{x}" cy="{y}" r="6" class="commit {type_class} {id} commit{idx}"/>"#,
x = fmt(c.x),
y = fmt(c.y),
type_class = type_class,
id = id,
idx = idx
);
}
if symbol_type == 1 {
let d = format!(
"M {},{}L{},{}M {},{}L{},{}",
fmt(c.x - 5.0),
fmt(c.y - 5.0),
fmt(c.x + 5.0),
fmt(c.y + 5.0),
fmt(c.x - 5.0),
fmt(c.y + 5.0),
fmt(c.x + 5.0),
fmt(c.y - 5.0)
);
let _ = write!(
&mut out,
r#"<path d="{d}" class="commit {type_class} {id} commit{idx}"/>"#,
d = escape_attr(&d),
type_class = type_class,
id = id,
idx = idx
);
}
}
}
out.push_str("</g>");
let commit_font_family = config_string(effective_config, &["fontFamily"])
.or_else(|| config_string(effective_config, &["themeVariables", "fontFamily"]))
.map(|s| s.trim().trim_end_matches(';').trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "\"trebuchet ms\", verdana, arial, sans-serif".to_string());
let apply_bbox_corrections = normalize_css_font_family(&commit_font_family)
== r#""trebuchet ms",verdana,arial,sans-serif"#;
let commit_label_style = crate::text::TextStyle {
font_family: Some(commit_font_family),
font_size: 10.0,
font_weight: None,
};
out.push_str(r#"<g class="commit-labels">"#);
for c in &layout.commits {
let show = (c.commit_type != 3 || c.custom_id.unwrap_or(false))
&& c.commit_type != 4
&& layout.show_commit_label;
if show {
let bbox_w = gitgraph_simple_text_bbox_width_px(
measurer,
&c.id,
&commit_label_style,
apply_bbox_corrections,
);
let bbox_h = measurer
.measure_svg_simple_text_bbox_height_px(&c.id, &commit_label_style)
.max(0.0);
let mut wrapper_transform: Option<String> = None;
let mut rect_transform: Option<String> = None;
let mut text_transform: Option<String> = None;
let mut rect_x = c.pos_with_offset - bbox_w / 2.0 - PY;
let mut rect_y = c.y + 13.5;
let rect_w = bbox_w + 2.0 * PY;
let rect_h = bbox_h + 2.0 * PY;
let mut text_x = c.pos_with_offset - bbox_w / 2.0;
let mut text_y = c.y + 25.0;
if direction == "TB" || direction == "BT" {
rect_x = c.x - (bbox_w + 4.0 * PX + 5.0);
rect_y = c.y - 12.0;
text_x = c.x - (bbox_w + 4.0 * PX);
text_y = c.y + bbox_h - 12.0;
}
if layout.rotate_commit_label {
if direction == "TB" || direction == "BT" {
let t = format!("rotate(-45, {}, {})", fmt(c.x), fmt(c.y));
rect_transform = Some(t.clone());
text_transform = Some(t);
} else {
let r_x = -7.5 - ((bbox_w + 10.0) / 25.0) * 9.5;
let r_y = 10.0 + (bbox_w / 25.0) * 8.5;
wrapper_transform = Some(format!(
"translate({}, {}) rotate(-45, {}, {})",
fmt(r_x),
fmt(r_y),
fmt(c.pos),
fmt(c.y)
));
}
}
out.push_str("<g");
if let Some(t) = &wrapper_transform {
let _ = write!(&mut out, r#" transform="{}""#, escape_attr(t));
}
out.push('>');
out.push_str(r#"<rect class="commit-label-bkg""#);
let _ = write!(
&mut out,
r#" x="{}" y="{}" width="{}" height="{}""#,
fmt(rect_x),
fmt(rect_y),
fmt(rect_w),
fmt(rect_h)
);
if let Some(t) = &rect_transform {
let _ = write!(&mut out, r#" transform="{}""#, escape_attr(t));
}
out.push_str("/>");
out.push_str(r#"<text class="commit-label""#);
let _ = write!(
&mut out,
r#" x="{}" y="{}""#,
fmt_display(text_x),
fmt_display(text_y)
);
if let Some(t) = &text_transform {
let _ = write!(&mut out, r#" transform="{}""#, escape_attr(t));
}
let _ = write!(&mut out, ">{}</text>", escape_xml(&c.id));
out.push_str("</g>");
}
if !c.tags.is_empty() {
let mut y_offset = 0.0;
let mut max_w: f64 = 0.0;
let mut max_h: f64 = 0.0;
let mut tag_values = c.tags.clone();
tag_values.reverse();
struct TagGeom {
y_offset: f64,
}
let mut elems: Vec<TagGeom> = Vec::new();
for tag_value in &tag_values {
let bbox_w = gitgraph_simple_text_bbox_width_px(
measurer,
tag_value,
&commit_label_style,
apply_bbox_corrections,
);
let bbox_h = measurer
.measure_svg_simple_text_bbox_height_px(tag_value, &commit_label_style)
.max(0.0);
max_w = max_w.max(bbox_w.max(0.0));
max_h = max_h.max(bbox_h.max(0.0));
elems.push(TagGeom { y_offset });
y_offset += 20.0;
}
for (i, tag_value) in tag_values.iter().enumerate() {
let y_off = elems.get(i).map(|e| e.y_offset).unwrap_or(0.0);
let h2 = max_h / 2.0;
let ly = c.y - 19.2 - y_off;
if direction == "TB" || direction == "BT" {
let y_origin = c.pos + y_off;
let points = format!(
"{} {} {} {} {} {} {} {} {} {} {} {}",
fmt(c.x),
fmt(y_origin + 2.0),
fmt(c.x),
fmt(y_origin - 2.0),
fmt(c.x + 10.0),
fmt(y_origin - h2 - 2.0),
fmt(c.x + 10.0 + max_w + 4.0),
fmt(y_origin - h2 - 2.0),
fmt(c.x + 10.0 + max_w + 4.0),
fmt(y_origin + h2 + 2.0),
fmt(c.x + 10.0),
fmt(y_origin + h2 + 2.0)
);
let poly_t =
format!("translate(12,12) rotate(45, {},{})", fmt(c.x), fmt(c.pos));
let hole_t =
format!("translate(12,12) rotate(45, {},{})", fmt(c.x), fmt(c.pos));
let text_t =
format!("translate(14,14) rotate(45, {},{})", fmt(c.x), fmt(c.pos));
let _ = write!(
&mut out,
r#"<polygon class="tag-label-bkg" points="{pts}" transform="{t}"/>"#,
pts = escape_attr(&points),
t = escape_attr(&poly_t)
);
let _ = write!(
&mut out,
r#"<circle cy="{cy}" cx="{cx}" r="1.5" class="tag-hole" transform="{t}"/>"#,
cy = fmt(y_origin),
cx = fmt(c.x + PX / 2.0),
t = escape_attr(&hole_t)
);
let _ = write!(
&mut out,
r#"<text y="{y}" class="tag-label" x="{x}" transform="{t}">{txt}</text>"#,
y = fmt(y_origin + 3.0),
x = fmt(c.x + 5.0),
t = escape_attr(&text_t),
txt = escape_xml(tag_value)
);
} else {
let points = format!(
"{} {} {} {} {} {} {} {} {} {} {} {}",
fmt(c.pos - max_w / 2.0 - PX / 2.0),
fmt(ly + PY),
fmt(c.pos - max_w / 2.0 - PX / 2.0),
fmt(ly - PY),
fmt(c.pos_with_offset - max_w / 2.0 - PX),
fmt(ly - h2 - PY),
fmt(c.pos_with_offset + max_w / 2.0 + PX),
fmt(ly - h2 - PY),
fmt(c.pos_with_offset + max_w / 2.0 + PX),
fmt(ly + h2 + PY),
fmt(c.pos_with_offset - max_w / 2.0 - PX),
fmt(ly + h2 + PY)
);
let _ = write!(
&mut out,
r#"<polygon class="tag-label-bkg" points="{pts}"/>"#,
pts = escape_attr(&points)
);
let _ = write!(
&mut out,
r#"<circle cy="{cy}" cx="{cx}" r="1.5" class="tag-hole"/>"#,
cy = fmt(ly),
cx = fmt(c.pos - max_w / 2.0 + PX / 2.0)
);
let _ = write!(
&mut out,
r#"<text y="{y}" class="tag-label" x="{x}">{txt}</text>"#,
y = fmt(c.y - 16.0 - y_off),
x = fmt(c.pos_with_offset - max_w / 2.0),
txt = escape_xml(tag_value)
);
}
}
}
}
out.push_str("</g>");
if let Some(title) = diagram_title.map(str::trim).filter(|t| !t.is_empty()) {
let _ = write!(
&mut out,
r#"<text text-anchor="middle" x="{x}" y="-25" class="gitTitleText" xmlns="http://www.w3.org/2000/svg">{text}</text>"#,
x = TITLE_X_PLACEHOLDER,
text = escape_xml(title),
);
}
out.push_str("</svg>\n");
let mut bb_dbg = SvgEmittedBoundsDebug {
bounds: Bounds {
min_x: 0.0,
min_y: 0.0,
max_x: 0.0,
max_y: 0.0,
},
min_x: None,
min_y: None,
max_x: None,
max_y: None,
};
let b = svg_emitted_bounds_from_svg_inner(&out, Some(&mut bb_dbg)).unwrap_or(Bounds {
min_x: vb_min_x,
min_y: vb_min_y,
max_x: vb_min_x + vb_w,
max_y: vb_min_y + vb_h,
});
let pad = VIEWBOX_PADDING_PX as f32;
fn next_down_f32(v: f32) -> f32 {
if v.is_nan() || v == f32::NEG_INFINITY {
return v;
}
if v == 0.0 {
return -f32::from_bits(1);
}
let bits = v.to_bits();
if v > 0.0 {
f32::from_bits(bits - 1)
} else {
f32::from_bits(bits + 1)
}
}
fn next_up_f32(v: f32) -> f32 {
if v.is_nan() || v == f32::INFINITY {
return v;
}
if v == 0.0 {
return f32::from_bits(1);
}
let bits = v.to_bits();
if v > 0.0 {
f32::from_bits(bits + 1)
} else {
f32::from_bits(bits - 1)
}
}
fn f32_round_up(v: f64) -> f32 {
let q = v as f32;
if !q.is_finite() {
return q;
}
if (q as f64) < v { next_up_f32(q) } else { q }
}
#[allow(dead_code)]
fn f32_round_down(v: f64) -> f32 {
let q = v as f32;
if !q.is_finite() {
return q;
}
if (q as f64) > v { next_down_f32(q) } else { q }
}
let bbox_x = b.min_x as f32;
let bbox_y = b.min_y as f32;
let dbg_viewbox = std::env::var("MERMAN_DEBUG_GITGRAPH_VIEWBOX").is_ok();
if dbg_viewbox {
if let Some(c) = &bb_dbg.min_x {
let raw = c.bounds.min_x as f32;
eprintln!(
"gitgraph viewbox dbg: before bbox_x={bbox_x:?} raw_min_x={raw:?} next_down={:?}",
next_down_f32(raw)
);
} else {
eprintln!("gitgraph viewbox dbg: before bbox_x={bbox_x:?} raw_min_x=<none>");
}
}
if dbg_viewbox {
eprintln!(
"gitgraph viewbox dbg: after bbox_x={bbox_x:?} bbox_y={bbox_y:?} b.min_x={:?} b.max_x={:?}",
b.min_x, b.max_x
);
}
let bbox_w = f32_round_up(b.max_x - b.min_x);
let bbox_h = f32_round_up(b.max_y - b.min_y);
let _ = &bb_dbg;
let vb_min_x = (bbox_x as f64) - (pad as f64);
let vb_min_y = (bbox_y as f64) - (pad as f64);
let vb_w = (bbox_w as f64) + 2.0 * (pad as f64);
let vb_h = (bbox_h as f64) + 2.0 * (pad as f64);
if dbg_viewbox {
eprintln!(
"gitgraph viewbox dbg: bbox_h={bbox_h:?} bbox_h_bits={} pad={pad:?} vb_min_x={vb_min_x:?} vb_min_y={vb_min_y:?} vb_w={vb_w:?} vb_h={vb_h:?}",
bbox_h.to_bits()
);
}
let view_box_attr = format!(
"{} {} {} {}",
fmt(vb_min_x),
fmt(vb_min_y),
fmt(vb_w),
fmt(vb_h)
);
if let Some((view_box, max_width)) =
crate::generated::gitgraph_root_overrides_11_12_2::lookup_gitgraph_root_viewport_override(
diagram_id,
)
{
let mut it = view_box.split_whitespace();
let vb_min_x = it.next().and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
let _vb_min_y = it.next().and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
let vb_w = it.next().and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
let title_x = fmt_string(vb_min_x + vb_w / 2.0);
out = out.replacen(VIEWBOX_PLACEHOLDER, view_box, 1);
out = out.replacen(MAX_WIDTH_PLACEHOLDER, max_width, 1);
out = out.replacen(TITLE_X_PLACEHOLDER, &title_x, 1);
return Ok(out);
}
out = out.replacen(VIEWBOX_PLACEHOLDER, &view_box_attr, 1);
out = out.replacen(MAX_WIDTH_PLACEHOLDER, &fmt_string(vb_w), 1);
out = out.replacen(TITLE_X_PLACEHOLDER, &fmt_string(vb_min_x + vb_w / 2.0), 1);
Ok(out)
}
#[cfg(test)]
mod tests {
#[test]
fn generated_gitgraph_text_override_paths_cover_known_literals() {
assert_eq!(
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_extra_px("7-c64d8fd"),
5.0 / 2048.0
);
assert_eq!(
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_left_px('A'),
2304.0 / 2048.0
);
assert_eq!(
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_right_px('C'),
-500.0 / 2048.0
);
assert_eq!(
crate::generated::gitgraph_text_overrides_11_12_2::
lookup_gitgraph_simple_text_bbox_width_extra_px("unknown"),
0.0
);
}
}