use std::{ cell::RefCell, collections::HashMap, fmt::Write as FmtWrite, io::{ErrorKind, Write as IoWrite}, path::Path, sync::LazyLock, }; use comrak::adapters::SyntaxHighlighterAdapter; use tracing::{debug, error}; use tree_sitter_grammar_repository::{Grammar, Language}; use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter}; thread_local! { static HIGHLIGHTER: RefCell = RefCell::new(Highlighter::new()); } macro_rules! count { () => (0); ($e:expr) => (1); ($e:expr, $($rest:expr),*) => (1 + count!($($rest),*)); } macro_rules! define_classes { ($($name:literal => $class:literal),*,) => { static HIGHLIGHT_NAMES: [&str; count!($($name),*)] = [ $($name),* ]; static HIGHLIGHT_CLASSES: [&str; count!($($name),*)] = [ $($class),* ]; }; } define_classes! { "attribute" => "attribute", "boolean" => "boolean", "carriage-return" => "carriage-return", "comment" => "comment", "comment.documentation" => "comment documentation", "constant" => "constant", "constant.builtin" => "constant builtin", "constructor" => "constructor", "constructor.builtin" => "constructor builtin", "embedded" => "embedded", "error" => "error", "escape" => "escape", "function" => "function", "function.builtin" => "function builtin", "keyword" => "keyword", "markup" => "markup", "markup.bold" => "markup bold", "markup.heading" => "markup heading", "markup.italic" => "markup italic", "markup.link" => "markup link", "markup.link.url" => "markup link url", "markup.list" => "markup list", "markup.list.checked" => "markup list checked", "markup.list.numbered" => "markup list numbered", "markup.list.unchecked" => "markup list unchecked", "markup.list.unnumbered" => "markup list unnumbered", "markup.quote" => "markup quote", "markup.raw" => "markup raw", "markup.raw.block" => "markup raw block", "markup.raw.inline" => "markup raw inline", "markup.strikethrough" => "markup strikethrough", "module" => "module", "number" => "number", "operator" => "operator", "property" => "property", "property.builtin" => "property builtin", "punctuation" => "punctuation", "punctuation.bracket" => "punctuation bracket", "punctuation.delimiter" => "punctuation delimiter", "punctuation.special" => "punctuation special", "string" => "string", "string.escape" => "string escape", "string.regexp" => "string regexp", "string.special" => "string special", "string.special.symbol" => "string special symbol", "tag" => "tag", "type" => "type", "type.builtin" => "type builtin", "variable" => "variable", "variable.builtin" => "variable builtin", "variable.member" => "variable member", "variable.parameter" => "variable parameter",} pub fn prime_highlighters() { let _res = HIGHLIGHTER_CONFIGS.len(); } static HIGHLIGHTER_CONFIGS: LazyLock> = LazyLock::new(|| { Grammar::VARIANTS .iter() .copied() .map(Grammar::highlight_configuration_params) .map(|v| { let mut configuration = HighlightConfiguration::new( v.language.into(), v.name, v.highlights_query, v.injection_query, v.locals_query, ) .unwrap_or_else(|e| panic!("bad query for {}: {e}", v.name)); configuration.configure(&HIGHLIGHT_NAMES); configuration }) .collect() }); pub fn fetch_highlighter_config(file: &Path) -> Option<&'static HighlightConfiguration> { Language::from_file_name(file) .map(Language::grammar) .map(Grammar::idx) .map(|idx| &HIGHLIGHTER_CONFIGS[idx]) } pub fn fetch_highlighter_config_by_token(token: &str) -> Option<&'static HighlightConfiguration> { Language::from_injection(token) .map(Language::grammar) .map(Grammar::idx) .map(|idx| &HIGHLIGHTER_CONFIGS[idx]) } pub struct ComrakHighlightAdapter; impl SyntaxHighlighterAdapter for ComrakHighlightAdapter { fn write_highlighted( &self, output: &mut dyn IoWrite, lang: Option<&str>, code: &str, ) -> std::io::Result<()> { let out = format_file(code, FileIdentifier::Token(lang.unwrap_or_default())) .map_err(|e| std::io::Error::new(ErrorKind::Other, e))?; output.write_all(out.as_bytes()) } fn write_pre_tag( &self, output: &mut dyn IoWrite, _attributes: HashMap, ) -> std::io::Result<()> { write!(output, r#"
"#)
    }

    fn write_code_tag(
        &self,
        _output: &mut dyn IoWrite,
        _attributes: HashMap,
    ) -> std::io::Result<()> {
        Ok(())
    }
}

#[derive(Copy, Clone, Debug)]
pub enum FileIdentifier<'a> {
    Path(&'a Path),
    Token(&'a str),
}

pub fn format_file(content: &str, identifier: FileIdentifier<'_>) -> anyhow::Result {
    let mut out = String::new();
    format_file_inner(&mut out, content, identifier, true)?;
    Ok(out)
}

pub fn format_file_inner(
    out: &mut String,
    content: &str,
    identifier: FileIdentifier<'_>,
    code_tag: bool,
) -> anyhow::Result<()> {
    let config = match identifier {
        FileIdentifier::Path(v) => fetch_highlighter_config(v),
        FileIdentifier::Token(v) => fetch_highlighter_config_by_token(v),
    };

    let line_prefix = if code_tag { "" } else { "" };
    let line_suffix = if code_tag { "\n" } else { "\n" };

    let Some(config) = config else {
        for line in content.lines() {
            out.push_str(line_prefix);
            v_htmlescape::b_escape(line.as_bytes(), out);
            out.push_str(line_suffix);
        }

        return Ok(());
    };

    HIGHLIGHTER.with_borrow_mut(|highlighter| {
        highlighter.parser().reset();

        let spans = highlighter.highlight(config, content.as_bytes(), None, |injection| {
            debug!(injection, "Highlighter switch requested");
            fetch_highlighter_config_by_token(injection)
        });

        let mut spans = match spans {
            Ok(v) => v,
            Err(error) => {
                error!(
                    ?error,
                    "Failed to run highlighter, falling back to plaintext"
                );

                for line in content.lines() {
                    out.push_str(line_prefix);
                    v_htmlescape::b_escape(line.as_bytes(), out);
                    out.push_str(line_suffix);
                }

                return Ok(());
            }
        };

        let mut tag_open = true;
        out.push_str(line_prefix);

        while let Some(span) = spans.next().transpose()? {
            if !tag_open {
                out.push_str(line_prefix);
                tag_open = true;
            }

            match span {
                HighlightEvent::Source { start, end } => {
                    let content = &content[start..end];

                    for (i, line) in content.lines().enumerate() {
                        if i != 0 {
                            out.push_str(line_suffix);
                            out.push_str(line_prefix);
                        }

                        v_htmlescape::b_escape(line.as_bytes(), out);
                    }

                    if content.ends_with('\n') {
                        out.push_str(line_suffix);
                        tag_open = false;
                    }
                }
                HighlightEvent::HighlightStart(highlight) => {
                    write!(
                        out,
                        r#""#,
                        HIGHLIGHT_CLASSES[highlight.0]
                    )?;
                }
                HighlightEvent::HighlightEnd => {
                    out.push_str("");
                }
            }
        }

        if tag_open {
            out.push_str(line_suffix);
        }

        Ok::<_, anyhow::Error>(())
    })?;

    Ok(())
}