use colored::Colorize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
mod colors {
use crate::theme;
use crate::theme::names::tokens;
pub fn accent_primary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_PRIMARY);
(c.r, c.g, c.b)
}
pub fn accent_secondary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_SECONDARY);
(c.r, c.g, c.b)
}
pub fn accent_tertiary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ACCENT_TERTIARY);
(c.r, c.g, c.b)
}
pub fn warning() -> (u8, u8, u8) {
let c = theme::current().color(tokens::WARNING);
(c.r, c.g, c.b)
}
pub fn error() -> (u8, u8, u8) {
let c = theme::current().color(tokens::ERROR);
(c.r, c.g, c.b)
}
pub fn text_secondary() -> (u8, u8, u8) {
let c = theme::current().color(tokens::TEXT_SECONDARY);
(c.r, c.g, c.b)
}
pub fn text_dim() -> (u8, u8, u8) {
let c = theme::current().color(tokens::TEXT_DIM);
(c.r, c.g, c.b)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct MarkdownReview {
pub content: String,
}
impl MarkdownReview {
#[must_use]
pub fn format(&self) -> String {
render_markdown_for_terminal(&self.content)
}
}
#[allow(clippy::too_many_lines)]
#[must_use]
pub fn render_markdown_for_terminal(markdown: &str) -> String {
let mut output = String::new();
let mut in_code_block = false;
let mut code_block_content = String::new();
for line in markdown.lines() {
if line.starts_with("```") {
if in_code_block {
let dim = colors::text_secondary();
for code_line in code_block_content.lines() {
writeln!(output, " {}", code_line.truecolor(dim.0, dim.1, dim.2))
.expect("write to string should not fail");
}
code_block_content.clear();
in_code_block = false;
} else {
in_code_block = true;
}
continue;
}
if in_code_block {
code_block_content.push_str(line);
code_block_content.push('\n');
continue;
}
if let Some(header) = line.strip_prefix("### ") {
let cyan = colors::accent_secondary();
let dim = colors::text_dim();
writeln!(
output,
"\n{} {} {}",
"─".truecolor(cyan.0, cyan.1, cyan.2),
style_header_text(header)
.truecolor(cyan.0, cyan.1, cyan.2)
.bold(),
"─"
.repeat(30usize.saturating_sub(header.len()))
.truecolor(dim.0, dim.1, dim.2)
)
.expect("write to string should not fail");
} else if let Some(header) = line.strip_prefix("## ") {
let purple = colors::accent_primary();
let dim = colors::text_dim();
writeln!(
output,
"\n{} {} {}",
"─".truecolor(purple.0, purple.1, purple.2),
style_header_text(header)
.truecolor(purple.0, purple.1, purple.2)
.bold(),
"─"
.repeat(32usize.saturating_sub(header.len()))
.truecolor(dim.0, dim.1, dim.2)
)
.expect("write to string should not fail");
} else if let Some(header) = line.strip_prefix("# ") {
let purple = colors::accent_primary();
let cyan = colors::accent_secondary();
writeln!(
output,
"{} {} {}",
"━━━".truecolor(purple.0, purple.1, purple.2),
style_header_text(header)
.truecolor(cyan.0, cyan.1, cyan.2)
.bold(),
"━━━".truecolor(purple.0, purple.1, purple.2)
)
.expect("write to string should not fail");
}
else if let Some(content) = line.strip_prefix("- ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(content);
writeln!(
output,
" {} {}",
"•".truecolor(coral.0, coral.1, coral.2),
styled
)
.expect("write to string should not fail");
} else if let Some(content) = line.strip_prefix("* ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(content);
writeln!(
output,
" {} {}",
"•".truecolor(coral.0, coral.1, coral.2),
styled
)
.expect("write to string should not fail");
}
else if line.chars().next().is_some_and(|c| c.is_ascii_digit()) && line.contains(". ") {
if let Some((num, rest)) = line.split_once(". ") {
let coral = colors::accent_tertiary();
let styled = style_line_content(rest);
writeln!(
output,
" {} {}",
format!("{}.", num)
.truecolor(coral.0, coral.1, coral.2)
.bold(),
styled
)
.expect("write to string should not fail");
}
}
else if line.trim().is_empty() {
output.push('\n');
}
else {
let styled = style_line_content(line);
writeln!(output, "{styled}").expect("write to string should not fail");
}
}
output
}
fn style_header_text(text: &str) -> String {
text.to_uppercase()
}
#[allow(clippy::too_many_lines)]
fn style_line_content(content: &str) -> String {
let mut result = String::new();
let mut chars = content.chars().peekable();
let mut current_text = String::new();
let text_color = colors::text_secondary();
let error_color = colors::error();
let warning_color = colors::warning();
let coral_color = colors::accent_tertiary();
let cyan_color = colors::accent_secondary();
while let Some(ch) = chars.next() {
match ch {
'[' => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
let mut badge = String::new();
for c in chars.by_ref() {
if c == ']' {
break;
}
badge.push(c);
}
let badge_upper = badge.to_uppercase();
let styled_badge = match badge_upper.as_str() {
"CRITICAL" => format!(
"[{}]",
"CRITICAL"
.truecolor(error_color.0, error_color.1, error_color.2)
.bold()
),
"HIGH" => format!(
"[{}]",
"HIGH"
.truecolor(error_color.0, error_color.1, error_color.2)
.bold()
),
"MEDIUM" => format!(
"[{}]",
"MEDIUM"
.truecolor(warning_color.0, warning_color.1, warning_color.2)
.bold()
),
"LOW" => format!(
"[{}]",
"LOW"
.truecolor(coral_color.0, coral_color.1, coral_color.2)
.bold()
),
_ => format!(
"[{}]",
badge.truecolor(cyan_color.0, cyan_color.1, cyan_color.2)
),
};
result.push_str(&styled_badge);
}
'*' if chars.peek() == Some(&'*') => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
chars.next();
let mut bold = String::new();
while let Some(c) = chars.next() {
if c == '*' && chars.peek() == Some(&'*') {
chars.next(); break;
}
bold.push(c);
}
result.push_str(
&bold
.truecolor(cyan_color.0, cyan_color.1, cyan_color.2)
.bold()
.to_string(),
);
}
'`' => {
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
current_text.clear();
}
let mut code = String::new();
for c in chars.by_ref() {
if c == '`' {
break;
}
code.push(c);
}
result.push_str(
&code
.truecolor(warning_color.0, warning_color.1, warning_color.2)
.to_string(),
);
}
_ => {
current_text.push(ch);
}
}
}
if !current_text.is_empty() {
result.push_str(
¤t_text
.truecolor(text_color.0, text_color.1, text_color.2)
.to_string(),
);
}
result
}