View Source MDEx.Document (MDEx v0.3.0)

Tree representation of a CommonMark document.

%MDEx.Document{
  nodes: [
    %MDEx.Paragraph{
      nodes: [
        %MDEx.Code{num_backticks: 1, literal: "Elixir"}
      ]
    }
  ]
}

Each node may contain attributes and children nodes as in the example above where MDEx.Document contains a MDEx.Paragraph node which contains a MDEx.Code node with the attributes :num_backticks and :literal.

You can check out each node's documentation in the Document Nodes section, for example MDEx.HtmlBlock.

The wrapping MDEx.Document module represents the root of a document and it implements some behaviours and protocols to enable operations to fetch, update, and manipulate the document tree.

Let's go through these operations in the examples below.

In these examples we will be using the ~M and ~m to format the content, see their documentation for more info.

String.Chars

Calling Kernel.to_string/1 or interpolating the document AST will format it as CommonMark text.

iex> to_string(~M[# Hello])
"# Hello"

Fragments (nodes without the parent %Document{}) are also formatted:

iex> to_string(%MDEx.Heading{nodes: [%MDEx.Text{literal: "Hello"}], level: 1})
"# Hello"

And finally interpolation as well:

iex> lang = "elixir"
iex> to_string(~m[`#{lang}`])
"`elixir`"

Access

The Access behaviour gives you the ability to fetch and update nodes using different types of keys.

Starting with a simple Markdown document with a single heading and a text, let's fetch only the text node by matching the MDEx.Text node:

iex> ~M[# Hello][%MDEx.Text{literal: "Hello"}]
[%MDEx.Text{literal: "Hello"}]

That's essentially the same as:

doc = %MDEx.Document{nodes: [%MDEx.Heading{nodes: [%MDEx.Text{literal: "Hello"}], level: 1, setext: false}]},

Enum.filter(
  doc,
  fn node -> node == %MDEx.Text{literal: "Hello"} end
)

The key can also be modules, atoms, and even functions! For example:

  • Fetchs all Code nodes, either by MDEx.Code module or the :code atom representing the Code node
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> doc[MDEx.Code]
[%MDEx.Code{num_backticks: 1, literal: "elixir"}, %MDEx.Code{num_backticks: 1, literal: "rust"}]
iex> doc[:code]
[%MDEx.Code{num_backticks: 1, literal: "elixir"}, %MDEx.Code{num_backticks: 1, literal: "rust"}]
  • Dynamically fetch Code nodes where the :literal (node content) starts with "eli" using a function to filter the result
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> doc[fn node -> String.starts_with?(Map.get(node, :literal, ""), "eli") end]
[%MDEx.Code{num_backticks: 1, literal: "elixir"}]

That's the most flexible option, in the case where struct, modules, or atoms are not enough to match the node you want.

This protocol also allows us to update nodes that matches a selector. In the example below we'll capitalize the content of all MDEx.Code nodes:

iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...>
...> Continue...
...> """
iex> update_in(doc, [:document, Access.key!(:nodes), Access.all(), :code, Access.key!(:literal)], fn literal ->
...>   String.upcase(literal)
...> end)
%MDEx.Document{
  nodes: [
    %MDEx.Heading{nodes: [%MDEx.Text{literal: "Languages"}], level: 1, setext: false},
    %MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "ELIXIR"}]},
    %MDEx.Paragraph{nodes: [%MDEx.Code{num_backticks: 1, literal: "RUST"}]},
    %MDEx.Paragraph{nodes: [%MDEx.Text{literal: "Continue…"}]}
  ]
}

Enumerable

Probably the most used protocol in Elixir, it allows us to call Enum functions to manipulate the document. Let's see some examples:

  • Count the nodes in a document:
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> Enum.count(doc)
7
  • Count how many nodes have the :literal attribute:
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> Enum.reduce(doc, 0, fn
...>   %{literal: _literal}, acc -> acc + 1
...>
...>   _node, acc -> acc
...> end)
3
  • Returns true if node is member of the document:
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> Enum.member?(doc, %MDEx.Code{literal: "elixir", num_backticks: 1})
true
  • Map each node:
iex> doc = ~M"""
...> # Languages
...>
...> `elixir`
...>
...> `rust`
...> """
iex> Enum.map(doc, fn %node{} -> inspect(node) end)
["MDEx.Document", "MDEx.Heading", "MDEx.Text", "MDEx.Paragraph", "MDEx.Code", "MDEx.Paragraph", "MDEx.Code"]

Traverse and Update

Finally you can use the low-level MDEx.traverse_and_update/2 and MDEx.traverse_and_update/3 APIs to traverse each node of the AST and either update the nodes or do some calculation with an accumulator.

Summary

Types

Fragment of a CommonMark document, a single node.

Selector to match a node in a document.

t()

Tree representation of a CommonMark document.

Types

Functions

fetch(document, selector)

@spec fetch(t(), selector()) :: {:ok, md_node()} | :error

Callback implementation for Access.fetch/2.

See the Access section for examples.

get_and_update(document, selector, fun)

Callback implementation for Access.get_and_update/3.

See the Access section for examples.

pop(document, key, default \\ nil)

Callback implementation for Access.fetch/2.

See the Access section for examples.