leaf-markdown-viewer 1.26.2

Terminal Markdown previewer with a GUI-like experience
use crate::theme::MarkdownTheme;
use ratatui::{
    style::{Modifier, Style},
    text::{Line, Span},
};
use std::{borrow::Cow, ops::Range};

use super::width;
use super::LINK_MARKER;

pub(super) fn find_match_char_ranges(
    text_chars: &[char],
    query_lower: &[Vec<char>],
) -> Vec<Range<usize>> {
    let qlen = query_lower.len();
    if qlen == 0 || text_chars.len() < qlen {
        return Vec::new();
    }
    let mut ranges = Vec::new();
    let mut i = 0;
    while i + qlen <= text_chars.len() {
        let matched = text_chars[i..i + qlen]
            .iter()
            .zip(query_lower)
            .all(|(t, ql)| t.to_lowercase().eq(ql.iter().copied()));
        if matched {
            ranges.push(i..i + qlen);
            i += qlen;
        } else {
            i += 1;
        }
    }
    ranges
}

pub(super) fn content_span_start(spans: &[Span<'_>]) -> usize {
    let has_pipe = spans.iter().take(4).any(|s| s.content.contains(''));
    if !has_pipe {
        return 0;
    }
    for (i, span) in spans.iter().enumerate() {
        if width::is_code_gutter_span(span.content.as_ref()) {
            return i + 1;
        }
    }
    0
}

pub(super) fn overlapping_ranges(ranges: &[Range<usize>], start: usize, end: usize) -> bool {
    ranges.iter().any(|r| r.start < end && r.end > start)
}

pub(crate) fn highlight_line<'a>(line: &Line<'a>, theme: &MarkdownTheme, query: &str) -> Line<'a> {
    let spans = &line.spans;
    let content_start = content_span_start(spans);

    let text_chars: Vec<char> = spans[content_start..]
        .iter()
        .filter(|s| s.content.as_ref() != LINK_MARKER)
        .flat_map(|s| s.content.chars())
        .collect();
    let query_lower: Vec<Vec<char>> = query.chars().map(|c| c.to_lowercase().collect()).collect();
    let ranges = find_match_char_ranges(&text_chars, &query_lower);
    if ranges.is_empty() {
        return line.clone();
    }

    let line_bg = theme.search_highlight_bg;
    let match_style_extra = Style::default()
        .bg(theme.search_match_bg)
        .add_modifier(Modifier::BOLD);
    let mut result: Vec<Span<'a>> = Vec::with_capacity(spans.len());

    for span in &spans[..content_start] {
        result.push(Span::styled(span.content.clone(), span.style.bg(line_bg)));
    }

    let mut char_offset: usize = 0;
    for span in &spans[content_start..] {
        if span.content.as_ref() == LINK_MARKER {
            result.push(span.clone());
            continue;
        }
        let span_len = span.content.chars().count();
        let span_end = char_offset + span_len;
        let base_style = span.style.bg(line_bg);

        if !overlapping_ranges(&ranges, char_offset, span_end) {
            result.push(Span::styled(span.content.clone(), base_style));
            char_offset = span_end;
            continue;
        }

        let span_chars: Vec<char> = span.content.chars().collect();
        let mut pos = 0;
        for range in &ranges {
            if range.end <= char_offset || range.start >= span_end {
                continue;
            }
            let local_start = range.start.saturating_sub(char_offset);
            let local_end = (range.end - char_offset).min(span_len);

            if local_start > pos {
                let text: String = span_chars[pos..local_start].iter().collect();
                result.push(Span::styled(Cow::Owned(text), base_style));
            }
            let text: String = span_chars[local_start..local_end].iter().collect();
            result.push(Span::styled(
                Cow::Owned(text),
                span.style.patch(match_style_extra),
            ));
            pos = local_end;
        }
        if pos < span_len {
            let text: String = span_chars[pos..].iter().collect();
            result.push(Span::styled(Cow::Owned(text), base_style));
        }

        char_offset = span_end;
    }

    Line::from(result)
}