use super::common::{color_to_hex, escape_into};
use crate::filter::{Cell, RenderGrid};
#[must_use]
pub fn write_html(grid: &RenderGrid) -> String {
let w = grid.width as usize;
let h = grid.height as usize;
let capacity = w.saturating_mul(h).saturating_mul(32).saturating_add(64);
let mut out = String::with_capacity(capacity);
out.push_str("<pre>\n");
if w == 0 || h == 0 {
out.push_str("</pre>\n");
return out;
}
for row in &grid.cells {
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("<span style=\"color:");
let hex = color_to_hex(run_color);
out.push_str(&hex);
out.push_str("\">");
for cell in &row[i..j] {
escape_into(&mut out, &cell_to_string(cell));
}
out.push_str("</span>");
i = j;
}
out.push('\n');
}
out.push_str("</pre>\n");
out
}
fn cell_to_string(cell: &Cell) -> String {
cell.ch.to_string()
}
#[cfg(test)]
mod tests {
use super::super::common::{escape_into, index_to_rgb};
use super::*;
use crate::filter::{Cell, Color, NamedColor, RenderGrid};
#[test]
fn empty_grid_emits_pre_wrapper() {
let grid = RenderGrid::empty();
let html = write_html(&grid);
assert!(html.starts_with("<pre>"));
assert!(html.contains("</pre>"));
}
#[test]
fn plain_ascii_pass_through() {
let grid = RenderGrid::from_text_rows(&[String::from("Hello")]);
let html = write_html(&grid);
assert!(html.contains("Hello"));
assert!(html.contains("<pre>"));
}
#[test]
fn escape_lt_gt_amp_quot() {
let mut s = String::new();
escape_into(&mut s, "<>&\"");
assert_eq!(s, "<>&"");
}
#[test]
fn escape_script_tag() {
let mut s = String::new();
escape_into(&mut s, "<script>");
assert_eq!(s, "<script>");
}
#[test]
fn escape_attribute_breakout() {
let mut s = String::new();
escape_into(&mut s, "\"><img onerror=alert(1)>");
assert!(!s.contains('"'));
assert!(!s.contains('<'));
assert!(!s.contains('>'));
assert!(s.contains("""));
assert!(s.contains("<img"));
}
#[test]
fn escape_ampersand_double_encoding() {
let mut s = String::new();
escape_into(&mut s, "&");
assert_eq!(s, "&amp;");
}
#[test]
fn escape_passes_through_cjk() {
let mut s = String::new();
escape_into(&mut s, "中文");
assert_eq!(s, "中文");
}
#[test]
fn escape_passes_through_emoji() {
let mut s = String::new();
escape_into(&mut s, "🦀");
assert_eq!(s, "🦀");
}
#[test]
fn escape_single_quote_unchanged() {
let mut s = String::new();
escape_into(&mut s, "it's");
assert_eq!(s, "it's");
}
#[test]
fn write_html_with_rgb_color() {
let cell = Cell {
ch: 'X',
fg: Color::Rgb(255, 128, 0),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let html = write_html(&grid);
assert!(html.contains("#FF8000"));
assert!(html.contains(">X</span>"));
}
#[test]
fn write_html_with_named_color() {
let cell = Cell {
ch: 'Y',
fg: Color::Named(NamedColor::BrightRed),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let html = write_html(&grid);
assert!(html.contains("#FF0000"));
}
#[test]
fn write_html_no_unescaped_metacharacters_in_output_for_xss_input() {
let cell = Cell {
ch: '<',
fg: Color::default(),
bg: None,
attrs: 0,
};
let grid = RenderGrid::from_rows(vec![vec![cell]]);
let html = write_html(&grid);
assert!(html.contains("><</span>"));
}
#[test]
fn write_html_coalesces_same_color_run() {
let g = RenderGrid::from_text_rows(&[String::from("ABC")]);
let html = write_html(&g);
let span_count = html.matches("<span").count();
assert_eq!(span_count, 1);
}
#[test]
fn index_to_rgb_grayscale_ramp() {
assert_eq!(index_to_rgb(232), 0x080808);
assert_eq!(index_to_rgb(255), 0xEEEEEE);
}
#[test]
fn index_to_rgb_cube_corner() {
assert_eq!(index_to_rgb(16), 0x000000);
assert_eq!(index_to_rgb(231), 0xFFFFFF);
}
#[test]
fn write_html_has_pre_wrapper_around_all_content() {
let grid = RenderGrid::from_text_rows(&[String::from("X")]);
let html = write_html(&grid);
let pre_open = html.find("<pre>").expect("opens with <pre>");
let pre_close = html.find("</pre>").expect("closes with </pre>");
assert!(pre_open < pre_close);
}
}