Abbaye

at 8d154b4

use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd, html};

pub(crate) fn extract_local_refs(md: &str) -> Vec<String> {
    let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
    let mut refs = Vec::new();
    for event in Parser::new_ext(md, opts) {
        let url: Option<pulldown_cmark::CowStr> = match event {
            Event::Start(Tag::Image { dest_url, .. }) => Some(dest_url),
            Event::Start(Tag::Link { dest_url, .. }) => Some(dest_url),
            Event::End(TagEnd::Image | TagEnd::Link) => None,
            _ => None,
        };
        if let Some(url) = url {
            let s = url.as_ref();
            if !s.contains("://") && !s.starts_with('#') && !s.is_empty() {
                refs.push(s.to_owned());
            }
        }
    }
    refs
}

pub(crate) fn render_markdown_html(md: &str) -> String {
    let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
    let parser = Parser::new_ext(md, opts);
    let mut buf = String::new();
    html::push_html(&mut buf, parser);
    buf
}

pub(crate) fn render_markdown_gemtext(md: &str) -> String {
    let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH;
    let parser = Parser::new_ext(md, opts);

    let mut out: Vec<String> = Vec::new();
    let mut buf = String::new();
    let mut links: Vec<(String, String)> = Vec::new();
    let mut last_blank = false;

    enum Ctx {
        Paragraph,
        BlockQuote,
        ListItem,
        Heading(u8),
    }
    let mut stack: Vec<Ctx> = Vec::new();

    let mut in_link = false;
    let mut link_url = String::new();
    let mut link_text = String::new();

    let mut in_code = false;
    let mut code_kind = String::new();
    let mut code_body = String::new();

    fn flush_line(
        out: &mut Vec<String>,
        buf: &mut String,
        links: &mut Vec<(String, String)>,
        stack: &[Ctx],
        _last_blank: &mut bool,
    ) {
        let text = buf.trim();
        if text.is_empty() && links.is_empty() {
            return;
        }

        if let Some(Ctx::Heading(lvl)) = stack.last() {
            let prefix = match lvl {
                1 => "#",
                2 => "##",
                _ => "###",
            };
            if !text.is_empty() {
                out.push(format!("{prefix} {text}"));
            }
            for (url, t) in links.drain(..) {
                out.push(format!("=> {url} {t}"));
            }
            *_last_blank = false;
        } else if let Some(Ctx::BlockQuote) = stack.last() {
            if !text.is_empty() {
                for line in text.lines() {
                    out.push(format!("> {line}"));
                }
            }
            for (url, t) in links.drain(..) {
                out.push(format!("=> {url} {t}"));
            }
            *_last_blank = false;
        } else if let Some(Ctx::ListItem) = stack.last() {
            if !text.is_empty() {
                out.push(format!("* {text}"));
            }
            for (url, t) in links.drain(..) {
                out.push(format!("=> {url} {t}"));
            }
            *_last_blank = false;
        } else {
            if !text.is_empty() {
                out.push(text.to_string());
            }
            for (url, t) in links.drain(..) {
                out.push(format!("=> {url} {t}"));
            }
            *_last_blank = false;
        }
        buf.clear();
    }

    for event in parser {
        match event {
            Event::Start(tag) => match tag {
                Tag::Heading { level, .. } => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    let lvl = level as u8;
                    stack.push(Ctx::Heading(if lvl > 3 { 3 } else { lvl }));
                }
                Tag::Paragraph => {
                    stack.push(Ctx::Paragraph);
                }
                Tag::BlockQuote(_) => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    stack.push(Ctx::BlockQuote);
                }
                Tag::CodeBlock(kind) => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    in_code = true;
                    code_kind = match kind {
                        CodeBlockKind::Fenced(info) => info.to_string(),
                        CodeBlockKind::Indented => String::new(),
                    };
                    code_body.clear();
                }
                Tag::List { .. } => {}
                Tag::Item => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    stack.push(Ctx::ListItem);
                }
                Tag::Link { dest_url, .. } => {
                    in_link = true;
                    link_url = dest_url.to_string();
                    link_text.clear();
                }
                Tag::Image { dest_url, .. } => {
                    in_link = true;
                    link_url = dest_url.to_string();
                    link_text.clear();
                }
                _ => {}
            },
            Event::End(tag) => match tag {
                TagEnd::Heading(_) => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    stack.pop();
                }
                TagEnd::Paragraph => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    if !matches!(stack.last(), Some(Ctx::BlockQuote | Ctx::ListItem)) {
                        out.push(String::new());
                        last_blank = true;
                    }
                    stack.pop();
                }
                TagEnd::BlockQuote(_) => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    out.push(String::new());
                    last_blank = true;
                    stack.pop();
                }
                TagEnd::CodeBlock => {
                    if !code_body.is_empty() {
                        out.push(format!("```{}", code_kind));
                        for line in code_body.trim().lines() {
                            out.push(line.to_string());
                        }
                        out.push("```".to_string());
                        out.push(String::new());
                        last_blank = true;
                    }
                    in_code = false;
                    code_kind.clear();
                    code_body.clear();
                }
                TagEnd::List(_) => {
                    out.push(String::new());
                    last_blank = true;
                }
                TagEnd::Item => {
                    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);
                    stack.pop();
                }
                TagEnd::Link => {
                    let t = link_text.trim().to_string();
                    if in_link && !link_url.is_empty() && !t.is_empty() {
                        links.push((link_url.clone(), t));
                    }
                    in_link = false;
                    link_url.clear();
                    link_text.clear();
                }
                TagEnd::Image => {
                    let t = if link_text.trim().is_empty() {
                        "image".to_string()
                    } else {
                        link_text.trim().to_string()
                    };
                    if in_link {
                        out.push(format!("=> {} {}", link_url, t));
                    }
                    in_link = false;
                    link_url.clear();
                    link_text.clear();
                }
                _ => {}
            },
            Event::Text(text) => {
                if in_code {
                    code_body.push_str(&text);
                } else if in_link {
                    link_text.push_str(&text);
                } else {
                    buf.push_str(&text);
                }
            }
            Event::Code(text) => {
                if in_link {
                    link_text.push_str(&text);
                } else {
                    buf.push_str(&text);
                }
            }
            Event::SoftBreak | Event::HardBreak => {
                if in_code {
                    code_body.push('\n');
                } else {
                    buf.push(' ');
                }
            }
            _ => {}
        }
    }

    flush_line(&mut out, &mut buf, &mut links, &stack, &mut last_blank);

    while out.last().is_some_and(|l| l.is_empty()) {
        out.pop();
    }

    out.join("\n") + "\n"
}