use crate::{LayoutSpec, StyleSpec, VerticalAlign, resolve_embedded_font};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use std::{borrow::Cow, fs, path::Path};
const SPACE: &str = " ";
const EXTRA_PADDING: u32 = 10;
pub(crate) const FALLBACK_WIDTH: u32 = 600;
pub(crate) const CHAR_WIDTH_RATIO: f32 = 0.6;
pub(crate) const LINE_HEIGHT_RATIO: f32 = 1.4;
pub(crate) fn indent(level: usize) -> String {
SPACE.repeat(level)
}
pub(crate) fn escape_char_xml(ch: char) -> Cow<'static, str> {
match ch {
'&' => Cow::Borrowed("&"),
'<' => Cow::Borrowed("<"),
'>' => Cow::Borrowed(">"),
'"' => Cow::Borrowed("""),
'\'' => Cow::Borrowed("'"),
_ => Cow::Owned(ch.to_string()),
}
}
pub(crate) fn escape_xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match escape_char_xml(ch) {
Cow::Borrowed(escaped) => out.push_str(escaped),
Cow::Owned(owned) => out.push_str(&owned),
}
}
out
}
pub(crate) fn build_svg_header(
layout: &LayoutSpec,
vw: u32,
vh: u32,
font_family: &str,
font_size: u32,
fill_color: &str,
background_color: &str,
) -> String {
let width = vw;
let needs_extra_buffer = matches!(layout.v_align, Some(VerticalAlign::Middle));
let height = if needs_extra_buffer {
vh + font_size + EXTRA_PADDING
} else {
vh
};
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {vw} {vh}" font-family="{font}" font-size="{fs}" fill="{fg}" style="background:{bg}">"#,
w = width,
h = height,
vw = vw,
vh = vh,
font = font_family,
fs = font_size,
fg = fill_color,
bg = background_color,
) + "\n"
}
pub(crate) fn build_svg_footer() -> &'static str {
"</svg>\n"
}
fn build_svg_style_tag(css: &str) -> String {
let indent = indent(1);
let mut out = String::new();
out.push_str(&format!("{indent}<style>\n"));
out.push_str(css);
out.push_str(&format!("{indent}</style>\n"));
out
}
fn embed_font_base64(ttf_path: &str, font_name: &str) -> Option<String> {
let root_relative = Path::new(env!("CARGO_MANIFEST_DIR")).join(ttf_path);
if !root_relative.exists() {
eprintln!("⚠️ Font file not found: {}", root_relative.display());
return None;
}
let bytes = fs::read(root_relative).ok()?;
let encoded = STANDARD.encode(bytes);
Some(format!(
" @font-face {{\
\n font-family: '{}';\
\n src: url(data:font/ttf;base64,{}) format('truetype');\
\n }}\
\n",
font_name, encoded,
))
}
pub(crate) fn inject_embed_font_css(style: &StyleSpec) -> (StyleSpec, String) {
if let Some(meta) = resolve_embedded_font(&style.font_family) {
let mut new_style = style.clone();
new_style.font_family = meta.css_family.to_string();
if let Some(css) = embed_font_base64(meta.path, meta.css_family) {
let style_tag = build_svg_style_tag(&css);
return (new_style, style_tag);
}
return (new_style, String::new());
}
let css = format!(
" svg, text, tspan, .cursor {{ font-family: '{}', monospace; }}\n",
style.font_family
);
let style_tag = build_svg_style_tag(&css);
(style.clone(), style_tag)
}