diff --git a/src/html.rs b/src/html.rs
index 421458f5..b7c1edf5 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -864,7 +864,11 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> {
self.output.write_all(b" href=\"")?;
let url = nl.url.as_bytes();
if self.options.render.unsafe_ || !dangerous_url(url) {
- self.escape_href(url)?;
+ if let Some(rewriter) = &self.options.extension.link_url_rewriter {
+ self.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
+ } else {
+ self.escape_href(url)?;
+ }
}
if !nl.title.is_empty() {
self.output.write_all(b"\" title=\"")?;
@@ -889,7 +893,11 @@ impl<'o, 'c: 'o> HtmlFormatter<'o, 'c> {
self.output.write_all(b" src=\"")?;
let url = nl.url.as_bytes();
if self.options.render.unsafe_ || !dangerous_url(url) {
- self.escape_href(url)?;
+ if let Some(rewriter) = &self.options.extension.image_url_rewriter {
+ self.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
+ } else {
+ self.escape_href(url)?;
+ }
}
self.output.write_all(b"\" alt=\"")?;
return Ok(true);
diff --git a/src/lib.rs b/src/lib.rs
index a5eb4407..16f0abe6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -92,7 +92,7 @@ pub use parser::{
parse_document, BrokenLinkCallback, BrokenLinkReference, ExtensionOptions,
ExtensionOptionsBuilder, ListStyleType, Options, ParseOptions, ParseOptionsBuilder, Plugins,
PluginsBuilder, RenderOptions, RenderOptionsBuilder, RenderPlugins, RenderPluginsBuilder,
- ResolvedReference,
+ ResolvedReference, URLRewriter,
};
pub use typed_arena::Arena;
pub use xml::format_document as format_xml;
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index cf74cbd8..95bd649c 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -152,6 +152,18 @@ pub struct Options<'c> {
pub render: RenderOptions,
}
+/// Trait for link and image URL rewrite extensions.
+pub trait URLRewriter: Sync {
+ /// Converts the given URL from Markdown to its representation when output as HTML.
+ fn to_html(&self, url: &str) -> String;
+}
+
+impl Debug for dyn URLRewriter {
+ fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
+ formatter.write_str("")
+ }
+}
+
#[non_exhaustive]
#[derive(Default, Debug, Clone, Builder)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
@@ -521,6 +533,50 @@ pub struct ExtensionOptions {
/// ```
#[builder(default)]
pub greentext: bool,
+
+ /// Wraps embedded image URLs using a custom trait object.
+ ///
+ /// ```
+ /// # use std::sync::Arc;
+ /// # use comrak::{markdown_to_html, ComrakOptions, URLRewriter};
+ /// let mut options = ComrakOptions::default();
+ ///
+ /// struct Rewriter {}
+ /// impl URLRewriter for Rewriter {
+ /// fn to_html(&self, url: &str) -> String {
+ /// format!("https://safe.example.com?url={}", url)
+ /// }
+ /// }
+ ///
+ /// options.extension.image_url_rewriter = Some(Arc::new(Rewriter{}));
+ ///
+ /// assert_eq!(markdown_to_html("![](http://unsafe.example.com/bad.png)", &options),
+ /// "\n");
+ /// ```
+ #[cfg_attr(feature = "arbitrary", arbitrary(value = None))]
+ pub image_url_rewriter: Option>,
+
+ /// Wraps link URLs using a custom trait object.
+ ///
+ /// ```
+ /// # use std::sync::Arc;
+ /// # use comrak::{markdown_to_html, ComrakOptions, URLRewriter};
+ /// let mut options = ComrakOptions::default();
+ ///
+ /// struct Rewriter {}
+ /// impl URLRewriter for Rewriter {
+ /// fn to_html(&self, url: &str) -> String {
+ /// format!("https://safe.example.com/norefer?url={}", url)
+ /// }
+ /// }
+ ///
+ /// options.extension.link_url_rewriter = Some(Arc::new(Rewriter{}));
+ ///
+ /// assert_eq!(markdown_to_html("[my link](http://unsafe.example.com/bad)", &options),
+ /// "my link
\n");
+ /// ```
+ #[cfg_attr(feature = "arbitrary", arbitrary(value = None))]
+ pub link_url_rewriter: Option>,
}
#[non_exhaustive]
diff --git a/src/tests.rs b/src/tests.rs
index 64ffee1a..560bc5d4 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -2,7 +2,7 @@ use crate::nodes::{AstNode, NodeValue, Sourcepos};
use crate::*;
use std::collections::HashMap;
use std::io::{self, Write};
-use std::panic;
+use std::panic::{self, AssertUnwindSafe};
mod api;
mod autolink;
@@ -21,6 +21,7 @@ mod options;
mod pathological;
mod plugins;
mod regressions;
+mod rewriter;
mod shortcodes;
mod spoiler;
mod strikethrough;
@@ -273,12 +274,12 @@ where
options.render.sourcepos = true;
opts(&mut options);
- let result = panic::catch_unwind(|| {
+ let result = panic::catch_unwind(AssertUnwindSafe(|| {
let arena = Arena::new();
let root = parse_document(&arena, md, &options);
amt.assert_match(root);
- });
+ }));
if let Err(err) = result {
let arena = Arena::new();
diff --git a/src/tests/rewriter.rs b/src/tests/rewriter.rs
new file mode 100644
index 00000000..1eff325b
--- /dev/null
+++ b/src/tests/rewriter.rs
@@ -0,0 +1,36 @@
+use std::sync::Arc;
+
+use super::*;
+
+const IMAGE_PROXY: &'static str = "https://safe.example.com?url=";
+const LINK_NOREFER: &'static str = "https://safe.example.com/norefer?url=";
+
+struct Rewriter {
+ prefix: &'static str,
+}
+
+impl URLRewriter for Rewriter {
+ fn to_html(&self, url: &str) -> String {
+ format!("{}{}", self.prefix, url)
+ }
+}
+
+#[test]
+fn image_url_rewriter() {
+ html_opts_i(
+ "![](http://unsafe.example.com/bad.png)",
+ "\n",
+ true,
+ |opts| opts.extension.image_url_rewriter = Some(Arc::new(Rewriter{prefix: IMAGE_PROXY}))
+ );
+}
+
+#[test]
+fn link_url_rewriter() {
+ html_opts_i(
+ "[my link](http://unsafe.example.com/bad)",
+ "my link
\n",
+ true,
+ |opts| opts.extension.link_url_rewriter = Some(Arc::new(Rewriter{prefix: LINK_NOREFER}))
+ );
+}