1use crate::assets::scripts::include;
2use crate::parser;
3use crate::utils;
4
5use mdbook_core::{
6 book::{Book, Chapter},
7 errors::Error,
8};
9use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
10use regex::Regex;
11use rust_embed::RustEmbed;
12
13lazy_static::lazy_static! {
15 static ref RE_EMBED_MACRO: Regex = Regex::new(r"\{%\s+.*?\s+%\}").unwrap();
16 static ref RE_IGNORE: Regex = Regex::new(r"(?si)<!--\s*embed\s+ignore\s+begin\s*-->(.*?)<!--\s*embed\s+ignore\s+end\s*-->").unwrap();
17 static ref RE_EMBED_IGNORE: Regex = Regex::new(r"\{%\s+embed-ignore\s+(.*?)\s+%\}").unwrap();
18}
19
20#[derive(RustEmbed)]
21#[folder = "src/assets/templates"]
22struct Assets;
23
24pub struct Embed;
25
26impl Embed {
27 pub fn new() -> Embed {
28 Embed
29 }
30}
31
32fn render_script_app(
33 ctx: &PreprocessorContext,
34 app: parser::EmbedApp,
35) -> Result<Option<String>, String> {
36 match app.name.as_str() {
37 "include" => include::include_script(ctx, app.options).map(Some),
38 _ => Ok(None),
39 }
40}
41
42fn render_template_app(
43 ctx: &PreprocessorContext,
44 app: parser::EmbedApp,
45) -> Result<Option<String>, String> {
46 let mut template = String::new();
47 let app_path = format!("{}.html", app.name);
48
49 let templates_folder =
51 utils::get_config_string(&ctx.config, "custom-templates-folder", "assets/templates");
52 if !templates_folder.is_empty() {
53 let joined_folder = ctx.root.join(templates_folder);
54 let joined_folder = joined_folder.to_string_lossy().to_string();
55 let template_path = format!("{}/{}", joined_folder, app_path);
56 if std::path::Path::new(&template_path).exists() {
57 template = std::fs::read_to_string(&template_path).unwrap_or_else(|_| String::new());
58 }
59 }
60
61 if template.is_empty() && Assets::iter().any(|name| name == app_path) {
63 let file = Assets::get(&app_path).unwrap();
65 template = String::from_utf8(file.data.to_vec()).unwrap_or_else(|_| String::new());
66 }
67
68 if template.is_empty() {
70 return Ok(None);
71 }
72
73 let mut should_exit = false;
75
76 let result = RE_EMBED_MACRO
77 .replace_all(&template, |caps: ®ex::Captures| {
78 if should_exit {
79 return "".to_string(); }
81
82 let input = caps.get(0).map_or("", |m| m.as_str());
83 let placeholder = parser::parse_placeholder(input);
84
85 if placeholder.is_none() {
86 return input.to_string();
87 }
88
89 let placeholder = placeholder.unwrap();
91 let found = app
92 .options
93 .iter()
94 .find(|option| option.name == placeholder.key);
95
96 if placeholder.default.is_empty() {
98 if found.is_none() || found.unwrap().value.is_empty() {
99 should_exit = true;
100 return input.to_string();
101 }
102 }
103
104 let mut value = if found.is_some() && !found.unwrap().value.is_empty() {
106 found.unwrap().value.clone()
107 } else {
108 placeholder.default.clone()
109 };
110
111 if placeholder.method == "markdown" {
113 value = utils::render_to_markdown(value.clone());
114 }
115
116 value
117 })
118 .to_string();
119
120 if should_exit {
122 return Err("Missing required options".to_string());
123 }
124
125 Ok(Some(utils::minify_html(result)))
126}
127
128fn render_embeds(ctx: &PreprocessorContext, chapter: Chapter, content: String) -> String {
129 let mut content = content;
130 if chapter.is_draft_chapter() {
131 return content; }
133
134 let chapter_path = chapter.path.unwrap().clone(); let mut ignored_sections: Vec<(String, String)> = Vec::new();
138
139 content = RE_IGNORE
140 .replace_all(&content, |caps: ®ex::Captures| {
141 let placeholder = format!(
143 "__MDBOOK_EMBEDIFY_IGNORE_{}_{:x}__",
144 ignored_sections.len(),
145 std::ptr::addr_of!(ignored_sections) as usize
146 );
147 let ignored_content = caps.get(0).unwrap().as_str();
148
149 ignored_sections.push((placeholder.clone(), ignored_content.to_string()));
150 placeholder
151 })
152 .to_string();
153
154 content = RE_EMBED_MACRO
155 .replace_all(&content, |caps: ®ex::Captures| {
156 let input = caps.get(0).map_or("", |m| m.as_str());
157 let app = parser::parse_app(input);
158 if app.is_none() {
159 return input.to_string();
160 }
161 let app = app.unwrap();
162
163 let mut rendered = render_template_app(ctx, app.clone());
165
166 if rendered.is_ok() && rendered.as_ref().unwrap().is_none() {
168 rendered = render_script_app(ctx, app.clone());
169 }
170
171 if !rendered.is_ok() {
173 let err = rendered.err().unwrap();
174 eprintln!(
175 "(mdbook-embedify): Error while rendering app \"{}\" in {:?}. {}",
176 app.name, chapter_path, err
177 );
178 return input.to_string();
179 }
180
181 if rendered.as_ref().unwrap().is_none() {
183 return input.to_string();
184 }
185
186 let options_string = {
189 let mut json_parts = Vec::new();
190 for option in &app.options {
191 json_parts.push(format!(
192 "data-option-{}=\"{}\"",
193 option.name,
194 option.value
195 ));
196 }
197 format!("{}", json_parts.join(" "))
198 };
199
200 format!(
201 "<!-- {} -->\n\n<div data-embedify data-app=\"{}\" {} style=\"display:none\"></div>\n\n{}",
202 input,
203 app.name,
204 options_string,
205 rendered.unwrap().unwrap()
206 )
207 })
208 .to_string();
209
210 content = RE_EMBED_IGNORE
213 .replace_all(&content, |caps: ®ex::Captures| {
214 let content_part = caps.get(1).map_or("", |m| m.as_str());
215 format!("{{% embed {} %}}", content_part)
216 })
217 .to_string();
218
219 for (placeholder, ignored_content) in ignored_sections.into_iter().rev() {
222 if let Some(pos) = content.find(&placeholder) {
224 content.replace_range(pos..pos + placeholder.len(), &ignored_content);
225 }
226 }
227
228 content
229}
230
231impl Preprocessor for Embed {
232 fn name(&self) -> &str {
233 "mdbook-embedify"
234 }
235
236 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
237 let config = &ctx.config;
238
239 let footer = utils::get_config_bool(config, "footer.enable");
240 let giscus = utils::get_config_bool(config, "giscus.enable");
241 let scroll_to_top = utils::get_config_bool(config, "scroll-to-top.enable");
242 let announcement_banner = utils::get_config_bool(config, "announcement-banner.enable");
243
244 book.for_each_mut(|item| {
245 if let mdbook_core::book::BookItem::Chapter(chapter) = item {
246 let mut content = chapter.content.clone();
247 if scroll_to_top {
249 content.push_str(&utils::create_scroll_to_top());
250 }
251 if announcement_banner {
253 content.push_str(&utils::create_announcement_banner(config));
254 }
255 if giscus {
257 content.push_str(&utils::create_giscus(config));
258 }
259 if footer {
261 content.push_str(&utils::create_footer(config));
262 }
263 chapter.content = render_embeds(ctx, chapter.clone(), content);
265 }
266 });
267
268 Ok(book)
270 }
271}