Current section
Files
Jump to
Current section
Files
lib/catalog.ex
defmodule Catalog do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@doc false
defmacro __using__(_) do
quote do
@before_compile unquote(__MODULE__)
Module.register_attribute(__MODULE__, :catalog_from_with_md5, accumulate: true)
import Catalog
end
end
defmacro __before_compile__(_) do
quote do
def __mix_recompile__? do
Enum.any?(@catalog_from_with_md5, fn {from, md5} ->
from |> Path.wildcard() |> Enum.sort() |> :erlang.md5() != md5
end)
end
end
end
@doc """
Processes all markdown files in `from` and stores them in the
module attribute `as`.
## Example
markdown(:posts, "posts/**.md")
markdown(:articles, "articles/**.md", build: Article)
## Options
* `:build` - the name of the module that will build each entry
* `:highlighters` - which code highlighters to use. `Catalog`
uses `Makeup` for syntax highlighting and you will need to add its
`.css` classes. You can generate the CSS classes by calling
`Makeup.stylesheet(:vim_style, "makeup")` inside `iex -S mix`.
You can replace `:vim_style` by any style of your choice
[defined here](https://hexdocs.pm/makeup/Makeup.Styles.HTML.StyleMap.html).
* `:earmark_options` - an [`%Earmark.Options{}`](https://hexdocs.pm/earmark/Earmark.Options.html) struct.
"""
defmacro markdown(as, from, opts \\ []),
do: macro(&Catalog.__extract_markdown__/2, as, from, opts)
@doc """
Processes all json files in `from` and stores them in the
module attribute `as`.
## Example
json(:countries, "countries/**.json")
## Options
* `:build` - the name of the module that will build each entry.
*
"""
defmacro json(as, from, opts \\ []),
do: macro(&Catalog.__extract_json__/2, as, from, opts)
defmacro file(as, from, opts \\ []),
do: macro(&Catalog.__extract_file__/2, as, from, opts)
defmacro yaml(as, from, opts \\ []),
do: macro(&Catalog.__extract_yaml__/2, as, from, opts)
defmacro toml(as, from, opts \\ []),
do: macro(&Catalog.__extract_toml__/2, as, from, opts)
defmacro csv(as, from, opts \\ []),
do: macro(&Catalog.__extract_csv__/2, as, from, opts)
defp macro(fun, as, from, opts) do
quote bind_quoted: [fun: fun, as: as, from: from, opts: opts] do
{paths, entries} = fun.(from, opts)
if [from] == paths do
[entry] = entries
Module.put_attribute(__MODULE__, as, entry)
else
Module.put_attribute(__MODULE__, as, entries)
end
for path <- paths do
@external_resource Path.relative_to_cwd(path)
end
@catalog_from_with_md5 {from, :erlang.md5(paths)}
end
end
def __extract_markdown__(from, opts) do
earmark_opts = Keyword.get(opts, :earmark_options, %Earmark.Options{})
highlighters = Keyword.get(opts, :highlighters, [])
decoder = &(&1 |> Earmark.as_html!(earmark_opts) |> highlight(highlighters))
extract(decoder, from, opts)
end
defp highlight(html, []) do
html
end
defp highlight(html, _) do
Catalog.Highlighter.highlight(html)
end
def __extract_json__(from, opts) do
jason_options = Keyword.get(opts, :jason_options, [])
parser = &Jason.decode!(&1, jason_options)
extract(parser, from, opts)
end
def __extract_file__(from, opts) do
extract(& &1, from, opts)
end
def __extract_yaml__(from, opts) do
parser = &YamlElixir.read_from_string!(&1, atoms: true)
extract(parser, from, opts)
end
def __extract_toml__(from, opts) do
parser = &Toml.decode!(&1, atoms: true)
extract(parser, from, opts)
end
def __extract_csv__(from, opts) do
parser = &(String.split(&1) |> CSV.decode!(headers: true) |> Enum.to_list())
extract(parser, from, opts)
end
defp extract(decoder, from, opts) do
builder = Keyword.get(opts, :build)
paths = from |> Path.wildcard() |> Enum.sort()
entries =
for path <- paths do
{attrs, body} = Catalog.FrontMatter.process!(File.read!(path), path)
body = decoder.(body)
if builder do
builder.build(path, attrs, body)
else
%{
path: path,
attrs: attrs,
body: body
}
end
end
{paths, entries}
end
end