mdv 4.2.1

Terminal Markdown Viewer
Documentation
use super::event::EventRenderer;
use super::syntax_set::load_full_syntax_set;
use super::syntax_theme::{CodeHighlightTheme, build_syntect_theme, default_theme_set};
use crate::config::Config;
use crate::theme::{Theme, ThemeManager, apply_custom_code_theme, apply_custom_theme};
use crate::user_themes;
use anyhow::Result;
use pulldown_cmark::Event;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;

/// Terminal renderer for markdown content
pub struct TerminalRenderer {
    config: Config,
    theme: Theme,
    syntax_set: &'static SyntaxSet,
    code_theme: CodeHighlightTheme,
}

impl TerminalRenderer {
    pub fn new(config: &Config) -> Result<Self> {
        let theme_manager = build_theme_manager(config);
        let mut theme = theme_manager.get_theme(&config.theme)?.clone();
        if let Some(overrides) = &config.custom_theme {
            apply_custom_theme(&mut theme, overrides)?;
        }

        if let Some(overrides) = &config.custom_code_theme {
            apply_custom_code_theme(&mut theme, overrides)?;
        }

        if (config.custom_theme.is_some() || config.custom_code_theme.is_some())
            && !theme.name.ends_with("+custom")
        {
            theme.name = format!("{}+custom", theme.name);
        }

        let syntax_set = load_full_syntax_set();
        let theme_set = default_theme_set();

        let code_theme = if config.custom_code_theme.is_some() {
            if config.code_theme.is_some() {
                log::info!(
                    "Ignoring '--code-theme' because '--custom-code-theme' overrides are applied."
                );
            }
            build_syntect_theme(&theme)
        } else {
            match config.code_theme.as_ref() {
                Some(requested_theme) => {
                    resolve_code_theme(requested_theme, &theme, &theme_manager, theme_set)
                }
                None => build_syntect_theme(&theme),
            }
        };

        Ok(Self {
            config: config.clone(),
            theme,
            syntax_set,
            code_theme,
        })
    }

    pub fn render(&self, events: Vec<Event<'static>>) -> Result<String> {
        let mut renderer =
            EventRenderer::new(&self.config, &self.theme, self.syntax_set, &self.code_theme);
        renderer.render_events(events)
    }

    pub fn to_html(&self, events: Vec<Event<'static>>) -> Result<String> {
        let events = events.into_iter().map(|event| match event {
            Event::Html(html) if html.as_ref().trim() == crate::markdown::BLANK_LINE_MARKER => {
                Event::HardBreak
            }
            Event::InlineHtml(html)
                if html.as_ref().trim() == crate::markdown::BLANK_LINE_MARKER =>
            {
                Event::HardBreak
            }
            other => other,
        });
        let mut html_output = String::new();
        pulldown_cmark::html::push_html(&mut html_output, events);
        Ok(html_output)
    }
}

fn resolve_code_theme(
    requested_theme: &str,
    main_theme: &Theme,
    theme_manager: &ThemeManager,
    theme_set: &ThemeSet,
) -> CodeHighlightTheme {
    if let Some(theme) = theme_set.themes.get(requested_theme) {
        return CodeHighlightTheme::syntect_only(theme.clone());
    }

    if let Some((actual_name, theme)) = theme_set
        .themes
        .iter()
        .find(|(name, _)| name.eq_ignore_ascii_case(requested_theme))
    {
        if actual_name.as_str() != requested_theme {
            log::info!(
                "Using syntax theme '{}' for '--code-theme {}'.",
                actual_name,
                requested_theme
            );
        }
        return CodeHighlightTheme::syntect_only(theme.clone());
    }

    if let Some(builtin_theme) = find_builtin_theme(theme_manager, requested_theme) {
        if !builtin_theme.name.eq_ignore_ascii_case(requested_theme) {
            log::info!(
                "Using built-in theme '{}' for '--code-theme {}'.",
                builtin_theme.name,
                requested_theme
            );
        }
        return build_syntect_theme(builtin_theme);
    }

    log::warn!(
        "Code theme '{}' not found; falling back to '{}'.",
        requested_theme,
        main_theme.name
    );
    build_syntect_theme(main_theme)
}

fn find_builtin_theme<'a>(
    theme_manager: &'a ThemeManager,
    requested_theme: &str,
) -> Option<&'a Theme> {
    if let Ok(theme) = theme_manager.get_theme(requested_theme) {
        return Some(theme);
    }

    let requested_lower = requested_theme.to_ascii_lowercase();
    theme_manager
        .list_themes()
        .into_iter()
        .find(|name| name.to_ascii_lowercase() == requested_lower)
        .and_then(|matched_name| theme_manager.get_theme(matched_name).ok())
}

pub(crate) fn build_theme_manager(config: &Config) -> ThemeManager {
    let mut manager = ThemeManager::new();
    if let Some(config_dir) = &config.config_dir {
        match user_themes::load_user_themes(config_dir, &manager) {
            Ok(themes) => {
                for theme in themes {
                    manager.add_theme(theme);
                }
            }
            Err(err) => {
                log::warn!(
                    "Failed to load user themes from '{}': {}",
                    config_dir.display(),
                    err
                );
            }
        }
    }
    manager
}