From 10965a6122e3ca6ad847871ec41eb01f0f595945 Mon Sep 17 00:00:00 2001 From: sharkdp Date: Wed, 17 Oct 2018 22:30:09 +0200 Subject: [PATCH] Implement syntax mapping This adds a `-m`/`--map-syntax` option that allows users to (re)map certain file extensions or file names to an existing syntax. For example: ``` bat --map-syntax .config:json ``` The option can be use multiple times. Note that you can easily make these mappings permanent by using `bat`s new configuration file. closes #169 --- src/app.rs | 19 +++++++++++++++++++ src/assets.rs | 10 ++++++++-- src/clap_app.rs | 16 ++++++++++++++++ src/main.rs | 1 + src/printer.rs | 2 +- src/syntax_mapping.rs | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 src/syntax_mapping.rs diff --git a/src/app.rs b/src/app.rs index fd25103b..30724b21 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,6 +19,7 @@ use errors::*; use inputfile::InputFile; use line_range::LineRange; use style::{OutputComponent, OutputComponents, OutputWrap}; +use syntax_mapping::SyntaxMapping; use util::transpose; #[derive(Debug, Clone, Copy, PartialEq)] @@ -66,6 +67,9 @@ pub struct Config<'a> { /// The syntax highlighting theme pub theme: String, + + /// File extension/name mappings + pub syntax_mapping: SyntaxMapping, } fn is_truecolor_terminal() -> bool { @@ -146,6 +150,20 @@ impl App { } }; + let mut syntax_mapping = SyntaxMapping::new(); + + if let Some(values) = self.matches.values_of("map-syntax") { + for from_to in values { + let parts: Vec<_> = from_to.split(":").collect(); + + if parts.len() != 2 { + return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into()); + } + + syntax_mapping.insert(parts[0].into(), parts[1].into()); + } + } + Ok(Config { true_color: is_truecolor_terminal(), language: self.matches.value_of("language"), @@ -202,6 +220,7 @@ impl App { .unwrap_or(String::from(BAT_THEME_DEFAULT)), line_range: transpose(self.matches.value_of("line-range").map(LineRange::from))?, output_components, + syntax_mapping, }) } diff --git a/src/assets.rs b/src/assets.rs index 119e91c9..ab9f0e4a 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -12,6 +12,7 @@ use dirs::PROJECT_DIRS; use errors::*; use inputfile::{InputFile, InputFileReader}; +use syntax_mapping::SyntaxMapping; pub const BAT_THEME_DEFAULT: &str = "Monokai Extended"; @@ -167,6 +168,7 @@ impl HighlightingAssets { language: Option<&str>, filename: InputFile, reader: &mut InputFileReader, + mapping: &SyntaxMapping, ) -> &SyntaxReference { let syntax = match (language, filename) { (Some(language), _) => self.syntax_set.find_syntax_by_token(language), @@ -174,10 +176,14 @@ impl HighlightingAssets { let path = Path::new(filename); let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let extension = path.extension().and_then(|x| x.to_str()).unwrap_or(""); + + let file_name = mapping.replace(file_name); + let extension = mapping.replace(extension); + let ext_syntax = self .syntax_set - .find_syntax_by_extension(file_name) - .or_else(|| self.syntax_set.find_syntax_by_extension(extension)); + .find_syntax_by_extension(&file_name) + .or_else(|| self.syntax_set.find_syntax_by_extension(&extension)); let line_syntax = if ext_syntax.is_none() { String::from_utf8(reader.first_line.clone()) .ok() diff --git a/src/clap_app.rs b/src/clap_app.rs index eab2e29c..fd917ee5 100644 --- a/src/clap_app.rs +++ b/src/clap_app.rs @@ -59,6 +59,22 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { .help("Display all supported languages.") .long_help("Display a list of supported languages for syntax highlighting."), ) + .arg( + Arg::with_name("map-syntax") + .short("m") + .long("map-syntax") + .multiple(true) + .takes_value(true) + .value_name("from:to") + .help("Map a file extension or name to an existing syntax") + .long_help( + "Map a file extension or file name to an existing syntax. For example, \ + to highlight *.conf files with the INI syntax, use '-m conf:ini'. \ + To highlight files named '.myignore' with the Git Ignore syntax, use \ + '-m .myignore:gitignore'.", + ) + .takes_value(true), + ) .arg( Arg::with_name("theme") .long("theme") diff --git a/src/main.rs b/src/main.rs index 31f9503f..a0317283 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ mod output; mod preprocessor; mod printer; mod style; +mod syntax_mapping; mod terminal; mod util; diff --git a/src/printer.rs b/src/printer.rs index 5bb72fc8..fbe14120 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -142,7 +142,7 @@ impl<'a> InteractivePrinter<'a> { }; // Determine the type of syntax for highlighting - let syntax = assets.get_syntax(config.language, file, reader); + let syntax = assets.get_syntax(config.language, file, reader, &config.syntax_mapping); Some(HighlightLines::new(syntax, theme)) }; diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs new file mode 100644 index 00000000..bf0e7351 --- /dev/null +++ b/src/syntax_mapping.rs @@ -0,0 +1,35 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct SyntaxMapping(HashMap); + +impl SyntaxMapping { + pub fn new() -> SyntaxMapping { + SyntaxMapping(HashMap::new()) + } + + pub fn insert(&mut self, from: String, to: String) -> Option { + self.0.insert(from, to) + } + + pub fn replace<'a>(&self, input: &'a str) -> Cow<'a, str> { + let mut out = Cow::from(input); + if let Some(value) = self.0.get(input) { + out = Cow::from(value.clone()) + } + out + } +} + +#[test] +fn basic() { + let mut map = SyntaxMapping::new(); + map.insert("Cargo.lock".into(), "toml".into()); + map.insert(".ignore".into(), ".gitignore".into()); + + assert_eq!("toml", map.replace("Cargo.lock")); + assert_eq!("other.lock", map.replace("other.lock")); + + assert_eq!(".gitignore", map.replace(".ignore")); +}