Skip to main content

mdbook_embedify/assets/scripts/
include.rs

1use crate::detect_lang;
2use crate::parser;
3
4use mdbook_preprocessor::PreprocessorContext;
5use std::fs;
6
7fn wrap_content_in_code_block(content: String, language: String, lang_detected: String) -> String {
8    let backticks = if lang_detected == "markdown" {
9        // Count the maximum number of consecutive backticks in the content
10        let max_backticks = content
11            .lines()
12            .filter_map(|line| {
13                let trimmed = line.trim();
14                if trimmed.starts_with("```") {
15                    Some(trimmed.chars().take_while(|&c| c == '`').count())
16                } else {
17                    None
18                }
19            })
20            .max()
21            .unwrap_or(2) // Default to 2 if no backticks found
22            + 1; // Add 1 to ensure we have enough backticks
23
24        "`".repeat(max_backticks)
25    } else {
26        "```".to_string()
27    };
28
29    format!("{}{}\n{}\n{}", backticks, language, content, backticks)
30}
31
32fn parse_include_range(input: String, max_line: usize) -> (usize, usize) {
33    let trimmed = input.trim();
34
35    if trimmed.is_empty() || trimmed == "-" {
36        return (1, max_line);
37    }
38
39    // Split by dash, but only accept exactly 2 parts
40    let parts: Vec<&str> = trimmed.split('-').collect();
41
42    if parts.len() != 2 {
43        return (1, max_line);
44    }
45
46    let start = parts[0].trim();
47    let end = parts[1].trim();
48
49    let start_line = if start.is_empty() {
50        1
51    } else {
52        match start.parse::<i32>() {
53            Ok(n) if n >= 1 && n as usize <= max_line => n as usize,
54            _ => 1, // Return 1 for invalid start line
55        }
56    };
57
58    let end_line = if end.is_empty() {
59        max_line
60    } else {
61        match end.parse::<i32>() {
62            Ok(n) if n >= start_line as i32 && n as usize <= max_line => n as usize,
63            _ => max_line, // Return max_line for invalid end line
64        }
65    };
66
67    (start_line, end_line)
68}
69
70pub fn include_script(
71    ctx: &PreprocessorContext,
72    options: Vec<parser::EmbedAppOption>,
73) -> Result<String, String> {
74    // get the file path from the options
75    let file_path_option = parser::get_option("file", options.clone());
76    if file_path_option.is_none() {
77        return Err("Option file is required".to_string());
78    }
79
80    let file_path = ctx
81        .root
82        .join(file_path_option.unwrap().value)
83        .to_string_lossy()
84        .to_string();
85
86    // read the file content
87    if !std::path::Path::new(&file_path).exists() {
88        return Err(format!("Cannot find file {}", file_path));
89    }
90
91    let content = fs::read_to_string(&file_path).unwrap_or_else(|_| String::new());
92
93    // get the range from the options
94    let range_option = parser::get_option("range", options.clone());
95    let max_line = content.lines().count();
96    let range = match range_option {
97        Some(option) => parse_include_range(option.value.clone(), max_line),
98        _ => (1, max_line), // Default to the full range if no range is provided
99    };
100
101    // get the content from the range
102    let start = range.0 - 1; // convert to 0-based index
103    let end = range.1; // end is exclusive
104    let lines: Vec<&str> = content.lines().collect();
105    let content = lines[start..end].join("\n");
106
107    // wrap the content in a code block
108    let lang_option = parser::get_option("lang", options.clone());
109    let lang_detected = detect_lang::detect_lang(file_path.clone(), Some(&ctx.config));
110
111    let language = match lang_option {
112        Some(option) => option.value,
113        _ => lang_detected.clone(),
114    };
115
116    Ok(wrap_content_in_code_block(content, language, lang_detected))
117}