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("
{:ok, 'MDEx'}
")
+ "<h1>{'Example:'}</h1><code>{:ok, 'MDEx'}</code>"
+
+ iex> MDEx.safe_html("{:ok, 'MDEx'}
", escape: [content: false])
+ "{: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("{", "{")
+ .replace("}", "}")
}
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