use std::{ io::{stdout, BufWriter, Write}, path::PathBuf, str::FromStr, }; use syntect::{ dumps::{dump_to_uncompressed_file, from_uncompressed_dump_file}, highlighting::{Color, FontStyle, ScopeSelectors, ThemeSet}, parsing::{ParseState, ScopeStack, SyntaxSet, SyntaxSetBuilder}, }; fn main() { match std::env::var("CGIT_SYNTECT_MODE").as_deref().ok() { Some("compile") => compile(), Some("theme") => theme(), None | Some("") | Some("highlight") => highlight(), _ => panic!("unknown mode"), } } fn compile() { let mut args = std::env::args(); args.next(); // arg0 let input: PathBuf = args.next().unwrap().into(); let output: PathBuf = args.next().unwrap().into(); let mut builder = SyntaxSetBuilder::new(); builder.add_plain_text_syntax(); builder.add_from_folder(input, true).unwrap(); let set = builder.build(); dump_to_uncompressed_file(&set, output).unwrap(); } fn theme() { let mut args = std::env::args(); args.next(); // arg0 let input: PathBuf = args.next().unwrap().into(); let css_path: PathBuf = args.next().unwrap().into(); let scopes_path: PathBuf = args.next().unwrap().into(); let theme = ThemeSet::get_theme(input).unwrap(); let mut css_gen = Vec::new(); let mut scopes_gen = Vec::new(); let mut global_css = String::new(); global_css.push_str(".highlight {\n"); if let Some(bg) = theme.settings.background { global_css.push_str(" background-color: #"); global_css.extend(hex_color(bg)); global_css.push_str(";\n"); } global_css.push_str("}\n"); css_gen.push(global_css); for (idx, item) in theme.scopes.into_iter().enumerate() { let selectors_str = selectors_to_string(item.scope); let mut css = String::new(); css.push_str(&format!("/* {selectors_str} */\n")); css.push_str(&format!(".hl-style{idx} {{\n")); if let Some(fg) = item.style.foreground { css.push_str(" color: #"); css.extend(hex_color(fg)); css.push_str(";\n"); } if let Some(bg) = item.style.background { css.push_str(" background-color: #"); css.extend(hex_color(bg)); css.push_str(";\n"); } if let Some(font) = item.style.font_style { if font.contains(FontStyle::BOLD) { css.push_str(" font-weight: bold;\n"); } if font.contains(FontStyle::UNDERLINE) { css.push_str(" text-decoration: underline;\n"); } if font.contains(FontStyle::ITALIC) { css.push_str(" font-style: italic;\n"); } } css.push_str("}\n"); css_gen.push(css); scopes_gen.push(selectors_str); } std::fs::write(css_path, css_gen.join("\n")).unwrap(); std::fs::write(scopes_path, scopes_gen.join("\n")).unwrap(); } fn selectors_to_string(selectors: ScopeSelectors) -> String { let mut scopes = Vec::new(); for scope in selectors.selectors { let mut ser = String::new(); ser.push_str(scope.path.to_string().trim()); for exc in scope.excludes { ser.push_str(" -"); ser.push_str(exc.to_string().trim()); } scopes.push(ser); } scopes.join(", ") } fn hex_color(color: Color) -> [char; 8] { const HEX_DIGIT_LOOKUP: &[u8; 16] = b"0123456789abcdef"; [ HEX_DIGIT_LOOKUP[(color.r >> 4) as usize] as char, HEX_DIGIT_LOOKUP[(color.r & 0xf) as usize] as char, HEX_DIGIT_LOOKUP[(color.g >> 4) as usize] as char, HEX_DIGIT_LOOKUP[(color.g & 0xf) as usize] as char, HEX_DIGIT_LOOKUP[(color.b >> 4) as usize] as char, HEX_DIGIT_LOOKUP[(color.b & 0xf) as usize] as char, HEX_DIGIT_LOOKUP[(color.a >> 4) as usize] as char, HEX_DIGIT_LOOKUP[(color.a & 0xf) as usize] as char, ] } fn highlight() { let mut args = std::env::args(); args.next(); // arg0 let highlight_file: PathBuf = args.next().unwrap().into(); let syntax_dump = std::env::var("CGIT_SYNTECT_SYNTAXES") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/usr/share/cgit-syntect/syntax.packdump")); let set: SyntaxSet = from_uncompressed_dump_file(syntax_dump).unwrap(); let scope_dump = std::env::var("CGIT_SYNTECT_SCOPES") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/usr/share/cgit-syntect/scopes")); let scopes = std::fs::read_to_string(scope_dump) .unwrap() .lines() .map(ScopeSelectors::from_str) .map(|sels| sels.unwrap()) .collect::>(); let mut line = String::new(); std::io::stdin().read_line(&mut line).unwrap(); if !line.ends_with('\n') { line.push('\n'); } let file_name = highlight_file .file_name() .and_then(|n| n.to_str()) .unwrap_or(""); let extension = highlight_file .extension() .and_then(|x| x.to_str()) .unwrap_or(""); let syntax = set .find_syntax_by_extension(file_name) .or_else(|| set.find_syntax_by_extension(extension)) .or_else(|| set.find_syntax_by_first_line(&line)) .unwrap_or_else(|| set.find_syntax_plain_text()); let mut stdout = BufWriter::new(stdout().lock()); write!(&mut stdout, r#"
"#).unwrap();

    let mut parse = ParseState::new(syntax);
    let mut stack = ScopeStack::new();
    loop {
        let mut cursor = 0;
        let ops = parse.parse_line(&line, &set).unwrap();
        let mut spans = Vec::new();

        for (at, op) in ops {
            if at != cursor {
                let mut max_power = f64::NEG_INFINITY;
                let mut selected = None;
                for (idx, scope) in scopes.iter().enumerate() {
                    if let Some(power) = scope.does_match(stack.as_slice()) {
                        if power.0 > max_power {
                            max_power = power.0;
                            selected = Some(idx);
                        }
                    }
                }
                spans.push((&line[cursor..at], selected));
            }
            stack.apply(&op).unwrap();
            cursor = at;
        }

        if line.len() != cursor {
            spans.push((&line[cursor..], None));
        }

        for (section, class) in spans {
            if let Some(idx) = class {
                write!(&mut stdout, r#""#).unwrap();
            }
            for ch in section.chars() {
                match ch {
                    '>' => write!(&mut stdout, ">").unwrap(),
                    '<' => write!(&mut stdout, "<").unwrap(),
                    '&' => write!(&mut stdout, "&").unwrap(),
                    '\'' => write!(&mut stdout, "'").unwrap(),
                    '"' => write!(&mut stdout, """).unwrap(),
                    _ => write!(&mut stdout, "{ch}").unwrap(),
                }
            }
            if class.is_some() {
                write!(&mut stdout, "").unwrap();
            }
        }

        stdout.flush().unwrap();
        line.clear();
        if std::io::stdin().read_line(&mut line).unwrap() == 0 {
            write!(&mut stdout, "\n").unwrap();
            break;
        }
        if !line.ends_with('\n') {
            line.push('\n');
        }
    }

    write!(&mut stdout, "
").unwrap(); }