MDEx.Pipe (MDEx v0.7.2)
View SourceMDEx.Pipe is a Req-like API to transform Markdown documents through a series of steps in a pipeline.
Its main use case it to enable plugins, for example:
document = """
# Project Diagram
```mermaid
graph TD
A[Enter Chart Definition] --> B(Preview)
B --> C{decide}
C --> D[Keep]
C --> E[Edit Definition]
E --> B
D --> F[Save Image and Code]
F --> B
```
"""
MDEx.new()
|> MDExMermaid.attach(mermaid_version: "11")
|> MDEx.to_html(document: document)
To understand how it works, let's write that Mermaid plugin.
Writing Plugins
Let's start with a simple plugin as example to render Mermaid diagrams.
In order to render Mermaid diagrams, we need to inject a <script>
into the document,
as outlined in their docs:
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
Note that the package version is specified in the URL, so we'll add an option
:mermaid_version
to the plugin to let users specify the version they want to use.
By default, we'll use the latest version:
MDEx.new() |> MDExMermaid.attach()
But users can override it:
MDEx.new() |> MDExMermaid.attach(mermaid_version: "11")
Let's get into the actual code, with comments to explain each part:
defmodule MDExMermaid do
alias MDEx.Pipe
@latest_version "11"
def attach(pipe, options \ []) do
pipe
# register option with prefix `:mermaid_` to avoid conflicts with other plugins
|> Pipe.register_options([:mermaid_version])
# merge all options given by users
|> Pipe.put_options(options)
# actual steps to manipulate the document
# see respective Pipe functions for more info
|> Pipe.append_steps(enable_unsafe: &enable_unsafe/1)
|> Pipe.append_steps(inject_script: &inject_script/1)
|> Pipe.append_steps(update_code_blocks: &update_code_blocks/1)
end
# to render raw html and <script> tags
defp enable_unsafe(pipe) do
Pipe.put_render_options(pipe, unsafe: true)
end
defp inject_script(pipe) do
version = Pipe.get_option(pipe, :mermaid_version, @latest_version)
script_node =
%MDEx.HtmlBlock{
literal: """
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@#{version}/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
"""
}
Pipe.put_node_in_document_root(pipe, script_node)
end
defp update_code_blocks(pipe) do
selector = fn
%MDEx.CodeBlock{info: "mermaid"} -> true
_ -> false
end
Pipe.update_nodes(
pipe,
selector,
&%MDEx.HtmlBlock{literal: "<pre class="mermaid">#{&1.literal}</pre>", nodes: &1.nodes}
)
end
end
Now we can attach/1
that plugin into any MDEx pipeline to render Mermaid diagrams.
Summary
Functions
Appends steps to the end of the existing pipeline's step list.
Retrieves an option value from the pipeline.
Retrieves a private value from the pipeline.
Retrieves one of the MDEx.sanitize_options/0
options from the pipeline.
Halts the pipeline execution.
Halts the pipeline execution with an exception.
Returns true
if the pipeline has the :sanitize
option set, otherwise false
.
Prepends steps to the beginning of the existing pipeline's step list.
Updates the pipeline's :extension
options.
Inserts node
into the document root at the specified position
.
Merges options into the pipeline's existing options.
Updates the pipeline's :parse
options.
Stores a value in the pipeline's private storage.
Updates the pipeline's :render
options.
Updates the pipeline's :sanitize
options.
Updates the pipeline's :syntax_highlight
options.
Registers a list of valid options that can be used by steps in the pipeline.
Executes the pipeline steps in order.
Updates nodes in the document that match selector
.
Updates a value in the pipeline's private storage using a function.
Types
@type step() :: (t() -> t()) | (t() -> {t(), Exception.t()}) | (t() -> {module(), atom(), [term()]})
Step in a pipeline.
It's a function that receives a MDEx.Pipe.t/0
struct and must return either one of the following:
- a
MDEx.Pipe.t/0
struct - a tuple with a
MDEx.Pipe.t/0
struct and anException.t/0
as{pipe, exception}
- a tuple with a module, function and arguments which will be invoked with
apply/3
@type t() :: %MDEx.Pipe{ current_steps: term(), document: MDEx.Document.t(), halted: boolean(), options: MDEx.options(), private: map(), registered_options: term(), steps: keyword() }
Pipeline state.
Functions
Appends steps to the end of the existing pipeline's step list.
Examples
Update an
:extension
option:iex> pipe = MDEx.new() iex> pipe = MDEx.Pipe.append_steps( ...> pipe, ...> enable_tables: fn pipe -> MDEx.Pipe.put_extension_options(pipe, table: true) end ...> ) iex> pipe |> MDEx.Pipe.run() |> MDEx.Pipe.get_option(:extension) [table: true]
Retrieves an option value from the pipeline.
Retrieves a private value from the pipeline.
Retrieves one of the MDEx.sanitize_options/0
options from the pipeline.
Halts the pipeline execution.
This function is used to stop the pipeline from processing any further steps. Once a pipeline is halted, no more steps will be executed. This is useful for plugins that need to stop processing when certain conditions are met or when an error occurs.
Examples
iex> pipe = MDEx.Pipe.halt(MDEx.new())
iex> pipe.halted
true
@spec halt(t(), Exception.t()) :: {t(), Exception.t()}
Halts the pipeline execution with an exception.
Returns true
if the pipeline has the :sanitize
option set, otherwise false
.
Prepends steps to the beginning of the existing pipeline's step list.
@spec put_extension_options(t(), MDEx.extension_options()) :: t()
Updates the pipeline's :extension
options.
Examples
iex> pipe = MDEx.Pipe.put_extension_options(MDEx.new(), table: true)
iex> MDEx.Pipe.get_option(pipe, :extension)[:table]
true
@spec put_node_in_document_root( t(), MDEx.Document.md_node(), position :: :top | :bottom ) :: t()
Inserts node
into the document root at the specified position
.
- By default, the node is inserted at the top of the document.
- Node must be a valid fragment node like a
MDEx.Heading
,MDEx.HtmlBlock
, etc.
Examples
iex> pipe = MDEx.new()
iex> pipe = MDEx.Pipe.append_steps(
...> pipe,
...> append_node: fn pipe ->
...> html_block = %MDEx.HtmlBlock{literal: "<p>Hello</p>"}
...> MDEx.Pipe.put_node_in_document_root(pipe, html_block, :bottom)
...> end)
iex> MDEx.to_html(pipe, document: "# Doc", render: [unsafe: true])
{:ok, "<h1>Doc</h1>\n<p>Hello</p>"}
Merges options into the pipeline's existing options.
This function handles both built-in options (:document
, :extension
, :parse
, :render
, :syntax_highlight
, and :sanitize
)
and user-defined options that have been registered with register_options/2
.
Examples
iex> pipe = MDEx.Pipe.register_options(MDEx.new(), [:custom_option])
iex> pipe = MDEx.Pipe.put_options(pipe, [
...> document: "# Hello",
...> extension: [table: true],
...> custom_option: "value"
...> ])
iex> MDEx.Pipe.get_option(pipe, :document)
"# Hello"
iex> MDEx.Pipe.get_option(pipe, :extension)[:table]
true
iex> MDEx.Pipe.get_option(pipe, :custom_option)
"value"
Built-in options are validated against their respective schemas:
iex> try do
...> MDEx.Pipe.put_options(MDEx.new(), [extension: [invalid: true]])
...> rescue
...> NimbleOptions.ValidationError -> :error
...> end
:error
@spec put_parse_options(t(), MDEx.parse_options()) :: t()
Updates the pipeline's :parse
options.
Examples
iex> pipe = MDEx.Pipe.put_parse_options(MDEx.new(), smart: true)
iex> MDEx.Pipe.get_option(pipe, :parse)[:smart]
true
Stores a value in the pipeline's private storage.
@spec put_render_options(t(), MDEx.render_options()) :: t()
Updates the pipeline's :render
options.
Examples
iex> pipe = MDEx.Pipe.put_render_options(MDEx.new(), escape: true)
iex> MDEx.Pipe.get_option(pipe, :render)[:escape]
true
@spec put_sanitize_options(t(), MDEx.sanitize_options()) :: t()
Updates the pipeline's :sanitize
options.
Examples
iex> pipe = MDEx.Pipe.put_sanitize_options(MDEx.new(), add_tags: ["MyComponent"])
iex> MDEx.Pipe.get_option(pipe, :sanitize)[:add_tags]
["MyComponent"]
@spec put_syntax_highlight_options(t(), MDEx.syntax_highlight_options()) :: t()
Updates the pipeline's :syntax_highlight
options.
Examples
iex> pipe = MDEx.Pipe.put_syntax_highlight_options(MDEx.new(), formatter: :html_linked)
iex> MDEx.Pipe.get_option(pipe, :syntax_highlight)[:formatter]
:html_linked
Registers a list of valid options that can be used by steps in the pipeline.
Examples
iex> pipe = MDEx.new()
iex> pipe = MDEx.Pipe.register_options(pipe, [:mermaid_version])
iex> pipe = MDEx.Pipe.put_options(pipe, mermaid_version: "11")
iex> pipe.options[:mermaid_version]
"11"
iex> MDEx.new(rendr: [unsafe: true])
** (ArgumentError) unknown option :rendr. Did you mean :render?
Executes the pipeline steps in order.
This function is usually not called directly,
prefer calling one of the to_*
functions in MDEx
module.
@spec update_nodes(t(), MDEx.Document.selector(), (MDEx.Document.md_node() -> MDEx.Document.md_node())) :: t()
Updates nodes in the document that match selector
.
Updates a value in the pipeline's private storage using a function.