use super::constants::*;
use super::parser::PacketDiagram;
use super::templates::{self, esc, fmt};
use crate::theme::Theme;
pub fn render(diag: &PacketDiagram, theme: Theme) -> String {
let vars = theme.resolve();
let block_fill = vars.packet_block_fill;
let block_stroke = vars.packet_block_stroke;
let text_color = vars.packet_text_color;
let svg_id = "mermaid-packet";
let words = &diag.words;
let total_row_height = ROW_HEIGHT + PADDING_Y;
let has_title = diag
.title
.as_deref()
.map(|t| !t.is_empty())
.unwrap_or(false);
let svg_h =
total_row_height * (words.len() as f64 + 1.0) - if has_title { 0.0 } else { ROW_HEIGHT };
let mut out = String::new();
out.push_str(&templates::svg_root(
svg_id,
&fmt(SVG_WIDTH),
&fmt(svg_h),
&fmt(SVG_WIDTH),
vars.font_family,
));
out.push_str("<g></g>");
for (row_number, word) in words.iter().enumerate() {
let word_y = row_number as f64 * total_row_height + PADDING_Y;
out.push_str("<g>");
for block in word {
let block_x = (block.start % BITS_PER_ROW) as f64 * BIT_WIDTH + 1.0;
let width = (block.end - block.start + 1) as f64 * BIT_WIDTH - PADDING_X;
let is_single = block.start == block.end;
let bit_number_y = word_y - BIT_NUMBER_Y_OFFSET;
out.push_str(&templates::block_rect(
block_x,
word_y,
width,
ROW_HEIGHT,
block_fill,
block_stroke,
));
out.push_str(&templates::block_label(
block_x + width / 2.0,
word_y + ROW_HEIGHT / 2.0,
&esc(&block.label),
text_color,
));
let start_x = block_x + if is_single { width / 2.0 } else { 0.0 };
let start_anchor = if is_single { "middle" } else { "start" };
out.push_str(&templates::bit_label(
start_x,
bit_number_y,
block.start,
"start",
start_anchor,
text_color,
));
if !is_single {
out.push_str(&templates::bit_label(
block_x + width,
bit_number_y,
block.end,
"end",
"end",
text_color,
));
}
}
out.push_str("</g>");
}
let title_text = esc(diag.title.as_deref().unwrap_or(""));
out.push_str(&templates::diagram_title(
SVG_WIDTH / 2.0,
svg_h - total_row_height / 2.0,
&title_text,
text_color,
));
out.push_str("</svg>");
out
}
#[cfg(test)]
mod tests {
use super::super::parser;
use super::*;
#[test]
fn basic_render_produces_svg() {
let input = "packet-beta\n 0-15: \"Source Port\"\n 16-31: \"Destination Port\"\n 32-63: \"Sequence Number\"\n 64-95: \"Acknowledgment Number\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains("<svg"), "no <svg");
assert!(svg.contains("Source Port"), "no field label");
assert!(svg.contains("packetBlock"), "no packet fields");
}
#[test]
fn renders_bit_numbers() {
let input = "packet-beta\n 0-7: \"Byte\"\n 8-15: \"Second\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains(">0<"), "no bit 0");
}
#[test]
fn multi_row_packet() {
let input = "packet-beta\n 0-15: \"Source Port\"\n 16-31: \"Destination Port\"\n 32-63: \"Sequence Number\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(svg.contains("Sequence Number"), "no second row field");
}
#[test]
fn viewbox_matches_reference_basic() {
let input = "packet-beta\n 0-7: \"Source\"\n 8-15: \"Dest\"\n 16-31: \"Data\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default);
assert!(
svg.contains("viewBox=\"0 0 1026 62\""),
"wrong viewBox: {}",
&svg[..200]
);
}
#[test]
fn dark_theme() {
let input = "packet-beta\n 0-15: \"A\"\n 16-31: \"B\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Dark);
assert!(svg.contains("#333"), "dark theme block fill missing");
}
#[test]
fn snapshot_default_theme() {
let input =
"packet-beta\n title TCP\n 0-15: \"Source Port\"\n 16-31: \"Destination Port\"\n 32-63: \"Sequence Number\"";
let diag = parser::parse(input).diagram;
let svg = render(&diag, crate::theme::Theme::Default);
insta::assert_snapshot!(crate::svg::normalize_floats(&svg));
}
}