Skip to content

Commit

Permalink
Add external links class option to Markdown configuration (#2717)
Browse files Browse the repository at this point in the history
* Add external links class option to Markdown configuration

* Validate external links class

* Rename external link test snapshots
  • Loading branch information
welpo authored Dec 7, 2024
1 parent ab0ad33 commit dded202
Show file tree
Hide file tree
Showing 13 changed files with 89 additions and 26 deletions.
21 changes: 20 additions & 1 deletion components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub struct Markdown {
pub highlight_themes_css: Vec<ThemeCss>,
/// Whether to render emoji aliases (e.g.: :smile: => 😄) in the markdown files
pub render_emoji: bool,
/// CSS class to add to external links
pub external_links_class: Option<String>,
/// 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,
Expand All @@ -60,6 +62,16 @@ pub struct Markdown {
}

impl Markdown {
pub fn validate_external_links_class(&self) -> Result<()> {
// Validate external link class doesn't contain quotes which would break HTML and aren't valid in CSS
if let Some(class) = &self.external_links_class {
if class.contains('"') || class.contains('\'') {
bail!("External link class '{}' cannot contain quotes", class)
}
}
Ok(())
}

/// Gets the configured highlight theme from the THEME_SET or the config's extra_theme_set
/// Returns None if the configured highlighting theme is set to use css
pub fn get_highlight_theme(&self) -> Option<&Theme> {
Expand Down Expand Up @@ -168,13 +180,19 @@ impl Markdown {
self.external_links_target_blank
|| self.external_links_no_follow
|| self.external_links_no_referrer
|| self.external_links_class.is_some()
}

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.is_empty() { "".to_owned() } else { format!("title=\"{}\" ", title) };

let class = self
.external_links_class
.as_ref()
.map_or("".to_owned(), |c| format!("class=\"{}\" ", c));

if self.external_links_target_blank {
// Security risk otherwise
rel_opts.push("noopener");
Expand All @@ -192,7 +210,7 @@ impl Markdown {
format!("rel=\"{}\" ", rel_opts.join(" "))
};

format!("<a {}{}{}href=\"{}\">", rel, target, title, url)
format!("<a {}{}{}{}href=\"{}\">", class, rel, target, title, url)
}
}

Expand All @@ -204,6 +222,7 @@ impl Default for Markdown {
highlight_theme: DEFAULT_HIGHLIGHT_THEME.to_owned(),
highlight_themes_css: Vec::new(),
render_emoji: false,
external_links_class: None,
external_links_target_blank: false,
external_links_no_follow: false,
external_links_no_referrer: false,
Expand Down
1 change: 1 addition & 0 deletions components/config/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ impl Config {

// this is the step at which missing extra syntax and highlighting themes are raised as errors
config.markdown.init_extra_syntaxes_and_highlight_themes(config_dir)?;
config.markdown.validate_external_links_class()?;

Ok(config)
}
Expand Down
36 changes: 31 additions & 5 deletions components/markdown/tests/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,37 +133,63 @@ fn can_use_smart_punctuation() {
insta::assert_snapshot!(body);
}

#[test]
fn can_use_external_links_class() {
let mut config = Config::default_for_test();

// external link class only
config.markdown.external_links_class = Some("external".to_string());
let body = common::render_with_config("<https://google.com>", config.clone()).unwrap().body;
insta::assert_snapshot!("external_link_class", body);

// internal link (should not add class)
let body =
common::render_with_config("[about](@/pages/about.md)", config.clone()).unwrap().body;
insta::assert_snapshot!("internal_link_no_class", body);

// reset class, set target blank only
config.markdown.external_links_class = None;
config.markdown.external_links_target_blank = true;
let body = common::render_with_config("<https://google.com>", config.clone()).unwrap().body;
insta::assert_snapshot!("external_link_target_blank", body);

// both class and target blank
config.markdown.external_links_class = Some("external".to_string());
let body = common::render_with_config("<https://google.com>", config).unwrap().body;
insta::assert_snapshot!("external_link_class_and_target_blank", body);
}

#[test]
fn can_use_external_links_options() {
let mut config = Config::default_for_test();

// no options
let body = common::render("<https://google.com>").unwrap().body;
insta::assert_snapshot!(body);
insta::assert_snapshot!("external_link_no_options", body);

// target blank
config.markdown.external_links_target_blank = true;
let body = common::render_with_config("<https://google.com>", config.clone()).unwrap().body;
insta::assert_snapshot!(body);
insta::assert_snapshot!("external_link_target_blank", body);

// no follow
config.markdown.external_links_target_blank = false;
config.markdown.external_links_no_follow = true;
let body = common::render_with_config("<https://google.com>", config.clone()).unwrap().body;
insta::assert_snapshot!(body);
insta::assert_snapshot!("external_link_no_follow", body);

// no referrer
config.markdown.external_links_no_follow = false;
config.markdown.external_links_no_referrer = true;
let body = common::render_with_config("<https://google.com>", config.clone()).unwrap().body;
insta::assert_snapshot!(body);
insta::assert_snapshot!("external_link_no_referrer", body);

// all of them
config.markdown.external_links_no_follow = true;
config.markdown.external_links_target_blank = true;
config.markdown.external_links_no_referrer = true;
let body = common::render_with_config("<https://google.com>", config).unwrap().body;
insta::assert_snapshot!(body);
insta::assert_snapshot!("external_link_all_options", body);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: components/rendering/tests/markdown.rs
assertion_line: 168
source: components/markdown/tests/markdown.rs
expression: body

snapshot_kind: text
---
<p><a rel="noopener nofollow noreferrer" target="_blank" href="https://google.com">https://google.com</a></p>

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: components/markdown/tests/markdown.rs
expression: body
snapshot_kind: text
---
<p><a class="external" href="https://google.com">https://google.com</a></p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: components/markdown/tests/markdown.rs
expression: body
snapshot_kind: text
---
<p><a class="external" rel="noopener" target="_blank" href="https://google.com">https://google.com</a></p>
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: components/rendering/tests/markdown.rs
assertion_line: 155
source: components/markdown/tests/markdown.rs
expression: body

snapshot_kind: text
---
<p><a rel="nofollow" href="https://google.com">https://google.com</a></p>

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: components/rendering/tests/markdown.rs
assertion_line: 144
source: components/markdown/tests/markdown.rs
expression: body

snapshot_kind: text
---
<p><a href="https://google.com">https://google.com</a></p>

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: components/rendering/tests/markdown.rs
assertion_line: 161
source: components/markdown/tests/markdown.rs
expression: body

snapshot_kind: text
---
<p><a rel="noreferrer" href="https://google.com">https://google.com</a></p>

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: components/rendering/tests/markdown.rs
assertion_line: 149
source: components/markdown/tests/markdown.rs
expression: body

snapshot_kind: text
---
<p><a rel="noopener" target="_blank" href="https://google.com">https://google.com</a></p>

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: components/markdown/tests/markdown.rs
expression: body
snapshot_kind: text
---
<p><a href="https://getzola.org/about/">about</a></p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: components/markdown/tests/markdown.rs
assertion_line: 148
expression: body
---
<p><a href="https://getzola.org/about/">about</a></p>
3 changes: 3 additions & 0 deletions docs/content/documentation/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ highlight_theme = "base16-ocean-dark"
# Unicode emoji equivalent in the rendered Markdown files. (e.g.: :smile: => 😄)
render_emoji = false

# CSS class to add to external links (e.g. "external-link")
external_links_class =

# 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
Expand Down

0 comments on commit dded202

Please sign in to comment.