From 1c7bfd6be8068bc0726e5ce6063d04b5b40dfb14 Mon Sep 17 00:00:00 2001 From: Nina Blanson Date: Wed, 14 Jun 2023 06:15:59 -0500 Subject: [PATCH] Fixes #1884 - Support Spoiler Tags (#3018) * Fixes #1884 - Switches markdown libraries and creates a custom rule to manage spoiler blocks * Add tests to cover invalid spoiler input * Consolidate tests, add comments * Make immutable, static instance of markdown parser --------- Co-authored-by: Nutomic --- Cargo.lock | 300 +++++++++++++----- crates/utils/Cargo.toml | 2 +- crates/utils/src/utils/markdown.rs | 82 ++++- .../utils/src/utils/markdown/spoiler_rule.rs | 200 ++++++++++++ 4 files changed, 507 insertions(+), 77 deletions(-) create mode 100644 crates/utils/src/utils/markdown/spoiler_rule.rs diff --git a/Cargo.lock b/Cargo.lock index e70471712..6bc9e014a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,12 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "argparse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" + [[package]] name = "arrayvec" version = "0.5.2" @@ -650,6 +656,15 @@ dependencies = [ "scoped-tls", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -898,24 +913,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "comrak" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf1e432b302dc6236dd0db580d182ce520bb24af82d6462e2d7a5e0a31c50d" -dependencies = [ - "entities", - "lazy_static", - "memchr", - "pest", - "pest_derive", - "regex", - "shell-words", - "typed-arena 1.7.0", - "unicode_categories", - "xdg", -] - [[package]] name = "config" version = "0.13.3" @@ -971,6 +968,26 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const_format" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.2.4" @@ -1278,6 +1295,17 @@ dependencies = [ "text_lines", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + [[package]] name = "derive_builder" version = "0.10.2" @@ -1477,26 +1505,6 @@ dependencies = [ "chrono", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.4" @@ -1538,6 +1546,12 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dprint-core" version = "0.59.0" @@ -1775,6 +1789,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "fancy-regex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -2116,6 +2140,15 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "html2md" version = "0.2.13" @@ -2794,7 +2827,6 @@ dependencies = [ "actix-web", "anyhow", "chrono", - "comrak", "deser-hjson", "diesel", "doku", @@ -2804,6 +2836,7 @@ dependencies = [ "itertools", "jsonwebtoken", "lettre", + "markdown-it", "once_cell", "openssl", "percent-encoding", @@ -2940,6 +2973,15 @@ version = "0.2.135" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + [[package]] name = "link-cplusplus" version = "1.0.7" @@ -2955,6 +2997,15 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linkify" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96dd5884008358112bc66093362197c7248ece00d46624e2cf71e50029f8cff5" +dependencies = [ + "memchr", +] + [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -3017,6 +3068,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "markdown-it" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53107ab22a09ae3b2eaedccf1d1c6aa58c1aa77e15689a799e0d8eda2b1a7d54" +dependencies = [ + "argparse", + "const_format", + "derivative", + "derive_more", + "downcast-rs", + "entities", + "html-escape", + "linkify", + "mdurl", + "once_cell", + "readonly", + "regex", + "stacker", + "syntect", + "unicode-general-category", +] + [[package]] name = "markup5ever" version = "0.10.1" @@ -3093,6 +3167,17 @@ dependencies = [ "digest", ] +[[package]] +name = "mdurl" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5736ba45bbac8f7ccc99a897f88ce85e508a18baec973a040f2514e6cdbff0d2" +dependencies = [ + "idna 0.2.3", + "once_cell", + "regex", +] + [[package]] name = "memchr" version = "2.5.0" @@ -3849,6 +3934,20 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plist" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" +dependencies = [ + "base64 0.21.2", + "indexmap", + "line-wrap", + "quick-xml 0.28.2", + "serde", + "time 0.3.15", +] + [[package]] name = "pmutil" version = "0.5.3" @@ -4073,6 +4172,15 @@ dependencies = [ "prost 0.11.0", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "quick-xml" version = "0.22.0" @@ -4103,6 +4211,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.28" @@ -4205,6 +4322,17 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "readonly" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb656d27c22b5c47154452686cae5e096f12e124daacb36a0bfcb32dbebb39e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4214,17 +4342,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.8", - "redox_syscall", - "thiserror", -] - [[package]] name = "regex" version = "1.8.4" @@ -4497,6 +4614,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -4736,12 +4859,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4833,6 +4950,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -4999,7 +5129,7 @@ dependencies = [ "swc_common", "swc_ecma_ast", "tracing", - "typed-arena 2.0.2", + "typed-arena", ] [[package]] @@ -5078,6 +5208,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "syntect" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "lazy_static", + "once_cell", + "plist", + "regex-syntax 0.6.27", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tap" version = "1.0.1" @@ -5771,12 +5924,6 @@ dependencies = [ "unchecked-index", ] -[[package]] -name = "typed-arena" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" - [[package]] name = "typed-arena" version = "2.0.2" @@ -5827,6 +5974,12 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +[[package]] +name = "unicode-general-category" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" + [[package]] name = "unicode-id" version = "0.3.3" @@ -5861,10 +6014,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "unicode-xid" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unreachable" @@ -5905,6 +6058,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + [[package]] name = "uuid" version = "1.2.1" @@ -6303,15 +6462,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" -[[package]] -name = "xdg" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" -dependencies = [ - "dirs", -] - [[package]] name = "xml5ever" version = "0.16.2" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 4e80c5df3..917e88bf1 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -43,7 +43,7 @@ deser-hjson = "1.0.2" smart-default = "0.6.0" jsonwebtoken = "8.1.1" lettre = "0.10.1" -comrak = { version = "0.14.0", default-features = false } +markdown-it = "0.5.0" totp-rs = { version = "4.2.0", features = ["gen_secret", "otpauth"] } [dev-dependencies] diff --git a/crates/utils/src/utils/markdown.rs b/crates/utils/src/utils/markdown.rs index a5ee53571..451c86bc7 100644 --- a/crates/utils/src/utils/markdown.rs +++ b/crates/utils/src/utils/markdown.rs @@ -1,3 +1,83 @@ +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 { - comrak::markdown_to_html(text, &comrak::ComrakOptions::default()) + MARKDOWN_PARSER.parse(text).xrender() +} + +#[cfg(test)] +mod tests { + 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 + ); + }); + } } diff --git a/crates/utils/src/utils/markdown/spoiler_rule.rs b/crates/utils/src/utils/markdown/spoiler_rule.rs new file mode 100644 index 000000000..1a564f07c --- /dev/null +++ b/crates/utils/src/utils/markdown/spoiler_rule.rs @@ -0,0 +1,200 @@ +// Custom Markdown plugin to manage spoilers. +// +// Matches the capability described in Lemmy UI: +// https://github.com/LemmyNet/lemmy-ui/blob/main/src/shared/utils.ts#L159 +// that is based off of: +// https://github.com/markdown-it/markdown-it-container/tree/master#example +// +// FORMAT: +// Input Markdown: ::: spoiler VISIBLE_TEXT\nHIDDEN_SPOILER\n:::\n +// Output HTML:
VISIBLE_TEXT

nHIDDEN_SPOILER

+// +// Anatomy of a spoiler: +// keyword +// ^ +// ::: spoiler VISIBLE_HINT +// ^ ^ +// begin fence visible text +// +// HIDDEN_SPOILER +// ^ +// hidden text +// +// ::: +// ^ +// end fence + +use markdown_it::{ + parser::{ + block::{BlockRule, BlockState}, + inline::InlineRoot, + }, + MarkdownIt, + Node, + NodeValue, + Renderer, +}; +use once_cell::sync::Lazy; +use regex::Regex; + +#[derive(Debug)] +struct SpoilerBlock { + visible_text: String, +} + +const SPOILER_PREFIX: &str = "::: spoiler "; +const SPOILER_SUFFIX: &str = ":::"; +const SPOILER_SUFFIX_NEWLINE: &str = ":::\n"; + +static SPOILER_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^::: spoiler .*$").expect("compile spoiler markdown regex.")); + +impl NodeValue for SpoilerBlock { + // Formats any node marked as a 'SpoilerBlock' into HTML. + // See the SpoilerBlockScanner#run implementation to see how these nodes get added to the tree. + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("details", &node.attrs); + fmt.open("summary", &[]); + // Not allowing special styling to the visible text to keep it simple. + // If allowed, would need to parse the child nodes to assign to visible vs hidden text sections. + fmt.text(&self.visible_text); + fmt.close("summary"); + fmt.open("p", &[]); + fmt.contents(&node.children); + fmt.close("p"); + fmt.close("details"); + fmt.cr(); + } +} + +struct SpoilerBlockScanner; + +impl BlockRule for SpoilerBlockScanner { + // Invoked on every line in the provided Markdown text to check if the BlockRule applies. + // + // NOTE: This does NOT support nested spoilers at this time. + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let first_line: &str = state.get_line(state.line).trim(); + + // 1. Check if the first line contains the spoiler syntax... + if !SPOILER_REGEX.is_match(first_line) { + return None; + } + + let begin_spoiler_line_idx: usize = state.line + 1; + let mut end_fence_line_idx: usize = begin_spoiler_line_idx; + let mut has_end_fence: bool = false; + + // 2. Search for the end of the spoiler and find the index of the last line of the spoiler. + // There could potentially be multiple lines between the beginning and end of the block. + // + // Block ends with a line with ':::' or ':::\n'; it must be isolated from other markdown. + while end_fence_line_idx < state.line_max && !has_end_fence { + let next_line: &str = state.get_line(end_fence_line_idx).trim(); + + if next_line.eq(SPOILER_SUFFIX) || next_line.eq(SPOILER_SUFFIX_NEWLINE) { + has_end_fence = true; + break; + } + + end_fence_line_idx += 1; + } + + // 3. If available, construct and return the spoiler node to add to the tree. + if has_end_fence { + let (spoiler_content, mapping) = state.get_lines( + begin_spoiler_line_idx, + end_fence_line_idx, + state.blk_indent, + true, + ); + + let mut node = Node::new(SpoilerBlock { + visible_text: String::from(first_line.replace(SPOILER_PREFIX, "").trim()), + }); + + // Add the spoiler content as children; marking as a child tells the tree to process the + // node again, which means other Markdown syntax (ex: emphasis, links) can be rendered. + node + .children + .push(Node::new(InlineRoot::new(spoiler_content, mapping))); + + // NOTE: Not using begin_spoiler_line_idx here because of incorrect results when + // state.line == 0 (subtracts an idx) vs the expected correct result (adds an idx). + Some((node, end_fence_line_idx - state.line + 1)) + } else { + None + } + } +} + +pub fn add(markdown_parser: &mut MarkdownIt) { + markdown_parser.block.add_rule::(); +} + +#[cfg(test)] +mod tests { + use crate::utils::markdown::spoiler_rule::add; + use markdown_it::MarkdownIt; + + #[test] + fn test_spoiler_markdown() { + let tests: Vec<_> = vec![ + ( + "invalid spoiler", + "::: spoiler click to see more\nbut I never finished", + "

::: spoiler click to see more\nbut I never finished

\n", + ), + ( + "another invalid spoiler", + "::: spoiler\nnever added the lead in\n:::", + "

::: spoiler\nnever added the lead in\n:::

\n", + ), + ( + "basic spoiler, but no newline at the end", + "::: spoiler click to see more\nhow spicy!\n:::", + "
click to see more

how spicy!\n

\n" + ), + ( + "basic spoiler with a newline at the end", + "::: spoiler click to see more\nhow spicy!\n:::\n", + "
click to see more

how spicy!\n

\n" + ), + ( + "spoiler with extra markdown on the call to action (no extra parsing)", + "::: spoiler _click to see more_\nhow spicy!\n:::\n", + "
_click to see more_

how spicy!\n

\n" + ), + ( + "spoiler with extra markdown in the fenced spoiler block", + "::: spoiler click to see more\n**how spicy!**\n*i have many lines*\n:::\n", + "
click to see more

how spicy!\ni have many lines\n

\n" + ), + ( + "spoiler mixed with other content", + "hey you\npsst, wanna hear a secret?\n::: spoiler lean in and i'll tell you\n**you are breathtaking!**\n:::\nwhatcha think about that?", + "

hey you\npsst, wanna hear a secret?

\n
lean in and i'll tell you

you are breathtaking!\n

\n

whatcha think about that?

\n" + ), + ( + "spoiler mixed with indented content", + "- did you know that\n::: spoiler the call was\n***coming from inside the house!***\n:::\n - crazy, right?", + "
    \n
  • did you know that
  • \n
\n
the call was

coming from inside the house!\n

\n
    \n
  • crazy, right?
  • \n
\n" + ) + ]; + + tests.iter().for_each(|&(msg, input, expected)| { + let md = &mut MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + add(md); + + assert_eq!( + md.parse(input).xrender(), + expected, + "Testing {}, with original input '{}'", + msg, + input + ); + }); + } +}