leaf-markdown-viewer 1.27.1

Terminal Markdown previewer with a GUI-like experience
use std::collections::HashMap;

use ratatui::{
    style::{Modifier, Style},
    text::{Line, Span},
};

use crate::theme::MarkdownTheme;

use super::blocks::push_rule_line;
use super::latex;
use super::lists::{ItemState, ListKind};
use super::spans::InlineStyleState;
use super::tables::TableBuf;
use super::toc::TocEntry;
use super::width::display_width;
use super::wrapping::push_wrapped_prefixed_lines;
use super::{LastBlock, LineMapState};
use ratatui::style::Color;

pub(super) fn to_superscript(n: usize) -> String {
    n.to_string().chars().map(latex::to_superscript).collect()
}

pub(super) fn push_footnote_reference_span(
    spans: &mut Vec<Span<'static>>,
    number: usize,
    theme: &MarkdownTheme,
) {
    spans.push(Span::styled(
        to_superscript(number),
        Style::default().fg(theme.footnote_ref),
    ));
}

pub(super) struct DefinitionSnapshot {
    pub(super) spans: Vec<Span<'static>>,
    pub(super) lines: Vec<Line<'static>>,
    pub(super) list_stack: Vec<ListKind>,
    pub(super) item_stack: Vec<ItemState>,
    pub(super) blockquote_depth: usize,
    pub(super) in_code: bool,
    pub(super) code_lang: String,
    pub(super) code_buf: String,
    pub(super) inline: InlineStyleState,
    pub(super) blockquote_color: Option<Color>,
    pub(super) in_heading: Option<u8>,
    pub(super) table: Option<TableBuf>,
    pub(super) last_block: LastBlock,
    pub(super) state: LineMapState,
    pub(super) toc: Vec<TocEntry>,
    pub(super) link_urls: Vec<String>,
}

impl DefinitionSnapshot {
    #[allow(clippy::too_many_arguments)]
    pub(super) fn take_from(
        spans: &mut Vec<Span<'static>>,
        lines: &mut Vec<Line<'static>>,
        list_stack: &mut Vec<ListKind>,
        item_stack: &mut Vec<ItemState>,
        blockquote_depth: &mut usize,
        in_code: &mut bool,
        code_lang: &mut String,
        code_buf: &mut String,
        inline: &mut InlineStyleState,
        blockquote_color: &mut Option<Color>,
        in_heading: &mut Option<u8>,
        table: &mut Option<TableBuf>,
        last_block: &mut LastBlock,
        state: &mut LineMapState,
        toc: &mut Vec<TocEntry>,
        link_urls: &mut Vec<String>,
    ) -> Self {
        Self {
            spans: std::mem::take(spans),
            lines: std::mem::take(lines),
            list_stack: std::mem::take(list_stack),
            item_stack: std::mem::take(item_stack),
            blockquote_depth: std::mem::replace(blockquote_depth, 0),
            in_code: std::mem::replace(in_code, false),
            code_lang: std::mem::take(code_lang),
            code_buf: std::mem::take(code_buf),
            inline: std::mem::take(inline),
            blockquote_color: std::mem::take(blockquote_color),
            in_heading: std::mem::take(in_heading),
            table: std::mem::take(table),
            last_block: std::mem::replace(last_block, LastBlock::Other),
            state: std::mem::replace(state, LineMapState::new()),
            toc: std::mem::take(toc),
            link_urls: std::mem::take(link_urls),
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub(super) fn restore_into(
        self,
        spans: &mut Vec<Span<'static>>,
        lines: &mut Vec<Line<'static>>,
        list_stack: &mut Vec<ListKind>,
        item_stack: &mut Vec<ItemState>,
        blockquote_depth: &mut usize,
        in_code: &mut bool,
        code_lang: &mut String,
        code_buf: &mut String,
        inline: &mut InlineStyleState,
        blockquote_color: &mut Option<Color>,
        in_heading: &mut Option<u8>,
        table: &mut Option<TableBuf>,
        last_block: &mut LastBlock,
        state: &mut LineMapState,
        toc: &mut Vec<TocEntry>,
        link_urls: &mut Vec<String>,
    ) {
        *spans = self.spans;
        *lines = self.lines;
        *list_stack = self.list_stack;
        *item_stack = self.item_stack;
        *blockquote_depth = self.blockquote_depth;
        *in_code = self.in_code;
        *code_lang = self.code_lang;
        *code_buf = self.code_buf;
        *inline = self.inline;
        *blockquote_color = self.blockquote_color;
        *in_heading = self.in_heading;
        *table = self.table;
        *last_block = self.last_block;
        *state = self.state;
        *toc = self.toc;
        *link_urls = self.link_urls;
    }
}

pub(super) struct ActiveDefinition {
    pub(super) label: String,
    pub(super) snapshot: DefinitionSnapshot,
}

#[derive(Default)]
pub(super) struct FootnotesBuf {
    refs_order: Vec<String>,
    refs_index: HashMap<String, usize>,
    defs_order: Vec<String>,
    definitions: HashMap<String, Vec<Line<'static>>>,
    def_source_line: HashMap<String, usize>,
    def_link_urls: HashMap<String, Vec<String>>,
    active: Option<ActiveDefinition>,
}

impl FootnotesBuf {
    pub(super) fn register_reference(&mut self, label: &str) -> usize {
        if let Some(&n) = self.refs_index.get(label) {
            return n;
        }
        let n = self.refs_index.len() + 1;
        self.refs_index.insert(label.to_string(), n);
        self.refs_order.push(label.to_string());
        n
    }

    pub(super) fn start_definition(
        &mut self,
        label: String,
        src_line: usize,
        snapshot: DefinitionSnapshot,
    ) {
        if !self.def_source_line.contains_key(&label) {
            self.defs_order.push(label.clone());
        }
        self.def_source_line.insert(label.clone(), src_line);
        self.active = Some(ActiveDefinition { label, snapshot });
    }

    pub(super) fn finish_definition(
        &mut self,
        captured_lines: Vec<Line<'static>>,
        captured_link_urls: Vec<String>,
    ) -> DefinitionSnapshot {
        let ActiveDefinition { label, snapshot } = self
            .active
            .take()
            .expect("finish_definition without active definition");
        self.definitions.insert(label.clone(), captured_lines);
        self.def_link_urls.insert(label, captured_link_urls);
        snapshot
    }

    pub(super) fn is_active(&self) -> bool {
        self.active.is_some()
    }

    pub(super) fn flush(
        &mut self,
        lines: &mut Vec<Line<'static>>,
        state: &mut LineMapState,
        link_urls: &mut Vec<String>,
        theme: &MarkdownTheme,
        render_width: usize,
    ) {
        if self.definitions.is_empty() {
            return;
        }

        let mut ordered: Vec<(String, usize)> = Vec::new();
        for label in &self.refs_order {
            if let Some(&n) = self.refs_index.get(label) {
                ordered.push((label.clone(), n));
            }
        }
        let mut next_number = self.refs_index.len() + 1;
        for label in &self.defs_order {
            if self.refs_index.contains_key(label) {
                continue;
            }
            ordered.push((label.clone(), next_number));
            next_number += 1;
        }

        push_rule_line(lines, render_width, theme);
        state.mark_all_new(lines.len());
        let title = "Notes";
        lines.push(Line::from(Span::styled(
            title.to_string(),
            Style::default()
                .fg(theme.footnote_ref)
                .add_modifier(Modifier::BOLD),
        )));
        lines.push(Line::from(Span::styled(
            "".repeat(display_width(title)),
            Style::default().fg(theme.heading_underline),
        )));
        state.mark_all_new(lines.len());

        for (label, number) in ordered {
            let Some(mut def_lines) = self.definitions.remove(&label) else {
                continue;
            };
            while def_lines.last().is_some_and(super::is_empty_line) {
                def_lines.pop();
            }
            if let Some(src) = self.def_source_line.get(&label) {
                state.current_src_line = *src;
            }
            let prefix_text = format!("{} ", to_superscript(number));
            let prefix_width = display_width(&prefix_text);
            let indent = " ".repeat(prefix_width);
            let mut first = true;
            for def_line in def_lines {
                let mut body: Vec<Span<'static>> = def_line.spans.into_iter().collect();
                recolor_default_text(&mut body, theme.text, theme.footnote_text);
                if first {
                    first = false;
                    let first_prefix = vec![Span::styled(
                        prefix_text.clone(),
                        Style::default().fg(theme.footnote_ref),
                    )];
                    let continuation_prefix = vec![Span::raw(indent.clone())];
                    push_wrapped_prefixed_lines(
                        lines,
                        &mut body,
                        first_prefix,
                        continuation_prefix,
                        render_width,
                    );
                } else if body.is_empty() {
                    lines.push(Line::from(""));
                } else {
                    let first_prefix = vec![Span::raw(indent.clone())];
                    let continuation_prefix = vec![Span::raw(indent.clone())];
                    push_wrapped_prefixed_lines(
                        lines,
                        &mut body,
                        first_prefix,
                        continuation_prefix,
                        render_width,
                    );
                }
            }
            state.mark_all_new(lines.len());
            if let Some(mut urls) = self.def_link_urls.remove(&label) {
                link_urls.append(&mut urls);
            }
        }

        lines.push(Line::from(""));
        state.mark_all_new(lines.len());
    }
}

fn recolor_default_text(spans: &mut [Span<'static>], from: Color, to: Color) {
    for span in spans.iter_mut() {
        if span.style.fg == Some(from) {
            span.style.fg = Some(to);
        }
    }
}