mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
mermaid_render: Fix overlapping text in diagrams with long labels (#59140)
Mermaid diagrams with long node labels rendered as an unreadable jumble: each label was painted as one long line over a box sized for wrapped text, overflowing into neighboring nodes. With this change, labels wrap to the box width the layout computed. **Root cause:** mermaid's default `htmlLabels: true` makes merman emit labels as HTML inside `<foreignObject>`, which resvg cannot rasterize. merman's resvg-safe pipeline replaces each one with a single-line `<text>` fallback that only honors explicit `<br>` breaks, so soft-wrapped labels collapse onto one long line — while the node boxes were laid out for the wrapped text. **Fix:** disable HTML labels in the site config (both the top-level key, which merman reads for node labels, and `flowchart.htmlLabels`, which it reads for edge labels). merman then emits native SVG `<text>`/`<tspan>` labels wrapped to the same measured width the layout used, and the foreignObject fallback never runs for these diagrams. One behavior preserved deliberately: a literal `\n` in label text used to become a line break as a side effect of the foreignObject fallback (`fallback.rs` converts `\n` when flattening HTML labels). `render_mermaid` now normalizes `\n` to `<br/>` before rendering so the native path keeps that behavior; the existing `backslash_n_converted_to_line_break` test covers it. Side effect: class diagram member text also switches from the lossy fallback to native SVG text; the accent-class test was updated accordingly (accent classes now land directly on `text`/`tspan` elements). Verified with `cargo nextest run -p mermaid_render`, `-p markdown`, `script/clippy -p mermaid_render`, and `cargo fmt --check`. Release Notes: - Fixed mermaid diagram labels overflowing and overlapping nodes when long labels wrap --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
parent
34cd17ff5e
commit
49eb6b2de7
2 changed files with 37 additions and 43 deletions
|
|
@ -122,7 +122,12 @@ fn to_merman_config(theme: &MermaidTheme) -> merman::MermaidConfig {
|
|||
"theme": "base",
|
||||
"darkMode": theme.dark_mode,
|
||||
"fontFamily": theme.font_family,
|
||||
// resvg can't rasterize HTML `<foreignObject>` labels, and the
|
||||
// fallback that replaces them loses soft wrapping, so emit native SVG
|
||||
// text labels. Nodes read the top-level key; edges read `flowchart`.
|
||||
"htmlLabels": false,
|
||||
"flowchart": {
|
||||
"htmlLabels": false,
|
||||
"padding": 16,
|
||||
},
|
||||
"themeVariables": theme_vars,
|
||||
|
|
@ -144,10 +149,6 @@ mod tests {
|
|||
!html_label_svg.contains("<foreignObject"),
|
||||
"got: {html_label_svg}"
|
||||
);
|
||||
assert!(
|
||||
html_label_svg.contains(r#"data-merman-foreignobject="fallback""#),
|
||||
"got: {html_label_svg}"
|
||||
);
|
||||
assert!(
|
||||
!html_label_svg.contains("&lt;"),
|
||||
"got: {html_label_svg}"
|
||||
|
|
@ -163,4 +164,28 @@ mod tests {
|
|||
assert!(!css_svg.contains("animation-name:"), "got: {css_svg}");
|
||||
assert!(!css_svg.contains("!important"), "got: {css_svg}");
|
||||
}
|
||||
|
||||
/// Soft-wrapped labels must render as native SVG `<tspan>` lines rather
|
||||
/// than the resvg-safe pipeline's single-line `<foreignObject>` fallback,
|
||||
/// which overflows the node box (see the `htmlLabels` comment in
|
||||
/// [`to_merman_config`]). If the fallback marker reappears after a merman
|
||||
/// upgrade, the config has stopped disabling HTML labels.
|
||||
#[test]
|
||||
fn long_labels_render_as_wrapped_svg_text() {
|
||||
let source = "flowchart TD\n \
|
||||
A[\"Pass 2: search transcript with annotation blocks excised, \
|
||||
map offsets back to buffer space\"] --> \
|
||||
|ambiguous or zero| B[\"Error describing where matches were found\"]";
|
||||
let svg = render_mermaid(source, &MermaidTheme::default()).expect("render failed");
|
||||
|
||||
assert!(
|
||||
!svg.contains(r#"data-merman-foreignobject="fallback""#),
|
||||
"labels went through the foreignObject fallback, which loses soft wrapping: {svg}"
|
||||
);
|
||||
let wrapped_line_count = svg.matches("text-outer-tspan").count();
|
||||
assert!(
|
||||
wrapped_line_count > 3,
|
||||
"expected long labels to wrap onto multiple tspan lines, got {wrapped_line_count}: {svg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,25 +268,7 @@ fn generics_not_double_escaped() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn backslash_n_converted_to_line_break() {
|
||||
let theme = rgb_theme();
|
||||
let source = r#"graph TD
|
||||
L7["Layer 7\nHTTP, FTP"]
|
||||
L6["Layer 6\nEncryption"]
|
||||
L7 --> L6"#;
|
||||
let svg = mermaid_render::render_to_svg(source, &theme).expect("render failed");
|
||||
assert!(
|
||||
!svg.contains(r"\n"),
|
||||
"Literal \\n should not appear in SVG output"
|
||||
);
|
||||
assert!(
|
||||
svg.contains(">Layer 7<") && svg.contains(">HTTP, FTP<"),
|
||||
"Label lines should be split into separate <text> elements"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_diagram_fallback_text_uses_accent_classes() {
|
||||
fn class_diagram_label_text_uses_accent_classes() {
|
||||
let theme = rgb_theme();
|
||||
let source = r#"classDiagram
|
||||
class Animal {
|
||||
|
|
@ -303,32 +285,19 @@ fn class_diagram_fallback_text_uses_accent_classes() {
|
|||
|
||||
use quick_xml::events::Event;
|
||||
let mut reader = quick_xml::Reader::from_str(&svg);
|
||||
let mut in_fallback = false;
|
||||
let mut accent_classes: Vec<String> = Vec::new();
|
||||
loop {
|
||||
match reader.read_event() {
|
||||
Ok(Event::Eof) => break,
|
||||
Ok(Event::Start(e)) => {
|
||||
if e.name().as_ref() == b"g" {
|
||||
if let Ok(Some(attr)) = e.try_get_attribute("data-merman-foreignobject") {
|
||||
if attr.value.as_ref() == b"fallback" {
|
||||
in_fallback = true;
|
||||
Ok(Event::Start(e)) if e.name().as_ref() == b"text" => {
|
||||
if let Ok(Some(class_attr)) = e.try_get_attribute("class") {
|
||||
let class = class_attr.unescape_value().unwrap_or_default().to_string();
|
||||
for token in class.split_whitespace() {
|
||||
if token.starts_with("zed-accent-") {
|
||||
accent_classes.push(token.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if in_fallback && e.name().as_ref() == b"text" {
|
||||
if let Ok(Some(class_attr)) = e.try_get_attribute("class") {
|
||||
let class = class_attr.unescape_value().unwrap_or_default().to_string();
|
||||
for token in class.split_whitespace() {
|
||||
if token.starts_with("zed-accent-") {
|
||||
accent_classes.push(token.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::End(e)) if e.name().as_ref() == b"g" => {
|
||||
in_fallback = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -336,7 +305,7 @@ fn class_diagram_fallback_text_uses_accent_classes() {
|
|||
|
||||
assert!(
|
||||
!accent_classes.is_empty(),
|
||||
"expected zed-accent-N classes on text elements in fallback groups",
|
||||
"expected zed-accent-N classes on label text elements",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue