Skip to content

Commit 7df8f59

Browse files
committed
feat: update to version 0.3.2 with new language detection configuration and enhancements
1 parent 4d46fe4 commit 7df8f59

10 files changed

Lines changed: 200 additions & 86 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mdbook-embedify"
3-
version = "0.3.1"
3+
version = "0.3.2"
44
edition = "2021"
55
license = "MIT"
66
readme = "README.md"
@@ -24,6 +24,7 @@ regex = "1.12.3"
2424
rust-embed = "8.11.0"
2525
serde_json = "1.0.149"
2626
serde_yaml = "0.9.34"
27+
serde = { version = "1.0.228", features = ["derive"] }
2728

2829
[dev-dependencies]
2930
dircmp = "0.2.0"

docs/assets/js/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
function appendVersion() {
22
const shield = document.createElement("span");
3-
shield.textContent = "0.3.1";
3+
shield.textContent = "0.3.2";
44
document.querySelector(".menu-title").appendChild(shield);
55
}
66

docs/book.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ footer.enable = true
4747
footer.message = "Copyright © 2025 • Created with ❤️ by [MR-Addict](https://github.com/MR-Addict)"
4848

4949
announcement-banner.enable = true
50-
announcement-banner.id = "0.3.1"
51-
announcement-banner.message = "*New version [0.3.1](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.3.1) relased*"
50+
announcement-banner.id = "0.3.2"
51+
announcement-banner.message = "*New version [0.3.2](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.3.2) relased*"
5252

5353
giscus.enable = true
5454
giscus.repo = "MR-Addict/mdbook-embedify"
@@ -60,6 +60,16 @@ giscus.theme = "book"
6060
giscus.lang = "en"
6161
giscus.loading = "lazy"
6262

63+
include.languages.dockerfile.extensions = [".dockerfile"]
64+
include.languages.dockerfile.filenames = [
65+
"Dockerfile",
66+
"dockerfile",
67+
"Containerfile",
68+
"Dockerfile.*",
69+
"dockerfile.*",
70+
"*.dockerfile",
71+
]
72+
6373
custom-templates-folder = "assets/templates"
6474

6575
# This is for the development environment

docs/src/development/language-matching.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ The language detection system intelligently identifies programming languages fro
2222

2323
Language definitions are stored in [src/assets/config/languages.yaml](https://github.com/MR-Addict/mdbook-embedify/tree/main/src/assets/config/languages.yaml). Each language entry supports multiple matching strategies.
2424

25+
### Custom Configuration
26+
27+
> Attention 💥
28+
>
29+
> Support since [v0.3.2](https://github.com/MR-Addict/mdbook-embedify/releases/tag/v0.3.2).
30+
31+
You can override or extend the language detection rules in your `book.toml`. This is useful when you want to add support for new file extensions or modify existing detection rules without waiting for a new release.
32+
33+
Example `book.toml` configuration:
34+
35+
```toml
36+
[preprocessor.embedify]
37+
# Add .dockerfile extension to dockerfile language
38+
include.languages.dockerfile.extensions = [".dockerfile"]
39+
# Add specific filenames to dockerfile language
40+
include.languages.dockerfile.filenames = ["Dockerfile", "*.dockerfile"]
41+
42+
# Add a completely new language
43+
include.languages.mylang.extensions = [".mlg"]
44+
```
45+
2546
### Language Definition Structure
2647

2748
```yaml

docs/src/local/announcement-banner.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Announcement banner allows you put important messages at the top of the page. It
1212
## Example
1313

1414
```text
15-
{% embed-ignore announcement-banner id="0.3.1" message="*New version [0.3.1](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.3.1) relased*" %}
15+
{% embed-ignore announcement-banner id="0.3.2" message="*New version [0.3.2](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.3.2) relased*" %}
1616
```
1717

1818
This book's announcement banner is enabled, you can see it at the top of this page.

docs/src/local/include.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
>
77
> Language is detected by file extension, which I manually referred, simplified and extended from [linguist](https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml).
88
>
9-
> If you find any issues with the language detection, please open an [issue](https://github.com/MR-Addict/mdbook-embedify/issues). Currenltly language map is under [languages.yaml](https://github.com/MR-Addict/mdbook-embedify/blob/main/src/assets/config/languages.yaml)
9+
> If you find any issues with the language detection, please open an [issue](https://github.com/MR-Addict/mdbook-embedify/issues). Currenltly language map is under [languages.yaml](https://github.com/MR-Addict/mdbook-embedify/blob/main/src/assets/config/languages.yaml).
10+
11+
> Tip 💡
12+
>
13+
> You can customize language detection rules in your `book.toml`. See [Language Matching](../development/language-matching.md#custom-configuration) for details.
1014
1115
The include app is for including source file and wrapping it as markdown code block.
1216

src/assets/scripts/include.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub fn include_script(
106106

107107
// wrap the content in a code block
108108
let lang_option = parser::get_option("lang", options.clone());
109-
let lang_detected = detect_lang::detect_lang(file_path.clone());
109+
let lang_detected = detect_lang::detect_lang(file_path.clone(), Some(&ctx.config));
110110

111111
let language = match lang_option {
112112
Some(option) => option.value,

src/detect_lang.rs

Lines changed: 108 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,20 @@
11
use lazy_static::lazy_static;
2-
use std::collections::HashMap;
2+
use mdbook_core::config::Config;
3+
use serde::Deserialize;
4+
use std::collections::{HashMap, HashSet};
35
use std::path::Path;
46

7+
#[derive(Deserialize, Clone)]
8+
struct LanguageConfig {
9+
name: Option<String>,
10+
extensions: Option<Vec<String>>,
11+
filenames: Option<Vec<String>>,
12+
}
13+
514
lazy_static! {
6-
static ref LANGUAGE_MAP: HashMap<String, String> = {
15+
static ref STATIC_LANGUAGES: Vec<LanguageConfig> = {
716
let yaml_content = include_str!("assets/config/languages.yaml");
8-
let languages: Vec<serde_yaml::Value> =
9-
serde_yaml::from_str(yaml_content).expect("Failed to parse languages.yaml");
10-
11-
let mut map = HashMap::new();
12-
for lang in languages {
13-
if let Some(name) = lang["name"].as_str() {
14-
// Handle extensions
15-
if let Some(extensions) = lang["extensions"].as_sequence() {
16-
for ext in extensions {
17-
if let Some(extension) = ext.as_str() {
18-
map.insert(extension.to_string(), name.to_string());
19-
}
20-
}
21-
}
22-
23-
// Handle exact filenames (non-pattern entries)
24-
if let Some(filenames) = lang["filenames"].as_sequence() {
25-
for filename in filenames {
26-
if let Some(filename_str) = filename.as_str() {
27-
// Only add to exact match map if it doesn't contain wildcards
28-
if !filename_str.contains('*') && !filename_str.contains('?') {
29-
map.insert(filename_str.to_string(), name.to_string());
30-
}
31-
}
32-
}
33-
}
34-
}
35-
}
36-
map
37-
};
38-
39-
static ref LANGUAGE_PATTERNS: Vec<(regex::Regex, String)> = {
40-
let yaml_content = include_str!("assets/config/languages.yaml");
41-
let languages: Vec<serde_yaml::Value> =
42-
serde_yaml::from_str(yaml_content).expect("Failed to parse languages.yaml");
43-
44-
let mut patterns = Vec::new();
45-
for lang in languages {
46-
if let Some(name) = lang["name"].as_str() {
47-
// Handle filename patterns (entries with wildcards)
48-
if let Some(filenames) = lang["filenames"].as_sequence() {
49-
for filename in filenames {
50-
if let Some(filename_str) = filename.as_str() {
51-
// Only add to patterns if it contains wildcards
52-
if filename_str.contains('*') || filename_str.contains('?') {
53-
// Convert glob pattern to regex
54-
let regex_pattern = glob_to_regex(filename_str);
55-
if let Ok(regex) = regex::Regex::new(&regex_pattern) {
56-
patterns.push((regex, name.to_string()));
57-
}
58-
}
59-
}
60-
}
61-
}
62-
}
63-
}
64-
patterns
17+
serde_yaml::from_str(yaml_content).expect("Failed to parse languages.yaml")
6518
};
6619
}
6720

@@ -87,34 +40,112 @@ fn glob_to_regex(glob: &str) -> String {
8740
regex
8841
}
8942

90-
pub fn detect_lang(path: String) -> String {
91-
let path_obj = Path::new(&path);
43+
fn 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+
}
9253

93-
// Get the filename
94-
if let Some(filename) = path_obj.file_name() {
95-
if let Some(filename_str) = filename.to_str() {
96-
// First, try to match the full filename (exact matches from filenames array)
97-
if let Some(language) = LANGUAGE_MAP.get(filename_str) {
98-
return language.clone();
54+
fn 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+
}
9961
}
62+
}
63+
}
64+
false
65+
}
10066

101-
// Then try the file extension (everything after the last dot)
102-
if let Some(last_dot_pos) = filename_str.rfind('.') {
103-
let extension = &filename_str[last_dot_pos..].to_lowercase();
104-
if let Some(language) = LANGUAGE_MAP.get(extension) {
105-
return language.clone();
67+
fn 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;
10674
}
10775
}
76+
}
77+
}
78+
false
79+
}
10880

109-
// Finally try filename patterns (wildcard entries from filenames array)
110-
for (pattern, language) in LANGUAGE_PATTERNS.iter() {
111-
if pattern.is_match(filename_str) {
112-
return language.clone();
81+
fn 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+
}
11391
}
11492
}
11593
}
11694
}
95+
false
96+
}
97+
98+
pub 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+
}
117149

118-
// If no extension found or extension not in map, return "plaintext"
119150
"plaintext".to_string()
120151
}

tests/detect_lang.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use mdbook_embedify::detect_lang::detect_lang;
1+
use mdbook_embedify::detect_lang;
2+
3+
fn detect_lang(path: String) -> String {
4+
detect_lang::detect_lang(path, None)
5+
}
26

37
#[cfg(test)]
48
mod basic_language_detection_tests {
@@ -521,3 +525,45 @@ mod wildcard_pattern_tests {
521525
assert_eq!(detect_lang("notmakefile".to_string()), "plaintext");
522526
}
523527
}
528+
529+
#[cfg(test)]
530+
mod config_detection_tests {
531+
use mdbook_embedify::detect_lang;
532+
use mdbook_core::config::Config;
533+
534+
#[test]
535+
fn test_config_overrides() {
536+
// Construct context
537+
let mut config = Config::default();
538+
// Set include.languages.dockerfile.extensions = [".mydev"]
539+
config.set("preprocessor.embedify.include.languages.dockerfile.extensions", vec![".mydev"]).unwrap();
540+
541+
assert_eq!(detect_lang::detect_lang("test.mydev".to_string(), Some(&config)), "dockerfile");
542+
}
543+
544+
#[test]
545+
fn test_config_filenames() {
546+
let mut config = Config::default();
547+
config.set("preprocessor.embedify.include.languages.makefile.filenames", vec!["MyMake"]).unwrap();
548+
549+
assert_eq!(detect_lang::detect_lang("MyMake".to_string(), Some(&config)), "makefile");
550+
}
551+
552+
#[test]
553+
fn test_config_patterns() {
554+
let mut config = Config::default();
555+
// Test wildcard pattern configuration
556+
config.set("preprocessor.embedify.include.languages.python.filenames", vec!["config_*.script"]).unwrap();
557+
558+
assert_eq!(detect_lang::detect_lang("config_test.script".to_string(), Some(&config)), "python");
559+
}
560+
561+
#[test]
562+
fn test_add_new_language() {
563+
let mut config = Config::default();
564+
// Add a completely new language
565+
config.set("preprocessor.embedify.include.languages.mylang.extensions", vec![".mlg"]).unwrap();
566+
567+
assert_eq!(detect_lang::detect_lang("test.mlg".to_string(), Some(&config)), "mylang");
568+
}
569+
}

0 commit comments

Comments
 (0)