diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 9ace5dcc..3946439a 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -63,14 +63,16 @@ impl HtmlHandlebars { debug!("[*]: Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; - let filename = Path::new(&ch.path).with_extension("html"); + let filepath = Path::new(&ch.path).with_extension("html"); let rendered = self.post_process(rendered, - filename.file_name().unwrap().to_str().unwrap_or(""), - ctx.book.get_html_config().get_playpen_config()); + &normalize_path(filepath.to_str() + .ok_or(Error::from(format!("Bad file name: {}", filepath.display())))?), + ctx.book.get_html_config().get_playpen_config() + ); // Write to file - info!("[*] Creating {:?} ✓", filename.display()); - ctx.book.write_file(filename, &rendered.into_bytes())?; + info!("[*] Creating {:?} ✓", filepath.display()); + ctx.book.write_file(filepath, &rendered.into_bytes())?; if ctx.is_index { self.render_index(ctx.book, ch, &ctx.destination)?; @@ -111,9 +113,9 @@ impl HtmlHandlebars { Ok(()) } - fn post_process(&self, rendered: String, filename: &str, playpen_config: &PlaypenConfig) -> String { - let rendered = build_header_links(&rendered, filename); - let rendered = fix_anchor_links(&rendered, filename); + fn post_process(&self, rendered: String, filepath: &str, playpen_config: &PlaypenConfig) -> String { + let rendered = build_header_links(&rendered, &filepath); + let rendered = fix_anchor_links(&rendered, &filepath); let rendered = fix_code_blocks(&rendered); let rendered = add_playpen_pre(&rendered, playpen_config); @@ -182,7 +184,7 @@ impl HtmlHandlebars { Ok(()) } - /// Helper function to write a file to the build directory, normalizing + /// Helper function to write a file to the build directory, normalizing /// the path to be relative to the book root. fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> { let mut data = Vec::new(); @@ -284,7 +286,7 @@ impl Renderer for HtmlHandlebars { let rendered = self.post_process(rendered, "print.html", book.get_html_config().get_playpen_config()); - + book.write_file( Path::new("print").with_extension("html"), &rendered.into_bytes(), @@ -412,7 +414,7 @@ fn make_data(book: &MDBook) -> Result /// Goes through the rendered HTML, making sure all header tags are wrapped in /// an anchor so people can link to sections directly. -fn build_header_links(html: &str, filename: &str) -> String { +fn build_header_links(html: &str, filepath: &str) -> String { let regex = Regex::new(r"(.*?)").unwrap(); let mut id_counter = HashMap::new(); @@ -422,14 +424,14 @@ fn build_header_links(html: &str, filename: &str) -> String { "Regex should ensure we only ever get numbers here", ); - wrap_header_with_link(level, &caps[2], &mut id_counter, filename) + wrap_header_with_link(level, &caps[2], &mut id_counter, filepath) }) .into_owned() } /// Wraps a single header tag with a link, making sure each tag gets its own /// unique ID by appending an auto-incremented number (if necessary). -fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap, filename: &str) +fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap, filepath: &str) -> String { let raw_id = id_from_content(content); @@ -443,11 +445,11 @@ fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap{text}"#, + r##"{text}"##, level = level, id = id, text = content, - filename = filename + filepath = filepath ) } @@ -457,7 +459,7 @@ fn id_from_content(content: &str) -> String { let mut content = content.to_string(); // Skip any tags or html-encoded stuff - let repl_sub = vec![ + const REPL_SUB: &[&str] = &[ "", "", "", @@ -470,27 +472,17 @@ fn id_from_content(content: &str) -> String { "'", """, ]; - for sub in repl_sub { + for sub in REPL_SUB { content = content.replace(sub, ""); } - let mut id = String::new(); - - for c in content.chars() { - if c.is_alphanumeric() || c == '-' || c == '_' { - id.push(c.to_ascii_lowercase()); - } else if c.is_whitespace() { - id.push(c); - } - } - - id + normalize_id(&content) } // anchors to the same page (href="#anchor") do not work because of // pointing to the root folder. This function *fixes* // that in a very inelegant way -fn fix_anchor_links(html: &str, filename: &str) -> String { +fn fix_anchor_links(html: &str, filepath: &str) -> String { let regex = Regex::new(r##"]+)href="#([^"]+)"([^>]*)>"##).unwrap(); regex .replace_all(html, |caps: &Captures| { @@ -499,9 +491,9 @@ fn fix_anchor_links(html: &str, filename: &str) -> String { let after = &caps[3]; format!( - "", + "", before = before, - filename = filename, + filepath = filepath, anchor = anchor, after = after ) @@ -592,6 +584,26 @@ struct RenderItemContext<'a> { is_index: bool, } +pub fn normalize_path(path: &str) -> String { + use std::path::is_separator; + path.chars() + .map(|ch| if is_separator(ch) { '/' } else { ch }) + .collect::() +} + +pub fn normalize_id(content: &str) -> String { + content.chars() + .filter_map(|ch| + if ch.is_alphanumeric() || ch == '_' { + Some(ch.to_ascii_lowercase()) + } else if ch.is_whitespace() { + Some('-') + } else { + None + } + ) + .collect::() +} #[cfg(test)] @@ -601,17 +613,39 @@ mod tests { #[test] fn original_build_header_links() { let inputs = vec![ - ("blah blah

Foo

", r#"blah blah

Foo

"#), - ("

Foo

", r#"

Foo

"#), - ("

Foo^bar

", r#"

Foo^bar

"#), - ("

", r#"

"#), - ("

", r#"

"#), - ("

Foo

Foo

", - r#"

Foo

Foo

"#), + ( + "blah blah

Foo

", + r##"blah blah

Foo

"##, + ), + ( + "

Foo

", + r##"

Foo

"##, + ), + ( + "

Foo^bar

", + r##"

Foo^bar

"##, + ), + ( + "

", + r##"

"## + ), + ( + "

", + r##"

"## + ), + ( + "

Foo

Foo

", + r##"

Foo

Foo

"## + ), ]; for (src, should_be) in inputs { - let got = build_header_links(src, "bar.rs"); + let filepath = "./some_chapter/some_section.html"; + let got = build_header_links(&src, filepath); + assert_eq!(got, should_be); + + // This is redundant for most cases + let got = fix_anchor_links(&got, filepath); assert_eq!(got, should_be); } } diff --git a/tests/dummy-book/first/index.md b/tests/dummy-book/first/index.md deleted file mode 100644 index d778d9db..00000000 --- a/tests/dummy-book/first/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# First Chapter - -more text. \ No newline at end of file diff --git a/tests/dummy-book/SUMMARY.md b/tests/dummy/book/SUMMARY.md similarity index 100% rename from tests/dummy-book/SUMMARY.md rename to tests/dummy/book/SUMMARY.md diff --git a/tests/dummy-book/conclusion.md b/tests/dummy/book/conclusion.md similarity index 100% rename from tests/dummy-book/conclusion.md rename to tests/dummy/book/conclusion.md diff --git a/tests/dummy/book/first/index.md b/tests/dummy/book/first/index.md new file mode 100644 index 00000000..200672b9 --- /dev/null +++ b/tests/dummy/book/first/index.md @@ -0,0 +1,5 @@ +# First Chapter + +more text. + +## Some Section diff --git a/tests/dummy-book/first/nested.md b/tests/dummy/book/first/nested.md similarity index 80% rename from tests/dummy-book/first/nested.md rename to tests/dummy/book/first/nested.md index dc09383f..a7a1cdda 100644 --- a/tests/dummy-book/first/nested.md +++ b/tests/dummy/book/first/nested.md @@ -4,4 +4,6 @@ This file has some testable code. ```rust assert!($TEST_STATUS); -``` \ No newline at end of file +``` + +## Some Section diff --git a/tests/dummy-book/intro.md b/tests/dummy/book/intro.md similarity index 100% rename from tests/dummy-book/intro.md rename to tests/dummy/book/intro.md diff --git a/tests/dummy-book/second.md b/tests/dummy/book/second.md similarity index 100% rename from tests/dummy-book/second.md rename to tests/dummy/book/second.md diff --git a/tests/helpers.rs b/tests/dummy/mod.rs similarity index 61% rename from tests/helpers.rs rename to tests/dummy/mod.rs index b626ec46..ca561d86 100644 --- a/tests/helpers.rs +++ b/tests/dummy/mod.rs @@ -1,26 +1,23 @@ -//! Helpers for tests which exercise the overall application, in particular -//! the `MDBook` initialization and build/rendering process. -//! //! This will create an entire book in a temporary directory using some //! dummy contents from the `tests/dummy-book/` directory. +// Not all features are used in all test crates, so... +#![allow(dead_code, unused_extern_crates)] -#![allow(dead_code, unused_variables, unused_imports)] extern crate tempdir; -use std::path::Path; -use std::fs::{self, File}; -use std::io::{Read, Write}; +use std::fs::{create_dir_all, File}; +use std::io::Write; use tempdir::TempDir; -const SUMMARY_MD: &'static str = include_str!("dummy-book/SUMMARY.md"); -const INTRO: &'static str = include_str!("dummy-book/intro.md"); -const FIRST: &'static str = include_str!("dummy-book/first/index.md"); -const NESTED: &'static str = include_str!("dummy-book/first/nested.md"); -const SECOND: &'static str = include_str!("dummy-book/second.md"); -const CONCLUSION: &'static str = include_str!("dummy-book/conclusion.md"); +const SUMMARY_MD: &'static str = include_str!("book/SUMMARY.md"); +const INTRO: &'static str = include_str!("book/intro.md"); +const FIRST: &'static str = include_str!("book/first/index.md"); +const NESTED: &'static str = include_str!("book/first/nested.md"); +const SECOND: &'static str = include_str!("book/second.md"); +const CONCLUSION: &'static str = include_str!("book/conclusion.md"); /// Create a dummy book in a temporary directory, using the contents of @@ -58,10 +55,10 @@ impl DummyBook { let temp = TempDir::new("dummy_book").unwrap(); let src = temp.path().join("src"); - fs::create_dir_all(&src).unwrap(); + create_dir_all(&src).unwrap(); let first = src.join("first"); - fs::create_dir_all(&first).unwrap(); + create_dir_all(&first).unwrap(); let to_substitute = if self.passing_test { "true" } else { "false" }; let nested_text = NESTED.replace("$TEST_STATUS", to_substitute); @@ -91,20 +88,3 @@ impl Default for DummyBook { DummyBook { passing_test: true } } } - - -/// Read the contents of the provided file into memory and then iterate through -/// the list of strings asserting that the file contains all of them. -pub fn assert_contains_strings>(filename: P, strings: &[&str]) { - let filename = filename.as_ref(); - - let mut content = String::new(); - File::open(&filename) - .expect("Couldn't open the provided file") - .read_to_string(&mut content) - .expect("Couldn't read the file's contents"); - - for s in strings { - assert!(content.contains(s), "Searching for {:?} in {}\n\n{}", s, filename.display(), content); - } -} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs new file mode 100644 index 00000000..da6af35c --- /dev/null +++ b/tests/helpers/mod.rs @@ -0,0 +1,24 @@ +//! Helpers for tests which exercise the overall application, in particular +//! the `MDBook` initialization and build/rendering process. + + +use std::path::Path; +use std::fs::File; +use std::io::Read; + + +/// Read the contents of the provided file into memory and then iterate through +/// the list of strings asserting that the file contains all of them. +pub fn assert_contains_strings>(filename: P, strings: &[&str]) { + let filename = filename.as_ref(); + + let mut content = String::new(); + File::open(&filename) + .expect("Couldn't open the provided file") + .read_to_string(&mut content) + .expect("Couldn't read the file's contents"); + + for s in strings { + assert!(content.contains(s), "Searching for {:?} in {}\n\n{}", s, filename.display(), content); + } +} diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 84866f1a..7de0838f 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -1,14 +1,18 @@ extern crate mdbook; extern crate tempdir; +mod dummy; mod helpers; + +use dummy::DummyBook; +use helpers::assert_contains_strings; use mdbook::MDBook; /// Make sure you can load the dummy book and build it without panicking. #[test] fn build_the_dummy_book() { - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); md.build().unwrap(); @@ -16,7 +20,7 @@ fn build_the_dummy_book() { #[test] fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); assert!(!temp.path().join("book").exists()); @@ -28,62 +32,76 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { #[test] fn make_sure_bottom_level_files_contain_links_to_chapters() { - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); md.build().unwrap(); let dest = temp.path().join("book"); let links = vec![ - "intro.html", - "first/index.html", - "first/nested.html", - "second.html", - "conclusion.html", + r#"href="intro.html""#, + r#"href="./first/index.html""#, + r#"href="./first/nested.html""#, + r#"href="./second.html""#, + r#"href="./conclusion.html""#, ]; let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"]; for filename in files_in_bottom_dir { - helpers::assert_contains_strings(dest.join(filename), &links); + assert_contains_strings(dest.join(filename), &links); } } #[test] fn check_correct_cross_links_in_nested_dir() { - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); md.build().unwrap(); let first = temp.path().join("book").join("first"); let links = vec![ r#""#, - "intro.html", - "first/index.html", - "first/nested.html", - "second.html", - "conclusion.html", + r#"href="intro.html""#, + r#"href="./first/index.html""#, + r#"href="./first/nested.html""#, + r#"href="./second.html""#, + r#"href="./conclusion.html""#, ]; let files_in_nested_dir = vec!["index.html", "nested.html"]; for filename in files_in_nested_dir { - helpers::assert_contains_strings(first.join(filename), &links); + assert_contains_strings(first.join(filename), &links); } + + assert_contains_strings( + first.join("index.html"), + &[ + r##"href="./first/index.html#some-section" id="some-section""## + ], + ); + + assert_contains_strings( + first.join("nested.html"), + &[ + r##"href="./first/nested.html#some-section" id="some-section""## + ], + ); } #[test] fn rendered_code_has_playpen_stuff() { - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); md.build().unwrap(); let nested = temp.path().join("book/first/nested.html"); let playpen_class = vec![r#"class="playpen""#]; - helpers::assert_contains_strings(nested, &playpen_class); + assert_contains_strings(nested, &playpen_class); let book_js = temp.path().join("book/book.js"); - helpers::assert_contains_strings(book_js, &[".playpen"]); + assert_contains_strings(book_js, &[".playpen"]); } #[test] @@ -96,7 +114,7 @@ fn chapter_content_appears_in_rendered_document() { ("conclusion.html", "Conclusion"), ]; - let temp = helpers::DummyBook::default().build(); + let temp = DummyBook::default().build(); let mut md = MDBook::new(temp.path()); md.build().unwrap(); @@ -104,6 +122,6 @@ fn chapter_content_appears_in_rendered_document() { for (filename, text) in content { let path = destination.join(filename); - helpers::assert_contains_strings(path, &[text]); + assert_contains_strings(path, &[text]); } -} \ No newline at end of file +} diff --git a/tests/testing.rs b/tests/testing.rs index b4d22394..e4cf8da5 100644 --- a/tests/testing.rs +++ b/tests/testing.rs @@ -1,13 +1,15 @@ -extern crate tempdir; extern crate mdbook; +extern crate tempdir; -mod helpers; +mod dummy; + +use dummy::DummyBook; use mdbook::MDBook; #[test] fn mdbook_can_correctly_test_a_passing_book() { - let temp = helpers::DummyBook::default() + let temp = DummyBook::default() .with_passing_test(true) .build(); let mut md = MDBook::new(temp.path()); @@ -17,10 +19,10 @@ fn mdbook_can_correctly_test_a_passing_book() { #[test] fn mdbook_detects_book_with_failing_tests() { - let temp = helpers::DummyBook::default() + let temp = DummyBook::default() .with_passing_test(false) .build(); let mut md: MDBook = MDBook::new(temp.path()); assert!(md.test(vec![]).is_err()); -} \ No newline at end of file +}