use crate::assets::scripts::include;
use crate::parser;
use crate::utils;
use mdbook_core::{
book::{Book, Chapter},
errors::Error,
};
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use regex::Regex;
use rust_embed::RustEmbed;
lazy_static::lazy_static! {
static ref RE_EMBED_MACRO: Regex = Regex::new(r"\{%\s+.*?\s+%\}").unwrap();
static ref RE_IGNORE: Regex = Regex::new(r"(?si)<!--\s*embed\s+ignore\s+begin\s*-->(.*?)<!--\s*embed\s+ignore\s+end\s*-->").unwrap();
static ref RE_EMBED_IGNORE: Regex = Regex::new(r"\{%\s+embed-ignore\s+(.*?)\s+%\}").unwrap();
}
#[derive(RustEmbed)]
#[folder = "src/assets/templates"]
struct Assets;
pub struct Embed;
impl Embed {
pub fn new() -> Embed {
Embed
}
}
fn render_script_app(
ctx: &PreprocessorContext,
app: parser::EmbedApp,
) -> Result<Option<String>, String> {
match app.name.as_str() {
"include" => include::include_script(ctx, app.options).map(Some),
_ => Ok(None),
}
}
fn render_template_app(
ctx: &PreprocessorContext,
app: parser::EmbedApp,
) -> Result<Option<String>, String> {
let mut template = String::new();
let app_path = format!("{}.html", app.name);
let templates_folder =
utils::get_config_string(&ctx.config, "custom-templates-folder", "assets/templates");
if !templates_folder.is_empty() {
let joined_folder = ctx.root.join(templates_folder);
let joined_folder = joined_folder.to_string_lossy().to_string();
let template_path = format!("{}/{}", joined_folder, app_path);
if std::path::Path::new(&template_path).exists() {
template = std::fs::read_to_string(&template_path).unwrap_or_else(|_| String::new());
}
}
if template.is_empty() && Assets::iter().any(|name| name == app_path) {
let file = Assets::get(&app_path).unwrap();
template = String::from_utf8(file.data.to_vec()).unwrap_or_else(|_| String::new());
}
if template.is_empty() {
return Ok(None);
}
let mut should_exit = false;
let result = RE_EMBED_MACRO
.replace_all(&template, |caps: ®ex::Captures| {
if should_exit {
return "".to_string(); }
let input = caps.get(0).map_or("", |m| m.as_str());
let placeholder = parser::parse_placeholder(input);
if placeholder.is_none() {
return input.to_string();
}
let placeholder = placeholder.unwrap();
let found = app
.options
.iter()
.find(|option| option.name == placeholder.key);
if placeholder.default.is_empty() {
if found.is_none() || found.unwrap().value.is_empty() {
should_exit = true;
return input.to_string();
}
}
let mut value = if found.is_some() && !found.unwrap().value.is_empty() {
found.unwrap().value.clone()
} else {
placeholder.default.clone()
};
if placeholder.method == "markdown" {
value = utils::render_to_markdown(value.clone());
}
value
})
.to_string();
if should_exit {
return Err("Missing required options".to_string());
}
Ok(Some(utils::minify_html(result)))
}
fn render_embeds(ctx: &PreprocessorContext, chapter: Chapter, content: String) -> String {
let mut content = content;
if chapter.is_draft_chapter() {
return content; }
let chapter_path = chapter.path.unwrap().clone();
let mut ignored_sections: Vec<(String, String)> = Vec::new();
content = RE_IGNORE
.replace_all(&content, |caps: ®ex::Captures| {
let placeholder = format!(
"__MDBOOK_EMBEDIFY_IGNORE_{}_{:x}__",
ignored_sections.len(),
std::ptr::addr_of!(ignored_sections) as usize
);
let ignored_content = caps.get(0).unwrap().as_str();
ignored_sections.push((placeholder.clone(), ignored_content.to_string()));
placeholder
})
.to_string();
content = RE_EMBED_MACRO
.replace_all(&content, |caps: ®ex::Captures| {
let input = caps.get(0).map_or("", |m| m.as_str());
let app = parser::parse_app(input);
if app.is_none() {
return input.to_string();
}
let app = app.unwrap();
let mut rendered = render_template_app(ctx, app.clone());
if rendered.is_ok() && rendered.as_ref().unwrap().is_none() {
rendered = render_script_app(ctx, app.clone());
}
if !rendered.is_ok() {
let err = rendered.err().unwrap();
eprintln!(
"(mdbook-embedify): Error while rendering app \"{}\" in {:?}. {}",
app.name, chapter_path, err
);
return input.to_string();
}
if rendered.as_ref().unwrap().is_none() {
return input.to_string();
}
let options_string = {
let mut json_parts = Vec::new();
for option in &app.options {
json_parts.push(format!(
"data-option-{}=\"{}\"",
option.name,
option.value
));
}
format!("{}", json_parts.join(" "))
};
format!(
"<!-- {} -->\n\n<div data-embedify data-app=\"{}\" {} style=\"display:none\"></div>\n\n{}",
input,
app.name,
options_string,
rendered.unwrap().unwrap()
)
})
.to_string();
content = RE_EMBED_IGNORE
.replace_all(&content, |caps: ®ex::Captures| {
let content_part = caps.get(1).map_or("", |m| m.as_str());
format!("{{% embed {} %}}", content_part)
})
.to_string();
for (placeholder, ignored_content) in ignored_sections.into_iter().rev() {
if let Some(pos) = content.find(&placeholder) {
content.replace_range(pos..pos + placeholder.len(), &ignored_content);
}
}
content
}
impl Preprocessor for Embed {
fn name(&self) -> &str {
"mdbook-embedify"
}
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let config = &ctx.config;
let footer = utils::get_config_bool(config, "footer.enable");
let giscus = utils::get_config_bool(config, "giscus.enable");
let scroll_to_top = utils::get_config_bool(config, "scroll-to-top.enable");
let announcement_banner = utils::get_config_bool(config, "announcement-banner.enable");
book.for_each_mut(|item| {
if let mdbook_core::book::BookItem::Chapter(chapter) = item {
let mut content = chapter.content.clone();
if scroll_to_top {
content.push_str(&utils::create_scroll_to_top());
}
if announcement_banner {
content.push_str(&utils::create_announcement_banner(config));
}
if giscus {
content.push_str(&utils::create_giscus(config));
}
if footer {
content.push_str(&utils::create_footer(config));
}
chapter.content = render_embeds(ctx, chapter.clone(), content);
}
});
Ok(book)
}
}