From f204ad7b549abe009db5811798a5dacc791dc890 Mon Sep 17 00:00:00 2001 From: Ophir Lojkine Date: Wed, 7 Oct 2026 09:46:39 +0000 Subject: [PATCH 1/2] refactor: centralize Markdown rendering options --- src/template_helpers.rs | 86 +++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index f027c3de3..fb63a1759 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -278,11 +278,22 @@ pub fn render_markdown_to_html( config: &impl MarkdownConfig, markdown_src: &str, ) -> Result { + let options = markdown_options( + config.allow_dangerous_html(), + config.allow_dangerous_protocol(), + ); + markdown::to_html_with_options(markdown_src, &options).map_err(|e| e.to_string()) +} + +fn markdown_options( + allow_dangerous_html: bool, + allow_dangerous_protocol: bool, +) -> markdown::Options { let mut options = markdown::Options::gfm(); - options.compile.allow_dangerous_html = config.allow_dangerous_html(); - options.compile.allow_dangerous_protocol = config.allow_dangerous_protocol(); + options.compile.allow_dangerous_html = allow_dangerous_html; + options.compile.allow_dangerous_protocol = allow_dangerous_protocol; options.compile.allow_any_img_src = true; - markdown::to_html_with_options(markdown_src, &options).map_err(|e| e.to_string()) + options } /// Helper to render markdown with configurable options @@ -301,10 +312,8 @@ impl MarkdownHelper { } fn get_preset_options(&self, preset_name: &str) -> Result { - let mut options = markdown::Options::gfm(); - options.compile.allow_dangerous_html = self.allow_dangerous_html; - options.compile.allow_dangerous_protocol = self.allow_dangerous_protocol; - options.compile.allow_any_img_src = true; + let mut options = + markdown_options(self.allow_dangerous_html, self.allow_dangerous_protocol); match preset_name { "default" => {} @@ -638,6 +647,49 @@ mod tests { assert_eq!(Some("

Heading

"), actual.as_str()); } + #[test] + fn markdown_policy_matches_function_and_helper_for_all_settings() { + use crate::template_helpers::render_markdown_to_html; + for html in [false, true] { + for protocol in [false, true] { + let mut config = crate::app_config::tests::test_config(); + config.markdown_allow_dangerous_html = html; + config.markdown_allow_dangerous_protocol = protocol; + let helper = MarkdownHelper::new(&config); + for source in [ + "
", + "[click](javascript:alert(1))", + "![image](data:image/png;base64,aGVsbG8=)", + "~~struck~~", + ] { + let content = Value::String(source.into()); + let rendered = render_markdown_to_html(&config, source).unwrap(); + assert_eq!(helper.call(&as_args(&content)).unwrap(), rendered); + let unsafe_preset = Value::String("allow_unsafe".into()); + let unsafe_rendered = helper + .call(&as_args_with_unsafe(&content, &unsafe_preset)) + .unwrap(); + config.markdown_allow_dangerous_html = true; + config.markdown_allow_dangerous_protocol = true; + assert_eq!( + unsafe_rendered, + render_markdown_to_html(&config, source).unwrap() + ); + config.markdown_allow_dangerous_html = html; + config.markdown_allow_dangerous_protocol = protocol; + } + let content = Value::String("text".into()); + let unknown = Value::String("unknown".into()); + assert_eq!( + helper + .call(&as_args_with_unsafe(&content, &unknown)) + .unwrap_err(), + "unknown markdown preset: unknown" + ); + } + } + } + // Optionally allow potentially unsafe html blocks // See https://spec.commonmark.org/0.31.2/#html-blocks mod markdown_html_blocks { @@ -709,21 +761,21 @@ mod tests { } } - fn as_args_with_unsafe<'a>( - contents: &'a Value, - allow_unsafe: &'a Value, - ) -> [PathAndJson<'a>; 2] { - [ - as_helper_arg(CONTENT_KEY, contents), - as_helper_arg("allow_unsafe", allow_unsafe), - ] - } - fn contents() -> Value { Value::String(UNSAFE_MARKUP.to_string()) } } + fn as_args_with_unsafe<'a>( + contents: &'a Value, + allow_unsafe: &'a Value, + ) -> [PathAndJson<'a>; 2] { + [ + as_helper_arg(CONTENT_KEY, contents), + as_helper_arg("allow_unsafe", allow_unsafe), + ] + } + fn as_args(contents: &Value) -> [PathAndJson<'_>; 1] { [as_helper_arg(CONTENT_KEY, contents)] } From bf6789bf2e1cea740917f369b76054a54c87fa27 Mon Sep 17 00:00:00 2001 From: Ophir Lojkine Date: Wed, 7 Oct 2026 10:20:23 +0000 Subject: [PATCH 2/2] test: reuse Markdown fixtures and argument helpers --- src/template_helpers.rs | 100 ++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index fb63a1759..0f97126d3 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -647,54 +647,52 @@ mod tests { assert_eq!(Some("

Heading

"), actual.as_str()); } - #[test] - fn markdown_policy_matches_function_and_helper_for_all_settings() { - use crate::template_helpers::render_markdown_to_html; - for html in [false, true] { - for protocol in [false, true] { + // Optionally allow potentially unsafe html blocks + // See https://spec.commonmark.org/0.31.2/#html-blocks + mod markdown_html_blocks { + + use super::*; + + #[test] + fn rendering_paths_share_configured_policy() { + use crate::template_helpers::render_markdown_to_html; + let unsafe_preset = Value::String("allow_unsafe".into()); + for source in [ + UNSAFE_MARKUP, + "[click](javascript:alert(1))", + "![image](data:image/png;base64,aGVsbG8=)", + "~~struck~~", + ] { + let content = Value::String(source.into()); let mut config = crate::app_config::tests::test_config(); - config.markdown_allow_dangerous_html = html; - config.markdown_allow_dangerous_protocol = protocol; - let helper = MarkdownHelper::new(&config); - for source in [ - "
", - "[click](javascript:alert(1))", - "![image](data:image/png;base64,aGVsbG8=)", - "~~struck~~", - ] { - let content = Value::String(source.into()); - let rendered = render_markdown_to_html(&config, source).unwrap(); - assert_eq!(helper.call(&as_args(&content)).unwrap(), rendered); - let unsafe_preset = Value::String("allow_unsafe".into()); - let unsafe_rendered = helper - .call(&as_args_with_unsafe(&content, &unsafe_preset)) - .unwrap(); - config.markdown_allow_dangerous_html = true; - config.markdown_allow_dangerous_protocol = true; + config.markdown_allow_dangerous_html = true; + config.markdown_allow_dangerous_protocol = true; + let unsafe_html = render_markdown_to_html(&config, source).unwrap(); + for (html, protocol) in [(false, false), (false, true), (true, false), (true, true)] + { + config.markdown_allow_dangerous_html = html; + config.markdown_allow_dangerous_protocol = protocol; + let helper = MarkdownHelper::new(&config); assert_eq!( - unsafe_rendered, + helper.call(&as_args(&content)).unwrap(), render_markdown_to_html(&config, source).unwrap() ); - config.markdown_allow_dangerous_html = html; - config.markdown_allow_dangerous_protocol = protocol; + assert_eq!( + helper + .call(&as_args_with_unsafe(&content, &unsafe_preset)) + .unwrap(), + unsafe_html + ); + let unknown = Value::String("unknown".into()); + assert_eq!( + helper + .call(&as_args_with_unsafe(&content, &unknown)) + .unwrap_err(), + "unknown markdown preset: unknown" + ); } - let content = Value::String("text".into()); - let unknown = Value::String("unknown".into()); - assert_eq!( - helper - .call(&as_args_with_unsafe(&content, &unknown)) - .unwrap_err(), - "unknown markdown preset: unknown" - ); } } - } - - // Optionally allow potentially unsafe html blocks - // See https://spec.commonmark.org/0.31.2/#html-blocks - mod markdown_html_blocks { - - use super::*; const UNSAFE_MARKUP: &str = "
"; const ESCAPED_UNSAFE_MARKUP: &str = "<table><tr><td>"; @@ -761,21 +759,21 @@ mod tests { } } + fn as_args_with_unsafe<'a>( + contents: &'a Value, + allow_unsafe: &'a Value, + ) -> [PathAndJson<'a>; 2] { + [ + as_helper_arg(CONTENT_KEY, contents), + as_helper_arg("allow_unsafe", allow_unsafe), + ] + } + fn contents() -> Value { Value::String(UNSAFE_MARKUP.to_string()) } } - fn as_args_with_unsafe<'a>( - contents: &'a Value, - allow_unsafe: &'a Value, - ) -> [PathAndJson<'a>; 2] { - [ - as_helper_arg(CONTENT_KEY, contents), - as_helper_arg("allow_unsafe", allow_unsafe), - ] - } - fn as_args(contents: &Value) -> [PathAndJson<'_>; 1] { [as_helper_arg(CONTENT_KEY, contents)] }