mdbook_embedify/
detect_lang.rs1use 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
21fn 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 let regex_pattern = glob_to_regex(pattern);
87 if let Ok(regex) = regex::Regex::new(®ex_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 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 for (name, config) in &overrides {
124 if !processed_overrides.contains(name) {
125 languages_check_list.push((name, config));
126 }
127 }
128
129 for (name, config) in &languages_check_list {
131 if match_exact(filename, config) {
132 return name.to_string();
133 }
134 }
135
136 for (name, config) in &languages_check_list {
138 if match_extension(filename, config) {
139 return name.to_string();
140 }
141 }
142
143 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}