Merge pull request #31 from Keats/anchors

Anchors
This commit is contained in:
Vincent Prouillet 2017-04-13 17:16:13 +09:00 committed by GitHub
commit 85f55ada9c
8 changed files with 123 additions and 5 deletions

View file

@ -4,6 +4,9 @@
- Fix RSS feed link and description
- Renamed `Page::url` and `Section::url` to `Page::path` and `Section::path`
- Pass `current_url` and `current_path` to every template
- Add id to headers to allow anchor linking
- Make relative link work with anchors
- Add option to render an anchor link automatically next to headers
## 0.0.3 (2017-04-05)
- Add some colours in console

View file

@ -158,6 +158,11 @@ to link to. The path to the file starts from the `content` directory.
For example, linking to a file located at `content/pages/about.md` would be `[my link](./pages/about.md).
### Anchors
Headers get an automatic id from their content in order to be able to add deep links. By default no links are actually created but
the `insert_anchor_links` option in `config.toml` can be set to `true` to link tags. The default template is very ugly and will need
CSS tweaks in your projet to look decent. The default template can also be easily overwritten by creating a `anchor-link.html` file in
the `templates` directory.
### Shortcodes
Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video.

View file

@ -30,6 +30,10 @@ pub struct Config {
pub generate_tags_pages: Option<bool>,
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true
pub generate_categories_pages: Option<bool>,
/// Whether to insert a link for each header like in Github READMEs. Defaults to false
/// The default template can be overridden by creating a `anchor-link.html` template and CSS will need to be
/// written if you turn that on.
pub insert_anchor_links: Option<bool>,
/// All user params set in [extra] in the config
pub extra: Option<HashMap<String, Toml>>,
@ -67,6 +71,7 @@ impl Config {
set_default!(config.generate_rss, false);
set_default!(config.generate_tags_pages, true);
set_default!(config.generate_categories_pages, true);
set_default!(config.insert_anchor_links, false);
Ok(config)
}
@ -104,6 +109,7 @@ impl Default for Config {
generate_rss: Some(false),
generate_tags_pages: Some(true),
generate_categories_pages: Some(true),
insert_anchor_links: Some(false),
extra: None,
}
}

View file

@ -4,6 +4,7 @@ use std::collections::HashMap;
use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag};
use regex::Regex;
use slug::slugify;
use syntect::dumps::from_binary;
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
@ -114,8 +115,28 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
let mut added_shortcode = false;
// Don't transform things that look like shortcodes in code blocks
let mut in_code_block = false;
// If we get text in header, we need to insert the id and a anchor
let mut in_header = false;
// the rendered html
let mut html = String::new();
let mut anchors: Vec<String> = vec![];
// We might have cases where the slug is already present in our list of anchor
// for example an article could have several titles named Example
// We add a counter after the slug if the slug is already present, which
// means we will have example, example-1, example-2 etc
fn find_anchor(anchors: &Vec<String>, name: String, level: u8) -> String {
if level == 0 && !anchors.contains(&name) {
return name.to_string();
}
let new_anchor = format!("{}-{}", name, level + 1);
if !anchors.contains(&new_anchor) {
return new_anchor;
}
find_anchor(anchors, name, level + 1)
}
{
let parser = Parser::new(content).map(|event| match event {
@ -177,6 +198,19 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
}
}
if in_header {
let id = find_anchor(&anchors, slugify(&text), 0);
anchors.push(id.clone());
let anchor_link = if config.insert_anchor_links.unwrap() {
let mut context = Context::new();
context.add("id", &id);
tera.render("anchor-link.html", &context).unwrap()
} else {
String::new()
};
return Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text)));
}
// Business as usual
Event::Text(text)
},
@ -207,14 +241,25 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
// Need to handle relative links
Event::Start(Tag::Link(ref link, ref title)) => {
if link.starts_with("./") {
let permalink = match permalinks.get(&link.replacen("./", "", 1)) {
Some(p) => p,
// First we remove the ./ since that's gutenberg specific
let clean_link = link.replacen("./", "", 1);
// Then we remove any potential anchor
// parts[0] will be the file path and parts[1] the anchor if present
let parts = clean_link.split('#').collect::<Vec<_>>();
match permalinks.get(parts[0]) {
Some(p) => {
let url = if parts.len() > 1 {
format!("{}#{}", p, parts[1])
} else {
p.to_string()
};
return Event::Start(Tag::Link(Owned(url), title.clone()));
},
None => {
error = Some(format!("Relative link {} not found.", link).into());
return Event::Html(Owned("".to_string()));
}
};
return Event::Start(Tag::Link(Owned(permalink.clone()), title.clone()));
}
return Event::Start(Tag::Link(link.clone(), title.clone()));
@ -228,6 +273,15 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
in_code_block = false;
event
},
Event::Start(Tag::Header(num)) => {
in_header = true;
// ugly eh
return Event::Html(Owned(format!("<h{} ", num)));
},
Event::End(Tag::Header(_)) => {
in_header = false;
event
},
// If we added shortcodes, don't close a paragraph since there's none
Event::End(Tag::Paragraph) => {
if added_shortcode {
@ -294,8 +348,8 @@ mod tests {
#[test]
fn test_markdown_to_html_simple() {
let res = markdown_to_html("# hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
assert_eq!(res, "<h1>hello</h1>\n");
let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
assert_eq!(res, "<p>hello</p>\n");
}
#[test]
@ -410,10 +464,37 @@ A quote
);
}
#[test]
fn test_markdown_to_html_relative_links_with_anchors() {
let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let res = markdown_to_html(
r#"[rel link](./pages/about.md#cv)"#,
&permalinks,
&GUTENBERG_TERA,
&Config::default()
).unwrap();
assert!(
res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
);
}
#[test]
fn test_markdown_to_html_relative_link_inexistant() {
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default());
assert!(res.is_err());
}
#[test]
fn test_markdown_to_html_add_id_to_headers() {
let res = markdown_to_html(r#"# Hello"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n");
}
#[test]
fn test_markdown_to_html_add_id_to_headers_same_slug() {
let res = markdown_to_html("# Hello\n# Hello", &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
}
}

View file

@ -23,6 +23,7 @@ lazy_static! {
("rss.xml", include_str!("templates/rss.xml")),
("sitemap.xml", include_str!("templates/sitemap.xml")),
("robots.txt", include_str!("templates/robots.txt")),
("anchor-link.html", include_str!("templates/anchor-link.html")),
("shortcodes/youtube.html", include_str!("templates/shortcodes/youtube.html")),
("shortcodes/vimeo.html", include_str!("templates/shortcodes/vimeo.html")),

View file

@ -0,0 +1,3 @@
<a class="anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">
🔗
</a>

View file

@ -6,3 +6,5 @@ date = "2017-01-01"
+++
A simple page with a slug defined
# Title

View file

@ -264,3 +264,20 @@ fn test_can_build_site_with_tags() {
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags</loc>"));
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags/tag-with-space</loc>"));
}
#[test]
fn test_can_build_site_and_insert_anchor_links() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
site.config.insert_anchor_links = Some(true);
site.load().unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
site.set_output_path(&public);
site.build().unwrap();
assert!(Path::new(&public).exists());
// anchor link inserted
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"anchor\" href=\"#title\""));
}