* 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 <me@nutomic.com>
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"
"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"
"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"
"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"
"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"
"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"
"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"
"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"
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"
"actix-web",
"anyhow",
"chrono",
- "comrak",
"deser-hjson",
"diesel",
"doku",
"itertools",
"jsonwebtoken",
"lettre",
+ "markdown-it",
"once_cell",
"openssl",
"percent-encoding",
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"
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"
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"
"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"
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"
"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"
"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"
]
[[package]]
-name = "redox_syscall"
-version = "0.2.16"
+name = "readonly"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+checksum = "eb656d27c22b5c47154452686cae5e096f12e124daacb36a0bfcb32dbebb39e3"
dependencies = [
- "bitflags 1.3.2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
]
[[package]]
-name = "redox_users"
-version = "0.4.3"
+name = "redox_syscall"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
- "getrandom 0.2.8",
- "redox_syscall",
- "thiserror",
+ "bitflags 1.3.2",
]
[[package]]
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"
"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"
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"
"swc_common",
"swc_ecma_ast",
"tracing",
- "typed-arena 2.0.2",
+ "typed-arena",
]
[[package]]
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"
"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"
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"
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"
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"
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"
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]
+use markdown_it::MarkdownIt;
+use once_cell::sync::Lazy;
+
+mod spoiler_rule;
+
+static MARKDOWN_PARSER: Lazy<MarkdownIt> = 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>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n"
+ ),
+ (
+ "line breaks",
+ "First\rSecond",
+ "<p>First\nSecond</p>\n"),
+ (
+ "emphasis",
+ "__bold__ **bold** *italic* ***bold+italic***",
+ "<p><strong>bold</strong> <strong>bold</strong> <em>italic</em> <em><strong>bold+italic</strong></em></p>\n"
+ ),
+ (
+ "blockquotes",
+ "> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n",
+ "<blockquote>\n<h4>Hello</h4>\n<ul>\n<li>Hola</li>\n<li>안영</li>\n</ul>\n<blockquote>\n<p>Goodbye</p>\n</blockquote>\n</blockquote>\n"
+ ),
+ (
+ "lists (ordered, unordered)",
+ "1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen",
+ "<ol>\n<li>pen</li>\n<li>apple</li>\n<li>apple pen</li>\n</ol>\n<ul>\n<li>pen</li>\n<li>pineapple</li>\n<li>pineapple pen</li>\n</ul>\n"
+ ),
+ (
+ "code and code blocks",
+ "this is my amazing `code snippet` and my amazing ```code block```",
+ "<p>this is my amazing <code>code snippet</code> and my amazing <code>code block</code></p>\n"
+ ),
+ (
+ "links",
+ "[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")",
+ "<p><a href=\"https://join-lemmy.org/\" title=\"Join Lemmy!\">Lemmy</a></p>\n"
+ ),
+ (
+ "images",
+ "![My linked image](https://image.com \"image alt text\")",
+ "<p><img src=\"https://image.com\" alt=\"My linked image\" title=\"image alt text\" /></p>\n"
+ ),
+ // Ensure any custom plugins are added to 'MARKDOWN_PARSER' implementation.
+ (
+ "basic spoiler",
+ "::: spoiler click to see more\nhow spicy!\n:::\n",
+ "<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n"
+ ),
+ ];
+
+ tests.iter().for_each(|&(msg, input, expected)| {
+ let result = markdown_to_html(input);
+
+ assert_eq!(
+ result, expected,
+ "Testing {}, with original input '{}'",
+ msg, input
+ );
+ });
+ }
}
--- /dev/null
+// 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: <details><summary>VISIBLE_TEXT</summary><p>nHIDDEN_SPOILER</p></details>
+//
+// 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<Regex> =
+ 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::<SpoilerBlockScanner>();
+}
+
+#[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",
+ "<p>::: spoiler click to see more\nbut I never finished</p>\n",
+ ),
+ (
+ "another invalid spoiler",
+ "::: spoiler\nnever added the lead in\n:::",
+ "<p>::: spoiler\nnever added the lead in\n:::</p>\n",
+ ),
+ (
+ "basic spoiler, but no newline at the end",
+ "::: spoiler click to see more\nhow spicy!\n:::",
+ "<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n"
+ ),
+ (
+ "basic spoiler with a newline at the end",
+ "::: spoiler click to see more\nhow spicy!\n:::\n",
+ "<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n"
+ ),
+ (
+ "spoiler with extra markdown on the call to action (no extra parsing)",
+ "::: spoiler _click to see more_\nhow spicy!\n:::\n",
+ "<details><summary>_click to see more_</summary><p>how spicy!\n</p></details>\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",
+ "<details><summary>click to see more</summary><p><strong>how spicy!</strong>\n<em>i have many lines</em>\n</p></details>\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?",
+ "<p>hey you\npsst, wanna hear a secret?</p>\n<details><summary>lean in and i'll tell you</summary><p><strong>you are breathtaking!</strong>\n</p></details>\n<p>whatcha think about that?</p>\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?",
+ "<ul>\n<li>did you know that</li>\n</ul>\n<details><summary>the call was</summary><p><em><strong>coming from inside the house!</strong></em>\n</p></details>\n<ul>\n<li>crazy, right?</li>\n</ul>\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
+ );
+ });
+ }
+}