Skip to main content

bytes_radar/core/
registry.rs

1use once_cell::sync::Lazy;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::path::Path;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
8pub enum LanguageType {
9    #[default]
10    Programming,
11    Markup,
12    Data,
13    Configuration,
14    Documentation,
15    Other,
16}
17
18impl Display for LanguageType {
19    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
20        match self {
21            LanguageType::Programming => write!(f, "Programming"),
22            LanguageType::Markup => write!(f, "Markup"),
23            LanguageType::Data => write!(f, "Data"),
24            LanguageType::Configuration => write!(f, "Configuration"),
25            LanguageType::Documentation => write!(f, "Documentation"),
26            LanguageType::Other => write!(f, "Other"),
27        }
28    }
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
32pub enum LineCommentPosition {
33    #[default]
34    Any,
35    Start,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct LanguageDefinition {
40    #[serde(default)]
41    pub name: String,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub display_name: Option<String>,
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub extensions: Vec<String>,
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub filenames: Vec<String>,
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub shebangs: Vec<String>,
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub env: Vec<String>,
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub mime_types: Vec<String>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "line_comment")]
55    pub line_comments: Vec<String>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub multi_line_comments: Vec<(String, String)>,
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub nested_comments: Vec<(String, String)>,
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub doc_quotes: Vec<(String, String)>,
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub quotes: Vec<(String, String)>,
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub verbatim_quotes: Vec<(String, String)>,
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub important_syntax: Vec<String>,
68    #[serde(default)]
69    pub language_type: LanguageType,
70    #[serde(default, skip_serializing_if = "is_false")]
71    pub is_literate: bool,
72    #[serde(default, skip_serializing_if = "is_false", alias = "nested")]
73    pub is_nested: bool,
74    #[serde(default, skip_serializing_if = "is_false")]
75    pub is_blank: bool,
76    #[serde(default = "default_true", skip_serializing_if = "is_true")]
77    pub case_sensitive: bool,
78    #[serde(default)]
79    pub line_comment_position: LineCommentPosition,
80}
81
82fn is_false(b: &bool) -> bool {
83    !b
84}
85
86fn is_true(b: &bool) -> bool {
87    *b
88}
89
90fn default_true() -> bool {
91    true
92}
93
94pub struct LanguageRegistry;
95
96impl LanguageRegistry {
97    pub fn get_language(name: &str) -> Option<&'static LanguageDefinition> {
98        LANGUAGE_MAP.get(name)
99    }
100
101    pub fn detect_by_extension(extension: &str) -> Option<&'static LanguageDefinition> {
102        let ext = extension.to_lowercase();
103        EXTENSION_MAP
104            .get(&ext)
105            .and_then(|name| LANGUAGE_MAP.get(name))
106    }
107
108    pub fn detect_by_filename(filename: &str) -> Option<&'static LanguageDefinition> {
109        let lower_filename = filename.to_lowercase();
110        FILENAME_MAP
111            .get(&lower_filename)
112            .and_then(|name| LANGUAGE_MAP.get(name))
113    }
114
115    pub fn detect_by_path<P: AsRef<Path>>(path: P) -> Option<&'static LanguageDefinition> {
116        let path = path.as_ref();
117
118        if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
119            if let Some(lang) = Self::detect_by_filename(filename) {
120                return Some(lang);
121            }
122        }
123
124        if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
125            return Self::detect_by_extension(extension);
126        }
127
128        None
129    }
130
131    pub fn all_languages() -> impl Iterator<Item = &'static LanguageDefinition> {
132        LANGUAGE_MAP.values()
133    }
134
135    pub fn languages_by_type(
136        lang_type: LanguageType,
137    ) -> impl Iterator<Item = &'static LanguageDefinition> {
138        LANGUAGE_MAP
139            .values()
140            .filter(move |lang| lang.language_type == lang_type)
141    }
142}
143
144fn create_languages() -> HashMap<String, LanguageDefinition> {
145    const LANGUAGES_JSON: &str = include_str!("../languages.json");
146    let mut languages: HashMap<String, LanguageDefinition> =
147        serde_json::from_str(LANGUAGES_JSON).expect("Failed to parse languages.json");
148
149    for (key, lang_def) in languages.iter_mut() {
150        if lang_def.name.is_empty() {
151            lang_def.name = key.clone();
152        }
153    }
154
155    languages
156}
157
158fn create_extension_map() -> HashMap<String, String> {
159    let mut map = HashMap::new();
160    for (name, lang) in LANGUAGE_MAP.iter() {
161        for ext in &lang.extensions {
162            map.insert(ext.clone(), name.clone());
163        }
164    }
165    map
166}
167
168fn create_filename_map() -> HashMap<String, String> {
169    let mut map = HashMap::new();
170    for (name, lang) in LANGUAGE_MAP.iter() {
171        for filename in &lang.filenames {
172            map.insert(filename.to_lowercase(), name.clone());
173        }
174    }
175    map
176}
177
178static LANGUAGE_MAP: Lazy<HashMap<String, LanguageDefinition>> = Lazy::new(create_languages);
179static EXTENSION_MAP: Lazy<HashMap<String, String>> = Lazy::new(create_extension_map);
180static FILENAME_MAP: Lazy<HashMap<String, String>> = Lazy::new(create_filename_map);