Skip to content

Commit d1a5480

Browse files
authored
feat: add optional config/1 callback to extensions (#104)
This callback can be used for an extension to validate its configuration in a way that can provide better errors for the user refactor: extract common elements from extensions refactor: reduce unnecessary code refactor: pass in configuration to extensions from the build task test: add tests for post and page extensions
1 parent 0677ce6 commit d1a5480

24 files changed

+642
-297
lines changed

.github/workflows/ci.yaml

+1-3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
elixir: [1.15.x, 1.16.x, 1.17.x]
15+
elixir: [1.16.x, 1.17.x]
1616
otp: [25.x, 26.x]
1717

1818
include:
19-
- elixir: 1.15.x
20-
otp: 24.x
2119
- elixir: 1.17.x
2220
otp: 27.x
2321

lib/mix/tasks/tableau.build.ex

+8-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ defmodule Mix.Tasks.Tableau.Build do
2525
|> Stream.map(fn {:ok, mod} -> mod end)
2626
|> Enum.to_list()
2727

28+
{:ok, config} = Tableau.Config.get()
29+
token = Map.put(token, :extensions, %{})
30+
2831
token = mods |> extensions_for(:pre_build) |> run_extensions(token)
2932

3033
graph = Tableau.Graph.insert(token.graph, mods)
@@ -61,9 +64,9 @@ defmodule Mix.Tasks.Tableau.Build do
6164
token
6265
end
6366

64-
defp validate_config(config_mod, raw_config) do
65-
if Code.ensure_loaded?(config_mod) do
66-
config_mod.new(raw_config)
67+
defp validate_config(module, raw_config) do
68+
if function_exported?(module, :config, 1) do
69+
module.config(raw_config)
6770
else
6871
{:ok, raw_config}
6972
end
@@ -81,17 +84,15 @@ defmodule Mix.Tasks.Tableau.Build do
8184
defp run_extensions(extensions, token) do
8285
for module <- extensions, reduce: token do
8386
token ->
84-
config_mod = Module.concat([module, Config])
85-
8687
raw_config =
8788
:tableau |> Application.get_env(module, %{enabled: true}) |> Map.new()
8889

8990
if raw_config[:enabled] do
90-
{:ok, config} = validate_config(config_mod, raw_config)
91+
{:ok, config} = validate_config(module, raw_config)
9192

9293
{:ok, key} = Tableau.Extension.key(module)
9394

94-
token = put_in(token[key], config)
95+
token = put_in(token.extensions[key], %{config: config})
9596

9697
case module.run(token) do
9798
{:ok, token} ->

lib/tableau/config.ex

-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ defmodule Tableau.Config do
2121
Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{})))
2222
end
2323

24-
defp atom do
25-
raw(&is_atom/1, message: "expected an atom")
26-
end
27-
2824
defp keyword(value) do
2925
list(tuple([atom(), value]))
3026
end

lib/tableau/converters/mdex_converter.ex

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ defmodule Tableau.MDExConverter do
22
@moduledoc """
33
Converter to parse markdown content with `MDEx`
44
"""
5-
def convert(_filepath, _front_matter, body, _opts) do
6-
{:ok, config} = Tableau.Config.get()
7-
5+
def convert(_filepath, _front_matter, body, %{config: config}) do
86
MDEx.to_html!(body, config.markdown[:mdex])
97
end
108
end

lib/tableau/extension.ex

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ defmodule Tableau.Extension do
6767
"""
6868
@callback run(token()) :: {:ok, token()} | :error
6969

70+
@doc """
71+
Optional callback to validate the config for an extension. Useful for
72+
providing more useful error messages for misconfigurations.
73+
"""
74+
@callback config(Keyword.t() | map()) :: {:ok, map()} | {:error, any()}
75+
76+
@optional_callbacks [
77+
config: 1
78+
]
79+
7080
defmacro __using__(opts) do
7181
opts = Keyword.validate!(opts, [:key, :enabled, :type, :priority])
7282

lib/tableau/extensions/common.ex

+63
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,78 @@
11
defmodule Tableau.Extension.Common do
22
@moduledoc false
33

4+
@doc """
5+
Expand content paths from a wildcard.
6+
"""
47
def paths(wildcard) do
58
wildcard |> Path.wildcard() |> Enum.sort()
69
end
710

11+
@doc """
12+
Build content entries from a list of paths.
13+
14+
Content should contain YAML frontmatter, with the body from the file passed to the callback.
15+
"""
816
def entries(paths, callback) do
917
for path <- paths do
1018
{front_matter, body} = Tableau.YamlFrontMatter.parse!(File.read!(path), atoms: true)
1119
"." <> ext = Path.extname(path)
1220
callback.(%{path: path, ext: String.to_atom(ext), front_matter: front_matter, pre_convert_body: body})
1321
end
1422
end
23+
24+
@doc """
25+
Builds a permalink from a template and frontmatter and/or config.
26+
27+
Frontmatter keys are substituted for colon prefixed template keys of the same name.
28+
29+
If no permalink template is provided, the permalink will be derived from the file path.
30+
31+
If the frontamtter contains a `:date` key, it is broken down into `:day`, `:month`, and `:year`
32+
components and those can be used in the permalink template.
33+
"""
34+
def build_permalink(%{permalink: permalink} = front_matter, _config) do
35+
permalink
36+
|> transform_permalink(front_matter)
37+
|> then(&Map.put(front_matter, :permalink, &1))
38+
end
39+
40+
def build_permalink(front_matter, %{permalink: permalink}) when not is_nil(permalink) do
41+
permalink
42+
|> transform_permalink(front_matter)
43+
|> then(&Map.put(front_matter, :permalink, &1))
44+
end
45+
46+
def build_permalink(%{file: filename} = front_matter, config) do
47+
filename
48+
|> Path.rootname()
49+
|> String.replace_prefix(config.dir, "")
50+
|> transform_permalink(front_matter)
51+
|> then(&Map.put(front_matter, :permalink, &1))
52+
end
53+
54+
defp transform_permalink(path, front_matter) do
55+
vars =
56+
front_matter
57+
|> Map.new(fn {k, v} -> {":#{k}", v} end)
58+
|> then(fn vars ->
59+
if is_struct(front_matter[:date], DateTime) do
60+
Map.merge(vars, %{
61+
":day" => front_matter.date.day |> to_string() |> String.pad_leading(2, "0"),
62+
":month" => front_matter.date.month |> to_string() |> String.pad_leading(2, "0"),
63+
":year" => front_matter.date.year
64+
})
65+
else
66+
vars
67+
end
68+
end)
69+
70+
path
71+
|> String.replace(Map.keys(vars), &to_string(Map.fetch!(vars, &1)))
72+
|> String.replace(" ", "-")
73+
|> String.replace("_", "-")
74+
|> String.replace(~r/[^[:alnum:]\/\-.]/, "")
75+
|> String.downcase()
76+
|> URI.encode()
77+
end
1578
end

lib/tableau/extensions/data_extension.ex

+13-1
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,21 @@ defmodule Tableau.DataExtension do
5757
"""
5858
use Tableau.Extension, key: :data, type: :pre_build, priority: 200
5959

60+
import Schematic
61+
62+
def config(config) do
63+
unify(
64+
map(%{
65+
optional(:enabled, true) => bool(),
66+
optional(:dir, "_data") => str()
67+
}),
68+
config
69+
)
70+
end
71+
6072
def run(token) do
6173
data =
62-
for file <- Path.wildcard(Path.join(token.data.dir, "**/*.{yml,yaml,exs}")), into: %{} do
74+
for file <- Path.wildcard(Path.join(token.extensions.data.config.dir, "**/*.{yml,yaml,exs}")), into: %{} do
6375
case Path.extname(file) do
6476
".exs" ->
6577
key = Path.basename(file, ".exs")

lib/tableau/extensions/data_extension/config.ex

-19
This file was deleted.

lib/tableau/extensions/page_extension.ex

+24-6
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,24 @@ defmodule Tableau.PageExtension do
6363

6464
use Tableau.Extension, key: :pages, type: :pre_build, priority: 100
6565

66+
import Schematic
67+
6668
alias Tableau.Extension.Common
67-
alias Tableau.PageExtension.Page
6869

69-
@config Map.new(Application.compile_env(:tableau, Tableau.PageExtension, %{}))
70+
def config(input) do
71+
unify(
72+
map(%{
73+
optional(:enabled, true) => bool(),
74+
optional(:dir, "_pages") => str(),
75+
optional(:permalink) => str(),
76+
optional(:layout) => str()
77+
}),
78+
input
79+
)
80+
end
7081

7182
def run(token) do
72-
{:ok, config} = Tableau.PageExtension.Config.new(@config)
73-
74-
{:ok, %{converters: converters}} = Tableau.Config.get()
83+
%{site: %{config: %{converters: converters}}, extensions: %{pages: %{config: config}}} = token
7584

7685
exts = Enum.map_join(converters, ",", fn {ext, _} -> to_string(ext) end)
7786

@@ -82,7 +91,7 @@ defmodule Tableau.PageExtension do
8291
|> Common.entries(fn %{path: path, ext: ext, front_matter: front_matter, pre_convert_body: pre_convert_body} ->
8392
renderer = fn assigns -> converters[ext].convert(path, front_matter, pre_convert_body, assigns) end
8493

85-
{Page.build(path, front_matter, pre_convert_body), renderer}
94+
{build(path, front_matter, pre_convert_body, config), renderer}
8695
end)
8796

8897
graph =
@@ -98,4 +107,13 @@ defmodule Tableau.PageExtension do
98107
|> Map.put(:pages, pages |> Enum.unzip() |> elem(0))
99108
|> Map.put(:graph, graph)}
100109
end
110+
111+
defp build(filename, front_matter, body, pages_config) do
112+
front_matter
113+
|> Map.put(:__tableau_page_extension__, true)
114+
|> Map.put(:body, body)
115+
|> Map.put(:file, filename)
116+
|> Map.put(:layout, Module.concat([front_matter.layout || pages_config.layout]))
117+
|> Common.build_permalink(pages_config)
118+
end
101119
end

lib/tableau/extensions/page_extension/config.ex

-22
This file was deleted.

lib/tableau/extensions/page_extension/page.ex

-45
This file was deleted.

0 commit comments

Comments
 (0)