From 956a5cc7fd417791c60cfc944909cba5fc570da4 Mon Sep 17 00:00:00 2001 From: Behnam Esfahbod Date: Fri, 1 Sep 2017 16:54:57 -0700 Subject: [PATCH] Fix heading links in nested pages Plus fixing the whitespace chars not being replaced by hyphen. Also expand tests for link creations, and add test for nested pages. Fixes Fixes --- build.rs | 2 + src/renderer/html_handlebars/hbs_renderer.rs | 82 ++++++++++++-------- tests/dummy/book/first/index.md | 2 + tests/dummy/book/first/nested.md | 4 +- tests/helpers/mod.rs | 3 - tests/rendered_output.rs | 34 +++++--- 6 files changed, 82 insertions(+), 45 deletions(-) diff --git a/build.rs b/build.rs index 261a7f2e..845f7b07 100644 --- a/build.rs +++ b/build.rs @@ -25,6 +25,8 @@ mod execs { } +// TODO: Drop after error_chain is fixed +#[allow(unused_doc_comment)] error_chain!{ foreign_links { Io(std::io::Error); diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 9ace5dcc..2f79e71e 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -63,14 +63,14 @@ 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(""), + filepath.to_str().unwrap_or(""), 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 +111,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 +182,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 +284,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 +412,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 +422,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 +443,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 +457,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![ + static REPL_SUB: &[&str] = &[ "", "", "", @@ -470,27 +470,25 @@ 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 == '_' { + if c.is_alphanumeric() || c == '_' { id.push(c.to_ascii_lowercase()); } else if c.is_whitespace() { - id.push(c); + id.push('-'); } } - id } // 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 +497,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 ) @@ -601,17 +599,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 index 215ebc71..200672b9 100644 --- a/tests/dummy/book/first/index.md +++ b/tests/dummy/book/first/index.md @@ -1,3 +1,5 @@ # First Chapter more text. + +## Some Section diff --git a/tests/dummy/book/first/nested.md b/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/helpers/mod.rs b/tests/helpers/mod.rs index e0682d15..da6af35c 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -1,8 +1,5 @@ //! 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. use std::path::Path; diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 6acdce7b..7de0838f 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -38,11 +38,11 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() { 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"]; @@ -61,11 +61,11 @@ fn check_correct_cross_links_in_nested_dir() { 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"]; @@ -73,6 +73,20 @@ fn check_correct_cross_links_in_nested_dir() { for filename in files_in_nested_dir { 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]