mod highlights; mod parsers; use core::str; use std::{ cell::RefCell, collections::HashMap, env::{ args, current_dir, }, fs::{ create_dir, read, }, process::Command, }; use dirs::cache_dir; use highlights::HIGHLIGHTS; use libloading::Library; use parsers::{ ALIASES, PARSERS, }; use tree_sitter::Language; use tree_sitter_highlight::{ HighlightConfiguration, Highlighter, HtmlRenderer, }; use url::Url; fn get_config<'a>( lang: &'a str, configs: &'a RefCell>, ) -> Option<&'static HighlightConfiguration> { let lang = ALIASES.get(lang).unwrap_or(&lang); configs.borrow_mut().entry(lang).or_insert_with(|| { let install_info = PARSERS .get(lang) .expect("A valid language should have been passed in"); let mut cache_dir = cache_dir().expect("Cache directory should be available"); cache_dir.push("yah"); let mut query_dir = current_dir().expect("Current directory should be available"); query_dir.push("nvim-treesitter"); query_dir.push("queries"); query_dir.push(lang); let mut parsers_dir = current_dir().expect("Current directory should be available"); parsers_dir.push(".parsers"); if !parsers_dir.exists() { create_dir(&parsers_dir).expect("`parsers_dir` should be able to be created"); } if !cache_dir.exists() { create_dir(&cache_dir).expect("`cache_dir` should be able to be created"); } let parser_path = parsers_dir.join(format!("{lang}.so")); let repo_path = cache_dir.join( Url::parse(install_info.url) .unwrap() .path_segments() .expect("The `url` field should contain a valid URL") .last() .unwrap(), ); if !parser_path.exists() { if !repo_path.exists() { Command::new("git") .arg("clone") .arg(install_info.url) .arg("--depth") .arg("1") .current_dir(&cache_dir) .spawn() .expect("`git` should successfully cloned the treesitter repository") .wait() .expect("`git` should have started"); } if install_info.requires_generate_from_grammar { if install_info.generate_requires_npm { Command::new("npm") .arg("install") .current_dir(&repo_path) .spawn() .expect("`npm` should successfully installed dependencies") .wait() .expect("`npm` should have started"); } Command::new("tree-sitter") .arg("generate") .arg("--no-bindings") .current_dir(&repo_path) .spawn() .expect("`tree-sitter` should successfully generated the parser") .wait() .expect("`tree-sitter` should have started"); } Command::new("tree-sitter") .arg("build") .arg("-o") .arg(&parser_path) .arg( install_info .location .map_or(repo_path.clone(), |location| repo_path.join(location)), ) .spawn() .expect("`tree-sitter` should successfully built the parser") .wait() .expect("`tree-sitter` should have started"); } let library = unsafe { Library::new(parser_path) .expect("`parser_path` should contain a path to a valid C dylib") }; let parser = unsafe { library .get:: Language>(format!("tree_sitter_{lang}").as_bytes()) .expect("`parser_path` should contain a path to treesitter parser dylib ")( ) }; std::mem::forget(library); // this causes the dylib to not be unloaded with dlclose after this // scope is dropped, and thus extending its lifetime to static let mut config = HighlightConfiguration::new( parser, *lang, read(query_dir.join("highlights.scm")) .map(|v| String::from_utf8(v).unwrap()) .unwrap_or(String::from("")) .as_str(), read(query_dir.join("injections.scm")) .map(|v| String::from_utf8(v).unwrap()) .unwrap_or(String::from("")) .as_str(), read(query_dir.join("locals.scm")) .map(|v| String::from_utf8(v).unwrap()) .unwrap_or(String::from("")) .as_str(), ) .unwrap(); config.configure(HIGHLIGHTS); Box::leak(Box::new(config)) }); configs.borrow_mut().get(lang).map(|v| *v) } fn main() { if let [_, lang, code_path] = args().collect::>().as_slice() { let code = String::from_utf8( read(code_path).expect("`code_path` should contain a path to a file"), ) .expect("`code_path` should contain a path to a valid UTF-8 file"); // decode code as it may be html encoded let code = html_escape::decode_html_entities(&code); let mut highlighter = Highlighter::new(); let configs = RefCell::new(HashMap::new()); let config = get_config(lang, &configs).expect("A valid language should have been passed in"); let events = highlighter .highlight(&config, code.as_bytes(), None, |v| { let lang = String::from(v).leak(); get_config(lang, &configs) }) .unwrap(); let mut renderer = HtmlRenderer::new(); renderer .render(events, code.as_bytes(), &|highlight, output| { output.extend(b"class=\""); let mut parts = HIGHLIGHTS[highlight.0].split('.').peekable(); while let Some(part) = parts.next() { output.extend(part.as_bytes()); if parts.peek().is_some() { output.extend(b" "); } } output.extend(b"\""); }) .unwrap(); print!("{}", String::from_utf8(renderer.html).expect("`renderer.html` should contain valid UTF-8")) } else { panic!( "Need as arguments, got {:?}", args().collect::>() ) } }