wikimark 0.5.2

Markdown-based wiki stored in a git repo
Documentation
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Parser, Tag, TagEnd};
use slug::slugify;
use std::sync::OnceLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::html::{
    start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground,
};
use syntect::parsing::{SyntaxReference, SyntaxSet};

fn get_syntax_for_block<'a>(set: &'a SyntaxSet, hint: &str) -> &'a SyntaxReference {
    set.find_syntax_by_name(hint).unwrap_or_else(|| {
        set.find_syntax_by_extension(hint)
            .unwrap_or_else(|| set.find_syntax_plain_text())
    })
}

use super::page::{Metadata, Page, Section, Toc};
use slab_tree::Tree;

enum ParsingPhase<'a> {
    Normal,
    Code(Box<HighlightLines<'a>>),
    Header(String),
}

pub struct ParseContext {
    syntax_set: SyntaxSet,
    theme_set: ThemeSet,
}
impl ParseContext {
    pub fn new() -> ParseContext {
        ParseContext {
            syntax_set: SyntaxSet::load_defaults_newlines(),
            theme_set: ThemeSet::load_defaults(),
        }
    }
}

static PARSE_CONTEXT: OnceLock<ParseContext> = OnceLock::new();

pub fn parse(md: &str, meta: &Metadata) -> Page {
    let parse_context = PARSE_CONTEXT.get_or_init(ParseContext::new);
    let theme = &parse_context.theme_set.themes["base16-ocean.dark"];
    let parser = Parser::new(md);
    let mut out = String::new();
    let mut phase = ParsingPhase::Normal;
    let mut toc_tree = Tree::new();
    toc_tree.set_root(Section {
        link: "".to_owned(),
        title: meta.title.clone(),
        level: 0,
    });
    let mut cur_section = toc_tree.root_mut().unwrap().node_id();

    {
        let toc = &mut toc_tree;
        let parser = parser.filter_map(move |event| match event {
            Event::Start(Tag::CodeBlock(ref info)) => {
                let info = match info {
                    CodeBlockKind::Indented => "",
                    CodeBlockKind::Fenced(i) => i,
                };
                let syntax = get_syntax_for_block(&parse_context.syntax_set, info);
                let highlighter = Box::new(HighlightLines::new(syntax, theme));
                phase = ParsingPhase::Code(highlighter);
                let snippet = start_highlighted_html_snippet(theme);
                Some(Event::Html(CowStr::Boxed(snippet.0.into_boxed_str())))
            }
            Event::End(TagEnd::CodeBlock) => {
                phase = ParsingPhase::Normal;
                Some(Event::Html(CowStr::Borrowed("</pre>")))
            }
            Event::Text(text) => match phase {
                ParsingPhase::Code(ref mut highlighter) => {
                    let ranges = highlighter
                        .highlight_line(&text, &parse_context.syntax_set)
                        .unwrap();
                    let h = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::Yes)
                        .unwrap();
                    Some(Event::Html(CowStr::Boxed(h.into_boxed_str())))
                }
                ParsingPhase::Header(ref mut h) => {
                    h.push_str(&text);
                    None
                }
                ParsingPhase::Normal => Some(Event::Text(text)),
            },
            Event::Start(Tag::Heading { level, .. }) => {
                let level = level as i32;
                if level <= toc.get_mut(cur_section).unwrap().data().level {
                    cur_section = toc
                        .get_mut(cur_section)
                        .unwrap()
                        .parent()
                        .expect("no parent")
                        .node_id();
                }
                cur_section = toc
                    .get_mut(cur_section)
                    .unwrap()
                    .append(Section {
                        link: String::new(),
                        title: String::new(),
                        level,
                    })
                    .node_id();
                phase = ParsingPhase::Header(String::new());
                None
            }
            Event::End(TagEnd::Heading(_)) => {
                let mut cur_phase = ParsingPhase::Normal;
                std::mem::swap(&mut cur_phase, &mut phase);
                let h = match cur_phase {
                    ParsingPhase::Header(h) => h,
                    _ => panic!("impossible phase"),
                };
                let mut sec = toc.get_mut(cur_section).unwrap();
                let data = sec.data();
                data.link = slugify(&h);
                data.title = h;
                Some(Event::Html(CowStr::from(format!(
                    "<h{n} id=\"{id}\">{t} <a class=\"zola-anchor\" href=\"#{id}\">🔗</a></h{n}>",
                    n = data.level,
                    id = data.link,
                    t = data.title
                ))))
            }
            _ => Some(event),
        });
        html::push_html(&mut out, parser);
    }
    Page {
        toc: Toc::new(toc_tree),
        content: out,
    }
}