Skip to content

Commit

Permalink
Add support for image and link URL rewriting
Browse files Browse the repository at this point in the history
  • Loading branch information
Meow authored and liamwhite committed Nov 1, 2024
1 parent b27a3dd commit a8dd83f
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 3 deletions.
12 changes: 10 additions & 2 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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=\"")?;
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 57 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::cmp::min;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::mem;
use std::panic::RefUnwindSafe;
use std::str;
use std::sync::{Arc, Mutex};
use typed_arena::Arena;
Expand Down Expand Up @@ -152,6 +153,18 @@ pub struct Options<'c> {
pub render: RenderOptions,
}

/// Trait for link and image URL rewrite extensions.
pub trait URLRewriter: RefUnwindSafe + 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("<dyn URLRewriter>")
}
}

#[non_exhaustive]
#[derive(Default, Debug, Clone, Builder)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
Expand Down Expand Up @@ -521,6 +534,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),
/// "<p><img src=\"https://safe.example.com?url=http://unsafe.example.com/bad.png\" alt=\"\" /></p>\n");
/// ```
#[cfg_attr(feature = "arbitrary", arbitrary(value = None))]
pub image_url_rewriter: Option<Arc<dyn URLRewriter>>,

/// 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),
/// "<p><a href=\"https://safe.example.com/norefer?url=http://unsafe.example.com/bad\">my link</a></p>\n");
/// ```
#[cfg_attr(feature = "arbitrary", arbitrary(value = None))]
pub link_url_rewriter: Option<Arc<dyn URLRewriter>>,
}

#[non_exhaustive]
Expand Down
1 change: 1 addition & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod options;
mod pathological;
mod plugins;
mod regressions;
mod rewriter;
mod shortcodes;
mod spoiler;
mod strikethrough;
Expand Down
36 changes: 36 additions & 0 deletions src/tests/rewriter.rs
Original file line number Diff line number Diff line change
@@ -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)",
"<p><img src=\"https://safe.example.com?url=http://unsafe.example.com/bad.png\" alt=\"\" /></p>\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)",
"<p><a href=\"https://safe.example.com/norefer?url=http://unsafe.example.com/bad\">my link</a></p>\n",
true,
|opts| opts.extension.link_url_rewriter = Some(Arc::new(Rewriter{prefix: LINK_NOREFER}))
);
}

0 comments on commit a8dd83f

Please sign in to comment.