Implement links in section headers.

This project already had a transitive dependency on regex; let's use it.

This isn't the most efficient solution, but it should be fine. It ends
up doing five full scans of the text. There's probably an easier way but
I'm mostly just trying to get this to work for now.

This also implements the same algorithm that rustdoc does for generating
the name for the link.

Fixes #204
This commit is contained in:
Steve Klabnik 2017-02-16 01:39:13 -05:00
parent d609988264
commit 38b3516b60
3 changed files with 41 additions and 0 deletions

View File

@ -24,6 +24,7 @@ log = "0.3"
env_logger = "0.3" env_logger = "0.3"
toml = { version = "0.2", features = ["serde"] } toml = { version = "0.2", features = ["serde"] }
open = "1.1" open = "1.1"
regex = "0.1.80"
# Watch feature # Watch feature
notify = { version = "3.0", optional = true } notify = { version = "3.0", optional = true }

View File

@ -74,6 +74,7 @@ extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate handlebars; extern crate handlebars;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate regex;
#[macro_use] extern crate log; #[macro_use] extern crate log;
pub mod book; pub mod book;

View File

@ -3,7 +3,9 @@ use renderer::Renderer;
use book::MDBook; use book::MDBook;
use book::bookitem::BookItem; use book::bookitem::BookItem;
use {utils, theme}; use {utils, theme};
use regex::{Regex, Captures};
use std::ascii::AsciiExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File}; use std::fs::{self, File};
use std::error::Error; use std::error::Error;
@ -91,6 +93,9 @@ impl Renderer for HtmlHandlebars {
// Render the handlebars template with the data // Render the handlebars template with the data
debug!("[*]: Render template"); debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data)); let rendered = try!(handlebars.render("index", &data));
// create links for headers
let rendered = build_header_links(rendered);
// Write to file // Write to file
let filename = Path::new(&ch.path).with_extension("html"); let filename = Path::new(&ch.path).with_extension("html");
@ -208,3 +213,37 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
debug!("[*]: JSON constructed"); debug!("[*]: JSON constructed");
Ok(data) Ok(data)
} }
fn build_header_links(mut html: String) -> String {
for header in &["h1", "h2", "h3", "h4", "h5"] {
let regex = Regex::new(&format!("<{h}>(.*?)</{h}>", h=header)).unwrap();
html = regex.replace_all(&html, |caps: &Captures| {
let text = &caps[1];
let mut id = text.to_string();
let repl_sub = vec!["<em>", "</em>", "<code>", "</code>",
"<strong>", "</strong>",
"&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in repl_sub {
id = id.replace(sub, "");
}
let id = id.chars().filter_map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
if c.is_ascii() {
Some(c.to_ascii_lowercase())
} else {
Some(c)
}
} else if c.is_whitespace() && c.is_ascii() {
Some('-')
} else {
None
}
}).collect::<String>();
format!("<a class=\"header\" href=\"#{id}\" name=\"{id}\"><{h}>{text}</{h}></a>", h=header, id=id, text=text)
});
}
html
}