From 2c681f343939d496bff9ef316bc4082744c01313 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 21 Nov 2020 22:20:54 +0100 Subject: [PATCH] Add some external link markdown tweaking options Closes #681, #695 --- CHANGELOG.md | 3 +- components/config/src/config/markup.rs | 43 ++++++++++++++ components/rendering/src/markdown.rs | 30 ++++++---- components/rendering/tests/markdown.rs | 58 +++++++++++++++++++ .../getting-started/configuration.md | 11 ++++ 5 files changed, 133 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80882acb..2cb35b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,13 @@ - Support `output_dir` in `config.toml` - Allow sections to be drafted - 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 - Add language as class to the `` block - Add bibtex to `load_data` - Add a `[markdown]` section to `config.toml` to configure rendering - 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) diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 1fa2cbb8..d918ef70 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -12,6 +12,46 @@ pub struct Markdown { pub highlight_theme: String, /// Whether to render emoji aliases (e.g.: :smile: => 😄) in the markdown files 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!("", rel, target, title, url) + } } impl Default for Markdown { @@ -20,6 +60,9 @@ impl Default for Markdown { highlight_code: false, highlight_theme: DEFAULT_HIGHLIGHT_THEME.to_owned(), render_emoji: false, + external_links_target_blank: false, + external_links_no_follow: false, + external_links_no_referrer: false, } } } diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index 64be7a58..b4be0d46 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -13,7 +13,6 @@ use utils::slugs::slugify_anchors; use utils::vec::InsertMany; use self::cmark::{Event, LinkType, Options, Parser, Tag}; -use pulldown_cmark::CodeBlockKind; mod codeblock; mod fence; @@ -101,11 +100,6 @@ fn fix_link( 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: // - it could be a relative link (starting with `@/`) // - it could be a link to a co-located asset @@ -211,7 +205,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result { let language = match kind { - CodeBlockKind::Fenced(fence_info) => { + cmark::CodeBlockKind::Fenced(fence_info) => { let fence_info = fence::FenceSettings::new(fence_info); fence_info.language } @@ -228,8 +222,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result (), - CodeBlockKind::Fenced(fence_info) => { + cmark::CodeBlockKind::Indented => (), + cmark::CodeBlockKind::Fenced(fence_info) => { // This selects the background color the same way that // start_coloured_html_snippet does let color = theme @@ -289,8 +283,22 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result { if markup.contains("") { diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index bcd9cb5b..b37bfb5a 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -1058,3 +1058,61 @@ fn emoji_aliases_are_ignored_when_disabled_in_config() { let res = render_content("Hello, World! :smile:", &context).unwrap(); assert_eq!(res.body, "

Hello, World! :smile:

\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("", &context).unwrap(); + assert_eq!(res.body, "

https://google.com

\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("", &context).unwrap(); + assert_eq!(res.body, "

https://google.com

\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("", &context).unwrap(); + assert_eq!( + res.body, + "

https://google.com/éllo

\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("", &context).unwrap(); + assert_eq!( + res.body, + "

https://google.com

\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("", &context).unwrap(); + assert_eq!(res.body, "

https://google.com

\n"); +} diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 5857fe87..0d7e4a7a 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -95,6 +95,7 @@ extra_syntaxes = [] # You can override the default output directory `public` by setting an another value. # output_dir = "docs" +# Configuration of the Markdown rendering [markdown] # When set to "true", all code blocks are highlighted. highlight_code = false @@ -107,6 +108,16 @@ highlight_theme = "base16-ocean-dark" # Unicode emoji equivalent in the rendered Markdown files. (e.g.: :smile: => 😄) 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. [link_checker] # Skip link checking for external URLs that start with these prefixes