commit
85f55ada9c
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
src/templates/anchor-link.html
Normal file
3
src/templates/anchor-link.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<a class="anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">
|
||||
🔗
|
||||
</a>
|
|
@ -6,3 +6,5 @@ date = "2017-01-01"
|
|||
+++
|
||||
|
||||
A simple page with a slug defined
|
||||
|
||||
# Title
|
||||
|
|
|
@ -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…
Reference in a new issue