Skip to content

Commit

Permalink
Fix anchor links in relative urls
Browse files Browse the repository at this point in the history
  • Loading branch information
cetra3 committed Feb 2, 2018
1 parent 70d366c commit 4b635ff
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 64 deletions.
72 changes: 13 additions & 59 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ impl HtmlHandlebars {
let filepath = Path::new(&ch.path).with_extension("html");
let rendered = self.post_process(
rendered,
&normalize_path(filepath.to_str().ok_or_else(|| {
Error::from(format!("Bad file name: {}", filepath.display()))
})?),
&ctx.html_config.playpen,
);

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

// This could cause a problem when someone displays
// code containing <base href=...>
// on the front page, however this case should be very very rare...
content = content.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");

self.write_file(destination, "index.html", content.as_bytes())?;

debug!(
Expand All @@ -136,11 +125,9 @@ impl HtmlHandlebars {
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
fn post_process(&self,
rendered: String,
filepath: &str,
playpen_config: &Playpen)
-> String {
let rendered = build_header_links(&rendered, filepath);
let rendered = fix_anchor_links(&rendered, filepath);
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);

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

let rendered = self.post_process(rendered,
"print.html",
&html_config.playpen);

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

/// 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, filepath: &str) -> String {
fn build_header_links(html: &str) -> String {
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
let mut id_counter = HashMap::new();

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

wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
wrap_header_with_link(level, &caps[2], &mut id_counter)
})
.into_owned()
}
Expand All @@ -466,8 +452,7 @@ fn build_header_links(html: &str, filepath: &str) -> String {
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str)
id_counter: &mut HashMap<String, usize>)
-> String {
let raw_id = id_from_content(content);

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

format!(
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
level = level,
id = id,
text = content,
filepath = filepath
text = content
)
}

Expand Down Expand Up @@ -516,25 +500,6 @@ fn id_from_content(content: &str) -> String {
normalize_id(trimmed)
}

// anchors to the same page (href="#anchor") do not work because of
// <base href="../"> pointing to the root folder. This function *fixes*
// that in a very inelegant way
fn fix_anchor_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];

format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after)
})
.into_owned()
}


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

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

pub fn normalize_id(content: &str) -> String {
content.chars()
Expand All @@ -653,37 +612,32 @@ mod tests {
let inputs = vec![
(
"blah blah <h1>Foo</h1>",
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h1>Foo</h1>",
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h3>Foo^bar</h3>",
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
),
(
"<h4></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
),
(
"<h4><em>Hï</em></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
),
(
"<h1>Foo</h1><h3>Foo</h3>",
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>"##,
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>"##,
),
];

for (src, should_be) in inputs {
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);
let got = build_header_links(&src);
assert_eq!(got, should_be);
}
}
Expand Down
22 changes: 19 additions & 3 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod fs;
mod string;
use errors::Error;
use regex::Regex;

use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
Expand Down Expand Up @@ -65,13 +66,27 @@ impl EventQuoteConverter {

// Adjusts links so that local markdown links are converted to html
fn adjust_links(event: Event) -> Event {

lazy_static! {
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
}

match event {
Event::Start(Tag::Link(dest, title)) => {
if dest.ends_with(".md") && !dest.starts_with("http://") && !dest.starts_with("https://") {
let html_link = [&dest[..dest.len() - 3], ".html"].concat();
if !HTTP_LINK.is_match(&dest) {
if let Some(caps) = MD_LINK.captures(&dest) {

let mut html_link = [&caps["link"], ".html"].concat();

return Event::Start(Tag::Link(Cow::from(html_link), title))
if let Some(anchor) = caps.name("anchor") {
html_link.push_str(anchor.as_str());
}

return Event::Start(Tag::Link(Cow::from(html_link), title))
}
}

Event::Start(Tag::Link(dest, title))
},
_ => event
Expand Down Expand Up @@ -144,6 +159,7 @@ mod tests {
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(render_markdown("[example](example.md)", false), "<p><a href=\"example.html\">example</a></p>\n");
assert_eq!(render_markdown("[example_anchor](example.md#anchor)", false), "<p><a href=\"example.html#anchor\">example_anchor</a></p>\n");
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions tests/rendered_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ fn check_correct_cross_links_in_nested_dir() {
assert_contains_strings(
first.join("index.html"),
&[
r##"href="first/index.html#some-section" id="some-section""##,
r##"href="#some-section" id="some-section""##,
],
);

assert_contains_strings(
first.join("nested.html"),
&[
r##"href="first/nested.html#some-section" id="some-section""##,
r##"href="#some-section" id="some-section""##,
],
);
}
Expand Down

0 comments on commit 4b635ff

Please sign in to comment.