diff --git a/examples/heex.exs b/examples/heex.exs index 5a2bf98..bdabf92 100644 --- a/examples/heex.exs +++ b/examples/heex.exs @@ -92,6 +92,7 @@ markdown = """ ```heex <%= @hello %> +{ @hello } ``` Elixir was created by José Valim. diff --git a/examples/live_view.exs b/examples/live_view.exs index d35b108..3c490f4 100644 --- a/examples/live_view.exs +++ b/examples/live_view.exs @@ -98,6 +98,7 @@ defmodule DemoLive do ```heex <%= @hello %> + { @hello } ``` Elixir was created by José Valim. diff --git a/lib/mdex.ex b/lib/mdex.ex index 3238511..3ee8e11 100644 --- a/lib/mdex.ex +++ b/lib/mdex.ex @@ -602,4 +602,39 @@ defmodule MDEx do defp maybe_trim({:ok, result}), do: {:ok, String.trim(result)} defp maybe_trim(error), do: error + + @doc """ + Utility function to sanitize and escape HTML. + + ## Examples + + iex> MDEx.safe_html("") + "" + + iex> MDEx.safe_html("

{'Example:'}

{:ok, 'MDEx'}") + "<h1>{'Example:'}</h1><code>{:ok, 'MDEx'}</code>" + + iex> MDEx.safe_html("

{'Example:'}

{:ok, 'MDEx'}", escape: [content: false]) + "

{'Example:'}

{:ok, 'MDEx'}" + + ## Options + + - `:sanitize` - clean HTML using these rules https://docs.rs/ammonia/latest/ammonia/fn.clean.html. Defaults to `true`. + - `:escape` - which entities should be escaped. Defaults to `[:content, :curly_braces_in_code]`. + - `:content` - escape common chars like `<`, `>`, `&`, and others in the HTML content; + - `:curly_braces_in_code` - escape `{` and `}` only inside `` tags, particularly useful for compiling HTML in LiveView; + """ + def safe_html(unsafe_html, opts \\ []) when is_binary(unsafe_html) and is_list(opts) do + sanitize = opt(opts, [:sanitize], true) + escape_content = opt(opts, [:escape, :content], true) + escape_curly_braces_in_code = opt(opts, [:escape, :curly_braces_in_code], true) + Native.safe_html(unsafe_html, sanitize, escape_content, escape_curly_braces_in_code) + end + + defp opt(opts, keys, default) do + case get_in(opts, keys) do + nil -> default + val -> val + end + end end diff --git a/lib/mdex/native.ex b/lib/mdex/native.ex index d748935..980e710 100644 --- a/lib/mdex/native.ex +++ b/lib/mdex/native.ex @@ -56,6 +56,8 @@ defmodule MDEx.Native do mode: mode, force_build: System.get_env("MDEX_BUILD") in ["1", "true"] + def safe_html(_unsafe_html, _sanitize, _escape_content, _escape_curly_braces_in_code), do: :erlang.nif_error(:nif_not_loaded) + # markdown # - to document (parse) # - to html diff --git a/native/comrak_nif/Cargo.lock b/native/comrak_nif/Cargo.lock index d27661c..88b5bca 100644 --- a/native/comrak_nif/Cargo.lock +++ b/native/comrak_nif/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "ammonia" version = "4.0.0" @@ -96,7 +102,7 @@ name = "autumn" version = "0.1.0" dependencies = [ "inkjet", - "phf", + "phf 0.11.3", "tree-sitter", "tree-sitter-highlight", "v_htmlescape", @@ -166,7 +172,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.95", ] [[package]] @@ -237,7 +243,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -282,7 +288,8 @@ dependencies = [ "inkjet", "lazy_static", "log", - "phf", + "lol_html", + "phf 0.11.3", "rustler", "serde", "tree-sitter", @@ -291,6 +298,12 @@ dependencies = [ "v_htmlescape", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "crc32fast" version = "1.4.2" @@ -300,6 +313,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.95", +] + [[package]] name = "darling" version = "0.20.10" @@ -321,7 +361,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.95", ] [[package]] @@ -332,7 +372,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -344,6 +384,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.95", +] + [[package]] name = "deunicode" version = "1.6.0" @@ -358,7 +411,22 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", ] [[package]] @@ -367,7 +435,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" dependencies = [ - "phf", + "phf 0.11.3", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", ] [[package]] @@ -418,6 +495,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -437,6 +520,26 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -445,7 +548,7 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -453,6 +556,11 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -471,7 +579,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -589,7 +697,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -639,7 +747,7 @@ dependencies = [ "cc", "once_cell", "serde", - "thiserror", + "thiserror 1.0.69", "toml", "tree-sitter", "tree-sitter-highlight", @@ -704,6 +812,23 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lol_html" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1058123f6262982b891dccc395cff0144d9439de366460b47fab719258b96e" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cssparser", + "encoding_rs", + "hashbrown", + "memchr", + "mime", + "selectors", + "thiserror 2.0.10", +] + [[package]] name = "mac" version = "0.1.1" @@ -723,19 +848,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", - "phf", - "phf_codegen", + "phf 0.11.3", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -751,6 +888,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "num-conv" version = "0.1.0" @@ -814,16 +957,46 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", + "phf_macros 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + [[package]] name = "phf_codegen" version = "0.11.3" @@ -834,6 +1007,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + [[package]] name = "phf_generator" version = "0.10.0" @@ -841,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", - "rand", + "rand 0.8.5", ] [[package]] @@ -851,7 +1034,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -864,7 +1061,16 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn", + "syn 2.0.95", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", ] [[package]] @@ -932,9 +1138,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.95", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.92" @@ -962,6 +1174,20 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -969,8 +1195,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -980,7 +1216,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -989,7 +1234,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -1030,6 +1293,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.43" @@ -1063,7 +1335,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -1103,6 +1375,30 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + [[package]] name = "serde" version = "1.0.217" @@ -1120,7 +1416,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -1144,6 +1440,16 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -1222,6 +1528,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.95" @@ -1241,7 +1558,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -1262,7 +1579,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -1294,7 +1611,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3" +dependencies = [ + "thiserror-impl 2.0.10", ] [[package]] @@ -1305,7 +1631,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", ] [[package]] @@ -1415,7 +1752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "042342584c5a7a0b833d9fc4e2bdab3f9868ddc6c4b339a1e01451c6720868bc" dependencies = [ "regex", - "thiserror", + "thiserror 1.0.69", "tree-sitter", ] @@ -1512,6 +1849,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1539,7 +1882,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.95", "wasm-bindgen-shared", ] @@ -1561,7 +1904,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1710,7 +2053,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", "synstructure", ] @@ -1732,7 +2075,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] [[package]] @@ -1752,7 +2095,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", "synstructure", ] @@ -1775,5 +2118,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.95", ] diff --git a/native/comrak_nif/Cargo.toml b/native/comrak_nif/Cargo.toml index 05841f3..f9057c4 100644 --- a/native/comrak_nif/Cargo.toml +++ b/native/comrak_nif/Cargo.toml @@ -26,6 +26,7 @@ autumn = { path = "vendor/autumn" } log = "0.4" lazy_static = "1.5" typed-arena = "2.0.2" +lol_html = "2.2" inkjet = { version = "0.10.5", default-features = false, features = [ "html", "language-bash", diff --git a/native/comrak_nif/src/lib.rs b/native/comrak_nif/src/lib.rs index 7cc003b..021178f 100644 --- a/native/comrak_nif/src/lib.rs +++ b/native/comrak_nif/src/lib.rs @@ -9,12 +9,15 @@ mod types; use comrak::{Arena, ComrakPlugins, Options}; use inkjet_adapter::InkjetAdapter; +use lol_html::html_content::ContentType; +use lol_html::{rewrite_str, text, RewriteStrSettings}; use rustler::{Encoder, Env, NifResult, Term}; use types::{atoms::ok, document::*, options::*}; rustler::init!( "Elixir.MDEx.Native", [ + safe_html, parse_document, markdown_to_html, markdown_to_html_with_options, @@ -48,7 +51,12 @@ fn markdown_to_html<'a>(env: Env<'a>, md: &str) -> NifResult> { let mut plugins = ComrakPlugins::default(); plugins.render.codefence_syntax_highlighter = Some(&inkjet_adapter); let unsafe_html = comrak::markdown_to_html_with_plugins(md, &Options::default(), &plugins); - let html = html_post_process(unsafe_html, ExFeaturesOptions::default().sanitize); + let html = do_safe_html( + unsafe_html, + ExFeaturesOptions::default().sanitize, + false, + true, + ); Ok((ok(), html).encode(env)) } @@ -76,12 +84,12 @@ fn markdown_to_html_with_options<'a>( let mut plugins = ComrakPlugins::default(); plugins.render.codefence_syntax_highlighter = Some(&inkjet_adapter); let unsafe_html = comrak::markdown_to_html_with_plugins(md, &comrak_options, &plugins); - let html = html_post_process(unsafe_html, options.features.sanitize); + let html = do_safe_html(unsafe_html, options.features.sanitize, false, true); Ok((ok(), html).encode(env)) } None => { let unsafe_html = comrak::markdown_to_html(md, &comrak_options); - let html = html_post_process(unsafe_html, options.features.sanitize); + let html = do_safe_html(unsafe_html, options.features.sanitize, false, true); Ok((ok(), html).encode(env)) } } @@ -225,7 +233,12 @@ fn document_to_html(env: Env<'_>, ex_document: ExDocument) -> NifResult let options = Options::default(); comrak::format_html_with_plugins(comrak_ast, &options, &mut buffer, &plugins).unwrap(); let unsafe_html = String::from_utf8(buffer).unwrap(); - let html = html_post_process(unsafe_html, ExFeaturesOptions::default().sanitize); + let html = do_safe_html( + unsafe_html, + ExFeaturesOptions::default().sanitize, + false, + true, + ); Ok((ok(), html).encode(env)) } @@ -261,14 +274,14 @@ fn document_to_html_with_options( comrak::format_html_with_plugins(comrak_ast, &comrak_options, &mut buffer, &plugins) .unwrap(); let unsafe_html = String::from_utf8(buffer).unwrap(); - let html = html_post_process(unsafe_html, options.features.sanitize); + let html = do_safe_html(unsafe_html, options.features.sanitize, false, true); Ok((ok(), html).encode(env)) } None => { let mut buffer = vec![]; comrak::format_commonmark(comrak_ast, &comrak_options, &mut buffer).unwrap(); let unsafe_html = String::from_utf8(buffer).unwrap(); - let html = html_post_process(unsafe_html, options.features.sanitize); + let html = do_safe_html(unsafe_html, options.features.sanitize, false, true); Ok((ok(), html).encode(env)) } } @@ -340,12 +353,63 @@ fn document_to_xml_with_options( } } +#[rustler::nif(schedule = "DirtyCpu")] +pub fn safe_html( + env: Env<'_>, + unsafe_html: String, + sanitize: bool, + escape_content: bool, + escape_curly_braces_in_code: bool, +) -> NifResult> { + Ok(do_safe_html( + unsafe_html, + sanitize, + escape_content, + escape_curly_braces_in_code, + ) + .encode(env)) +} + // https://github.com/p-jackson/entities/blob/1d166204433c2ee7931251a5494f94c7e35be9d6/src/entities.rs -fn html_post_process(unsafe_html: String, sanitize: bool) -> String { +fn do_safe_html( + unsafe_html: String, + sanitize: bool, + escape_content: bool, + escape_curly_braces_in_code: bool, +) -> String { let html = match sanitize { true => ammonia::clean(&unsafe_html), false => unsafe_html, }; - html.replace('{', "{").replace('}', "}") + let html = match escape_curly_braces_in_code { + true => rewrite_str( + &html, + RewriteStrSettings { + element_content_handlers: vec![text!("code", |chunk| { + chunk.replace( + &chunk + .as_str() + .replace('{', "{") + .replace('}', "}"), + ContentType::Html, + ); + + Ok(()) + })], + ..RewriteStrSettings::new() + }, + ) + .unwrap(), + false => html, + }; + + let html = match escape_content { + true => v_htmlescape::escape(&html).to_string(), + false => html, + }; + + // TODO: not so clean solution to undo double escaping, could be better + html.replace("&lbrace;", "{") + .replace("&rbrace;", "}") } diff --git a/native/comrak_nif/vendor/autumn/Cargo.lock b/native/comrak_nif/vendor/autumn/Cargo.lock new file mode 100644 index 0000000..c8ece61 --- /dev/null +++ b/native/comrak_nif/vendor/autumn/Cargo.lock @@ -0,0 +1,341 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "autumn" +version = "0.1.0" +dependencies = [ + "inkjet", + "phf", + "tree-sitter", + "tree-sitter-highlight", + "v_htmlescape", +] + +[[package]] +name = "cc" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +dependencies = [ + "shlex", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inkjet" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd9fd670f1a42725a90e7a97f5e6a777fc56f9031c1ee0ebcea8f91a6bb9c2f" +dependencies = [ + "anyhow", + "cc", + "once_cell", + "serde", + "thiserror", + "toml", + "tree-sitter", + "tree-sitter-highlight", + "v_htmlescape", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tree-sitter" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-highlight" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042342584c5a7a0b833d9fc4e2bdab3f9868ddc6c4b339a1e01451c6720868bc" +dependencies = [ + "regex", + "thiserror", + "tree-sitter", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +dependencies = [ + "memchr", +] diff --git a/test/mdex_test.exs b/test/mdex_test.exs index 0009592..d3ab85e 100644 --- a/test/mdex_test.exs +++ b/test/mdex_test.exs @@ -115,17 +115,6 @@ defmodule MDExTest do features: [syntax_highlight_inline_style: false] ) end - - test "encode curly braces in inline code" do - assert_output( - ~S""" - `{:mdex, "~> 0.1"}` - """, - ~S""" -

{:mdex, "~> 0.1"}

- """ - ) - end end test "render emoji shortcodes" do @@ -203,6 +192,69 @@ defmodule MDExTest do assert MDEx.to_html!("", render: [unsafe_: true], features: [sanitize: true]) == "

" end + + test "encode curly braces in inline code" do + assert_output( + ~S""" + `{:mdex, "~> 0.1"}` + """, + ~S""" +

{:mdex, "~> 0.1"}

+ """ + ) + end + + test "preserve curly braces outside inline code" do + assert_output( + ~S""" + # {Title} `{:code}` + + - Elixir {:ex} + + ```elixir + {:ok, "code"} + ``` + """, + ~S""" +

{Title} {:code}

+
    +
  • Elixir {:ex}
  • +
+
{:ok, "code"}
+        
+ """ + ) + end + end + + describe "safe html" do + test "sanitize" do + assert MDEx.safe_html("tag", + sanitize: true, + escape: [content: false, curly_braces_in_code: false] + ) == "tag" + end + + test "escape tags" do + assert MDEx.safe_html("content", + sanitize: false, + escape: [content: true, curly_braces_in_code: false] + ) == "<span>content</span>" + end + + test "escape curly braces in code tags" do + assert MDEx.safe_html("

{test}

{:foo}", + sanitize: false, + escape: [content: false, curly_braces_in_code: true] + ) == "

{test}

{:foo}" + end + + test "enable all by default" do + assert MDEx.safe_html( + "{:example} {:ok, 'foo'}" + ) == + "<span>{:example} <code>{:ok, 'foo'}</code></span>" + end end describe "to_commonmark" do