diff --git a/lib/mdex.ex b/lib/mdex.ex index 25ef81c..1a49968 100644 --- a/lib/mdex.ex +++ b/lib/mdex.ex @@ -603,11 +603,38 @@ defmodule MDEx do defp maybe_trim({:ok, result}), do: {:ok, String.trim(result)} defp maybe_trim(error), do: error - # TODO: spec/docs - def safe_html(unsafe_html, opts) do - sanitize = Keyword.get(opts, :sanitize, true) - escape_tags = Keyword.get(opts, :escape_tags, true) - escape_curly_braces_in_code = Keyword.get(opts, :escape_curly_braces_in_code, true) - Native.safe_html(unsafe_html, sanitize, escape_tags, escape_curly_braces_in_code) + @doc """ + Utility function to sanitize and escape HTML. + + ## Example + + iex> MDEx.safe_html("") + "" + + iex> MDEx.safe_html("Hello") + "<span>Hello</span>" + + iex> MDEx.safe_html("

{'Example:'}

{:ok, 'MDEx'}") + "<h1>{'Example:'}</h1><code>{:ok, 'MDEx'}</code>" + + ## 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/native/comrak_nif/src/lib.rs b/native/comrak_nif/src/lib.rs index c25608e..911d660 100644 --- a/native/comrak_nif/src/lib.rs +++ b/native/comrak_nif/src/lib.rs @@ -382,18 +382,16 @@ fn do_safe_html( false => unsafe_html, }; - let html = match escape_tags { - true => v_htmlescape::escape(&html).to_string(), - false => html, - }; - let html = match escape_curly_braces_in_code { true => rewrite_str( &html, RewriteStrSettings { - element_content_handlers: vec![text!("code", |t| { - t.replace( - &t.as_str().replace('{', "{").replace('}', "}"), + element_content_handlers: vec![text!("code", |chunk| { + chunk.replace( + &chunk + .as_str() + .replace('{', "{") + .replace('}', "}"), ContentType::Html, ); @@ -406,5 +404,12 @@ fn do_safe_html( false => html, }; - html + let html = match escape_tags { + 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/test/mdex_test.exs b/test/mdex_test.exs index 1acacb8..d3ab85e 100644 --- a/test/mdex_test.exs +++ b/test/mdex_test.exs @@ -231,19 +231,29 @@ defmodule MDExTest do test "sanitize" do assert MDEx.safe_html("tag", sanitize: true, - escape_tags: false, - escape_curly_braces_in_code: false + escape: [content: false, curly_braces_in_code: false] ) == "tag" end test "escape tags" do - assert MDEx.safe_html("tag", sanitize: false, escape_tags: true, escape_curly_braces_in_code: false) == - "<span>tag</span>" + 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_tags: false, escape_curly_braces_in_code: true) == - "

{test}

{:foo}" + 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