defmodule NimblePublisher do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
{from, paths} = NimblePublisher.__extract__(__MODULE__, opts)
for path <- paths do
@external_resource Path.relative_to_cwd(path)
end
def __mix_recompile__? do
unquote(from) |> Path.wildcard() |> Enum.sort() |> :erlang.md5() !=
unquote(:erlang.md5(paths))
end
# TODO: Remove me once we require Elixir v1.11+.
def __phoenix_recompile__?, do: __mix_recompile__?()
end
end
@doc false
def __extract__(module, opts) do
builder = Keyword.fetch!(opts, :build)
from = Keyword.fetch!(opts, :from)
as = Keyword.fetch!(opts, :as)
parser_module = Keyword.get(opts, :parser)
for highlighter <- Keyword.get(opts, :highlighters, []) do
Application.ensure_all_started(highlighter)
end
paths = from |> Path.wildcard() |> Enum.sort()
entries =
Enum.flat_map(paths, fn path ->
parsed_contents = parse_contents!(path, File.read!(path), parser_module)
build_entry(builder, path, parsed_contents, opts)
end)
Module.put_attribute(module, as, entries)
{from, paths}
end
@doc """
Highlights all code blocks in an already generated HTML document.
It uses Makeup and expects the existing highlighters applications to
be already started.
Options:
* `:regex` - the regex used to find code blocks in the HTML document. The regex
should have two capture groups: the first one should be the language name
and the second should contain the code to be highlighted. The default
regex to match with generated HTML documents is:
~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/
"""
defdelegate highlight(html, options \\ []), to: NimblePublisher.Highlighter
defp build_entry(builder, path, {_attrs, _body} = parsed_contents, opts) do
build_entry(builder, path, [parsed_contents], opts)
end
defp build_entry(builder, path, parsed_contents, opts) when is_list(parsed_contents) do
converter_module = Keyword.get(opts, :html_converter)
Enum.map(parsed_contents, fn {attrs, body} ->
body =
case converter_module do
nil -> path |> Path.extname() |> String.downcase() |> convert_body(body, opts)
module -> module.convert(path, body, attrs, opts)
end
builder.build(path, attrs, body)
end)
end
defp parse_contents!(path, contents, nil) do
case parse_contents(path, contents) do
{:ok, attrs, body} ->
{attrs, body}
{:error, message} ->
raise """
#{message}
Each entry must have a map with attributes, followed by --- and a body. For example:
%{
title: "Hello World"
}
---
Hello world!
"""
end
end
defp parse_contents!(path, contents, parser_module) do
parser_module.parse(path, contents)
end
defp parse_contents(path, contents) do
case :binary.split(contents, ["\n---\n", "\r\n---\r\n"]) do
[_] ->
{:error, "could not find separator --- in #{inspect(path)}"}
[code, body] ->
case Code.eval_string(code, []) do
{%{} = attrs, _} ->
{:ok, attrs, body}
{other, _} ->
{:error,
"expected attributes for #{inspect(path)} to return a map, got: #{inspect(other)}"}
end
end
end
defp convert_body(extname, body, opts) when extname in [".md", ".markdown", ".livemd"] do
earmark_opts = Keyword.get(opts, :earmark_options, %Earmark.Options{})
html = Earmark.as_html!(body, earmark_opts)
case Keyword.get(opts, :highlighters, []) do
[] -> html
[_ | _] -> highlight(html)
end
end
defp convert_body(_extname, body, _opts) do
body
end
end