MDEx.Pipe (MDEx v0.5.0)
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(version: "11")
|> MDEx.to_html(document: document)
Writing Plugins
To understand how it works, let's write that Mermaid plugin showed above.
In order to render Mermaid diagrams, we need to inject this <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>
The :version
option will be options to let users load a specific version,
but it will default to the latest version:
MDEx.new() |> MDExMermaid.attach()
Or to customize the version:
MDEx.new() |> MDExMermaid.attach(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(mermaid_version: options[:version])
# 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
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 whenever that plugin is attached to a pipeline,
MDEx will run all functions defined in the attach/1
function.
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.
Halts the pipeline execution.
Halts the pipeline execution with an exception.
Prepends steps to the beginning of the existing pipeline's step list.
Updates the pipeline's :extension
options.
Updates the pipeline's :features
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.
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.
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.
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_features_options(t(), MDEx.features_options()) :: t()
Updates the pipeline's :features
options.
Examples
iex> pipe = MDEx.Pipe.put_features_options(MDEx.new(), sanitize: [add_tags: ["MyComponent"]])
iex> MDEx.Pipe.get_option(pipe, :features)[:sanitize][:add_tags]
["MyComponent"]
@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 (like :document
, :extension
, :parse
, :render
, :features
)
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"
@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
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.