rns-git 0.1.2

Reticulum Git transport tools
Documentation
#[cfg(feature = "syntax-highlighting")]
use std::path::Path;
#[cfg(feature = "syntax-highlighting")]
use std::sync::OnceLock;

#[cfg(feature = "syntax-highlighting")]
use syntect::easy::HighlightLines;
#[cfg(feature = "syntax-highlighting")]
use syntect::highlighting::{Style, Theme, ThemeSet};
#[cfg(feature = "syntax-highlighting")]
use syntect::parsing::{SyntaxReference, SyntaxSet};
#[cfg(feature = "syntax-highlighting")]
use syntect::util::LinesWithEndings;

pub(crate) fn literal_block(content: &str, path: Option<&str>, language: Option<&str>) -> String {
    let body =
        highlighted_content(content, path, language).unwrap_or_else(|| escape_micron(content));
    block_from_body(&body)
}

pub(crate) fn plain_literal_block(content: &str) -> String {
    block_from_body(&escape_micron(content))
}

fn block_from_body(body: &str) -> String {
    let mut out = String::from("`=\n");
    out.push_str(body);
    if !body.ends_with('\n') {
        out.push('\n');
    }
    out.push_str("`=\n");
    out
}

fn escape_micron(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('`', "\\`")
        .replace('\t', "   ")
}

#[cfg(not(feature = "syntax-highlighting"))]
fn highlighted_content(
    _content: &str,
    _path: Option<&str>,
    _language: Option<&str>,
) -> Option<String> {
    None
}

#[cfg(feature = "syntax-highlighting")]
fn highlighted_content(
    content: &str,
    path: Option<&str>,
    language: Option<&str>,
) -> Option<String> {
    let syntax_set = syntax_set();
    let syntax = syntax_for(syntax_set, path, language)?;
    let theme = theme()?;
    let mut highlighter = HighlightLines::new(syntax, theme);
    let mut out = String::new();

    for line in LinesWithEndings::from(content) {
        let ranges = highlighter.highlight_line(line, syntax_set).ok()?;
        for (style, text) in ranges {
            push_colored_span(&mut out, style, text);
        }
    }

    Some(escape_line_start_controls(&out))
}

#[cfg(feature = "syntax-highlighting")]
fn syntax_set() -> &'static SyntaxSet {
    static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
    SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
}

#[cfg(feature = "syntax-highlighting")]
fn theme_set() -> &'static ThemeSet {
    static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
    THEME_SET.get_or_init(ThemeSet::load_defaults)
}

#[cfg(feature = "syntax-highlighting")]
fn theme() -> Option<&'static Theme> {
    theme_set().themes.get("base16-ocean.dark")
}

#[cfg(feature = "syntax-highlighting")]
fn syntax_for<'a>(
    syntax_set: &'a SyntaxSet,
    path: Option<&str>,
    language: Option<&str>,
) -> Option<&'a SyntaxReference> {
    if let Some(language) = language.and_then(normalize_language) {
        if let Some(syntax) = syntax_set.find_syntax_by_token(language) {
            return Some(syntax);
        }
    }

    let extension = path
        .and_then(|path| Path::new(path).extension())
        .and_then(|extension| extension.to_str())
        .filter(|extension| !extension.is_empty())?;
    syntax_set.find_syntax_by_extension(extension)
}

#[cfg(feature = "syntax-highlighting")]
fn normalize_language(language: &str) -> Option<&str> {
    let language = language.trim();
    if language.is_empty() {
        None
    } else {
        Some(language)
    }
}

#[cfg(feature = "syntax-highlighting")]
fn push_colored_span(out: &mut String, style: Style, text: &str) {
    let mut escaped = escape_micron(text);
    while escaped.starts_with('\n') {
        out.push('\n');
        escaped.remove(0);
    }
    let mut trailing_newlines = 0;
    while escaped.ends_with('\n') {
        escaped.pop();
        trailing_newlines += 1;
    }
    if escaped.is_empty() {
        for _ in 0..trailing_newlines {
            out.push('\n');
        }
        return;
    }
    out.push_str(&format!(
        "`FT{:02x}{:02x}{:02x}",
        style.foreground.r, style.foreground.g, style.foreground.b
    ));
    out.push_str(&escaped);
    out.push_str("`f");
    for _ in 0..trailing_newlines {
        out.push('\n');
    }
}

#[cfg(feature = "syntax-highlighting")]
fn escape_line_start_controls(value: &str) -> String {
    let mut out = String::new();
    for segment in value.split_inclusive('\n') {
        let (line, newline) = segment
            .strip_suffix('\n')
            .map(|line| (line, "\n"))
            .unwrap_or((segment, ""));
        if line.starts_with('-') || line.starts_with('>') || line.starts_with('<') {
            out.push('\\');
        }
        out.push_str(line);
        out.push_str(newline);
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn plain_literal_blocks_escape_backslashes_ticks_and_expand_tabs() {
        let out = plain_literal_block("path\\name\t`tick`\n");

        assert!(out.contains("path\\\\name   \\`tick\\`"));
    }

    #[test]
    fn literal_block_fallback_escape_backslashes_ticks_and_expand_tabs() {
        let out = literal_block("path\\name\t`tick`\n", Some("blob.unknown"), None);

        assert!(!out.contains("`FT"));
        assert!(out.contains("path\\\\name   \\`tick\\`"));
    }

    #[cfg(feature = "syntax-highlighting")]
    #[test]
    fn highlighted_literal_blocks_keep_newlines_outside_color_tags_and_escape_headings() {
        let out = literal_block(">not a heading\nlet value = 1;\n", Some("main.rs"), None);
        assert!(!out.contains("`f\n`f"));
        assert!(out.ends_with("`=\n"));
    }

    #[cfg(feature = "syntax-highlighting")]
    #[test]
    fn highlighted_line_start_micron_controls_are_escaped() {
        let escaped = escape_line_start_controls("-dash\n>heading\n<align\nplain\n");
        assert_eq!(escaped, "\\-dash\n\\>heading\n\\<align\nplain\n");
    }
}