aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar René Kijewski <kijewski@library.vetmed.fu-berlin.de>2022-02-01 15:01:17 +0100
committerLibravatar Dirkjan Ochtman <dirkjan@ochtman.nl>2022-02-07 22:30:37 +0100
commitfd8bfa43c0c20e4af195824ff95503e41ddb82e8 (patch)
treeb366f80cf13c83d93b2141490eb3730c6a6a13b5
parent0aab78c6775493fb13fabce2b5eaef7ac83e6665 (diff)
downloadaskama-fd8bfa43c0c20e4af195824ff95503e41ddb82e8.tar.gz
askama-fd8bfa43c0c20e4af195824ff95503e41ddb82e8.tar.bz2
askama-fd8bfa43c0c20e4af195824ff95503e41ddb82e8.zip
Add markdown filter
-rw-r--r--askama/Cargo.toml1
-rw-r--r--askama_shared/Cargo.toml2
-rw-r--r--askama_shared/src/filters/mod.rs56
-rw-r--r--askama_shared/src/generator.rs41
-rw-r--r--testing/Cargo.toml5
-rw-r--r--testing/tests/markdown.rs75
6 files changed, 176 insertions, 4 deletions
diff --git a/askama/Cargo.toml b/askama/Cargo.toml
index 281e232..3dc305e 100644
--- a/askama/Cargo.toml
+++ b/askama/Cargo.toml
@@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
default = ["config", "humansize", "num-traits", "urlencode"]
config = ["askama_derive/config", "askama_shared/config"]
humansize = ["askama_shared/humansize"]
+markdown = ["askama_shared/markdown"]
urlencode = ["askama_shared/percent-encoding"]
serde-json = ["askama_derive/json", "askama_shared/json"]
serde-yaml = ["askama_derive/yaml", "askama_shared/yaml"]
diff --git a/askama_shared/Cargo.toml b/askama_shared/Cargo.toml
index b75ec77..e19ad35 100644
--- a/askama_shared/Cargo.toml
+++ b/askama_shared/Cargo.toml
@@ -13,10 +13,12 @@ edition = "2018"
default = ["config", "humansize", "num-traits", "percent-encoding"]
config = ["serde", "toml"]
json = ["serde", "serde_json"]
+markdown = ["comrak"]
yaml = ["serde", "serde_yaml"]
[dependencies]
askama_escape = { version = "0.10.2", path = "../askama_escape" }
+comrak = { version = "0.12", optional = true, default-features = false }
humansize = { version = "1.1.0", optional = true }
mime = "0.3"
mime_guess = "2"
diff --git a/askama_shared/src/filters/mod.rs b/askama_shared/src/filters/mod.rs
index 35bdd5a..9fb10d2 100644
--- a/askama_shared/src/filters/mod.rs
+++ b/askama_shared/src/filters/mod.rs
@@ -47,7 +47,7 @@ const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
// Askama or should refer to a local `filters` module. It should contain all the
// filters shipped with Askama, even the optional ones (since optional inclusion
// in the const vector based on features seems impossible right now).
-pub const BUILT_IN_FILTERS: [&str; 27] = [
+pub const BUILT_IN_FILTERS: &[&str] = &[
"abs",
"capitalize",
"center",
@@ -73,8 +73,10 @@ pub const BUILT_IN_FILTERS: [&str; 27] = [
"urlencode",
"urlencode_strict",
"wordcount",
- "json", // Optional feature; reserve the name anyway
- "yaml", // Optional feature; reserve the name anyway
+ // optional features, reserve the names anyway:
+ "json",
+ "markdown",
+ "yaml",
];
/// Marks a string (or other `Display` type) as safe
@@ -379,6 +381,54 @@ pub fn wordcount<T: fmt::Display>(s: T) -> Result<usize> {
Ok(s.split_whitespace().count())
}
+#[cfg(feature = "markdown")]
+pub fn markdown<E, S>(
+ e: E,
+ s: S,
+ options: Option<&comrak::ComrakOptions>,
+) -> Result<MarkupDisplay<E, String>>
+where
+ E: Escaper,
+ S: AsRef<str>,
+{
+ use comrak::{
+ markdown_to_html, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions,
+ ComrakRenderOptions,
+ };
+
+ const DEFAULT_OPTIONS: ComrakOptions = ComrakOptions {
+ extension: ComrakExtensionOptions {
+ strikethrough: true,
+ tagfilter: true,
+ table: true,
+ autolink: true,
+ // default:
+ tasklist: false,
+ superscript: false,
+ header_ids: None,
+ footnotes: false,
+ description_lists: false,
+ front_matter_delimiter: None,
+ },
+ parse: ComrakParseOptions {
+ // default:
+ smart: false,
+ default_info_string: None,
+ },
+ render: ComrakRenderOptions {
+ unsafe_: false,
+ escape: true,
+ // default:
+ hardbreaks: false,
+ github_pre_lang: false,
+ width: 0,
+ },
+ };
+
+ let s = markdown_to_html(s.as_ref(), options.unwrap_or(&DEFAULT_OPTIONS));
+ Ok(MarkupDisplay::new_safe(s, e))
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index 63a0154..c3beb88 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -1097,6 +1097,45 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
DisplayWrap::Unwrapped
}
+ #[cfg(not(feature = "markdown"))]
+ fn _visit_markdown_filter(
+ &mut self,
+ _buf: &mut Buffer,
+ _args: &[Expr<'_>],
+ ) -> Result<DisplayWrap, CompileError> {
+ Err("the `markdown` filter requires the `markdown` feature to be enabled".into())
+ }
+
+ #[cfg(feature = "markdown")]
+ fn _visit_markdown_filter(
+ &mut self,
+ buf: &mut Buffer,
+ args: &[Expr<'_>],
+ ) -> Result<DisplayWrap, CompileError> {
+ let (md, options) = match args {
+ [md] => (md, None),
+ [md, options] => (md, Some(options)),
+ _ => return Err("markdown filter expects no more than one option argument".into()),
+ };
+
+ buf.write(&format!(
+ "::askama::filters::markdown({}, ",
+ self.input.escaper
+ ));
+ self.visit_expr(buf, md)?;
+ match options {
+ Some(options) => {
+ buf.write(", ::core::option::Option::Some(");
+ self.visit_expr(buf, options)?;
+ buf.write(")");
+ }
+ None => buf.write(", ::core::option::Option::None"),
+ }
+ buf.write(")?");
+
+ Ok(DisplayWrap::Wrapped)
+ }
+
fn visit_filter(
&mut self,
buf: &mut Buffer,
@@ -1115,6 +1154,8 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
} else if name == "join" {
self._visit_join_filter(buf, args)?;
return Ok(DisplayWrap::Unwrapped);
+ } else if name == "markdown" {
+ return self._visit_markdown_filter(buf, args);
}
if name == "tojson" {
diff --git a/testing/Cargo.toml b/testing/Cargo.toml
index 008b200..bd7bbc9 100644
--- a/testing/Cargo.toml
+++ b/testing/Cargo.toml
@@ -7,10 +7,13 @@ edition = "2018"
publish = false
[features]
-default = ["serde_json", "askama/serde-json"]
+default = ["serde-json", "markdown"]
+serde-json = ["serde_json", "askama/serde-json"]
+markdown = ["comrak", "askama/markdown"]
[dependencies]
askama = { path = "../askama", version = "0.11.0-beta.1" }
+comrak = { version = "0.12", default-features = false, optional = true }
serde_json = { version = "1.0", optional = true }
[dev-dependencies]
diff --git a/testing/tests/markdown.rs b/testing/tests/markdown.rs
new file mode 100644
index 0000000..e0150f6
--- /dev/null
+++ b/testing/tests/markdown.rs
@@ -0,0 +1,75 @@
+#![cfg(feature = "markdown")]
+
+use askama::Template;
+use comrak::{ComrakOptions, ComrakRenderOptions};
+
+#[derive(Template)]
+#[template(source = "{{before}}{{content|markdown}}{{after}}", ext = "html")]
+struct MarkdownTemplate<'a> {
+ before: &'a str,
+ after: &'a str,
+ content: &'a str,
+}
+
+#[test]
+fn test_markdown() {
+ let s = MarkdownTemplate {
+ before: "before",
+ after: "after",
+ content: "* 1\n* <script>alert('Lol, hacked!')</script>\n* 3",
+ };
+ assert_eq!(
+ s.render().unwrap(),
+ "\
+before\
+<ul>\n\
+<li>1</li>\n\
+<li>\n\
+&lt;script&gt;alert('Lol, hacked!')&lt;/script&gt;\n\
+</li>\n\
+<li>3</li>\n\
+</ul>\n\
+after",
+ );
+}
+
+#[derive(Template)]
+#[template(
+ source = "{{before}}{{content|markdown(options)}}{{after}}",
+ ext = "html"
+)]
+struct MarkdownWithOptionsTemplate<'a> {
+ before: &'a str,
+ after: &'a str,
+ content: &'a str,
+ options: &'a ComrakOptions,
+}
+
+#[test]
+fn test_markdown_with_options() {
+ let s = MarkdownWithOptionsTemplate {
+ before: "before",
+ after: "after",
+ content: "* 1\n* <script>alert('Lol, hacked!')</script>\n* 3",
+ options: &ComrakOptions {
+ render: ComrakRenderOptions {
+ unsafe_: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ };
+ assert_eq!(
+ s.render().unwrap(),
+ "\
+before\
+<ul>\n\
+<li>1</li>\n\
+<li>\n\
+<script>alert('Lol, hacked!')</script>\n\
+</li>\n\
+<li>3</li>\n\
+</ul>\n\
+after",
+ );
+}