use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::{LinesWithEndings, as_24_bit_terminal_escaped};
pub fn render_markdown(text: &str) -> String {
let mut output = String::new();
let mut in_code_block = false;
let mut code_lang = String::new();
let mut code_buffer = String::new();
for line in text.lines() {
if line.starts_with("```") {
if in_code_block {
output.push_str(&highlight_code(&code_buffer, &code_lang));
output.push('\n');
code_buffer.clear();
code_lang.clear();
in_code_block = false;
} else {
code_lang = line.trim_start_matches('`').trim().to_string();
in_code_block = true;
}
continue;
}
if in_code_block {
code_buffer.push_str(line);
code_buffer.push('\n');
} else {
output.push_str(&render_inline(line));
output.push('\n');
}
}
if in_code_block && !code_buffer.is_empty() {
output.push_str(&highlight_code(&code_buffer, &code_lang));
output.push('\n');
}
output
}
fn highlight_code(code: &str, lang: &str) -> String {
let ss = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let theme = &ts.themes["base16-ocean.dark"];
let syntax = ss
.find_syntax_by_token(lang)
.or_else(|| ss.find_syntax_by_extension(lang))
.unwrap_or_else(|| ss.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(syntax, theme);
let mut result = String::new();
for line in LinesWithEndings::from(code) {
match highlighter.highlight_line(line, &ss) {
Ok(ranges) => {
let escaped = as_24_bit_terminal_escaped(&ranges, false);
result.push_str(&escaped);
}
Err(_) => {
result.push_str(line);
}
}
}
result.push_str("\x1b[0m");
result
}
fn render_inline(line: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '`'
&& let Some(end) = find_closing(&chars, i + 1, '`')
{
let code: String = chars[i + 1..end].iter().collect();
result.push_str(&format!("\x1b[36m{code}\x1b[0m")); i = end + 1;
continue;
}
if i + 1 < chars.len()
&& chars[i] == '*'
&& chars[i + 1] == '*'
&& let Some(end) = find_double_closing(&chars, i + 2, '*')
{
let text: String = chars[i + 2..end].iter().collect();
result.push_str(&format!("\x1b[1m{text}\x1b[0m")); i = end + 2;
continue;
}
if chars[i] == '*' || chars[i] == '_' {
let marker = chars[i];
if i + 1 < chars.len()
&& chars[i + 1] != ' '
&& let Some(end) = find_closing(&chars, i + 1, marker)
&& end > i + 1
{
let text: String = chars[i + 1..end].iter().collect();
result.push_str(&format!("\x1b[3m{text}\x1b[0m")); i = end + 1;
continue;
}
}
if i == 0 && chars[i] == '#' {
let level = chars.iter().take_while(|&&c| c == '#').count();
let text: String = chars[level..].iter().collect();
let text = text.trim_start();
result.push_str(&format!("\x1b[1;4m{text}\x1b[0m")); return result;
}
if i == 0 && (chars[i] == '-' || chars[i] == '*') && chars.get(1) == Some(&' ') {
let text: String = chars[2..].iter().collect();
result.push_str(&format!(" \x1b[36m•\x1b[0m {text}"));
return result;
}
result.push(chars[i]);
i += 1;
}
result
}
fn find_closing(chars: &[char], start: usize, marker: char) -> Option<usize> {
(start..chars.len()).find(|&i| chars[i] == marker)
}
fn find_double_closing(chars: &[char], start: usize, marker: char) -> Option<usize> {
(start..chars.len().saturating_sub(1)).find(|&i| chars[i] == marker && chars[i + 1] == marker)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_heading() {
let result = render_inline("# Hello World");
assert!(result.contains("Hello World"));
assert!(result.contains("\x1b[1;4m")); }
#[test]
fn test_render_list_item() {
let result = render_inline("- item one");
assert!(result.contains("•"));
assert!(result.contains("item one"));
}
#[test]
fn test_highlight_code_doesnt_panic() {
let result = highlight_code("fn main() {}\n", "rust");
assert!(!result.is_empty());
}
}