From c4ddce62f57997065a24540c6a8f3df19aea5972 Mon Sep 17 00:00:00 2001 From: welpo Date: Sat, 30 Nov 2024 02:06:44 +0100 Subject: [PATCH 1/3] Add external links class option to Markdown configuration --- components/config/src/config/markup.rs | 11 +++++++- components/markdown/tests/markdown.rs | 26 +++++++++++++++++++ ...kdown__can_use_external_links_class-2.snap | 6 +++++ ...kdown__can_use_external_links_class-3.snap | 6 +++++ ...kdown__can_use_external_links_class-4.snap | 6 +++++ ...arkdown__can_use_external_links_class.snap | 6 +++++ .../getting-started/configuration.md | 3 +++ 7 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap create mode 100644 components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap create mode 100644 components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap create mode 100644 components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 9ea5b2337b..8008c872e6 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -36,6 +36,8 @@ pub struct Markdown { pub highlight_themes_css: Vec, /// 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, /// 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, @@ -168,6 +170,7 @@ 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 { @@ -175,6 +178,11 @@ impl Markdown { 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"); @@ -192,7 +200,7 @@ impl Markdown { format!("rel=\"{}\" ", rel_opts.join(" ")) }; - format!("", rel, target, title, url) + format!("", class, rel, target, title, url) } } @@ -204,6 +212,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, diff --git a/components/markdown/tests/markdown.rs b/components/markdown/tests/markdown.rs index e8cdcd42c2..9fbddb706c 100644 --- a/components/markdown/tests/markdown.rs +++ b/components/markdown/tests/markdown.rs @@ -133,6 +133,32 @@ 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("", config.clone()).unwrap().body; + insta::assert_snapshot!(body); + + // internal link (should not add class) + let body = + common::render_with_config("[about](@/pages/about.md)", config.clone()).unwrap().body; + insta::assert_snapshot!(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("", config.clone()).unwrap().body; + insta::assert_snapshot!(body); + + // both class and target blank + config.markdown.external_links_class = Some("external".to_string()); + let body = common::render_with_config("", config).unwrap().body; + insta::assert_snapshot!(body); +} + #[test] fn can_use_external_links_options() { let mut config = Config::default_for_test(); diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap new file mode 100644 index 0000000000..1d1e334d58 --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap @@ -0,0 +1,6 @@ +--- +source: components/markdown/tests/markdown.rs +expression: body +snapshot_kind: text +--- +

about

diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap new file mode 100644 index 0000000000..16af5ca410 --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap @@ -0,0 +1,6 @@ +--- +source: components/markdown/tests/markdown.rs +expression: body +snapshot_kind: text +--- +

https://google.com

diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap new file mode 100644 index 0000000000..da6b5de88e --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap @@ -0,0 +1,6 @@ +--- +source: components/markdown/tests/markdown.rs +expression: body +snapshot_kind: text +--- +

https://google.com

diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap b/components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap new file mode 100644 index 0000000000..ab22c3f7f7 --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap @@ -0,0 +1,6 @@ +--- +source: components/markdown/tests/markdown.rs +expression: body +snapshot_kind: text +--- +

https://google.com

diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 0591b021d5..5b83712db2 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -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 From a4d6eafd60ddc49a1db6c6c6333879a2f3a26a6f Mon Sep 17 00:00:00 2001 From: welpo Date: Thu, 5 Dec 2024 17:36:43 +0100 Subject: [PATCH 2/3] Validate external links class --- components/config/src/config/markup.rs | 10 ++++++++++ components/config/src/config/mod.rs | 1 + 2 files changed, 11 insertions(+) diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 8008c872e6..318524b24f 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -62,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> { diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index a1f1cfaf41..2bc30ecd9b 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -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) } From 456e280b2beb551fb70b09f4c04e3650fa736ae7 Mon Sep 17 00:00:00 2001 From: welpo Date: Sat, 7 Dec 2024 00:09:16 +0100 Subject: [PATCH 3/3] Rename external link test snapshots --- components/markdown/tests/markdown.rs | 18 +++++++++--------- ...down__can_use_external_links_options-2.snap | 8 -------- ...> markdown__external_link_all_options.snap} | 6 ++---- ...snap => markdown__external_link_class.snap} | 0 ..._external_link_class_and_target_blank.snap} | 0 ... => markdown__external_link_no_follow.snap} | 6 ++---- ...=> markdown__external_link_no_options.snap} | 6 ++---- ...> markdown__external_link_no_referrer.snap} | 6 ++---- ... markdown__external_link_target_blank.snap} | 0 ...p => markdown__internal_link_no_class.snap} | 0 ...kdown__internal_link_without_class.snap.new | 6 ++++++ 11 files changed, 23 insertions(+), 33 deletions(-) delete mode 100644 components/markdown/tests/snapshots/markdown__can_use_external_links_options-2.snap rename components/markdown/tests/snapshots/{markdown__can_use_external_links_options-5.snap => markdown__external_link_all_options.snap} (66%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_class.snap => markdown__external_link_class.snap} (100%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_class-4.snap => markdown__external_link_class_and_target_blank.snap} (100%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_options-3.snap => markdown__external_link_no_follow.snap} (58%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_options.snap => markdown__external_link_no_options.snap} (54%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_options-4.snap => markdown__external_link_no_referrer.snap} (59%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_class-3.snap => markdown__external_link_target_blank.snap} (100%) rename components/markdown/tests/snapshots/{markdown__can_use_external_links_class-2.snap => markdown__internal_link_no_class.snap} (100%) create mode 100644 components/markdown/tests/snapshots/markdown__internal_link_without_class.snap.new diff --git a/components/markdown/tests/markdown.rs b/components/markdown/tests/markdown.rs index 9fbddb706c..7abfc1b3d6 100644 --- a/components/markdown/tests/markdown.rs +++ b/components/markdown/tests/markdown.rs @@ -140,23 +140,23 @@ fn can_use_external_links_class() { // external link class only config.markdown.external_links_class = Some("external".to_string()); let body = common::render_with_config("", config.clone()).unwrap().body; - insta::assert_snapshot!(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!(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("", config.clone()).unwrap().body; - insta::assert_snapshot!(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("", config).unwrap().body; - insta::assert_snapshot!(body); + insta::assert_snapshot!("external_link_class_and_target_blank", body); } #[test] @@ -165,31 +165,31 @@ fn can_use_external_links_options() { // no options let body = common::render("").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("", 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("", 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("", 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("", config).unwrap().body; - insta::assert_snapshot!(body); + insta::assert_snapshot!("external_link_all_options", body); } #[test] diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-2.snap b/components/markdown/tests/snapshots/markdown__can_use_external_links_options-2.snap deleted file mode 100644 index ae1b79e732..0000000000 --- a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-2.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: components/rendering/tests/markdown.rs -assertion_line: 149 -expression: body - ---- -

https://google.com

- diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-5.snap b/components/markdown/tests/snapshots/markdown__external_link_all_options.snap similarity index 66% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_options-5.snap rename to components/markdown/tests/snapshots/markdown__external_link_all_options.snap index 40edd8ddb2..0949ee0ea1 100644 --- a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-5.snap +++ b/components/markdown/tests/snapshots/markdown__external_link_all_options.snap @@ -1,8 +1,6 @@ --- -source: components/rendering/tests/markdown.rs -assertion_line: 168 +source: components/markdown/tests/markdown.rs expression: body - +snapshot_kind: text ---

https://google.com

- diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap b/components/markdown/tests/snapshots/markdown__external_link_class.snap similarity index 100% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_class.snap rename to components/markdown/tests/snapshots/markdown__external_link_class.snap diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap b/components/markdown/tests/snapshots/markdown__external_link_class_and_target_blank.snap similarity index 100% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_class-4.snap rename to components/markdown/tests/snapshots/markdown__external_link_class_and_target_blank.snap diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-3.snap b/components/markdown/tests/snapshots/markdown__external_link_no_follow.snap similarity index 58% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_options-3.snap rename to components/markdown/tests/snapshots/markdown__external_link_no_follow.snap index 27a53f5693..ce7424825c 100644 --- a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-3.snap +++ b/components/markdown/tests/snapshots/markdown__external_link_no_follow.snap @@ -1,8 +1,6 @@ --- -source: components/rendering/tests/markdown.rs -assertion_line: 155 +source: components/markdown/tests/markdown.rs expression: body - +snapshot_kind: text ---

https://google.com

- diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_options.snap b/components/markdown/tests/snapshots/markdown__external_link_no_options.snap similarity index 54% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_options.snap rename to components/markdown/tests/snapshots/markdown__external_link_no_options.snap index 4f539aa00b..b184b12c36 100644 --- a/components/markdown/tests/snapshots/markdown__can_use_external_links_options.snap +++ b/components/markdown/tests/snapshots/markdown__external_link_no_options.snap @@ -1,8 +1,6 @@ --- -source: components/rendering/tests/markdown.rs -assertion_line: 144 +source: components/markdown/tests/markdown.rs expression: body - +snapshot_kind: text ---

https://google.com

- diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-4.snap b/components/markdown/tests/snapshots/markdown__external_link_no_referrer.snap similarity index 59% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_options-4.snap rename to components/markdown/tests/snapshots/markdown__external_link_no_referrer.snap index ef73ab0bce..78e9bfbab0 100644 --- a/components/markdown/tests/snapshots/markdown__can_use_external_links_options-4.snap +++ b/components/markdown/tests/snapshots/markdown__external_link_no_referrer.snap @@ -1,8 +1,6 @@ --- -source: components/rendering/tests/markdown.rs -assertion_line: 161 +source: components/markdown/tests/markdown.rs expression: body - +snapshot_kind: text ---

https://google.com

- diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap b/components/markdown/tests/snapshots/markdown__external_link_target_blank.snap similarity index 100% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_class-3.snap rename to components/markdown/tests/snapshots/markdown__external_link_target_blank.snap diff --git a/components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap b/components/markdown/tests/snapshots/markdown__internal_link_no_class.snap similarity index 100% rename from components/markdown/tests/snapshots/markdown__can_use_external_links_class-2.snap rename to components/markdown/tests/snapshots/markdown__internal_link_no_class.snap diff --git a/components/markdown/tests/snapshots/markdown__internal_link_without_class.snap.new b/components/markdown/tests/snapshots/markdown__internal_link_without_class.snap.new new file mode 100644 index 0000000000..02f5f43eaa --- /dev/null +++ b/components/markdown/tests/snapshots/markdown__internal_link_without_class.snap.new @@ -0,0 +1,6 @@ +--- +source: components/markdown/tests/markdown.rs +assertion_line: 148 +expression: body +--- +

about