use crate::highlighter::{HighlightResult, HighlightSegment};
use std::{
format,
string::{String, ToString},
vec::Vec,
};
fn json_escape(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'\x08' => escaped.push_str("\\b"),
'\x0c' => escaped.push_str("\\f"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c if c.is_control() => escaped.push_str(&format!("\\u{:04x}", c as u32)),
c => escaped.push(c),
}
}
escaped
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat {
Html,
Css,
Json,
Xml,
Ansi,
}
pub trait Exporter {
fn export(&self, result: &HighlightResult<'_>) -> String;
}
pub struct HtmlExporter {
pub include_css: bool,
pub inline_styles: bool,
}
impl HtmlExporter {
pub fn new(include_css: bool, inline_styles: bool) -> Self {
Self { include_css, inline_styles }
}
}
impl Exporter for HtmlExporter {
fn export(&self, result: &HighlightResult<'_>) -> String {
let mut html = String::new();
if self.include_css {
html.push_str("<style>\n");
html.push_str(".highlight { font-family: 'Courier New', monospace; white-space: pre }\n");
html.push_str(".highlight .bold { font-weight: bold }\n");
html.push_str(".highlight .italic { font-style: italic }\n");
html.push_str(".highlight .underline { text-decoration: underline }\n");
html.push_str("</style>\n");
}
html.push_str("<div class=\"highlight\">");
for segment in &result.segments {
if self.inline_styles {
html.push_str(&self.segment_to_html_inline(segment));
}
else {
html.push_str(&self.segment_to_html_class(segment));
}
}
html.push_str("</div>");
html
}
}
impl HtmlExporter {
fn segment_to_html_inline(&self, segment: &HighlightSegment<'_>) -> String {
let mut style_attrs = Vec::new();
if let Some(color) = &segment.style.color {
style_attrs.push(format!("color: {}", color))
}
if let Some(bg_color) = &segment.style.background_color {
style_attrs.push(format!("background-color: {}", bg_color))
}
if segment.style.bold {
style_attrs.push("font-weight: bold".to_string())
}
if segment.style.italic {
style_attrs.push("font-style: italic".to_string())
}
if segment.style.underline {
style_attrs.push("text-decoration: underline".to_string())
}
let escaped_text = html_escape(&segment.text);
if style_attrs.is_empty() { escaped_text } else { format!("<span style=\"{}\">{}</span>", style_attrs.join("; "), escaped_text) }
}
fn segment_to_html_class(&self, segment: &HighlightSegment<'_>) -> String {
let escaped_text = html_escape(&segment.text);
let mut classes = Vec::new();
if segment.style.bold {
classes.push("bold")
}
if segment.style.italic {
classes.push("italic")
}
if segment.style.underline {
classes.push("underline")
}
if classes.is_empty() { escaped_text } else { format!("<span class=\"{}\">{}</span>", classes.join(" "), escaped_text) }
}
}
pub struct CssExporter;
impl Exporter for CssExporter {
fn export(&self, _result: &HighlightResult<'_>) -> String {
let mut css = String::new();
css.push_str(".highlight {\n");
css.push_str(" font-family: 'Courier New', monospace;\n");
css.push_str(" white-space: pre;\n");
css.push_str("}\n\n");
css.push_str(".highlight .bold { font-weight: bold }\n");
css.push_str(".highlight .italic { font-style: italic }\n");
css.push_str(".highlight .underline { text-decoration: underline }\n");
css
}
}
pub struct JsonExporter {
pub pretty: bool,
}
impl Exporter for JsonExporter {
fn export(&self, result: &HighlightResult<'_>) -> String {
let mut json = String::new();
json.push_str("{\"segments\":[");
for (i, segment) in result.segments.iter().enumerate() {
if i > 0 {
json.push(',')
}
json.push_str("{\"span\":{\"start\":");
json.push_str(&segment.span.start.to_string());
json.push_str(",\"end\":");
json.push_str(&segment.span.end.to_string());
json.push_str("},\"style\":{");
let mut style_parts = Vec::new();
style_parts.push(format!("\"bold\":{}", segment.style.bold));
style_parts.push(format!("\"italic\":{}", segment.style.italic));
style_parts.push(format!("\"underline\":{}", segment.style.underline));
if let Some(color) = &segment.style.color {
style_parts.push(format!("\"color\":\"{}\"", json_escape(color)))
}
if let Some(bg) = &segment.style.background_color {
style_parts.push(format!("\"background_color\":\"{}\"", json_escape(bg)))
}
json.push_str(&style_parts.join(","));
json.push_str("},\"text\":\"");
json.push_str(&json_escape(&segment.text));
json.push_str("\"}")
}
json.push_str("],\"source\":\"");
json.push_str(&json_escape(&result.source));
json.push_str("\"}");
json
}
}
pub struct AnsiExporter;
impl Exporter for AnsiExporter {
fn export(&self, result: &HighlightResult<'_>) -> String {
let mut output = String::new();
for segment in &result.segments {
output.push_str(&self.segment_to_ansi(segment))
}
output
}
}
impl AnsiExporter {
fn segment_to_ansi(&self, segment: &HighlightSegment<'_>) -> String {
let mut codes = Vec::new();
codes.push("0");
if segment.style.bold {
codes.push("1")
}
if segment.style.italic {
codes.push("3")
}
if segment.style.underline {
codes.push("4")
}
if let Some(color) = &segment.style.color {
match color.as_str() {
"#FF0000" | "#F44747" | "#DC322F" | "#FF5555" => codes.push("31"), "#00FF00" | "#6A9955" | "#859900" => codes.push("32"), "#FFFF00" | "#F1FA8C" | "#E6DB74" => codes.push("33"), "#0000FF" | "#569CD6" | "#005CC5" => codes.push("34"), "#FF00FF" | "#FF79C6" | "#D73A49" => codes.push("35"), "#00FFFF" | "#9CDCFE" | "#2AA198" => codes.push("36"), _ => codes.push("37"), }
}
if codes.len() > 1 { format!("\x1b[{}m{}\x1b[0m", codes.join(";"), segment.text) } else { segment.text.to_string() }
}
}
fn html_escape(text: &str) -> String {
text.chars()
.map(|c| match c {
'<' => "<".to_string(),
'>' => ">".to_string(),
'&' => "&".to_string(),
'"' => """.to_string(),
'\'' => "'".to_string(),
_ => c.to_string(),
})
.collect()
}