Skip to content

Commit 4b635ff

Browse files
author
cetra3
committed
Fix anchor links in relative urls
1 parent 70d366c commit 4b635ff

File tree

3 files changed

+34
-64
lines changed

3 files changed

+34
-64
lines changed

src/renderer/html_handlebars/hbs_renderer.rs

+13-59
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@ impl HtmlHandlebars {
8686
let filepath = Path::new(&ch.path).with_extension("html");
8787
let rendered = self.post_process(
8888
rendered,
89-
&normalize_path(filepath.to_str().ok_or_else(|| {
90-
Error::from(format!("Bad file name: {}", filepath.display()))
91-
})?),
9289
&ctx.html_config.playpen,
9390
);
9491

@@ -115,14 +112,6 @@ impl HtmlHandlebars {
115112
File::open(destination.join(&ch.path.with_extension("html")))?
116113
.read_to_string(&mut content)?;
117114

118-
// This could cause a problem when someone displays
119-
// code containing <base href=...>
120-
// on the front page, however this case should be very very rare...
121-
content = content.lines()
122-
.filter(|line| !line.contains("<base href="))
123-
.collect::<Vec<&str>>()
124-
.join("\n");
125-
126115
self.write_file(destination, "index.html", content.as_bytes())?;
127116

128117
debug!(
@@ -136,11 +125,9 @@ impl HtmlHandlebars {
136125
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
137126
fn post_process(&self,
138127
rendered: String,
139-
filepath: &str,
140128
playpen_config: &Playpen)
141129
-> String {
142-
let rendered = build_header_links(&rendered, filepath);
143-
let rendered = fix_anchor_links(&rendered, filepath);
130+
let rendered = build_header_links(&rendered);
144131
let rendered = fix_code_blocks(&rendered);
145132
let rendered = add_playpen_pre(&rendered, playpen_config);
146133

@@ -330,7 +317,6 @@ impl Renderer for HtmlHandlebars {
330317
let rendered = handlebars.render("index", &data)?;
331318

332319
let rendered = self.post_process(rendered,
333-
"print.html",
334320
&html_config.playpen);
335321

336322
self.write_file(&destination, "print.html", &rendered.into_bytes())?;
@@ -449,15 +435,15 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
449435

450436
/// Goes through the rendered HTML, making sure all header tags are wrapped in
451437
/// an anchor so people can link to sections directly.
452-
fn build_header_links(html: &str, filepath: &str) -> String {
438+
fn build_header_links(html: &str) -> String {
453439
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
454440
let mut id_counter = HashMap::new();
455441

456442
regex.replace_all(html, |caps: &Captures| {
457443
let level = caps[1].parse()
458444
.expect("Regex should ensure we only ever get numbers here");
459445

460-
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
446+
wrap_header_with_link(level, &caps[2], &mut id_counter)
461447
})
462448
.into_owned()
463449
}
@@ -466,8 +452,7 @@ fn build_header_links(html: &str, filepath: &str) -> String {
466452
/// unique ID by appending an auto-incremented number (if necessary).
467453
fn wrap_header_with_link(level: usize,
468454
content: &str,
469-
id_counter: &mut HashMap<String, usize>,
470-
filepath: &str)
455+
id_counter: &mut HashMap<String, usize>)
471456
-> String {
472457
let raw_id = id_from_content(content);
473458

@@ -481,11 +466,10 @@ fn wrap_header_with_link(level: usize,
481466
*id_count += 1;
482467

483468
format!(
484-
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
469+
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
485470
level = level,
486471
id = id,
487-
text = content,
488-
filepath = filepath
472+
text = content
489473
)
490474
}
491475

@@ -516,25 +500,6 @@ fn id_from_content(content: &str) -> String {
516500
normalize_id(trimmed)
517501
}
518502

519-
// anchors to the same page (href="#anchor") do not work because of
520-
// <base href="../"> pointing to the root folder. This function *fixes*
521-
// that in a very inelegant way
522-
fn fix_anchor_links(html: &str, filepath: &str) -> String {
523-
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
524-
regex.replace_all(html, |caps: &Captures| {
525-
let before = &caps[1];
526-
let anchor = &caps[2];
527-
let after = &caps[3];
528-
529-
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
530-
before = before,
531-
filepath = filepath,
532-
anchor = anchor,
533-
after = after)
534-
})
535-
.into_owned()
536-
}
537-
538503

539504
// The rust book uses annotations for rustdoc to test code snippets,
540505
// like the following:
@@ -624,12 +589,6 @@ struct RenderItemContext<'a> {
624589
html_config: HtmlConfig,
625590
}
626591

627-
pub fn normalize_path(path: &str) -> String {
628-
use std::path::is_separator;
629-
path.chars()
630-
.map(|ch| if is_separator(ch) { '/' } else { ch })
631-
.collect::<String>()
632-
}
633592

634593
pub fn normalize_id(content: &str) -> String {
635594
content.chars()
@@ -653,37 +612,32 @@ mod tests {
653612
let inputs = vec![
654613
(
655614
"blah blah <h1>Foo</h1>",
656-
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
615+
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
657616
),
658617
(
659618
"<h1>Foo</h1>",
660-
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
619+
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
661620
),
662621
(
663622
"<h3>Foo^bar</h3>",
664-
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
623+
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
665624
),
666625
(
667626
"<h4></h4>",
668-
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
627+
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
669628
),
670629
(
671630
"<h4><em>Hï</em></h4>",
672-
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
631+
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
673632
),
674633
(
675634
"<h1>Foo</h1><h3>Foo</h3>",
676-
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>"##,
635+
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
677636
),
678637
];
679638

680639
for (src, should_be) in inputs {
681-
let filepath = "./some_chapter/some_section.html";
682-
let got = build_header_links(&src, filepath);
683-
assert_eq!(got, should_be);
684-
685-
// This is redundant for most cases
686-
let got = fix_anchor_links(&got, filepath);
640+
let got = build_header_links(&src);
687641
assert_eq!(got, should_be);
688642
}
689643
}

src/utils/mod.rs

+19-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
pub mod fs;
44
mod string;
55
use errors::Error;
6+
use regex::Regex;
67

78
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
89
OPTION_ENABLE_TABLES};
@@ -65,13 +66,27 @@ impl EventQuoteConverter {
6566

6667
// Adjusts links so that local markdown links are converted to html
6768
fn adjust_links(event: Event) -> Event {
69+
70+
lazy_static! {
71+
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
72+
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
73+
}
74+
6875
match event {
6976
Event::Start(Tag::Link(dest, title)) => {
70-
if dest.ends_with(".md") && !dest.starts_with("http://") && !dest.starts_with("https://") {
71-
let html_link = [&dest[..dest.len() - 3], ".html"].concat();
77+
if !HTTP_LINK.is_match(&dest) {
78+
if let Some(caps) = MD_LINK.captures(&dest) {
79+
80+
let mut html_link = [&caps["link"], ".html"].concat();
7281

73-
return Event::Start(Tag::Link(Cow::from(html_link), title))
82+
if let Some(anchor) = caps.name("anchor") {
83+
html_link.push_str(anchor.as_str());
84+
}
85+
86+
return Event::Start(Tag::Link(Cow::from(html_link), title))
87+
}
7488
}
89+
7590
Event::Start(Tag::Link(dest, title))
7691
},
7792
_ => event
@@ -144,6 +159,7 @@ mod tests {
144159
#[test]
145160
fn it_can_adjust_markdown_links() {
146161
assert_eq!(render_markdown("[example](example.md)", false), "<p><a href=\"example.html\">example</a></p>\n");
162+
assert_eq!(render_markdown("[example_anchor](example.md#anchor)", false), "<p><a href=\"example.html#anchor\">example_anchor</a></p>\n");
147163
}
148164

149165
#[test]

tests/rendered_output.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,14 @@ fn check_correct_cross_links_in_nested_dir() {
9999
assert_contains_strings(
100100
first.join("index.html"),
101101
&[
102-
r##"href="first/index.html#some-section" id="some-section""##,
102+
r##"href="#some-section" id="some-section""##,
103103
],
104104
);
105105

106106
assert_contains_strings(
107107
first.join("nested.html"),
108108
&[
109-
r##"href="first/nested.html#some-section" id="some-section""##,
109+
r##"href="#some-section" id="some-section""##,
110110
],
111111
);
112112
}

0 commit comments

Comments
 (0)