use super::common::{color_to_hex, escape_into};
use crate::filter::RenderGrid;
const CELL_WIDTH: u32 = 10;
const LINE_HEIGHT: u32 = 14;
const FONT_FAMILY: &str = "monospace";
#[must_use]
pub fn write_svg(grid: &RenderGrid) -> String {
let w = grid.width as usize;
let h = grid.height as usize;
let capacity = w.saturating_mul(h).saturating_mul(64).saturating_add(256);
let mut out = String::with_capacity(capacity);
let total_w = (w as u32) * CELL_WIDTH;
let total_h = (h as u32) * LINE_HEIGHT;
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"");
push_u32(&mut out, total_w);
out.push_str("\" height=\"");
push_u32(&mut out, total_h);
out.push_str("\" version=\"1.1\">\n");
if w == 0 || h == 0 {
out.push_str("</svg>\n");
return out;
}
for (y, row) in grid.cells.iter().enumerate() {
let y_coord = ((y as u32) + 1) * LINE_HEIGHT;
out.push_str(" <text x=\"0\" y=\"");
push_u32(&mut out, y_coord);
out.push_str("\" font-family=\"");
out.push_str(FONT_FAMILY);
out.push_str("\" font-size=\"12\" xml:space=\"preserve\">");
let mut i = 0;
while i < row.len() {
let run_color = row[i].fg;
let mut j = i + 1;
while j < row.len() && row[j].fg == run_color {
j += 1;
}
out.push_str("<tspan fill=\"");
let hex = color_to_hex(run_color);
out.push_str(&hex);
out.push_str("\">");
let mut buf = String::new();
for cell in &row[i..j] {
buf.push(cell.ch);
}
escape_into(&mut out, &buf);
out.push_str("</tspan>");
i = j;
}
out.push_str("</text>\n");
}
out.push_str("</svg>\n");
out
}
fn push_u32(out: &mut String, mut n: u32) {
if n == 0 {
out.push('0');
return;
}
let mut buf = [0u8; 10];
let mut i = 0;
while n > 0 {
buf[i] = (n % 10) as u8 + b'0';
n /= 10;
i += 1;
}
while i > 0 {
i -= 1;
out.push(buf[i] as char);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{Cell, Color, RenderGrid};
#[test]
fn empty_grid_emits_svg_wrapper() {
let grid = RenderGrid::empty();
let svg = write_svg(&grid);
assert!(svg.contains("<?xml"));
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
}
#[test]
fn no_external_resource_attrs_emitted() {
let grid = RenderGrid::from_text_rows(&[String::from("AB")]);
let svg = write_svg(&grid);
assert!(!svg.contains("xlink:href"));
assert!(!svg.contains("<image"));
assert!(!svg.contains("<use "));
assert!(!svg.contains("<script"));
assert!(!svg.contains("<foreignObject"));
assert!(!svg.contains("style=\""));
}
#[test]
fn double_quoted_attributes_only() {
let grid = RenderGrid::from_text_rows(&[String::from("X")]);
let svg = write_svg(&grid);
assert!(!svg.contains("='"));
}
#[test]
fn escapes_xss_payload_in_cell() {
let cell = Cell {
ch: '<',
fg: Color::default(),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let svg = write_svg(&grid);
assert!(svg.contains("<"));
}
#[test]
fn cjk_passes_through() {
let grid = RenderGrid::from_text_rows(&[String::from("漢字")]);
let svg = write_svg(&grid);
assert!(svg.contains("漢"));
assert!(svg.contains("字"));
}
}