\\n"}}
"""
@spec parse_fragment(String.t(), keyword()) :: {:ok, Document.md_node()} | nil
def parse_fragment(markdown, opts \\ []) when is_binary(markdown) do
case parse_document(markdown, opts) do
{:ok, %Document{nodes: [%MDEx.Paragraph{nodes: [node]}]}} -> {:ok, node}
{:ok, %Document{nodes: [node]}} -> {:ok, node}
_ -> nil
end
end
@doc """
Same as `parse_fragment/2` but raises if the parsing fails.
> #### Experimental {: .warning}
>
> Consider this function experimental and subject to change.
"""
def parse_fragment!(markdown, opts \\ []) when is_binary(markdown) do
case parse_fragment(markdown, opts) do
{:ok, fragment} -> fragment
_ -> raise %InvalidInputError{found: markdown}
end
end
@doc """
Convert Markdown or `MDEx.Document` to HTML using default options.
Use `to_html/2` to pass options and customize the generated HTML.
## Examples
iex> MDEx.to_html("# MDEx")
{:ok, "
"}
Fragments of a document are also supported:
iex> MDEx.to_html(%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "MDEx"}]})
{:ok, "
MDEx
"}
"""
@spec to_html(md_or_doc :: String.t() | Document.t()) ::
{:ok, String.t()}
| {:error, MDEx.DecodeError.t()}
| {:error, MDEx.InvalidInputError.t()}
def to_html(md_or_doc)
def to_html(md_or_doc) when is_binary(md_or_doc) do
md_or_doc
|> Native.markdown_to_html()
|> maybe_trim()
end
def to_html(%Document{} = doc) do
doc
|> Native.document_to_html()
|> maybe_trim()
rescue
ErlangError ->
{:error, %DecodeError{document: doc}}
end
def to_html(md_or_doc) do
if is_fragment(md_or_doc) do
to_html(%Document{nodes: List.wrap(md_or_doc)})
else
{:error, %InvalidInputError{found: md_or_doc}}
end
end
@doc """
Same as `to_html/1` but raises an error if the conversion fails.
"""
@spec to_html!(md_or_doc :: String.t() | Document.t()) :: String.t()
def to_html!(md_or_doc) do
case to_html(md_or_doc) do
{:ok, html} -> html
{:error, error} -> raise error
end
end
@doc """
Convert Markdown or `MDEx.Document` to HTML using custom options.
## Options
See the [Options](#module-options) section for the available options.
## Examples
iex> MDEx.to_html("Hello ~world~ there", extension: [strikethrough: true])
{:ok, "
Hello world there
"}
iex> MDEx.to_html("", extension: [autolink: true], render: [unsafe_: true])
{:ok, ""}
"""
@spec to_html(md_or_doc :: String.t() | Document.t(), opts :: keyword()) ::
{:ok, String.t()}
| {:error, MDEx.DecodeError.t()}
| {:error, MDEx.InvalidInputError.t()}
def to_html(md_or_doc, opts)
def to_html(md_or_doc, opts) when is_binary(md_or_doc) and is_list(opts) do
md_or_doc
|> Native.markdown_to_html_with_options(comrak_options(opts))
# |> maybe_wrap_error()
|> maybe_trim()
end
def to_html(%Document{} = doc, opts) when is_list(opts) do
doc
|> Native.document_to_html_with_options(comrak_options(opts))
|> maybe_trim()
rescue
ErlangError ->
{:error, %DecodeError{document: doc}}
end
def to_html(md_or_doc, opts) do
if is_fragment(md_or_doc) do
to_html(%Document{nodes: List.wrap(md_or_doc)}, opts)
else
{:error, %InvalidInputError{found: md_or_doc}}
end
end
@doc """
Same as `to_html/2` but raises error if the conversion fails.
"""
@spec to_html!(md_or_doc :: String.t() | Document.t(), opts :: keyword()) :: String.t()
def to_html!(md_or_doc, opts) do
case to_html(md_or_doc, opts) do
{:ok, html} -> html
{:error, error} -> raise error
end
end
@doc """
Convert Markdown or `MDEx.Document` to XML using default options.
Use `to_xml/2` to pass options and customize the generated XML.
## Examples
iex> {:ok, xml} = MDEx.to_xml("# MDEx")
iex> xml
\"""
MDEx
\"""
iex> {:ok, xml} = MDEx.to_xml("Implemented with:\\n1. Elixir\\n2. Rust")
iex> xml
\"""
Implemented with:ElixirRust
\"""
iex> {:ok, xml} = MDEx.to_xml(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "MDEx"}], level: 3, setext: false}]})
iex> xml
\"""
MDEx
\"""
Fragments of a document are also supported:
iex> {:ok, xml} = MDEx.to_xml(%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "MDEx"}]})
iex> xml
\"""
MDEx
\"""
"""
@spec to_xml(md_or_doc :: String.t() | Document.t()) ::
{:ok, String.t()}
| {:error, MDEx.DecodeError.t()}
| {:error, MDEx.InvalidInputError.t()}
def to_xml(md_or_doc)
def to_xml(md_or_doc) when is_binary(md_or_doc) do
md_or_doc
|> Native.markdown_to_xml()
# |> maybe_trim()
end
def to_xml(%Document{} = doc) do
doc
|> Native.document_to_xml()
# |> maybe_trim()
rescue
ErlangError ->
{:error, %DecodeError{document: doc}}
end
def to_xml(md_or_doc) do
if is_fragment(md_or_doc) do
to_xml(%Document{nodes: List.wrap(md_or_doc)})
else
{:error, %InvalidInputError{found: md_or_doc}}
end
end
@doc """
Same as `to_xml/1` but raises an error if the conversion fails.
"""
@spec to_xml!(md_or_doc :: String.t() | Document.t()) :: String.t()
def to_xml!(md_or_doc) do
case to_xml(md_or_doc) do
{:ok, xml} -> xml
{:error, error} -> raise error
end
end
@doc """
Convert Markdown or `MDEx.Document` to XML using custom options.
## Options
See the [Options](#module-options) section for the available options.
## Examples
iex> {:ok, xml} = MDEx.to_xml("Hello ~world~ there", extension: [strikethrough: true])
iex> xml
\"""
Hello world there
\"""
iex> {:ok, xml} = MDEx.to_xml("", extension: [autolink: true], render: [unsafe_: true])
iex> xml
\"""
<marquee>visit https://beaconcms.org</marquee>
\"""
"""
@spec to_xml(md_or_doc :: String.t() | Document.t(), opts :: keyword()) ::
{:ok, String.t()}
| {:error, MDEx.DecodeError.t()}
| {:error, MDEx.InvalidInputError.t()}
def to_xml(md_or_doc, opts)
def to_xml(md_or_doc, opts) when is_binary(md_or_doc) and is_list(opts) do
md_or_doc
|> Native.markdown_to_xml_with_options(comrak_options(opts))
# |> maybe_wrap_error()
# |> maybe_trim()
end
def to_xml(%Document{} = doc, opts) when is_list(opts) do
doc
|> Native.document_to_xml_with_options(comrak_options(opts))
|> maybe_trim()
rescue
ErlangError ->
{:error, %DecodeError{document: doc}}
end
def to_xml(md_or_doc, opts) do
if is_fragment(md_or_doc) do
to_xml(%Document{nodes: List.wrap(md_or_doc)}, opts)
else
{:error, %InvalidInputError{found: md_or_doc}}
end
end
@doc """
Same as `to_xml/2` but raises error if the conversion fails.
"""
@spec to_xml!(md_or_doc :: String.t() | Document.t(), opts :: keyword()) :: String.t()
def to_xml!(md_or_doc, opts) do
case to_xml(md_or_doc, opts) do
{:ok, xml} -> xml
{:error, error} -> raise error
end
end
@doc """
Convert an AST to CommonMark using default options.
To customize the output, use `to_commonmark/2`.
## Example
iex> MDEx.to_commonmark(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "Hello"}], level: 3, setext: false}]})
{:ok, "### Hello"}
"""
@spec to_commonmark(Document.t()) :: {:ok, String.t()} | {:error, MDEx.DecodeError.t()}
def to_commonmark(%Document{} = doc) do
doc
|> Native.document_to_commonmark()
# |> maybe_wrap_error()
|> maybe_trim()
end
@doc """
Same as `to_commonmark/1` but raises `MDEx.DecodeError` if the conversion fails.
"""
@spec to_commonmark!(Document.t()) :: String.t()
def to_commonmark!(%Document{} = doc) do
case to_commonmark(doc) do
{:ok, md} -> md
{:error, error} -> raise error
end
end
@doc """
Convert an AST to CommonMark with custom options.
## Options
See the [Options](#module-options) section for the available options.
"""
@spec to_commonmark(Document.t(), keyword()) :: {:ok, String.t()} | {:error, MDEx.DecodeError.t()}
def to_commonmark(%Document{} = doc, opts) when is_list(opts) do
doc
|> Native.document_to_commonmark_with_options(comrak_options(opts))
# |> maybe_wrap_error()
|> maybe_trim()
end
@doc """
Same as `to_commonmark/2` but raises `MDEx.DecodeError` if the conversion fails.
"""
@spec to_commonmark!(Document.t(), keyword()) :: String.t()
def to_commonmark!(%Document{} = doc, opts) when is_list(opts) do
case to_commonmark(doc, opts) do
{:ok, md} -> md
{:error, error} -> raise error
end
end
@doc """
Traverse and update the Markdown document preserving the tree structure format.
## Examples
Traverse an entire Markdown document:
iex> import MDEx.Sigil
iex> doc = ~M\"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> \"""
iex> MDEx.traverse_and_update(doc, fn
...> %MDEx.Code{literal: "elixir"} = node -> %{node | literal: "ex"}
...> %MDEx.Code{literal: "rust"} = node -> %{node | literal: "rs"}
...> node -> node
...> end)
%MDEx.Document{
nodes: [
%MDEx.Heading{nodes: [%MDEx.Text{literal: "Languages"}], level: 1, setext: false},
%MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "ex"}]},
%MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "rs"}]}
]
}
Or fragments of a document:
iex> fragment = MDEx.parse_fragment!("Lang: `elixir`")
iex> MDEx.traverse_and_update(fragment, fn
...> %MDEx.Code{literal: "elixir"} = node -> %{node | literal: "ex"}
...> node -> node
...> end)
%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Lang: "}, %MDEx.Code{num_backticks: 1, literal: "ex"}]}
"""
@spec traverse_and_update(MDEx.Document.t(), (MDEx.Document.md_node() -> MDEx.Document.md_node())) :: MDEx.Document.t()
def traverse_and_update(ast, fun), do: MDEx.Document.Traversal.traverse_and_update(ast, fun)
@doc """
Traverse and update the Markdown document preserving the tree structure format and keeping an accumulator.
## Example
iex> import MDEx.Sigil
iex> doc = ~M\"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> \"""
iex> MDEx.traverse_and_update(doc, 0, fn
...> %MDEx.Code{literal: "elixir"} = node, acc -> {%{node | literal: "ex"}, acc + 1}
...> %MDEx.Code{literal: "rust"} = node, acc -> {%{node | literal: "rs"}, acc + 1}
...> node, acc -> {node, acc}
...> end)
{%MDEx.Document{
nodes: [
%MDEx.Heading{nodes: [%MDEx.Text{literal: "Languages"}], level: 1, setext: false},
%MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "ex"}]},
%MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "rs"}]}
]
}, 2}
Also works with fragments.
"""
@spec traverse_and_update(MDEx.Document.t(), term(), (MDEx.Document.md_node() -> MDEx.Document.md_node())) :: MDEx.Document.t()
def traverse_and_update(ast, acc, fun), do: MDEx.Document.Traversal.traverse_and_update(ast, acc, fun)
defp comrak_options(opts) do
extension = Keyword.get(opts, :extension, %{})
parse = Keyword.get(opts, :parse, %{})
render = Keyword.get(opts, :render, %{})
features = Keyword.get(opts, :features, %{})
%MDEx.Types.Options{
extension: struct(MDEx.Types.ExtensionOptions, extension),
parse: struct(MDEx.Types.ParseOptions, parse),
render: struct(MDEx.Types.RenderOptions, render),
features: struct(MDEx.Types.FeaturesOptions, features)
}
end
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'}"
## 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