use std::borrow::Cow;
use std::fmt::Write as _;
use crate::cells::cell_len;
use crate::segment::Segment;
use crate::style::Style;
use crate::terminal_theme::TerminalTheme;
pub(super) fn html_escape(s: &str) -> Cow<'_, str> {
if !s.contains(['&', '<', '>', '"']) {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
Cow::Owned(out)
}
pub(super) fn find_or_insert_class(
cache: &mut Vec<(Style, String)>,
stylesheet: &mut String,
style: &Style,
css: &str,
) -> String {
for (cached_style, class_name) in cache.iter() {
if cached_style == style {
return class_name.clone();
}
}
let mut class_name = String::new();
write!(class_name, "r{}", cache.len() + 1).unwrap();
writeln!(stylesheet, ".{} {{ {} }}", class_name, css).unwrap();
cache.push((style.clone(), class_name.clone()));
class_name
}
pub(super) fn build_svg_chrome(
width: f64,
height: f64,
theme: &TerminalTheme,
title: &str,
unique_id: &str,
) -> String {
let bg = theme.background_color.hex();
let mut chrome = String::new();
writeln!(
chrome,
"<rect fill=\"{}\" stroke=\"rgba(255,255,255,0.35)\" stroke-width=\"1\" \
x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" rx=\"8\"/>",
bg, width, height,
)
.unwrap();
let dot_colors = ["#ff5f57", "#febc2e", "#28c840"];
for (i, color) in dot_colors.iter().enumerate() {
let cx = 16.0 + (i as f64) * 22.0;
writeln!(
chrome,
" <circle cx=\"{:.0}\" cy=\"18\" r=\"5\" fill=\"{}\"/>",
cx, color
)
.unwrap();
}
if !title.is_empty() {
writeln!(
chrome,
" <text class=\"{}-title\" fill=\"{}\" x=\"{}\" y=\"23\" \
text-anchor=\"middle\">{}</text>",
unique_id,
theme.foreground_color.hex(),
width / 2.0,
svg_escape(title),
)
.unwrap();
}
chrome
}
pub(super) fn build_svg_text(
buffer: &[Segment],
theme: &TerminalTheme,
unique_id: &str,
char_width: f64,
line_height: f64,
padding_top: f64,
padding_left: f64,
) -> (String, String, String, String) {
let mut matrix = String::new();
let mut backgrounds = String::new();
let mut styles = String::new();
let lines_defs = String::new();
let mut style_cache: Vec<(String, String)> = Vec::new();
let mut y = padding_top + line_height;
let mut x: f64;
let mut line_segments: Vec<Vec<(String, Option<Style>)>> = Vec::new();
let mut current_line: Vec<(String, Option<Style>)> = Vec::new();
for seg in buffer {
if seg.is_control() {
continue;
}
let parts: Vec<&str> = seg.text.split('\n').collect();
for (i, part) in parts.iter().enumerate() {
if !part.is_empty() {
current_line.push((part.to_string(), seg.style().cloned()));
}
if i + 1 < parts.len() {
line_segments.push(std::mem::take(&mut current_line));
}
}
}
if !current_line.is_empty() {
line_segments.push(current_line);
}
for line in &line_segments {
x = padding_left;
for (text, style) in line {
let escaped = svg_escape(text);
let text_width = cell_len(text) as f64 * char_width;
if let Some(ref style) = style {
if let Some(bgcolor) = style.bgcolor() {
let bg_triplet = bgcolor.get_truecolor(Some(theme), false);
writeln!(
backgrounds,
" <rect fill=\"{}\" x=\"{:.1}\" y=\"{:.1}\" \
width=\"{:.1}\" height=\"{:.1}\"/>",
bg_triplet.hex(),
x,
y - line_height + 3.0,
text_width,
line_height,
)
.unwrap();
}
let css = style.get_html_style(Some(theme));
if !css.is_empty() {
let class_name =
find_or_insert_svg_class(&mut style_cache, &mut styles, unique_id, &css);
writeln!(
matrix,
" <text class=\"{}\" x=\"{:.1}\" y=\"{:.1}\" \
textLength=\"{:.1}\">{}</text>",
class_name, x, y, text_width, escaped
)
.unwrap();
} else {
writeln!(
matrix,
" <text fill=\"{}\" x=\"{:.1}\" y=\"{:.1}\" \
textLength=\"{:.1}\">{}</text>",
theme.foreground_color.hex(),
x,
y,
text_width,
escaped
)
.unwrap();
}
} else {
writeln!(
matrix,
" <text fill=\"{}\" x=\"{:.1}\" y=\"{:.1}\" \
textLength=\"{:.1}\">{}</text>",
theme.foreground_color.hex(),
x,
y,
text_width,
escaped
)
.unwrap();
}
x += text_width;
}
y += line_height;
}
(matrix, backgrounds, styles, lines_defs)
}
pub(super) fn find_or_insert_svg_class(
cache: &mut Vec<(String, String)>,
styles: &mut String,
unique_id: &str,
css: &str,
) -> String {
for (cached_css, class_name) in cache.iter() {
if cached_css == css {
return class_name.clone();
}
}
let mut class_name = String::new();
write!(class_name, "{}-s{}", unique_id, cache.len() + 1).unwrap();
let svg_style = css_to_svg_style(css);
writeln!(styles, " .{} {{ {} }}", class_name, svg_style).unwrap();
cache.push((css.to_string(), class_name.clone()));
class_name
}
pub(super) fn css_to_svg_style(css: &str) -> String {
let mut result = String::new();
for part in css.split(';') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((key, value)) = part.split_once(':') {
let key = key.trim();
let value = value.trim();
let svg_key = match key {
"color" => Some("fill"),
"font-weight" => Some("font-weight"),
"font-style" => Some("font-style"),
"text-decoration" => Some("text-decoration"),
_ => None, };
if let Some(svg_key) = svg_key {
if !result.is_empty() {
result.push_str("; ");
}
write!(result, "{}: {}", svg_key, value).unwrap();
}
}
}
result
}
pub(super) fn svg_escape(s: &str) -> Cow<'_, str> {
if !s.contains(['&', '<', '>', '"', '\'']) {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
Cow::Owned(out)
}