mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-26 18:00:31 +00:00
* 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>
This commit is contained in:
parent
f3f95e5d2f
commit
1c7bfd6be8
4 changed files with 507 additions and 77 deletions
300
Cargo.lock
generated
300
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,3 +1,83 @@
|
|||
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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
200
crates/utils/src/utils/markdown/spoiler_rule.rs
Normal file
200
crates/utils/src/utils/markdown/spoiler_rule.rs
Normal file
|
@ -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: <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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue