Add some external link markdown tweaking options

Closes #681, #695
This commit is contained in:
Vincent Prouillet 2020-11-21 22:20:54 +01:00
parent 047ce32efd
commit 2c681f3439
5 changed files with 133 additions and 12 deletions

View file

@ -6,12 +6,13 @@
- Support `output_dir` in `config.toml` - Support `output_dir` in `config.toml`
- Allow sections to be drafted - Allow sections to be drafted
- Allow specifying default language in filenames - Allow specifying default language in filenames
- Render emoji in Markdown content if the option is enabled - Render emoji in Markdown content if the `render_emoji` option is enabled
- Enable YouTube privacy mode for the YouTube shortcode - Enable YouTube privacy mode for the YouTube shortcode
- Add language as class to the `<code>` block - Add language as class to the `<code>` block
- Add bibtex to `load_data` - Add bibtex to `load_data`
- Add a `[markdown]` section to `config.toml` to configure rendering - Add a `[markdown]` section to `config.toml` to configure rendering
- Add `highlight_code` and `highlight_theme` to a `[markdown]` section in `config.toml` - Add `highlight_code` and `highlight_theme` to a `[markdown]` section in `config.toml`
- Add `external_links_target_blank`, `external_links_no_follow` and `external_links_no_referrer`
## 0.12.2 (2020-09-28) ## 0.12.2 (2020-09-28)

View file

@ -12,6 +12,46 @@ pub struct Markdown {
pub highlight_theme: String, pub highlight_theme: String,
/// Whether to render emoji aliases (e.g.: :smile: => 😄) in the markdown files /// Whether to render emoji aliases (e.g.: :smile: => 😄) in the markdown files
pub render_emoji: bool, pub render_emoji: bool,
/// Whether external links are to be opened in a new tab
/// If this is true, a `rel="noopener"` will always automatically be added for security reasons
pub external_links_target_blank: bool,
/// Whether to set rel="nofollow" for all external links
pub external_links_no_follow: bool,
/// Whether to set rel="noreferrer" for all external links
pub external_links_no_referrer: bool,
}
impl Markdown {
pub fn has_external_link_tweaks(&self) -> bool {
self.external_links_target_blank
|| self.external_links_no_follow
|| self.external_links_no_referrer
}
pub fn construct_external_link_tag(&self, url: &str, title: &str) -> String {
let mut rel_opts = Vec::new();
let mut target = "".to_owned();
let title = if title == "" { "".to_owned() } else { format!("title=\"{}\" ", title) };
if self.external_links_target_blank {
// Security risk otherwise
rel_opts.push("noopener");
target = "target=\"_blank\" ".to_owned();
}
if self.external_links_no_follow {
rel_opts.push("nofollow");
}
if self.external_links_no_referrer {
rel_opts.push("noreferrer");
}
let rel = if rel_opts.is_empty() {
"".to_owned()
} else {
format!("rel=\"{}\" ", rel_opts.join(" "))
};
format!("<a {}{}{}href=\"{}\">", rel, target, title, url)
}
} }
impl Default for Markdown { impl Default for Markdown {
@ -20,6 +60,9 @@ impl Default for Markdown {
highlight_code: false, highlight_code: false,
highlight_theme: DEFAULT_HIGHLIGHT_THEME.to_owned(), highlight_theme: DEFAULT_HIGHLIGHT_THEME.to_owned(),
render_emoji: false, render_emoji: false,
external_links_target_blank: false,
external_links_no_follow: false,
external_links_no_referrer: false,
} }
} }
} }

View file

@ -13,7 +13,6 @@ use utils::slugs::slugify_anchors;
use utils::vec::InsertMany; use utils::vec::InsertMany;
use self::cmark::{Event, LinkType, Options, Parser, Tag}; use self::cmark::{Event, LinkType, Options, Parser, Tag};
use pulldown_cmark::CodeBlockKind;
mod codeblock; mod codeblock;
mod fence; mod fence;
@ -101,11 +100,6 @@ fn fix_link(
return Ok(link.to_string()); return Ok(link.to_string());
} }
// TODO: remove me in a few versions when people have upgraded
if link.starts_with("./") && link.contains(".md") {
println!("It looks like the link `{}` is using the previous syntax for internal links: start with @/ instead", link);
}
// A few situations here: // A few situations here:
// - it could be a relative link (starting with `@/`) // - it could be a relative link (starting with `@/`)
// - it could be a link to a co-located asset // - it could be a link to a co-located asset
@ -211,7 +205,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
} }
Event::Start(Tag::CodeBlock(ref kind)) => { Event::Start(Tag::CodeBlock(ref kind)) => {
let language = match kind { let language = match kind {
CodeBlockKind::Fenced(fence_info) => { cmark::CodeBlockKind::Fenced(fence_info) => {
let fence_info = fence::FenceSettings::new(fence_info); let fence_info = fence::FenceSettings::new(fence_info);
fence_info.language fence_info.language
} }
@ -228,8 +222,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
let theme = &THEME_SET.themes[context.config.highlight_theme()]; let theme = &THEME_SET.themes[context.config.highlight_theme()];
match kind { match kind {
CodeBlockKind::Indented => (), cmark::CodeBlockKind::Indented => (),
CodeBlockKind::Fenced(fence_info) => { cmark::CodeBlockKind::Fenced(fence_info) => {
// This selects the background color the same way that // This selects the background color the same way that
// start_coloured_html_snippet does // start_coloured_html_snippet does
let color = theme let color = theme
@ -289,8 +283,22 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
return Event::Html("".into()); return Event::Html("".into());
} }
}; };
if is_external_link(&link)
Event::Start(Tag::Link(link_type, fixed_link.into(), title)) && context.config.markdown.has_external_link_tweaks()
{
let mut escaped = String::new();
// write_str can fail but here there are no reasons it should (afaik?)
cmark::escape::escape_href(&mut escaped, &link).expect("Could not write to buffer");
Event::Html(
context
.config
.markdown
.construct_external_link_tag(&escaped, &title)
.into(),
)
} else {
Event::Start(Tag::Link(link_type, fixed_link.into(), title))
}
} }
Event::Html(ref markup) => { Event::Html(ref markup) => {
if markup.contains("<!-- more -->") { if markup.contains("<!-- more -->") {

View file

@ -1058,3 +1058,61 @@ fn emoji_aliases_are_ignored_when_disabled_in_config() {
let res = render_content("Hello, World! :smile:", &context).unwrap(); let res = render_content("Hello, World! :smile:", &context).unwrap();
assert_eq!(res.body, "<p>Hello, World! :smile:</p>\n"); assert_eq!(res.body, "<p>Hello, World! :smile:</p>\n");
} }
#[test]
fn basic_external_links_unchanged() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("<https://google.com>", &context).unwrap();
assert_eq!(res.body, "<p><a href=\"https://google.com\">https://google.com</a></p>\n");
}
#[test]
fn can_set_target_blank_for_external_link() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.markdown.external_links_target_blank = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("<https://google.com>", &context).unwrap();
assert_eq!(res.body, "<p><a rel=\"noopener\" target=\"_blank\" href=\"https://google.com\">https://google.com</a></p>\n");
}
#[test]
fn can_set_nofollow_for_external_link() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.markdown.external_links_no_follow = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
// Testing href escaping while we're there
let res = render_content("<https://google.com/éllo>", &context).unwrap();
assert_eq!(
res.body,
"<p><a rel=\"nofollow\" href=\"https://google.com/%C3%A9llo\">https://google.com/éllo</a></p>\n"
);
}
#[test]
fn can_set_noreferrer_for_external_link() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.markdown.external_links_no_referrer = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("<https://google.com>", &context).unwrap();
assert_eq!(
res.body,
"<p><a rel=\"noreferrer\" href=\"https://google.com\">https://google.com</a></p>\n"
);
}
#[test]
fn can_set_all_options_for_external_link() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.markdown.external_links_target_blank = true;
config.markdown.external_links_no_follow = true;
config.markdown.external_links_no_referrer = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("<https://google.com>", &context).unwrap();
assert_eq!(res.body, "<p><a rel=\"noopener nofollow noreferrer\" target=\"_blank\" href=\"https://google.com\">https://google.com</a></p>\n");
}

View file

@ -95,6 +95,7 @@ extra_syntaxes = []
# You can override the default output directory `public` by setting an another value. # You can override the default output directory `public` by setting an another value.
# output_dir = "docs" # output_dir = "docs"
# Configuration of the Markdown rendering
[markdown] [markdown]
# When set to "true", all code blocks are highlighted. # When set to "true", all code blocks are highlighted.
highlight_code = false highlight_code = false
@ -107,6 +108,16 @@ highlight_theme = "base16-ocean-dark"
# Unicode emoji equivalent in the rendered Markdown files. (e.g.: :smile: => 😄) # Unicode emoji equivalent in the rendered Markdown files. (e.g.: :smile: => 😄)
render_emoji = false render_emoji = false
# Whether external links are to be opened in a new tab
# If this is true, a `rel="noopener"` will always automatically be added for security reasons
external_links_target_blank = false
# Whether to set rel="nofollow" for all external links
external_links_no_follow = false
# Whether to set rel="noreferrer" for all external links
external_links_no_referrer = false
# Configuration of the link checker. # Configuration of the link checker.
[link_checker] [link_checker]
# Skip link checking for external URLs that start with these prefixes # Skip link checking for external URLs that start with these prefixes