commit
85f55ada9c
|
@ -4,6 +4,9 @@
|
||||||
- Fix RSS feed link and description
|
- Fix RSS feed link and description
|
||||||
- Renamed `Page::url` and `Section::url` to `Page::path` and `Section::path`
|
- Renamed `Page::url` and `Section::url` to `Page::path` and `Section::path`
|
||||||
- Pass `current_url` and `current_path` to every template
|
- 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)
|
## 0.0.3 (2017-04-05)
|
||||||
- Add some colours in console
|
- 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).
|
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
|
### Shortcodes
|
||||||
Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video.
|
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>,
|
pub generate_tags_pages: Option<bool>,
|
||||||
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true
|
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true
|
||||||
pub generate_categories_pages: Option<bool>,
|
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
|
/// All user params set in [extra] in the config
|
||||||
pub extra: Option<HashMap<String, Toml>>,
|
pub extra: Option<HashMap<String, Toml>>,
|
||||||
|
@ -67,6 +71,7 @@ impl Config {
|
||||||
set_default!(config.generate_rss, false);
|
set_default!(config.generate_rss, false);
|
||||||
set_default!(config.generate_tags_pages, true);
|
set_default!(config.generate_tags_pages, true);
|
||||||
set_default!(config.generate_categories_pages, true);
|
set_default!(config.generate_categories_pages, true);
|
||||||
|
set_default!(config.insert_anchor_links, false);
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
@ -104,6 +109,7 @@ impl Default for Config {
|
||||||
generate_rss: Some(false),
|
generate_rss: Some(false),
|
||||||
generate_tags_pages: Some(true),
|
generate_tags_pages: Some(true),
|
||||||
generate_categories_pages: Some(true),
|
generate_categories_pages: Some(true),
|
||||||
|
insert_anchor_links: Some(false),
|
||||||
extra: None,
|
extra: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::collections::HashMap;
|
||||||
use pulldown_cmark as cmark;
|
use pulldown_cmark as cmark;
|
||||||
use self::cmark::{Parser, Event, Tag};
|
use self::cmark::{Parser, Event, Tag};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use slug::slugify;
|
||||||
use syntect::dumps::from_binary;
|
use syntect::dumps::from_binary;
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::parsing::SyntaxSet;
|
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;
|
let mut added_shortcode = false;
|
||||||
// Don't transform things that look like shortcodes in code blocks
|
// Don't transform things that look like shortcodes in code blocks
|
||||||
let mut in_code_block = false;
|
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
|
// the rendered html
|
||||||
let mut html = String::new();
|
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 {
|
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
|
// Business as usual
|
||||||
Event::Text(text)
|
Event::Text(text)
|
||||||
},
|
},
|
||||||
|
@ -207,14 +241,25 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
|
||||||
// Need to handle relative links
|
// Need to handle relative links
|
||||||
Event::Start(Tag::Link(ref link, ref title)) => {
|
Event::Start(Tag::Link(ref link, ref title)) => {
|
||||||
if link.starts_with("./") {
|
if link.starts_with("./") {
|
||||||
let permalink = match permalinks.get(&link.replacen("./", "", 1)) {
|
// First we remove the ./ since that's gutenberg specific
|
||||||
Some(p) => p,
|
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 => {
|
None => {
|
||||||
error = Some(format!("Relative link {} not found.", link).into());
|
error = Some(format!("Relative link {} not found.", link).into());
|
||||||
return Event::Html(Owned("".to_string()));
|
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()));
|
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;
|
in_code_block = false;
|
||||||
event
|
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
|
// If we added shortcodes, don't close a paragraph since there's none
|
||||||
Event::End(Tag::Paragraph) => {
|
Event::End(Tag::Paragraph) => {
|
||||||
if added_shortcode {
|
if added_shortcode {
|
||||||
|
@ -294,8 +348,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_markdown_to_html_simple() {
|
fn test_markdown_to_html_simple() {
|
||||||
let res = markdown_to_html("# hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
|
||||||
assert_eq!(res, "<h1>hello</h1>\n");
|
assert_eq!(res, "<p>hello</p>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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]
|
#[test]
|
||||||
fn test_markdown_to_html_relative_link_inexistant() {
|
fn test_markdown_to_html_relative_link_inexistant() {
|
||||||
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default());
|
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default());
|
||||||
assert!(res.is_err());
|
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")),
|
("rss.xml", include_str!("templates/rss.xml")),
|
||||||
("sitemap.xml", include_str!("templates/sitemap.xml")),
|
("sitemap.xml", include_str!("templates/sitemap.xml")),
|
||||||
("robots.txt", include_str!("templates/robots.txt")),
|
("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/youtube.html", include_str!("templates/shortcodes/youtube.html")),
|
||||||
("shortcodes/vimeo.html", include_str!("templates/shortcodes/vimeo.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
|
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</loc>"));
|
||||||
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/tags/tag-with-space</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