MDEx.Document (MDEx v0.5.0)

View Source

Tree representation of a Markdown 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:

  • Fetches 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

You can also 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.

Examples

Update all code block nodes filtees by the selector function

Add line "// Modified" in Rust block codes:

iex> doc = ~M"""
...> # Code Examples
...>
...> ```elixir
...> def hello do
...>   :world
...> end
...> ```
...>
...> ```rust
...> fn main() {
...>   println!("Hello");
...> }
...> ```
...> """
iex> selector = fn
...>   %MDEx.CodeBlock{info: "rust"} -> true
...>   _ -> false
...> end
iex> update_in(doc, [:document, Access.key!(:nodes), Access.all(), selector], fn node ->
...>   %{node | literal: "// Modified\n" <> node.literal}
...> end)
%MDEx.Document{
  nodes: [
    %MDEx.Heading{
      nodes: [%MDEx.Text{literal: "Code Examples"}],
      level: 1,
      setext: false
    },
    %MDEx.CodeBlock{
      info: "elixir",
      literal: "def hello do\n  :world\nend\n"
    },
    %MDEx.CodeBlock{
      info: "rust",
      literal: "// Modified\nfn main() {\n  println!(\"Hello\");\n}\n"
    }
  ]
}

Collect headings by level

iex> doc = ~M"""
...> # Main Title
...>
...> ## Section 1
...>
...> ### Subsection
...>
...> ## Section 2
...> """
iex> Enum.reduce(doc, %{}, fn
...>   %MDEx.Heading{level: level, nodes: [%MDEx.Text{literal: text}]}, acc ->
...>     Map.update(acc, level, [text], &[text | &1])
...>   _node, acc -> acc
...> end)
%{
  1 => ["Main Title"],
  2 => ["Section 2", "Section 1"],
  3 => ["Subsection"]
}

Extract and transform task list items

iex> doc = ~M"""
...> # Todo List
...>
...> - [ ] Buy groceries
...> - [x] Call mom
...> - [ ] Read book
...> """
iex> Enum.map(doc, fn
...>   %MDEx.TaskItem{checked: checked, nodes: [%MDEx.Paragraph{nodes: [%MDEx.Text{literal: text}]}]} ->
...>     {checked, text}
...>   _ -> nil
...> end)
...> |> Enum.reject(&is_nil/1)
[
  {false, "Buy groceries"},
  {true, "Call mom"},
  {false, "Read book"}
]

Bump all heading levels, except level 6

iex> doc = ~M"""
...> # Main Title
...>
...> ## Subtitle
...>
...> ###### Notes
...> """
iex> selector = fn
...>   %MDEx.Heading{level: level} when level < 6 -> true
...>   _ -> false
...> end
iex> update_in(doc, [:document, Access.key!(:nodes), Access.all(), selector], fn node ->
...>   %{node | level: node.level + 1}
...> end)
%MDEx.Document{
  nodes: [
    %MDEx.Heading{nodes: [%MDEx.Text{literal: "Main Title"}], level: 2, setext: false},
    %MDEx.Heading{nodes: [%MDEx.Text{literal: "Subtitle"}], level: 3, setext: false},
    %MDEx.Heading{nodes: [%MDEx.Text{literal: "Notes"}], level: 6, setext: false}
  ]
}

Summary

Types

Fragment of a Markdown document, a single node. May contain children nodes.

Selector used to match nodes in the document.

t()

Tree root of a Markdown document, including all children nodes.

Types

md_node()

Fragment of a Markdown document, a single node. May contain children nodes.

selector()

@type selector() :: md_node() | module() | atom() | (md_node() -> boolean())

Selector used to match nodes in the document.

Valid selectors can be the module or struct, an atom representing the node name, or a function that receives a node and returns a boolean.

See MDEx.Document for more info and examples.

t()

@type t() :: %MDEx.Document{nodes: [md_node()]}

Tree root of a Markdown document, including all children nodes.

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.

wrap(document)