Skip to content

Commit 5eb57be

Browse files
authored
Merge pull request rust-lang#419 from behnam/nested
Fix heading links in nested pages
2 parents f4cde50 + 8aaf2e6 commit 5eb57be

File tree

12 files changed

+164
-102
lines changed

12 files changed

+164
-102
lines changed

src/renderer/html_handlebars/hbs_renderer.rs

+73-39
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,16 @@ impl HtmlHandlebars {
6363
debug!("[*]: Render template");
6464
let rendered = ctx.handlebars.render("index", &ctx.data)?;
6565

66-
let filename = Path::new(&ch.path).with_extension("html");
66+
let filepath = Path::new(&ch.path).with_extension("html");
6767
let rendered = self.post_process(rendered,
68-
filename.file_name().unwrap().to_str().unwrap_or(""),
69-
ctx.book.get_html_config().get_playpen_config());
68+
&normalize_path(filepath.to_str()
69+
.ok_or(Error::from(format!("Bad file name: {}", filepath.display())))?),
70+
ctx.book.get_html_config().get_playpen_config()
71+
);
7072

7173
// Write to file
72-
info!("[*] Creating {:?} ✓", filename.display());
73-
ctx.book.write_file(filename, &rendered.into_bytes())?;
74+
info!("[*] Creating {:?} ✓", filepath.display());
75+
ctx.book.write_file(filepath, &rendered.into_bytes())?;
7476

7577
if ctx.is_index {
7678
self.render_index(ctx.book, ch, &ctx.destination)?;
@@ -111,9 +113,9 @@ impl HtmlHandlebars {
111113
Ok(())
112114
}
113115

114-
fn post_process(&self, rendered: String, filename: &str, playpen_config: &PlaypenConfig) -> String {
115-
let rendered = build_header_links(&rendered, filename);
116-
let rendered = fix_anchor_links(&rendered, filename);
116+
fn post_process(&self, rendered: String, filepath: &str, playpen_config: &PlaypenConfig) -> String {
117+
let rendered = build_header_links(&rendered, &filepath);
118+
let rendered = fix_anchor_links(&rendered, &filepath);
117119
let rendered = fix_code_blocks(&rendered);
118120
let rendered = add_playpen_pre(&rendered, playpen_config);
119121

@@ -182,7 +184,7 @@ impl HtmlHandlebars {
182184
Ok(())
183185
}
184186

185-
/// Helper function to write a file to the build directory, normalizing
187+
/// Helper function to write a file to the build directory, normalizing
186188
/// the path to be relative to the book root.
187189
fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> {
188190
let mut data = Vec::new();
@@ -284,7 +286,7 @@ impl Renderer for HtmlHandlebars {
284286

285287
let rendered = self.post_process(rendered, "print.html",
286288
book.get_html_config().get_playpen_config());
287-
289+
288290
book.write_file(
289291
Path::new("print").with_extension("html"),
290292
&rendered.into_bytes(),
@@ -412,7 +414,7 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
412414

413415
/// Goes through the rendered HTML, making sure all header tags are wrapped in
414416
/// an anchor so people can link to sections directly.
415-
fn build_header_links(html: &str, filename: &str) -> String {
417+
fn build_header_links(html: &str, filepath: &str) -> String {
416418
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
417419
let mut id_counter = HashMap::new();
418420

@@ -422,14 +424,14 @@ fn build_header_links(html: &str, filename: &str) -> String {
422424
"Regex should ensure we only ever get numbers here",
423425
);
424426

425-
wrap_header_with_link(level, &caps[2], &mut id_counter, filename)
427+
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
426428
})
427429
.into_owned()
428430
}
429431

430432
/// Wraps a single header tag with a link, making sure each tag gets its own
431433
/// unique ID by appending an auto-incremented number (if necessary).
432-
fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<String, usize>, filename: &str)
434+
fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<String, usize>, filepath: &str)
433435
-> String {
434436
let raw_id = id_from_content(content);
435437

@@ -443,11 +445,11 @@ fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<S
443445
*id_count += 1;
444446

445447
format!(
446-
r#"<a class="header" href="{filename}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"#,
448+
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
447449
level = level,
448450
id = id,
449451
text = content,
450-
filename = filename
452+
filepath = filepath
451453
)
452454
}
453455

@@ -457,7 +459,7 @@ fn id_from_content(content: &str) -> String {
457459
let mut content = content.to_string();
458460

459461
// Skip any tags or html-encoded stuff
460-
let repl_sub = vec![
462+
const REPL_SUB: &[&str] = &[
461463
"<em>",
462464
"</em>",
463465
"<code>",
@@ -470,27 +472,17 @@ fn id_from_content(content: &str) -> String {
470472
"&#39;",
471473
"&quot;",
472474
];
473-
for sub in repl_sub {
475+
for sub in REPL_SUB {
474476
content = content.replace(sub, "");
475477
}
476478

477-
let mut id = String::new();
478-
479-
for c in content.chars() {
480-
if c.is_alphanumeric() || c == '-' || c == '_' {
481-
id.push(c.to_ascii_lowercase());
482-
} else if c.is_whitespace() {
483-
id.push(c);
484-
}
485-
}
486-
487-
id
479+
normalize_id(&content)
488480
}
489481

490482
// anchors to the same page (href="#anchor") do not work because of
491483
// <base href="../"> pointing to the root folder. This function *fixes*
492484
// that in a very inelegant way
493-
fn fix_anchor_links(html: &str, filename: &str) -> String {
485+
fn fix_anchor_links(html: &str, filepath: &str) -> String {
494486
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
495487
regex
496488
.replace_all(html, |caps: &Captures| {
@@ -499,9 +491,9 @@ fn fix_anchor_links(html: &str, filename: &str) -> String {
499491
let after = &caps[3];
500492

501493
format!(
502-
"<a{before}href=\"{filename}#{anchor}\"{after}>",
494+
"<a{before}href=\"{filepath}#{anchor}\"{after}>",
503495
before = before,
504-
filename = filename,
496+
filepath = filepath,
505497
anchor = anchor,
506498
after = after
507499
)
@@ -592,6 +584,26 @@ struct RenderItemContext<'a> {
592584
is_index: bool,
593585
}
594586

587+
pub fn normalize_path(path: &str) -> String {
588+
use std::path::is_separator;
589+
path.chars()
590+
.map(|ch| if is_separator(ch) { '/' } else { ch })
591+
.collect::<String>()
592+
}
593+
594+
pub fn normalize_id(content: &str) -> String {
595+
content.chars()
596+
.filter_map(|ch|
597+
if ch.is_alphanumeric() || ch == '_' {
598+
Some(ch.to_ascii_lowercase())
599+
} else if ch.is_whitespace() {
600+
Some('-')
601+
} else {
602+
None
603+
}
604+
)
605+
.collect::<String>()
606+
}
595607

596608

597609
#[cfg(test)]
@@ -601,17 +613,39 @@ mod tests {
601613
#[test]
602614
fn original_build_header_links() {
603615
let inputs = vec![
604-
("blah blah <h1>Foo</h1>", r#"blah blah <a class="header" href="bar.rs#foo" id="foo"><h1>Foo</h1></a>"#),
605-
("<h1>Foo</h1>", r#"<a class="header" href="bar.rs#foo" id="foo"><h1>Foo</h1></a>"#),
606-
("<h3>Foo^bar</h3>", r#"<a class="header" href="bar.rs#foobar" id="foobar"><h3>Foo^bar</h3></a>"#),
607-
("<h4></h4>", r#"<a class="header" href="bar.rs#" id=""><h4></h4></a>"#),
608-
("<h4><em>Hï</em></h4>", r#"<a class="header" href="bar.rs#hï" id="hï"><h4><em>Hï</em></h4></a>"#),
609-
("<h1>Foo</h1><h3>Foo</h3>",
610-
r#"<a class="header" href="bar.rs#foo" id="foo"><h1>Foo</h1></a><a class="header" href="bar.rs#foo-1" id="foo-1"><h3>Foo</h3></a>"#),
616+
(
617+
"blah blah <h1>Foo</h1>",
618+
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
619+
),
620+
(
621+
"<h1>Foo</h1>",
622+
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
623+
),
624+
(
625+
"<h3>Foo^bar</h3>",
626+
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
627+
),
628+
(
629+
"<h4></h4>",
630+
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##
631+
),
632+
(
633+
"<h4><em>Hï</em></h4>",
634+
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##
635+
),
636+
(
637+
"<h1>Foo</h1><h3>Foo</h3>",
638+
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##
639+
),
611640
];
612641

613642
for (src, should_be) in inputs {
614-
let got = build_header_links(src, "bar.rs");
643+
let filepath = "./some_chapter/some_section.html";
644+
let got = build_header_links(&src, filepath);
645+
assert_eq!(got, should_be);
646+
647+
// This is redundant for most cases
648+
let got = fix_anchor_links(&got, filepath);
615649
assert_eq!(got, should_be);
616650
}
617651
}

tests/dummy-book/first/index.md

-3
This file was deleted.
File renamed without changes.
File renamed without changes.

tests/dummy/book/first/index.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# First Chapter
2+
3+
more text.
4+
5+
## Some Section

tests/dummy-book/first/nested.md renamed to tests/dummy/book/first/nested.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ This file has some testable code.
44

55
```rust
66
assert!($TEST_STATUS);
7-
```
7+
```
8+
9+
## Some Section
File renamed without changes.
File renamed without changes.

tests/helpers.rs renamed to tests/dummy/mod.rs

+12-32
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
//! Helpers for tests which exercise the overall application, in particular
2-
//! the `MDBook` initialization and build/rendering process.
3-
//!
41
//! This will create an entire book in a temporary directory using some
52
//! dummy contents from the `tests/dummy-book/` directory.
63
4+
// Not all features are used in all test crates, so...
5+
#![allow(dead_code, unused_extern_crates)]
76

8-
#![allow(dead_code, unused_variables, unused_imports)]
97
extern crate tempdir;
108

11-
use std::path::Path;
12-
use std::fs::{self, File};
13-
use std::io::{Read, Write};
9+
use std::fs::{create_dir_all, File};
10+
use std::io::Write;
1411

1512
use tempdir::TempDir;
1613

1714

18-
const SUMMARY_MD: &'static str = include_str!("dummy-book/SUMMARY.md");
19-
const INTRO: &'static str = include_str!("dummy-book/intro.md");
20-
const FIRST: &'static str = include_str!("dummy-book/first/index.md");
21-
const NESTED: &'static str = include_str!("dummy-book/first/nested.md");
22-
const SECOND: &'static str = include_str!("dummy-book/second.md");
23-
const CONCLUSION: &'static str = include_str!("dummy-book/conclusion.md");
15+
const SUMMARY_MD: &'static str = include_str!("book/SUMMARY.md");
16+
const INTRO: &'static str = include_str!("book/intro.md");
17+
const FIRST: &'static str = include_str!("book/first/index.md");
18+
const NESTED: &'static str = include_str!("book/first/nested.md");
19+
const SECOND: &'static str = include_str!("book/second.md");
20+
const CONCLUSION: &'static str = include_str!("book/conclusion.md");
2421

2522

2623
/// Create a dummy book in a temporary directory, using the contents of
@@ -58,10 +55,10 @@ impl DummyBook {
5855
let temp = TempDir::new("dummy_book").unwrap();
5956

6057
let src = temp.path().join("src");
61-
fs::create_dir_all(&src).unwrap();
58+
create_dir_all(&src).unwrap();
6259

6360
let first = src.join("first");
64-
fs::create_dir_all(&first).unwrap();
61+
create_dir_all(&first).unwrap();
6562

6663
let to_substitute = if self.passing_test { "true" } else { "false" };
6764
let nested_text = NESTED.replace("$TEST_STATUS", to_substitute);
@@ -91,20 +88,3 @@ impl Default for DummyBook {
9188
DummyBook { passing_test: true }
9289
}
9390
}
94-
95-
96-
/// Read the contents of the provided file into memory and then iterate through
97-
/// the list of strings asserting that the file contains all of them.
98-
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
99-
let filename = filename.as_ref();
100-
101-
let mut content = String::new();
102-
File::open(&filename)
103-
.expect("Couldn't open the provided file")
104-
.read_to_string(&mut content)
105-
.expect("Couldn't read the file's contents");
106-
107-
for s in strings {
108-
assert!(content.contains(s), "Searching for {:?} in {}\n\n{}", s, filename.display(), content);
109-
}
110-
}

tests/helpers/mod.rs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Helpers for tests which exercise the overall application, in particular
2+
//! the `MDBook` initialization and build/rendering process.
3+
4+
5+
use std::path::Path;
6+
use std::fs::File;
7+
use std::io::Read;
8+
9+
10+
/// Read the contents of the provided file into memory and then iterate through
11+
/// the list of strings asserting that the file contains all of them.
12+
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
13+
let filename = filename.as_ref();
14+
15+
let mut content = String::new();
16+
File::open(&filename)
17+
.expect("Couldn't open the provided file")
18+
.read_to_string(&mut content)
19+
.expect("Couldn't read the file's contents");
20+
21+
for s in strings {
22+
assert!(content.contains(s), "Searching for {:?} in {}\n\n{}", s, filename.display(), content);
23+
}
24+
}

0 commit comments

Comments
 (0)