kanban-tui 0.4.0

Terminal user interface for the kanban project management tool
Documentation
use pulldown_cmark::{CowStr, Event, Parser, Tag, TagEnd};
use ratatui::prelude::Stylize;
use ratatui::style::Style;
use ratatui::text::{Line, Span};

pub fn render_markdown(text: &str) -> Vec<Line<'static>> {
    let parser = Parser::new(text);
    let mut renderer = MarkdownRenderer::new();

    for event in parser {
        renderer.process_event(event);
    }

    renderer.finish()
}

struct MarkdownRenderer {
    lines: Vec<Line<'static>>,
    current_line: Vec<Span<'static>>,
    in_code_block: bool,
    code_block_lang: String,
    code_block_content: String,
    in_emphasis: bool,
    in_strong: bool,
}

impl MarkdownRenderer {
    fn new() -> Self {
        Self {
            lines: Vec::new(),
            current_line: Vec::new(),
            in_code_block: false,
            code_block_lang: String::new(),
            code_block_content: String::new(),
            in_emphasis: false,
            in_strong: false,
        }
    }

    fn process_event(&mut self, event: Event) {
        match event {
            Event::Start(tag) => self.handle_tag_start(tag),
            Event::End(tag_end) => self.handle_tag_end(tag_end),
            Event::Text(text) => self.handle_text(text),
            Event::Code(code) => self.handle_inline_code(code),
            Event::SoftBreak | Event::HardBreak => self.handle_break(),
            _ => {}
        }
    }

    fn handle_tag_start(&mut self, tag: Tag) {
        match tag {
            Tag::CodeBlock(kind) => {
                self.in_code_block = true;
                self.code_block_lang = match kind {
                    pulldown_cmark::CodeBlockKind::Fenced(lang) => lang.to_string(),
                    pulldown_cmark::CodeBlockKind::Indented => String::new(),
                };
            }
            Tag::Emphasis => {
                self.in_emphasis = true;
            }
            Tag::Strong => {
                self.in_strong = true;
            }
            Tag::Paragraph | Tag::Heading { .. } => {
                if !self.current_line.is_empty() {
                    self.flush_line();
                }
            }
            Tag::List(_) => {}
            Tag::Item => {}
            _ => {}
        }
    }

    fn handle_tag_end(&mut self, tag: TagEnd) {
        match tag {
            TagEnd::CodeBlock => {
                self.in_code_block = false;
                self.render_code_block();
            }
            TagEnd::Emphasis => {
                self.in_emphasis = false;
            }
            TagEnd::Strong => {
                self.in_strong = false;
            }
            TagEnd::Paragraph | TagEnd::Heading(_) | TagEnd::Item => {
                if !self.current_line.is_empty() {
                    self.flush_line();
                }
                self.lines.push(Line::from(""));
            }
            _ => {}
        }
    }

    fn handle_text(&mut self, text: CowStr) {
        if self.in_code_block {
            self.code_block_content.push_str(&text);
        } else {
            let mut style = Style::default();
            if self.in_strong {
                style = style.bold();
            }
            if self.in_emphasis {
                style = style.italic();
            }

            let text_str = text.to_string();
            self.current_line.push(Span::styled(text_str, style));
        }
    }

    fn handle_inline_code(&mut self, code: CowStr) {
        let style = Style::default().italic();
        self.current_line
            .push(Span::styled(format!("`{}`", code), style));
    }

    fn handle_break(&mut self) {
        self.flush_line();
    }

    fn render_code_block(&mut self) {
        if self.code_block_content.is_empty() {
            return;
        }

        self.lines.push(Line::from(""));

        let highlighted_lines = highlight_code(&self.code_block_lang, &self.code_block_content);

        for line_spans in highlighted_lines {
            let mut indented_spans = vec![Span::raw("  ")];
            indented_spans.extend(line_spans);
            self.lines.push(Line::from(indented_spans));
        }

        self.lines.push(Line::from(""));
        self.code_block_content.clear();
    }

    fn flush_line(&mut self) {
        if !self.current_line.is_empty() {
            let line = Line::from(std::mem::take(&mut self.current_line));
            self.lines.push(line);
        }
    }

    fn finish(mut self) -> Vec<Line<'static>> {
        self.flush_line();
        self.lines
    }
}

fn highlight_code(_language: &str, code: &str) -> Vec<Vec<Span<'static>>> {
    code.lines()
        .map(|line| vec![Span::raw(line.to_string())])
        .collect()
}