diff --git a/crates/utils/src/utils/markdown.rs b/crates/utils/src/utils/markdown.rs deleted file mode 100644 index 5f851589b..000000000 --- a/crates/utils/src/utils/markdown.rs +++ /dev/null @@ -1,86 +0,0 @@ -use markdown_it::MarkdownIt; -use once_cell::sync::Lazy; - -mod spoiler_rule; - -static MARKDOWN_PARSER: Lazy = Lazy::new(|| { - let mut parser = MarkdownIt::new(); - markdown_it::plugins::cmark::add(&mut parser); - markdown_it::plugins::extra::add(&mut parser); - spoiler_rule::add(&mut parser); - - parser -}); - -pub fn markdown_to_html(text: &str) -> String { - MARKDOWN_PARSER.parse(text).xrender() -} - -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] - #![allow(clippy::indexing_slicing)] - - use crate::utils::markdown::markdown_to_html; - - #[test] - fn test_basic_markdown() { - let tests: Vec<_> = vec![ - ( - "headings", - "# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", - "

h1

\n

h2

\n

h3

\n

h4

\n
h5
\n
h6
\n" - ), - ( - "line breaks", - "First\rSecond", - "

First\nSecond

\n"), - ( - "emphasis", - "__bold__ **bold** *italic* ***bold+italic***", - "

bold bold italic bold+italic

\n" - ), - ( - "blockquotes", - "> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n", - "
\n

Hello

\n\n
\n

Goodbye

\n
\n
\n" - ), - ( - "lists (ordered, unordered)", - "1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen", - "
    \n
  1. pen
  2. \n
  3. apple
  4. \n
  5. apple pen
  6. \n
\n\n" - ), - ( - "code and code blocks", - "this is my amazing `code snippet` and my amazing ```code block```", - "

this is my amazing code snippet and my amazing code block

\n" - ), - ( - "links", - "[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")", - "

Lemmy

\n" - ), - ( - "images", - "![My linked image](https://image.com \"image alt text\")", - "

\"My

\n" - ), - // Ensure any custom plugins are added to 'MARKDOWN_PARSER' implementation. - ( - "basic spoiler", - "::: spoiler click to see more\nhow spicy!\n:::\n", - "
click to see more

how spicy!\n

\n" - ), - ]; - - tests.iter().for_each(|&(msg, input, expected)| { - let result = markdown_to_html(input); - - assert_eq!( - result, expected, - "Testing {}, with original input '{}'", - msg, input - ); - }); - } -} diff --git a/crates/utils/src/utils/markdown/link_rule.rs b/crates/utils/src/utils/markdown/link_rule.rs new file mode 100644 index 000000000..bd231d5ef --- /dev/null +++ b/crates/utils/src/utils/markdown/link_rule.rs @@ -0,0 +1,37 @@ +use markdown_it::generics::inline::full_link; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; + +/// Renders markdown links. Copied directly from markdown-it source, unlike original code it also +/// sets `rel=nofollow` attribute. +/// +/// TODO: We can set nofollow only if post was not made by mod/admin, but then we have to construct +/// new parser for every invocation which might have performance implications. +/// https://github.com/markdown-it-rust/markdown-it/blob/master/src/plugins/cmark/inline/link.rs +#[derive(Debug)] +pub struct Link { + pub url: String, + pub title: Option, +} + +impl NodeValue for Link { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + attrs.push(("href", self.url.clone())); + attrs.push(("rel", "nofollow".to_string())); + + if let Some(title) = &self.title { + attrs.push(("title", title.clone())); + } + + fmt.open("a", &attrs); + fmt.contents(&node.children); + fmt.close("a"); + } +} + +pub fn add(md: &mut MarkdownIt) { + full_link::add::(md, |href, title| Node::new(Link { + url: href.unwrap_or_default(), + title, + })); +} \ No newline at end of file diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs new file mode 100644 index 000000000..3c1cd4c03 --- /dev/null +++ b/crates/utils/src/utils/markdown/mod.rs @@ -0,0 +1,88 @@ +use markdown_it::MarkdownIt; +use once_cell::sync::Lazy; + +mod spoiler_rule; +mod link_rule; + +static MARKDOWN_PARSER: Lazy = Lazy::new(|| { + let mut parser = MarkdownIt::new(); + markdown_it::plugins::cmark::add(&mut parser); + markdown_it::plugins::extra::add(&mut parser); + spoiler_rule::add(&mut parser); + link_rule::add(&mut parser); + + parser +}); + +pub fn markdown_to_html(text: &str) -> String { + MARKDOWN_PARSER.parse(text).xrender() +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use crate::utils::markdown::markdown_to_html; + + #[test] + fn test_basic_markdown() { + let tests: Vec<_> = vec![ + ( + "headings", + "# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", + "

h1

\n

h2

\n

h3

\n

h4

\n
h5
\n
h6
\n" + ), + ( + "line breaks", + "First\rSecond", + "

First\nSecond

\n"), + ( + "emphasis", + "__bold__ **bold** *italic* ***bold+italic***", + "

bold bold italic bold+italic

\n" + ), + ( + "blockquotes", + "> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n", + "
\n

Hello

\n
    \n
  • Hola
  • \n
  • 안영
  • \n
\n
\n

Goodbye

\n
\n
\n" + ), + ( + "lists (ordered, unordered)", + "1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen", + "
    \n
  1. pen
  2. \n
  3. apple
  4. \n
  5. apple pen
  6. \n
\n
    \n
  • pen
  • \n
  • pineapple
  • \n
  • pineapple pen
  • \n
\n" + ), + ( + "code and code blocks", + "this is my amazing `code snippet` and my amazing ```code block```", + "

this is my amazing code snippet and my amazing code block

\n" + ), + ( + "links", + "[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")", + "

Lemmy

\n" + ), + ( + "images", + "![My linked image](https://image.com \"image alt text\")", + "

\"My

\n" + ), + // Ensure any custom plugins are added to 'MARKDOWN_PARSER' implementation. + ( + "basic spoiler", + "::: spoiler click to see more\nhow spicy!\n:::\n", + "
click to see more

how spicy!\n

\n" + ), + ]; + + tests.iter().for_each(|&(msg, input, expected)| { + let result = markdown_to_html(input); + + assert_eq!( + result, expected, + "Testing {}, with original input '{}'", + msg, input + ); + }); + } +}