MDEx.Document (MDEx v0.5.0)
View SourceTree 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.
Tree root of a Markdown document, including all children nodes.
Functions
Callback implementation for Access.fetch/2
.
Callback implementation for Access.get_and_update/3
.
Callback implementation for Access.fetch/2
.
Types
@type md_node() :: MDEx.FrontMatter.t() | MDEx.BlockQuote.t() | MDEx.List.t() | MDEx.ListItem.t() | MDEx.DescriptionList.t() | MDEx.DescriptionItem.t() | MDEx.DescriptionTerm.t() | MDEx.DescriptionDetails.t() | MDEx.CodeBlock.t() | MDEx.HtmlBlock.t() | MDEx.Paragraph.t() | MDEx.Heading.t() | MDEx.ThematicBreak.t() | MDEx.FootnoteDefinition.t() | MDEx.FootnoteReference.t() | MDEx.Table.t() | MDEx.TableRow.t() | MDEx.TableCell.t() | MDEx.Text.t() | MDEx.TaskItem.t() | MDEx.SoftBreak.t() | MDEx.LineBreak.t() | MDEx.Code.t() | MDEx.HtmlInline.t() | MDEx.Raw.t() | MDEx.Emph.t() | MDEx.Strong.t() | MDEx.Strikethrough.t() | MDEx.Superscript.t() | MDEx.Link.t() | MDEx.Image.t() | MDEx.ShortCode.t() | MDEx.Math.t() | MDEx.MultilineBlockQuote.t() | MDEx.Escaped.t() | MDEx.WikiLink.t() | MDEx.Underline.t() | MDEx.Subscript.t() | MDEx.SpoileredText.t() | MDEx.EscapedTag.t() | MDEx.Alert.t()
Fragment of a Markdown document, a single node. May contain children nodes.
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.
@type t() :: %MDEx.Document{nodes: [md_node()]}
Tree root of a Markdown document, including all children nodes.
Functions
Callback implementation for Access.fetch/2
.
See the Access section for examples.
Callback implementation for Access.get_and_update/3
.
See the Access section for examples.
Callback implementation for Access.fetch/2
.
See the Access section for examples.