#![deny(missing_docs, warnings)]
use failure::Fail;
use lazy_static::lazy_static;
use log::*;
use mdbook::book::{Book, Chapter};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::BookItem;
use regex::Regex;
use std::path::{Path, PathBuf};
static _ESCAPE_CHAR: &'static str = "/";
pub struct Superimport;
impl Preprocessor for Superimport {
fn name(&self) -> &str {
"mdbook-superimport"
}
fn run(
&self,
ctx: &PreprocessorContext,
mut book: Book,
) -> Result<Book, mdbook::errors::Error> {
debug!("Running `run` method in superimport Preprocessor trait impl");
let book_src_dir = ctx.root.join(&ctx.config.book.src);
for section in book.sections.iter_mut() {
process_chapter(section, &book_src_dir)?;
}
Ok(book)
}
}
fn process_chapter(book_item: &mut BookItem, book_src_dir: &PathBuf) -> mdbook::errors::Result<()> {
if let BookItem::Chapter(ref mut chapter) = book_item {
debug!("Processing chapter {}", chapter.name);
let chapter_dir = chapter
.path
.parent()
.map(|dir| book_src_dir.join(dir))
.expect("All book items have a parent");
let mut content = chapter.content.clone();
let simports = SuperImport::find_unescaped_superimports(chapter);
for simport in simports.iter().rev() {
let new_content = match simport.read_content_between_tags(&chapter_dir) {
Ok(new_content) => new_content,
Err(err) => panic!("Error reading content for superimport: {:#?}", err),
};
content = content.replace(simport.full_simport_text, &new_content);
}
chapter.content = content;
for sub_item in chapter.sub_items.iter_mut() {
process_chapter(sub_item, book_src_dir)?;
}
}
Ok(())
}
#[derive(Debug, PartialEq)]
struct SuperImport<'a> {
host_chapter: &'a Chapter,
file: PathBuf,
full_simport_text: &'a str,
tag: &'a str,
start: usize,
end: usize,
}
lazy_static! {
static ref SUPERIMPORT_REGEX: Regex = Regex::new(
r"(?x) # (?x) means insignificant whitespace mode
# allows us to put comments and space things out.
/\{\{\#.*\}\} # escaped import such as `/{{ #superimport some-file.txt@some-tag }}`
| # OR
# Non escaped import -> `{{ #superimport some-file.txt@some-tag }}`
\{\{\s* # opening braces and whitespace
\#superimport # #superimport
\s+ # separating whitespace
(?P<file>[a-zA-Z0-9\s_.\-/\\]+) # some-file.txt
@ # @ symbol that denotes the name of a tag
(?P<tag>[a-zA-Z0-9_.\-]+) # some-tag (alphanumeric underscores and dashes)
\s*\}\} # whitespace and closing braces
"
).unwrap();
}
impl<'a> SuperImport<'a> {
fn find_unescaped_superimports(chapter: &Chapter) -> Vec<SuperImport> {
let mut simports = vec![];
let matches = SUPERIMPORT_REGEX.captures_iter(chapter.content.as_str());
for capture_match in matches {
let full_capture = capture_match.get(0).unwrap();
let full_simport_text = &chapter.content[full_capture.start()..full_capture.end()];
if full_simport_text.starts_with(r"/") {
continue;
}
let file = capture_match["file"].into();
let tag = capture_match.get(2).unwrap();
let simport = SuperImport {
host_chapter: chapter,
file,
full_simport_text,
tag: &chapter.content[tag.start()..tag.end()],
start: full_capture.start(),
end: full_capture.end(),
};
simports.push(simport);
}
simports
}
}
#[derive(Debug, Fail, PartialEq)]
enum TagError {
#[fail(display = "Could not find `@superimport start {}`", tag)]
#[allow(unused)] MissingStartTag { tag: String },
}
impl<'a> SuperImport<'a> {
fn read_content_between_tags(&self, chapter_dir: &PathBuf) -> Result<String, TagError> {
debug!(
r#"Reading content in chapter "{}" for superimport "{:#?}" "#,
self.host_chapter.name, self.full_simport_text
);
let path = Path::join(&chapter_dir, &self.file);
let content = String::from_utf8(::std::fs::read(&path).unwrap()).unwrap();
let start_regex = Regex::new(&format!(
r"(?x) # Insignificant whitespace mode (allows for comments)
@superimport
\s+ # Separating whitespace
start
\s+ # Separating whitespace
{tag}
.*? # Characters between start import tag and end of line
[\n\r] # New line right before the start import tag
(?P<content_to_import> # Everything in between the start and end import lines
(.|\n|\r)*
)
[\n\r] # New line right before the end import tag
.*? # Characters between start of end import line and end import tag
@superimport
\s+ # Separating whitespace
end
\s+ # Separating whitespace
{tag}
",
tag = regex::escape(self.tag)
))
.unwrap();
let captures = start_regex.captures(&content).unwrap();
let content_between_tags = captures["content_to_import"].to_string();
Ok(content_between_tags)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simports_from_chapter() {
let tag_import_chapter = make_tag_import_chapter();
let simports = SuperImport::find_unescaped_superimports(&tag_import_chapter);
let expected_simports = vec![SuperImport {
host_chapter: &tag_import_chapter,
file: "./fixture.css".into(),
full_simport_text: "{{#superimport ./fixture.css@cool-css }}",
tag: "cool-css",
start: 20,
end: 60,
}];
assert_eq!(simports, expected_simports);
}
#[test]
fn ignore_escaped_simport() {
let escaped_import_chapter = make_escaped_import_chapter();
let simports = SuperImport::find_unescaped_superimports(&escaped_import_chapter);
assert_eq!(simports.len(), 0);
}
#[test]
fn content_between_tags() {
let tag_import_chapter = make_tag_import_chapter();
let simport = &SuperImport::find_unescaped_superimports(&tag_import_chapter)[0];
let chapter_dir = "book/src/test-cases/tag-import";
let chapter_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), chapter_dir);
let content_between_tags = simport.read_content_between_tags(&chapter_dir.into());
let expected_content = r#"
.this-will-be-included {
display: block;
}
"#;
assert_eq!(content_between_tags.unwrap(), expected_content);
}
#[test]
fn replace_chapter() {
let tag_import_chapter = make_tag_import_chapter();
let mut item = BookItem::Chapter(tag_import_chapter);
process_chapter(&mut item, &"".into()).unwrap();
let expected_content = r#"# Tag Import
```md
.this-will-be-included {
display: block;
}
```
"#;
match item {
BookItem::Chapter(tag_import_chapter) => {
assert_eq!(tag_import_chapter.content.as_str(), expected_content);
}
_ => panic!(""),
};
}
#[test]
fn replace_escaped_simport() {
let escaped_import_chapter = make_escaped_import_chapter();
let expected_content = r#"# Escaped Superimport
```
/{{#superimport ./ignored.txt@foo-bar}}
```
"#;
let mut item = BookItem::Chapter(escaped_import_chapter);
process_chapter(&mut item, &"".into()).unwrap();
match item {
BookItem::Chapter(escaped_chapter) => {
assert_eq!(escaped_chapter.content.as_str(), expected_content);
}
_ => panic!(""),
};
}
fn make_tag_import_chapter() -> Chapter {
let chapter = "book/src/test-cases/tag-import/README.md";
let tag_import_chapter = Chapter::new(
"Tag Import",
include_str!("../book/src/test-cases/tag-import/README.md").to_string(),
&format!("{}/{}", env!("CARGO_MANIFEST_DIR"), chapter),
vec![],
);
tag_import_chapter
}
fn make_escaped_import_chapter() -> Chapter {
let chapter = "book/src/test-cases/escaped/README.md";
let tag_import_chapter = Chapter::new(
"Escaped",
include_str!("../book/src/test-cases/escaped/README.md").to_string(),
&format!("{}/{}", env!("CARGO_MANIFEST_DIR"), chapter),
vec![],
);
tag_import_chapter
}
}