Skip to content

Commit 16d1c14

Browse files
authored
feat: post extension (#22)
1 parent d766c22 commit 16d1c14

File tree

8 files changed

+223
-10
lines changed

8 files changed

+223
-10
lines changed

config/config.exs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import Config
22

3+
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
4+
35
import_config "#{Mix.env()}.exs"

lib/mix/tasks/tableau.build.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Mix.Tasks.Tableau.Build do
66
@moduledoc "Task to build the tableau site"
77
@shortdoc "Builds the site"
88

9-
@config Application.compile_env(:tableau, :config, %{})
9+
@config Application.compile_env(:tableau, :config, %{}) |> Map.new()
1010

1111
@impl Mix.Task
1212
def run(argv) do
@@ -57,7 +57,8 @@ defmodule Mix.Tasks.Tableau.Build do
5757
defp pre_build_extensions(modules) do
5858
for {mod, _, _} <- modules,
5959
mod = Module.concat([to_string(mod)]),
60-
match?({:ok, :pre_build}, Tableau.Extension.type(mod)) do
60+
match?({:ok, :pre_build}, Tableau.Extension.type(mod)),
61+
Tableau.Extension.enabled?(mod) do
6162
mod
6263
end
6364
|> Enum.sort_by(& &1.__tableau_extension_priority__())

lib/tableau/config.ex

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
defmodule Tableau.Config do
2-
@moduledoc false
2+
@moduledoc """
3+
Project configuration.
4+
5+
* `:include_dir` - Directory that is just copied to the output directory. Defaults to `extra`.
6+
* `:timezone` - Timezone to use when parsing date times. Defaults to `Etc/UTC`.
7+
"""
38

49
import Schematic
510

6-
defstruct include_dir: "extra"
11+
defstruct include_dir: "extra",
12+
timezone: "Etc/UTC"
713

814
def new(config) do
915
unify(schematic(), config)
1016
end
1117

1218
defp schematic do
13-
schema(__MODULE__, %{
14-
optional(:include_dir) => str()
15-
})
19+
schema(
20+
__MODULE__,
21+
%{
22+
optional(:include_dir) => str(),
23+
optional(:timezone) => str()
24+
},
25+
convert: false
26+
)
1627
end
1728
end

lib/tableau/extension.ex

+12-1
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ defmodule Tableau.Extension do
4343
@callback run(map()) :: :ok | :error
4444

4545
defmacro __using__(opts) do
46-
opts = Keyword.validate!(opts, [:type, :priority])
46+
opts = Keyword.validate!(opts, [:enabled, :type, :priority])
4747

4848
prelude =
4949
quote do
5050
def __tableau_extension_type__, do: unquote(opts)[:type]
51+
def __tableau_extension_enabled__, do: unquote(opts)[:enabled] || true
5152
def __tableau_extension_priority__, do: unquote(opts)[:priority] || 0
5253
end
5354

@@ -68,4 +69,14 @@ defmodule Tableau.Extension do
6869
:error
6970
end
7071
end
72+
73+
@doc false
74+
@spec enabled?(module()) :: boolean()
75+
def enabled?(module) do
76+
if function_exported?(module, :__tableau_extension_enabled__, 0) do
77+
module.__tableau_extension_enabled__()
78+
else
79+
false
80+
end
81+
end
7182
end
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
defmodule Tableau.PostExtension.Config do
2+
import Schematic
3+
4+
defstruct enabled: true, dir: "_posts", future: false
5+
6+
def new(input), do: unify(schematic(), input)
7+
8+
def schematic do
9+
schema(
10+
__MODULE__,
11+
%{
12+
optional(:enabled) => bool(),
13+
optional(:dir) => str(),
14+
optional(:future) => bool()
15+
},
16+
convert: false
17+
)
18+
end
19+
end
20+
21+
defmodule Tableau.PostExtension.Posts.Post do
22+
{:ok, config} = Tableau.Config.new(Map.new(Application.compile_env(:tableau, :config, %{})))
23+
24+
@config config
25+
26+
def build(filename, attrs, body) do
27+
attrs
28+
|> Map.put(:body, body)
29+
|> Map.put(:file, filename)
30+
|> Map.put(:layout, Module.concat([attrs.layout]))
31+
|> Map.put(
32+
:date,
33+
DateTime.from_naive!(
34+
Code.eval_string(attrs.date) |> elem(0),
35+
@config.timezone
36+
)
37+
)
38+
|> Map.put(
39+
:permalink,
40+
attrs.permalink
41+
|> String.replace(":title", attrs.title)
42+
|> String.replace(" ", "-")
43+
|> String.replace(~r/[^[:alnum:]\/\-]/, "")
44+
|> String.downcase()
45+
)
46+
end
47+
48+
def parse(_file_path, content) do
49+
Tableau.YamlFrontMatter.parse!(content, atoms: true)
50+
end
51+
end
52+
53+
defmodule Tableau.PostExtension do
54+
@moduledoc """
55+
Markdown files (with YAML frontmatter) in the configured posts directory will be automatically compiled into Tableau pages.
56+
57+
Certain frontmatter keys are required and all keys are passed as options to the `Tableau.Page`.
58+
59+
## Options
60+
61+
Frontmatter is compiled with `yaml_elixir` and supports atom keys by prefixing a key with a colon `:title:`. Certain required keys must be presented as atoms, but all user provided keys may be string or atom keys.
62+
63+
* `:id` - An Elixir module to be used when compiling the backing `Tableau.Page`
64+
* `:title` - The title of the post
65+
* `:permalink` - The permalink of the post. `:title` will be replaced with the posts title and non alphanumeric characters removed
66+
* `:date` - An Elixir `NaiveDateTime`, often presented as a `sigil_N`
67+
* `:layout` - A Tableau layout module.
68+
69+
## Example
70+
71+
```markdown
72+
---
73+
:id: "Update.Volume3"
74+
:title: "The elixir-tools Update Vol. 3"
75+
:permalink: "/news/:title"
76+
:date: "~N[2023-09-19 01:00:00]"
77+
:layout: "ElixirTools.PostLayout"
78+
---
79+
```
80+
"""
81+
{:ok, config} =
82+
Tableau.PostExtension.Config.new(Map.new(Application.compile_env(:tableau, :posts, %{})))
83+
84+
@config config
85+
86+
use Tableau.Extension, enabled: @config.enabled, type: :pre_build, priority: 100
87+
88+
def run(_site) do
89+
Module.create(
90+
Tableau.PostExtension.Posts,
91+
quote do
92+
use NimblePublisher,
93+
build: __MODULE__.Post,
94+
from: "#{unquote(@config.dir)}/*.md",
95+
as: :posts,
96+
highlighters: [:makeup_elixir],
97+
parser: Tableau.PostExtension.Posts.Post
98+
99+
def posts(_opts \\ []) do
100+
@posts
101+
|> Enum.sort_by(& &1.date, {:desc, DateTime})
102+
|> then(fn posts ->
103+
if unquote(@config.future) do
104+
posts
105+
else
106+
Enum.reject(posts, &(DateTime.compare(&1.date, DateTime.utc_now()) == :gt))
107+
end
108+
end)
109+
end
110+
end,
111+
Macro.Env.location(__ENV__)
112+
)
113+
114+
for post <- apply(Tableau.PostExtension.Posts, :posts, []) do
115+
{:module, _module, _binary, _term} =
116+
Module.create(
117+
Module.concat([post.id]),
118+
quote do
119+
@external_resource unquote(post.file)
120+
use Tableau.Page, unquote(Macro.escape(Keyword.new(post)))
121+
122+
def template(_assigns) do
123+
unquote(post.body)
124+
end
125+
end,
126+
Macro.Env.location(__ENV__)
127+
)
128+
end
129+
130+
:ok
131+
end
132+
end

lib/tableau/yaml_front_matter.ex

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright (c) 2017 Sebastian De Deyne [email protected]
2+
# Vendored from github.com/sebastiandedeyne/yaml_front_matter
3+
4+
defmodule Tableau.YamlFrontMatter.Error do
5+
defexception message: "Error parsing yaml front matter"
6+
end
7+
8+
defmodule Tableau.YamlFrontMatter do
9+
def parse(string, opts \\ []) do
10+
string
11+
|> split_string
12+
|> process_parts(opts)
13+
end
14+
15+
def parse!(string, opts \\ []) do
16+
case parse(string, opts) do
17+
{:ok, matter, body} -> {matter, body}
18+
{:error, _} -> raise Tableau.YamlFrontMatter.Error
19+
end
20+
end
21+
22+
defp split_string(string) do
23+
split_pattern = ~r/[\s\r\n]---[\s\r\n]/s
24+
25+
string
26+
|> (&String.trim_leading(&1)).()
27+
|> (&("\n" <> &1)).()
28+
|> (&Regex.split(split_pattern, &1, parts: 3)).()
29+
end
30+
31+
defp process_parts([_, yaml, body], opts) do
32+
case parse_yaml(yaml, opts) do
33+
{:ok, yaml} -> {:ok, yaml, body}
34+
{:error, error} -> {:error, error}
35+
end
36+
end
37+
38+
defp process_parts(_, _), do: {:error, :invalid_front_matter}
39+
40+
defp parse_yaml(yaml, opts) do
41+
case YamlElixir.read_from_string(yaml, opts) do
42+
{:ok, parsed} -> {:ok, parsed}
43+
error -> error
44+
end
45+
end
46+
end

mix.exs

+6-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ defmodule Tableau.MixProject do
3737
{:libgraph, "~> 0.16.0"},
3838
{:plug_cowboy, "~> 2.0"},
3939
{:plug_static_index_html, "~> 1.0"},
40-
{:schematic, "~> 0.3"},
40+
{:schematic, "~> 0.3.1"},
41+
{:nimble_publisher, "~> 1.0"},
42+
{:yaml_elixir, "~> 2.9"},
43+
{:makeup_elixir, ">= 0.0.0"},
44+
{:tz, "~> 0.26.2"},
45+
4146
# {:yaml_front_matter, "~> 1.0"},
4247
# {:jason, "~> 1.4"},
4348
# {:req, "~> 0.3", only: :test},

mix.lock

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
33
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
44
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
5+
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
56
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
67
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
78
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
@@ -12,11 +13,15 @@
1213
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
1314
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
1415
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
16+
"nimble_publisher": {:hex, :nimble_publisher, "1.0.0", "e7c6b1dc0505a87dcb12e47427bce9f659eba36ab6f0afb8267d21f2ac701871", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "959ab4d9ff41a33a7269082ccf7e9fb76b840d850c872733dd4201591b6ea6f4"},
1517
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
1618
"plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"},
1719
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
1820
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
1921
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
20-
"schematic": {:hex, :schematic, "0.3.0", "936df3904d0d17de543530fbe8e7ea8f7e3b7f0a5da0fedfa85440a2ca77bdfc", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bd6dafb8792f77e4b321ba529572bd97792a3a6b7cec75d736b7ecc4031dff9e"},
22+
"schematic": {:hex, :schematic, "0.3.1", "be633c1472959dc0ace22dd0e1f1445b099991fec39f6d6e5273d35ebd217ac4", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "52c419b5c405286e2d0369b9ca472b00b850c59a8b0bdf0dd69172ad4418d5ea"},
2123
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
24+
"tz": {:hex, :tz, "0.26.2", "a40e4bb223344c6fc7b74dda25df1f26b88a30db23fa6e55de843bd79148ccdb", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "224b0618dd1e032778a094040bc710ef9aff6e2fa8fffc2716299486f27b9e68"},
25+
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
26+
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
2227
}

0 commit comments

Comments
 (0)