Browse Source

Merge pull request #31 from Keats/anchors

Anchors
index-subcmd
Vincent Prouillet GitHub 7 years ago
parent
commit
85f55ada9c
8 changed files with 123 additions and 5 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +5
    -0
      README.md
  3. +6
    -0
      src/config.rs
  4. +86
    -5
      src/markdown.rs
  5. +1
    -0
      src/site.rs
  6. +3
    -0
      src/templates/anchor-link.html
  7. +2
    -0
      test_site/content/posts/fixed-slug.md
  8. +17
    -0
      tests/site.rs

+ 3
- 0
CHANGELOG.md 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


+ 5
- 0
README.md 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.


+ 6
- 0
src/config.rs 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,
}
}


+ 86
- 5
src/markdown.rs 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");
}
}

+ 1
- 0
src/site.rs 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")),


+ 3
- 0
src/templates/anchor-link.html View File

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

+ 2
- 0
test_site/content/posts/fixed-slug.md View File

@@ -6,3 +6,5 @@ date = "2017-01-01"
+++

A simple page with a slug defined

# Title

+ 17
- 0
tests/site.rs 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\""));
}

Loading…
Cancel
Save