Skip to main content

mdbook_embedify/
detect_lang.rs

1use lazy_static::lazy_static;
2use mdbook_core::config::Config;
3use serde::Deserialize;
4use std::collections::{HashMap, HashSet};
5use std::path::Path;
6
7#[derive(Deserialize, Clone)]
8struct LanguageConfig {
9    name: Option<String>,
10    extensions: Option<Vec<String>>,
11    filenames: Option<Vec<String>>,
12}
13
14lazy_static! {
15    static ref STATIC_LANGUAGES: Vec<LanguageConfig> = {
16        let yaml_content = include_str!("assets/config/languages.yaml");
17        serde_yaml::from_str(yaml_content).expect("Failed to parse languages.yaml")
18    };
19}
20
21/// Convert a glob pattern to a regex pattern
22fn glob_to_regex(glob: &str) -> String {
23    let mut regex = String::new();
24    regex.push('^');
25
26    for ch in glob.chars() {
27        match ch {
28            '*' => regex.push_str(".*"),
29            '?' => regex.push('.'),
30            '.' => regex.push_str("\\."),
31            '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '+' | '\\' => {
32                regex.push('\\');
33                regex.push(ch);
34            }
35            _ => regex.push(ch),
36        }
37    }
38
39    regex.push('$');
40    regex
41}
42
43fn get_language_overrides(config: Option<&Config>) -> HashMap<String, LanguageConfig> {
44    if let Some(config) = config {
45        if let Ok(Some(languages)) =
46            config.get::<HashMap<String, LanguageConfig>>("preprocessor.embedify.include.languages")
47        {
48            return languages;
49        }
50    }
51    HashMap::new()
52}
53
54fn match_exact(filename: &str, config: &LanguageConfig) -> bool {
55    if let Some(filenames) = &config.filenames {
56        for pattern in filenames {
57            if !pattern.contains('*') && !pattern.contains('?') {
58                if pattern == filename {
59                    return true;
60                }
61            }
62        }
63    }
64    false
65}
66
67fn match_extension(filename: &str, config: &LanguageConfig) -> bool {
68    if let Some(extensions) = &config.extensions {
69        if let Some(last_dot_pos) = filename.rfind('.') {
70            let extension = &filename[last_dot_pos..].to_lowercase();
71            for ext in extensions {
72                if ext.to_lowercase() == *extension {
73                    return true;
74                }
75            }
76        }
77    }
78    false
79}
80
81fn match_pattern(filename: &str, config: &LanguageConfig) -> bool {
82    if let Some(filenames) = &config.filenames {
83        for pattern in filenames {
84            if pattern.contains('*') || pattern.contains('?') {
85                // Wildcard match
86                let regex_pattern = glob_to_regex(pattern);
87                if let Ok(regex) = regex::Regex::new(&regex_pattern) {
88                    if regex.is_match(filename) {
89                        return true;
90                    }
91                }
92            }
93        }
94    }
95    false
96}
97
98pub fn detect_lang(path: String, config: Option<&Config>) -> String {
99    let path_obj = Path::new(&path);
100    let filename = match path_obj.file_name().and_then(|f| f.to_str()) {
101        Some(f) => f,
102        None => return "plaintext".to_string(),
103    };
104
105    let overrides = get_language_overrides(config);
106    let mut languages_check_list: Vec<(&str, &LanguageConfig)> =
107        Vec::with_capacity(STATIC_LANGUAGES.len() + overrides.len());
108    let mut processed_overrides = HashSet::new();
109
110    // 1. Add static languages (checking for overrides)
111    for config in STATIC_LANGUAGES.iter() {
112        if let Some(name) = &config.name {
113            if let Some(override_config) = overrides.get(name) {
114                languages_check_list.push((name, override_config));
115                processed_overrides.insert(name);
116            } else {
117                languages_check_list.push((name, config));
118            }
119        }
120    }
121
122    // 2. Add remaining overrides
123    for (name, config) in &overrides {
124        if !processed_overrides.contains(name) {
125            languages_check_list.push((name, config));
126        }
127    }
128
129    // Pass 1: Check Exact Filenames
130    for (name, config) in &languages_check_list {
131        if match_exact(filename, config) {
132            return name.to_string();
133        }
134    }
135
136    // Pass 2: Check Extensions
137    for (name, config) in &languages_check_list {
138        if match_extension(filename, config) {
139            return name.to_string();
140        }
141    }
142
143    // Pass 3: Check Patterns
144    for (name, config) in &languages_check_list {
145        if match_pattern(filename, config) {
146            return name.to_string();
147        }
148    }
149
150    "plaintext".to_string()
151}