defmodule FancyFences do
@moduledoc """
A tiny wrapper around `ExDoc.Markdown.Earmark` that supports fancy fences.
"""
@behaviour ExDoc.Markdown
@impl ExDoc.Markdown
def available?, do: ExDoc.Markdown.Earmark.available?()
@impl ExDoc.Markdown
def to_ast(text, opts) do
to_ast(text, opts, [])
end
defp to_ast(text, opts, applied_processors) do
{_fences_opts, doc_opts} = Keyword.split(opts, [:fences])
text
|> ExDoc.Markdown.Earmark.to_ast(doc_opts)
|> Enum.reduce([], fn block, acc ->
acc ++ maybe_apply_fence_processors(block, opts, applied_processors)
end)
end
defp maybe_apply_fence_processors(
{:blockquote, attrs, content, meta},
opts,
applied_processors
) do
content =
Enum.reduce(content, [], fn block, acc ->
acc ++ maybe_apply_fence_processors(block, opts, applied_processors)
end)
[{:blockquote, attrs, content, meta}]
end
defp maybe_apply_fence_processors(
{:pre, _pre_attrs, [{:code, [class: fence], content, _code_meta}], _pre_meta} = block,
opts,
applied_processors
) do
fence_processors = Keyword.get(opts, :fences, %{})
cond do
# we want to avoid infinite recursion here, just return the block
fence in applied_processors ->
[block]
# if we have defined a custom fence processor apply it (recursively)
Map.has_key?(fence_processors, fence) ->
# a configured fence processor must be an mfa tuple
{module, function, args} = fence_processors[fence]
code = Enum.join(content, "\n")
apply(module, function, [code | args])
|> to_ast(opts, [fence | applied_processors])
# in any other case return the original block
true ->
[block]
end
end
defp maybe_apply_fence_processors(block, _opts, _applied_processors), do: [block]
end