leaf-markdown-viewer 1.26.2

Terminal Markdown previewer with a GUI-like experience
use std::borrow::Cow;

pub(super) struct FenceInfo<'a> {
    pub(super) backtick_count: usize,
    pub(super) info: &'a str,
    pub(super) prefix: &'a str,
}

pub(super) fn normalize_code_fences(src: &str) -> Cow<'_, str> {
    if !needs_fence_normalization(src) {
        return Cow::Borrowed(src);
    }

    let raw_lines: Vec<&str> = src.lines().collect();
    let mut result: Vec<String> = raw_lines.iter().map(|l| l.to_string()).collect();
    let mut changed = false;
    let mut i = 0;

    while i < raw_lines.len() {
        let Some(fi) = parse_fence_line(raw_lines[i]) else {
            i += 1;
            continue;
        };
        if fi.info.is_empty() {
            i += 1;
            continue;
        }

        let open_line = i;
        let open_prefix = fi.prefix;
        let fence_len = fi.backtick_count;
        let mut max_inner_fence = 0usize;
        let mut close_line: Option<(usize, &str)> = None;
        let mut j = i + 1;
        let mut depth = 1usize;

        while j < raw_lines.len() && depth > 0 {
            if let Some(inner) = parse_fence_line(raw_lines[j]) {
                if !inner.info.is_empty() {
                    max_inner_fence = max_inner_fence.max(inner.backtick_count);
                    depth += 1;
                } else if depth > 1 {
                    depth -= 1;
                } else {
                    close_line = Some((j, inner.prefix));
                    depth = 0;
                }
            }
            j += 1;
        }

        if max_inner_fence >= fence_len {
            if let Some((cl, cl_prefix)) = close_line {
                let new_fence = "`".repeat(max_inner_fence + 1);
                result[open_line] = format!("{}{}{}", open_prefix, new_fence, fi.info);
                result[cl] = format!("{}{}", cl_prefix, new_fence);
                changed = true;
            }
        }

        i = j;
    }

    if changed {
        Cow::Owned(result.join("\n"))
    } else {
        Cow::Borrowed(src)
    }
}

pub(super) fn needs_fence_normalization(src: &str) -> bool {
    let mut in_fenced = false;
    let mut fence_len = 0usize;
    for line in src.lines() {
        if let Some(fi) = parse_fence_line(line) {
            if !in_fenced && !fi.info.is_empty() {
                in_fenced = true;
                fence_len = fi.backtick_count;
            } else if in_fenced {
                if !fi.info.is_empty() && fi.backtick_count >= fence_len {
                    return true;
                }
                if fi.info.is_empty() {
                    in_fenced = false;
                }
            }
        }
    }
    false
}

pub(super) fn parse_fence_line(line: &str) -> Option<FenceInfo<'_>> {
    let mut rest = line;
    let mut prefix_end = 0;

    loop {
        let trimmed = rest.trim_start();
        let spaces = rest.len() - trimmed.len();
        prefix_end += spaces;
        if let Some(after_gt) = trimmed.strip_prefix('>') {
            prefix_end += 1;
            rest = after_gt;
            if let Some(after_space) = rest.strip_prefix(' ') {
                prefix_end += 1;
                rest = after_space;
            }
        } else {
            break;
        }
    }

    let leading_spaces = rest.len() - rest.trim_start().len();
    if leading_spaces > 3 {
        return None;
    }
    let trimmed = rest.trim_start();
    let backtick_count = trimmed.chars().take_while(|c| *c == '`').count();
    if backtick_count >= 3 {
        let info = trimmed[backtick_count..].trim();
        Some(FenceInfo {
            backtick_count,
            info,
            prefix: &line[..prefix_end],
        })
    } else {
        None
    }
}