use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{LazyLock, Mutex};
use ansi_to_tui::IntoText;
use pulldown_cmark::{
Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, LinkType, Options, Parser, Tag, TagEnd,
};
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::text::{Line, Span, Text};
use syntect::easy::HighlightLines;
use syntect::highlighting::{
Color as SynColor, ScopeSelectors, StyleModifier, Theme, ThemeItem, ThemeSettings,
};
use syntect::parsing::{SyntaxReference, SyntaxSet};
use syntect::util::{LinesWithEndings, as_24_bit_terminal_escaped};
use crate::shared::theme::Palette;
use crate::shared::wrap;
#[derive(Debug, Clone, Copy, Default)]
pub struct RenderOpts {
pub soft_break_as_newline: bool,
pub table_row_separators: bool,
pub render_mermaid: bool,
}
pub fn render(input: &str, width: usize, palette: &Palette) -> Text<'static> {
render_with(input, width, palette, RenderOpts::default())
}
pub fn render_with(
input: &str,
width: usize,
palette: &Palette,
opts: RenderOpts,
) -> Text<'static> {
let normalized = normalize_delimiters(input);
let mut parse_opts = Options::empty();
parse_opts.insert(Options::ENABLE_STRIKETHROUGH);
parse_opts.insert(Options::ENABLE_TASKLISTS);
parse_opts.insert(Options::ENABLE_MATH);
parse_opts.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(&normalized, parse_opts);
let mut writer = Writer::new(*palette, width);
writer.soft_break_as_newline = opts.soft_break_as_newline;
writer.table_row_separators = opts.table_row_separators;
writer.render_mermaid = opts.render_mermaid;
writer.run(&normalized, parser.into_offset_iter());
Text::from(writer.lines)
}
pub fn highlight_code(code: &str, lang: &str, palette: &Palette) -> Vec<Line<'static>> {
let Some(syntax) = resolve_syntax(lang) else {
return code
.split('\n')
.map(|l| Line::from(Span::styled(l.to_string(), Style::new().fg(palette.text))))
.collect();
};
let theme = code_theme(palette);
let mut hl = HighlightLines::new(syntax, theme);
let mut out: Vec<Line<'static>> = Vec::new();
for line in LinesWithEndings::from(code) {
out.extend(highlight_line_or_plain(&mut hl, line));
}
while out
.last()
.is_some_and(|l| l.spans.iter().all(|s| s.content.trim().is_empty()))
{
out.pop();
}
out
}
fn heading_style(level: u8, palette: &Palette) -> Style {
let base = Style::new().fg(palette.accent).add_modifier(Modifier::BOLD);
match level {
1 => base.add_modifier(Modifier::UNDERLINED),
2 => base,
_ => base.add_modifier(Modifier::ITALIC),
}
}
fn code_style() -> Style {
Style::new().add_modifier(Modifier::REVERSED)
}
fn inline_code_style(palette: &Palette) -> Style {
Style::new().fg(palette.keycap_fg).bg(palette.keycap_bg)
}
fn link_style(palette: &Palette) -> Style {
Style::new()
.fg(palette.accent)
.add_modifier(Modifier::UNDERLINED)
}
fn blockquote_style() -> Style {
Style::new().add_modifier(Modifier::DIM | Modifier::ITALIC)
}
static CODE_THEMES: LazyLock<Mutex<HashMap<Palette, &Theme>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn code_theme(palette: &Palette) -> &'static Theme {
let mut cache = CODE_THEMES.lock().expect("CODE_THEMES poisoned");
cache
.entry(*palette)
.or_insert_with(|| Box::leak(Box::new(build_code_theme(palette))))
}
mod code;
mod html;
mod latex;
mod mermaid;
mod speak;
mod table;
mod writer;
#[allow(unused_imports)]
pub use self::speak::speakable_text;
use self::{code::*, html::*, latex::*, mermaid::*, table::*, writer::*};
#[cfg(test)]
pub(super) mod testkit {
use super::*;
pub(super) fn rendered_text(input: &str) -> String {
rendered_text_w(input, 80)
}
pub(super) fn rendered_text_w(input: &str, width: usize) -> String {
render(input, width, &Palette::default())
.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn max_line_width(input: &str, width: usize) -> usize {
let text = render(input, width, &Palette::default());
text.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| wrap::display_width(&s.content.chars().collect::<Vec<_>>()))
.sum::<usize>()
})
.max()
.unwrap_or(0)
}
pub(super) fn fg_colors(input: &str, palette: &Palette) -> Vec<Color> {
render(input, 80, palette)
.lines
.iter()
.flat_map(|l| l.spans.iter())
.filter_map(|s| s.style.fg)
.collect()
}
pub(super) const TABLE_MD: &str = "\
| Алгоритм | Время | Память |
| :--- | :--- | :--- |
| QuickSort | O(n log n) | O(log n) |
| MergeSort | O(n log n) | O(n) |";
pub(super) const CODE_MD: &str = "```rust\nfn main() {\n let s = \"hi\";\n // c\n}\n```";
}