use std::borrow::{Borrow, Cow};
use aho_corasick::AhoCorasick;
use syntastica_core::theme::ResolvedTheme;
use crate::{
style::{Color, Style},
Highlights, ThemedHighlights,
};
pub trait Renderer {
fn head(&mut self) -> Cow<'static, str> {
"".into()
}
fn tail(&mut self) -> Cow<'static, str> {
"".into()
}
fn newline(&mut self) -> Cow<'static, str> {
"\n".into()
}
fn escape<'a>(&mut self, text: &'a str) -> Cow<'a, str> {
text.into()
}
fn unstyled<'a>(&mut self, text: &'a str) -> Cow<'a, str> {
text.into()
}
fn styled<'a>(&mut self, text: &'a str, style: Style) -> Cow<'a, str>;
}
pub fn render(
highlights: &Highlights<'_>,
renderer: &mut impl Renderer,
theme: impl Borrow<ResolvedTheme>,
) -> String {
let last_line = highlights.len().saturating_sub(1);
let mut out = renderer.head().into_owned();
for (index, line) in highlights.iter().enumerate() {
for (text, style) in line {
let escaped = renderer.escape(text);
match style.and_then(|key| theme.borrow().find_style(key)) {
Some(style) => out += &renderer.styled(&escaped, style),
None => out += &renderer.unstyled(&escaped),
}
}
if index != last_line {
out += &renderer.newline();
}
}
out + &renderer.tail()
}
pub fn resolve_styles<'src>(
highlights: &Highlights<'src>,
theme: impl Borrow<ResolvedTheme>,
) -> ThemedHighlights<'src> {
let mut new_highlights = Vec::with_capacity(highlights.len());
for line in highlights.iter() {
let mut new_line = Vec::with_capacity(line.len());
for (text, key) in line {
new_line.push((*text, key.and_then(|key| theme.borrow().find_style(key))));
}
new_highlights.push(new_line);
}
new_highlights
}
pub struct HtmlRenderer;
impl HtmlRenderer {
pub fn new() -> Self {
Self
}
}
impl Default for HtmlRenderer {
fn default() -> Self {
Self
}
}
impl Renderer for HtmlRenderer {
fn newline(&mut self) -> Cow<'static, str> {
"<br>".into()
}
fn escape(&mut self, text: &str) -> Cow<'static, str> {
AhoCorasick::new(["&", "<", ">", " ", "\n", "\t"])
.unwrap()
.replace_all(
text,
&[
"&",
"<",
">",
" ",
"<br>",
" ",
],
)
.into()
}
fn styled(&mut self, text: &str, style: Style) -> Cow<'static, str> {
let (r, g, b) = style.color().into_components();
let mut css = format!("color:rgb({r},{g},{b});");
if let Some(color) = style.bg() {
let (r, g, b) = color.into_components();
css += &format!("background-color:rgb({r},{g},{b});");
}
if style.underline() && style.strikethrough() {
css += "text-decoration: underline line-through;"
} else if style.underline() {
css += "text-decoration: underline;"
} else if style.strikethrough() {
css += "text-decoration: line-through;"
}
if style.bold() {
css += "font-weight: bold;"
}
if style.italic() {
css += "font-style: italic;"
}
format!("<span style=\"{css}\">{text}</span>").into()
}
}
#[derive(Default)]
pub struct TerminalRenderer {
background_color: Option<Color>,
}
impl TerminalRenderer {
pub fn new(background_color: Option<Color>) -> Self {
Self { background_color }
}
}
impl Renderer for TerminalRenderer {
fn unstyled<'a>(&mut self, text: &'a str) -> Cow<'a, str> {
match self.background_color {
Some(color) => {
let (r, g, b) = color.into_components();
format!("\x1b[48;2;{r};{g};{b}m{text}\x1b[0m").into()
}
None => text.into(),
}
}
fn styled(&mut self, text: &str, style: Style) -> Cow<'static, str> {
let (r, g, b) = style.color().into_components();
let mut params = format!("38;2;{r};{g};{b};");
if let Some(color) = style.bg().or(self.background_color) {
let (r, g, b) = color.into_components();
params += &format!("48;2;{r};{g};{b};");
}
if style.underline() {
params += "4;"
}
if style.strikethrough() {
params += "9;"
}
if style.italic() {
params += "3;"
}
if style.bold() {
params += "1;"
}
params.truncate(params.len() - 1);
format!("\x1b[{params}m{text}\x1b[0m").into()
}
}