use crate::entities::decode_entities_minimal_cow;
use crate::model::{Bounds, LayoutNode};
use crate::text::{MermaidMarkdownWordType, TextMeasurer, TextStyle, WrapMode};
use std::fmt::Write as _;
use std::time::Duration;
use super::super::{escape_attr_display, escape_xml_into, fmt};
use super::ClassSvgNote;
use super::bounds::{include_path_bounds, include_xywh};
use super::label::class_note_html_div_style;
use super::node::ClassNodeRenderPosition;
use super::rough::{class_rough_rect_stroke_path_and_bounds, class_rough_seed};
pub(super) struct ClassNoteRenderContext<'a> {
pub diagram_id: &'a str,
pub effective_config: &'a serde_json::Value,
pub measurer: &'a dyn TextMeasurer,
pub text_style: &'a TextStyle,
pub line_height: f64,
pub use_html_labels: bool,
pub timing_enabled: bool,
}
pub(super) struct ClassNoteRenderState<'a> {
pub out: &'a mut String,
pub content_bounds: &'a mut Option<Bounds>,
pub sanitize_config: &'a mut Option<merman_core::MermaidConfig>,
pub borrowed_sanitize_config: Option<&'a merman_core::MermaidConfig>,
}
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct ClassNoteRenderStats {
pub notes_sanitize: Duration,
pub path_bounds: Duration,
pub path_bounds_calls: usize,
}
pub(super) fn render_class_note_node(
state: ClassNoteRenderState<'_>,
note: &ClassSvgNote,
layout_node: &LayoutNode,
position: ClassNodeRenderPosition,
ctx: &ClassNoteRenderContext<'_>,
) -> ClassNoteRenderStats {
let out = &mut *state.out;
let content_bounds = &mut *state.content_bounds;
let sanitize_config = &mut *state.sanitize_config;
let borrowed_sanitize_config = state.borrowed_sanitize_config;
let mut stats = ClassNoteRenderStats::default();
let note_src = note.text.trim();
let note_text = decode_entities_minimal_cow(note_src);
let (label_w_raw, label_h_raw) = if ctx.use_html_labels {
match (layout_node.label_width, layout_node.label_height) {
(Some(w), Some(h)) => (w, h),
_ => {
let note_html_config = class_note_sanitize_config(
borrowed_sanitize_config,
sanitize_config,
ctx.effective_config,
);
let metrics = crate::class::class_html_measure_note_metrics(
ctx.measurer,
ctx.text_style,
note_src,
note_html_config,
);
(metrics.width, metrics.height)
}
}
} else {
let mut metrics =
ctx.measurer
.measure_wrapped(¬e_text, ctx.text_style, None, WrapMode::SvgLike);
if let Some(width) = crate::class::class_svg_single_line_plain_label_width_px(
note_text.as_ref(),
ctx.measurer,
ctx.text_style,
) {
metrics.width = width;
}
(metrics.width, metrics.height)
};
let label_w = label_w_raw.max(1.0);
let label_h = if ctx.use_html_labels {
label_h_raw.max(ctx.line_height).max(1.0)
} else {
label_h_raw.max(1.0)
};
let w = layout_node.width.max(1.0);
let h = layout_node.height.max(1.0);
let left = -w / 2.0;
let top = -h / 2.0;
let label_x = -label_w / 2.0;
let label_y = if ctx.use_html_labels {
-label_h / 2.0
} else {
-label_h / 2.0 - crate::class::class_svg_create_text_bbox_y_offset_px(ctx.text_style)
};
let (note_stroke_d, note_stroke_pb) = class_rough_rect_stroke_path_and_bounds(
left,
top,
w,
h,
class_rough_seed(ctx.diagram_id, ¬e.id),
);
include_xywh(
content_bounds,
position.node_bounds_tx + left,
position.node_bounds_ty + top,
w,
h,
);
include_xywh(
content_bounds,
position.node_bounds_tx + label_x,
position.node_bounds_ty + label_y,
label_w,
label_h,
);
let path_bounds_start = ctx.timing_enabled.then(std::time::Instant::now);
include_path_bounds(
content_bounds,
¬e_stroke_pb,
position.node_bounds_tx,
position.node_bounds_ty,
);
if let Some(s) = path_bounds_start {
stats.path_bounds += s.elapsed();
stats.path_bounds_calls += 1;
}
if ctx.use_html_labels {
let note_div_style = class_note_html_div_style(label_w, 200);
let _ = write!(
out,
r##"<g class="node undefined" id="{}" transform="translate({}, {})"><g class="basic label-container"><path d="M{} {} L{} {} L{} {} L{} {}" stroke="none" stroke-width="0" fill="#fff5ad" style="fill:#fff5ad !important;stroke:#aaaa33 !important"/><path d="{}" stroke="#aaaa33" stroke-width="1.3" fill="none" stroke-dasharray="0 0" style="fill:#fff5ad !important;stroke:#aaaa33 !important"/></g><g class="label" style="text-align:left !important;white-space:nowrap !important" transform="translate({}, {})"><rect/><foreignObject width="{}" height="{}"><div style="{}" xmlns="http://www.w3.org/1999/xhtml"><span style="text-align:left !important;white-space:nowrap !important" class="nodeLabel"><p>"##,
escape_attr_display(¬e.id),
fmt(position.node_tx),
fmt(position.node_ty),
fmt(left),
fmt(top),
fmt(left + w),
fmt(top),
fmt(left + w),
fmt(top + h),
fmt(left),
fmt(top + h),
escape_attr_display(¬e_stroke_d),
fmt(label_x),
fmt(label_y),
fmt(label_w),
fmt(label_h),
escape_attr_display(¬e_div_style),
);
let sanitize_start = ctx.timing_enabled.then(std::time::Instant::now);
let note_html_config = class_note_sanitize_config(
borrowed_sanitize_config,
sanitize_config,
ctx.effective_config,
);
let note_html = crate::class::class_note_html_fragment(note_src, note_html_config);
if let Some(s) = sanitize_start {
stats.notes_sanitize += s.elapsed();
}
out.push_str(¬e_html);
out.push_str("</p></span></div></foreignObject></g></g>");
} else {
let note_label_style = "text-align:left !important;white-space:nowrap !important";
let _ = write!(
out,
r##"<g class="node undefined" id="{}" transform="translate({}, {})"><g class="basic label-container"><path d="M{} {} L{} {} L{} {} L{} {}" stroke="none" stroke-width="0" fill="#fff5ad" style="fill:#fff5ad !important;stroke:#aaaa33 !important"/><path d="{}" stroke="#aaaa33" stroke-width="1.3" fill="none" stroke-dasharray="0 0" style="fill:#fff5ad !important;stroke:#aaaa33 !important"/></g><g class="label" style="{}" transform="translate({}, {})"><rect/><g><rect class="background" style="stroke: none"/>"##,
escape_attr_display(¬e.id),
fmt(position.node_tx),
fmt(position.node_ty),
fmt(left),
fmt(top),
fmt(left + w),
fmt(top),
fmt(left + w),
fmt(top + h),
fmt(left),
fmt(top + h),
escape_attr_display(¬e_stroke_d),
escape_attr_display(note_label_style),
fmt(label_x),
fmt(label_y),
);
write_class_svg_text_markdown_with_style(out, note_text.as_ref(), note_label_style);
out.push_str("</g></g></g>");
}
stats
}
fn class_note_sanitize_config<'a>(
borrowed_sanitize_config: Option<&'a merman_core::MermaidConfig>,
owned_sanitize_config: &'a mut Option<merman_core::MermaidConfig>,
effective_config: &serde_json::Value,
) -> &'a merman_core::MermaidConfig {
if let Some(config) = borrowed_sanitize_config {
return config;
}
owned_sanitize_config
.get_or_insert_with(|| merman_core::MermaidConfig::from_value(effective_config.clone()))
}
fn write_class_svg_text_markdown_with_style(out: &mut String, markdown: &str, style: &str) {
let markdown = markdown
.strip_prefix('`')
.and_then(|s| s.strip_suffix('`'))
.unwrap_or(markdown);
let _ = write!(
out,
r#"<text y="-10.1" style="{}">"#,
escape_attr_display(style)
);
let lines = crate::text::mermaid_markdown_to_lines(markdown, true);
if lines.len() == 1 && lines[0].is_empty() {
out.push_str(r#"<tspan class="text-outer-tspan" x="0" y="-0.1em" dy="1.1em"/>"#);
out.push_str("</text>");
return;
}
for (idx, words) in lines.iter().enumerate() {
if idx == 0 {
out.push_str(r#"<tspan class="text-outer-tspan" x="0" y="-0.1em" dy="1.1em">"#);
} else {
let y_em = if idx == 1 {
"1em".to_string()
} else {
format!("{:.1}em", 1.0 + (idx as f64 - 1.0) * 1.1)
};
let _ = write!(
out,
r#"<tspan class="text-outer-tspan" x="0" y="{}" dy="1.1em">"#,
y_em
);
}
for (word_idx, (word, ty)) in words.iter().enumerate() {
let is_strong = *ty == MermaidMarkdownWordType::Strong;
let is_em = *ty == MermaidMarkdownWordType::Em;
let font_style = if is_em { "italic" } else { "normal" };
let font_weight = if is_strong { "bold" } else { "normal" };
let _ = write!(
out,
r#"<tspan font-style="{}" class="text-inner-tspan" font-weight="{}">"#,
font_style, font_weight
);
if word_idx == 0 {
escape_xml_into(out, word);
} else {
out.push(' ');
escape_xml_into(out, word);
}
out.push_str("</tspan>");
}
out.push_str("</tspan>");
}
out.push_str("</text>");
}