defmodule MDEx do @external_resource "README.md" @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) alias MDEx.Native alias MDEx.Document alias MDEx.DecodeError alias MDEx.InvalidInputError import MDEx.Document, only: [is_fragment: 1] @doc """ Parse a `markdown` string and returns a `MDEx.Document`. ## Options See the [Options](#module-options) section for the available `opts`. ## Examples iex> MDEx.parse_document!(\""" ...> # Languages ...> ...> - Elixir ...> - Rust ...> \""") %MDEx.Document{ nodes: [ %MDEx.Heading{nodes: [%MDEx.Text{literal: "Languages"}], level: 1, setext: false}, %MDEx.List{ nodes: [ %MDEx.ListItem{ nodes: [%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Elixir"}]}], list_type: :bullet, marker_offset: 0, padding: 2, start: 1, delimiter: :period, bullet_char: "-", tight: false }, %MDEx.ListItem{ nodes: [%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Rust"}]}], list_type: :bullet, marker_offset: 0, padding: 2, start: 1, delimiter: :period, bullet_char: "-", tight: false } ], list_type: :bullet, marker_offset: 0, padding: 2, start: 1, delimiter: :period, bullet_char: "-", tight: true } ] } iex> MDEx.parse_document!("Darth Vader is ||Luke's father||", extension: [spoiler: true]) %MDEx.Document{ nodes: [ %MDEx.Paragraph{ nodes: [ %MDEx.Text{literal: "Darth Vader is "}, %MDEx.SpoileredText{nodes: [%MDEx.Text{literal: "Luke's father"}]} ] } ] } """ @spec parse_document(String.t(), keyword()) :: {:ok, Document.t()} | {:error, term()} def parse_document(markdown, opts \\ []) when is_binary(markdown) do Native.parse_document(markdown, comrak_options(opts)) end @doc """ Same as `parse_document/2` but raises if the parsing fails. """ @spec parse_document!(String.t(), keyword()) :: Document.t() def parse_document!(markdown, opts \\ []) when is_binary(markdown) do case parse_document(markdown, opts) do {:ok, doc} -> doc {:error, error} -> raise error end end @doc """ Parse a `markdown` string and returns only the node that represents the fragment. Usually that means filtering out the parent document and paragraphs. That's useful to generate fragment nodes and inject them into the document when you're manipulating it. Use `parse_document/2` to generate a complete document. > #### Experimental {: .warning} > > Consider this function experimental and subject to change. ## Examples iex> MDEx.parse_fragment("# Elixir") {:ok, %MDEx.Heading{nodes: [%MDEx.Text{literal: "Elixir"}], level: 1, setext: false}} iex> MDEx.parse_fragment("

Elixir

") {:ok, %MDEx.HtmlBlock{nodes: [], block_type: 6, literal: "

Elixir

\\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, "

MDEx

"} iex> MDEx.to_html("Implemented with:\\n1. Elixir\\n2. Rust") {:ok, "

Implemented with:

\\n
    \\n
  1. Elixir
  2. \\n
  3. Rust
  4. \\n
"} iex> MDEx.to_html(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "MDEx"}], level: 3, setext: false}]}) {:ok, "

MDEx

"} 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("visit https://beaconcms.org", extension: [autolink: true], render: [unsafe_: true]) {:ok, "

visit https://beaconcms.org

"} """ @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: Elixir Rust \""" 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("visit https://beaconcms.org", 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 end