defmodule Surface.Catalogue do
@moduledoc """
A behaviour to provide additional information about the catalogue.
Optional for local catalogues. Usually required if you want to share
your components as a library.
"""
@doc """
Returns a keyword list of config options to be used by the catalogue tool.
Available options:
* `head_css` - CSS related content to be added to the `<head>...</head>` section
of each example or playground.
* `head_js` - JS related content to be added to the `<head>...</head>` section
of each example or playground.
* `example` - A keyword list of options to be applied for all examples
in in the catalogue.
* `playground` - A keyword list of options to be applied for all playgrounds
in in the catalogue.
"""
@callback config :: keyword()
@default_config [
head_css: """
<link phx-track-static rel="stylesheet" href="/assets/app.css"/>
""",
head_js: """
<script defer type="module" src="/assets/app.js"></script>
"""
]
defmacro __using__(_opts) do
quote do
@behaviour Surface.Catalogue
import Surface.Catalogue, only: [load_asset: 2]
end
end
@doc """
Loads a text file as module attribute so you can inject its content directly
in `head_css` or `head_js` config options.
Useful to avoid depending on external css or js code. The path should be relative
to the caller's folder.
Available options:
* `as` - the name of the module attribute to be generated.
"""
defmacro load_asset(file, opts) do
as = Keyword.fetch!(opts, :as)
quote do
path = Path.join(__DIR__, unquote(file))
@external_resource path
Module.put_attribute(__MODULE__, unquote(as), File.read!(path))
end
end
@doc false
def get_metadata(module) do
case Code.fetch_docs(module) do
{:docs_v1, _, _, "text/markdown", docs, %{catalogue: meta}, _} ->
doc = Map.get(docs, "en")
meta |> Map.new() |> Map.put(:doc, doc)
_ ->
nil
end
end
@doc false
def get_config(module) do
meta = get_metadata(module)
user_config = Map.get(meta, :config, [])
catalogue = Keyword.get(user_config, :catalogue)
catalogue_config = get_catalogue_config(catalogue)
{type_config, catalogue_config} = Keyword.split(catalogue_config, [:example, :playground])
@default_config
|> Keyword.merge(catalogue_config)
|> Keyword.merge(type_config[meta.type] || [])
|> Keyword.merge(user_config)
end
@doc false
def fetch_subject!(config, type, caller) do
case Keyword.fetch(config, :subject) do
{:ok, subject} ->
subject
_ ->
message = """
no subject defined for #{inspect(type)}
Hint: You can define the subject using the :subject option. Example:
use #{inspect(type)}, subject: MyApp.MyButton
"""
Surface.IOHelper.compile_error(message, caller.file, caller.line)
end
end
defp get_catalogue_config(nil) do
[]
end
defp get_catalogue_config(catalogue) do
if module_loaded?(catalogue) do
catalogue.config()
else
[]
end
end
defp module_loaded?(module) do
match?({:module, _mod}, Code.ensure_compiled(module))
end
end