use crate::error::MermaidError; use crate::theme::MermaidTheme; const DEFAULT_ROW_HEIGHT: f64 = 32.0; const DEFAULT_BIT_WIDTH: f64 = 32.0; const DEFAULT_BITS_PER_ROW: u32 = 32; const DEFAULT_SHOW_BITS: bool = true; const DEFAULT_PADDING_X: f64 = 5.0; const DEFAULT_PADDING_Y: f64 = 5.0; pub fn render_packet_diagram_to_svg( mermaid_source: &str, _theme: &MermaidTheme, ) -> Result { let diagram = parse_packet_diagram(mermaid_source)?; let padding_y = DEFAULT_PADDING_Y + if DEFAULT_SHOW_BITS { 10.0 } else { 0.0 }; let total_row_height = DEFAULT_ROW_HEIGHT + padding_y; let svg_width = DEFAULT_BIT_WIDTH * (DEFAULT_BITS_PER_ROW as f64) + 2.0; let svg_height = total_row_height * ((diagram.rows.len() + 1) as f64) - if diagram.title.is_some() { 0.0 } else { DEFAULT_ROW_HEIGHT }; let mut svg = String::new(); svg.push_str(&format!( "" )); svg.push_str( "", ); svg.push_str(""); for (row_idx, row) in diagram.rows.iter().enumerate() { let word_y = row_idx as f64 * total_row_height + padding_y; for block in row { let block_x = 1.0 + (block.start % DEFAULT_BITS_PER_ROW) as f64 * DEFAULT_BIT_WIDTH; let width = (block.end - block.start + 1) as f64 * DEFAULT_BIT_WIDTH - DEFAULT_PADDING_X; svg.push_str(&format!( "" )); let label_x = block_x + width / 2.0; let label_y = word_y + DEFAULT_ROW_HEIGHT / 2.0; svg.push_str(&format!( "{}", escape_xml(&block.label) )); if DEFAULT_SHOW_BITS { let bit_y = word_y - 2.0; if block.start == block.end { svg.push_str(&format!( "{}", block.start )); } else { let end_x = block_x + width; svg.push_str(&format!( "{}", block.start )); svg.push_str(&format!( "{}", block.end )); } } } } svg.push_str(""); let title_x = svg_width / 2.0; let title_y = svg_height - total_row_height / 2.0; svg.push_str(&format!( "{}", diagram .title .as_deref() .map(escape_xml) .unwrap_or_default() )); svg.push_str(""); Ok(svg) } #[derive(Debug, Clone)] struct PacketDiagram { title: Option, rows: Vec>, } #[derive(Debug, Clone)] struct PacketBlock { start: u32, end: u32, label: String, } fn parse_packet_diagram(input: &str) -> Result { let lines = input.lines().enumerate(); let mut found_header = false; let mut title: Option = None; let mut blocks: Vec = Vec::new(); for (idx, raw) in lines { let line_no = idx + 1; let line = raw.trim(); if line.is_empty() || line.starts_with("%%") { continue; } if !found_header { if line.split_whitespace().next() != Some("packet-beta") { return Err(MermaidError::ParseError { line: line_no, message: "Expected 'packet-beta' declaration".to_string(), }); } found_header = true; continue; } if let Some(rest) = line.strip_prefix("title ") { let t = rest.trim(); if !t.is_empty() { title = Some(t.to_string()); } continue; } let Some((range_raw, label_raw)) = line.split_once(':') else { return Err(MermaidError::ParseError { line: line_no, message: format!("Invalid packet block: {line}"), }); }; let (start, end) = parse_range(range_raw.trim(), line_no)?; let label = parse_label(label_raw.trim(), line_no)?; blocks.push(PacketBlock { start, end, label }); } if !found_header { return Err(MermaidError::ParseError { line: 1, message: "Expected 'packet-beta' declaration".to_string(), }); } if blocks.is_empty() { return Err(MermaidError::ParseError { line: 1, message: "Packet diagram requires at least one block".to_string(), }); } ensure_contiguous(&blocks)?; let rows = split_into_rows(blocks, DEFAULT_BITS_PER_ROW); Ok(PacketDiagram { title, rows }) } fn parse_range(s: &str, line: usize) -> Result<(u32, u32), MermaidError> { let s = s.trim(); if let Some((start_str, end_str)) = s.split_once('-') { let start: u32 = start_str .trim() .parse() .map_err(|_| MermaidError::ParseError { line, message: format!("Invalid packet start: {start_str}"), })?; let end: u32 = end_str .trim() .parse() .map_err(|_| MermaidError::ParseError { line, message: format!("Invalid packet end: {end_str}"), })?; if end < start { return Err(MermaidError::ParseError { line, message: format!("Packet block {start}-{end} is invalid (end < start)"), }); } return Ok((start, end)); } let start: u32 = s.parse().map_err(|_| MermaidError::ParseError { line, message: format!("Invalid packet bit index: {s}"), })?; Ok((start, start)) } fn parse_label(s: &str, line: usize) -> Result { let s = s.trim(); if let Some(stripped) = s.strip_prefix('"').and_then(|t| t.strip_suffix('"')) { return Ok(stripped.to_string()); } if let Some(stripped) = s.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')) { return Ok(stripped.to_string()); } if s.is_empty() { return Err(MermaidError::ParseError { line, message: "Packet block label cannot be empty".to_string(), }); } Ok(s.to_string()) } fn ensure_contiguous(blocks: &[PacketBlock]) -> Result<(), MermaidError> { let mut last: Option = None; for block in blocks { if let Some(last_bit) = last { if block.start != last_bit + 1 { return Err(MermaidError::ParseError { line: 1, message: format!( "Packet block {}-{} is not contiguous. It should start from {}.", block.start, block.end, last_bit + 1 ), }); } } last = Some(block.end); } Ok(()) } fn split_into_rows(blocks: Vec, bits_per_row: u32) -> Vec> { let mut rows: Vec> = Vec::new(); let mut word: Vec = Vec::new(); let mut row = 1_u32; for block in blocks { let mut cur = block; loop { let (fitting, remainder) = split_block_at_row_boundary(&cur, row, bits_per_row); word.push(fitting); if word .last() .is_some_and(|b| b.end.saturating_add(1) == row.saturating_mul(bits_per_row)) { rows.push(std::mem::take(&mut word)); row = row.saturating_add(1); } let Some(next) = remainder else { break; }; cur = next; } } if !word.is_empty() { rows.push(word); } rows } fn split_block_at_row_boundary( block: &PacketBlock, row: u32, bits_per_row: u32, ) -> (PacketBlock, Option) { let row_end_exclusive = row.saturating_mul(bits_per_row); if block.end.saturating_add(1) <= row_end_exclusive { return (block.clone(), None); } let first = PacketBlock { start: block.start, end: row_end_exclusive.saturating_sub(1), label: block.label.clone(), }; let second = PacketBlock { start: row_end_exclusive, end: block.end, label: block.label.clone(), }; (first, Some(second)) } fn escape_xml(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") }