Skip to content

Commit a2fb6b0

Browse files
committed
feat: RSS extension
1 parent 583b593 commit a2fb6b0

File tree

5 files changed

+182
-42
lines changed

5 files changed

+182
-42
lines changed

lib/mix/tasks/tableau.build.ex

+56-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Mix.Tasks.Tableau.Build do
1111
@impl Mix.Task
1212
def run(argv) do
1313
{:ok, config} = Tableau.Config.new(@config)
14-
site = %{config: config}
14+
token = %{site: %{config: config}}
1515
Mix.Task.run("app.start", ["--preload-modules"])
1616

1717
{opts, _argv} = OptionParser.parse!(argv, strict: [out: :string])
@@ -20,11 +20,27 @@ defmodule Mix.Tasks.Tableau.Build do
2020

2121
mods = :code.all_available()
2222

23-
for module <- pre_build_extensions(mods) do
24-
with :error <- module.run(%{site: site}) do
25-
Logger.error("#{inspect(module)} failed to run")
23+
token =
24+
for module <- pre_build_extensions(mods), reduce: token do
25+
token ->
26+
config_mod = Module.concat([module, Config])
27+
28+
{:ok, config} =
29+
Application.get_env(:tableau, module, %{}) |> Map.new() |> config_mod.new()
30+
31+
{:ok, key} = Tableau.Extension.key(module)
32+
33+
token = put_in(token[key], config)
34+
35+
case module.run(token) do
36+
{:ok, token} ->
37+
token
38+
39+
:error ->
40+
Logger.error("#{inspect(module)} failed to run")
41+
token
42+
end
2643
end
27-
end
2844

2945
mods = :code.all_available()
3046
graph = Tableau.Graph.new(mods)
@@ -35,12 +51,12 @@ defmodule Mix.Tasks.Tableau.Build do
3551
{mod, Map.new(mod.__tableau_opts__() || [])}
3652
end
3753

38-
{mods, pages} = Enum.unzip(pages)
54+
{page_mods, pages} = Enum.unzip(pages)
3955

40-
site = Map.put(site, :pages, pages)
56+
token = put_in(token.site[:pages], pages)
4157

42-
for mod <- mods do
43-
content = Tableau.Document.render(graph, mod, %{site: site})
58+
for mod <- page_mods do
59+
content = Tableau.Document.render(graph, mod, token)
4460
permalink = mod.__tableau_permalink__()
4561
dir = Path.join(out, permalink)
4662

@@ -52,6 +68,27 @@ defmodule Mix.Tasks.Tableau.Build do
5268
if File.exists?(config.include_dir) do
5369
File.cp_r!(config.include_dir, out)
5470
end
71+
72+
for module <- post_write_extensions(mods), reduce: token do
73+
token ->
74+
config_mod = Module.concat([module, Config])
75+
76+
{:ok, config} =
77+
Application.get_env(:tableau, module, %{}) |> Map.new() |> config_mod.new()
78+
79+
{:ok, key} = Tableau.Extension.key(module)
80+
81+
token = put_in(token[key], config)
82+
83+
case module.run(token) do
84+
{:ok, token} ->
85+
token
86+
87+
:error ->
88+
Logger.error("#{inspect(module)} failed to run")
89+
token
90+
end
91+
end
5592
end
5693

5794
defp pre_build_extensions(modules) do
@@ -63,4 +100,14 @@ defmodule Mix.Tasks.Tableau.Build do
63100
end
64101
|> Enum.sort_by(& &1.__tableau_extension_priority__())
65102
end
103+
104+
defp post_write_extensions(modules) do
105+
for {mod, _, _} <- modules,
106+
mod = Module.concat([to_string(mod)]),
107+
match?({:ok, :post_write}, Tableau.Extension.type(mod)),
108+
Tableau.Extension.enabled?(mod) do
109+
mod
110+
end
111+
|> Enum.sort_by(& &1.__tableau_extension_priority__())
112+
end
66113
end

lib/tableau/config.ex

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ defmodule Tableau.Config do
44
55
* `:include_dir` - Directory that is just copied to the output directory. Defaults to `extra`.
66
* `:timezone` - Timezone to use when parsing date times. Defaults to `Etc/UTC`.
7+
* `:url` - The URL of your website.
78
"""
89

910
import Schematic
1011

11-
defstruct include_dir: "extra",
12-
timezone: "Etc/UTC"
12+
defstruct [:url, include_dir: "extra", timezone: "Etc/UTC"]
1313

1414
def new(config) do
1515
unify(schematic(), config)
@@ -20,7 +20,8 @@ defmodule Tableau.Config do
2020
__MODULE__,
2121
%{
2222
optional(:include_dir) => str(),
23-
optional(:timezone) => str()
23+
optional(:timezone) => str(),
24+
url: str()
2425
},
2526
convert: false
2627
)

lib/tableau/extension.ex

+27-10
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,27 @@ defmodule Tableau.Extension do
44
55
An extension can be used to generate other kinds of content.
66
7+
## Options
8+
9+
* `:key` - The key in which the extensions configuration and data is loaded.
10+
* `:type` - The type of extension. See below for a description.
11+
* `:priority` - An integer used for ordering extensions of the same type.
12+
* `:enabled` - Whether or not to enable the extension. Defaults to true, and can be configured differently based on the extension.
13+
714
## Types
815
916
There are currently the following extension types:
1017
1118
- `:pre_build` - executed before tableau builds your site and writes anything to disk.
19+
- `:post_write` - executed after tableau builds your site and writes everthing to disk.
1220
13-
## Priority
14-
15-
Extensions can be assigned a numeric priority for used with sorting.
21+
## Example
1622
1723
```elixir
1824
defmodule MySite.PostsExtension do
19-
use Tableau.Extension, type: :pre_build, priority: 300
25+
use Tableau.Extension, key: :posts, type: :pre_build, priority: 300
2026
21-
def run(_site) do
27+
def run(token) do
2228
posts = Path.wildcard("_posts/**/*.md")
2329
2430
for post <- post do
@@ -27,27 +33,28 @@ defmodule Tableau.Extension do
2733
|> then(&File.write(Path.join(Path.rootname(post), "index.html"), &1))
2834
end
2935
30-
:ok
36+
{:ok, token}
3137
end
3238
end
3339
```
3440
'''
3541

36-
@typep extension_type :: :pre_build
42+
@typep extension_type :: :pre_build | :post_write
3743

3844
@doc """
3945
The extension entry point.
4046
41-
The function is passed the a set of default assigns.
47+
The function is passed a token and can return a new token with new data loaded into it.
4248
"""
43-
@callback run(map()) :: :ok | :error
49+
@callback run(map()) :: {:ok, map()} | :error
4450

4551
defmacro __using__(opts) do
46-
opts = Keyword.validate!(opts, [:enabled, :type, :priority])
52+
opts = Keyword.validate!(opts, [:key, :enabled, :type, :priority])
4753

4854
prelude =
4955
quote do
5056
def __tableau_extension_type__, do: unquote(opts)[:type]
57+
def __tableau_extension_key__, do: unquote(opts)[:key]
5158
def __tableau_extension_enabled__, do: unquote(opts)[:enabled] || true
5259
def __tableau_extension_priority__, do: unquote(opts)[:priority] || 0
5360
end
@@ -70,6 +77,16 @@ defmodule Tableau.Extension do
7077
end
7178
end
7279

80+
@doc false
81+
@spec key(module()) :: extension_type()
82+
def key(module) do
83+
if function_exported?(module, :__tableau_extension_key__, 0) do
84+
{:ok, module.__tableau_extension_key__()}
85+
else
86+
:error
87+
end
88+
end
89+
7390
@doc false
7491
@spec enabled?(module()) :: boolean()
7592
def enabled?(module) do

lib/tableau/extensions/post_extension.ex

+24-20
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,14 @@ defmodule Tableau.PostExtension do
7878
---
7979
```
8080
"""
81-
{:ok, config} = Tableau.PostExtension.Config.new(Map.new(Application.compile_env(:tableau, :posts, %{})))
81+
{:ok, config} =
82+
Tableau.PostExtension.Config.new(Map.new(Application.compile_env(:tableau, :posts, %{})))
8283

8384
@config config
8485

85-
use Tableau.Extension, enabled: @config.enabled, type: :pre_build, priority: 100
86+
use Tableau.Extension, key: :posts, enabled: @config.enabled, type: :pre_build, priority: 100
8687

87-
def run(_site) do
88+
def run(token) do
8889
Module.create(
8990
Tableau.PostExtension.Posts,
9091
quote do
@@ -110,22 +111,25 @@ defmodule Tableau.PostExtension do
110111
Macro.Env.location(__ENV__)
111112
)
112113

113-
for post <- apply(Tableau.PostExtension.Posts, :posts, []) do
114-
{:module, _module, _binary, _term} =
115-
Module.create(
116-
Module.concat([post.id]),
117-
quote do
118-
@external_resource unquote(post.file)
119-
use Tableau.Page, unquote(Macro.escape(Keyword.new(post)))
120-
121-
def template(_assigns) do
122-
unquote(post.body)
123-
end
124-
end,
125-
Macro.Env.location(__ENV__)
126-
)
127-
end
128-
129-
:ok
114+
posts =
115+
for post <- apply(Tableau.PostExtension.Posts, :posts, []) do
116+
{:module, _module, _binary, _term} =
117+
Module.create(
118+
Module.concat([post.id]),
119+
quote do
120+
@external_resource unquote(post.file)
121+
use Tableau.Page, unquote(Macro.escape(Keyword.new(post)))
122+
123+
def template(_assigns) do
124+
unquote(post.body)
125+
end
126+
end,
127+
Macro.Env.location(__ENV__)
128+
)
129+
130+
post
131+
end
132+
133+
{:ok, Map.put(token, :posts, posts)}
130134
end
131135
end
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
defmodule Tableau.RSSExtension.Config do
2+
import Schematic
3+
4+
defstruct [:title, :description, language: "en-us", enabled: true]
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(:language) => str(),
14+
title: str(),
15+
description: str()
16+
},
17+
convert: false
18+
)
19+
end
20+
end
21+
22+
defmodule Tableau.RSSExtension do
23+
use Tableau.Extension, key: :rss, type: :post_write, priority: 200
24+
25+
def run(%{site: %{config: %{url: url}}, posts: posts, rss: rss} = token) do
26+
prelude =
27+
"""
28+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
29+
<channel>
30+
<atom:link href="#{url}/feed.xml" rel="self" type="application/rss+xml" />
31+
<title>#{rss.title}</title>
32+
<link>#{url}</link>
33+
<description>#{rss.description}</description>
34+
<language>#{rss.language}</language>
35+
<generator>Tableau v#{version()}</generator>
36+
"""
37+
38+
# html
39+
items =
40+
for post <- posts, into: "" do
41+
"""
42+
<item>
43+
<title>#{post.title}</title>
44+
<link>https://#{Path.join(url, post.permalink)}</link>
45+
<pubDate>#{Calendar.strftime(post.date, "%a, %d %b %Y %X %Z")}</pubDate>
46+
<guid>http://#{Path.join(url, post.permalink)}</guid>
47+
<description><![CDATA[ #{post.body} ]]></description>
48+
</item>
49+
"""
50+
end
51+
52+
# html
53+
postlude =
54+
"""
55+
</channel>
56+
</rss>
57+
"""
58+
59+
File.mkdir_p!("_site")
60+
File.write!("_site/feed.xml", prelude <> items <> postlude)
61+
62+
{:ok, token}
63+
end
64+
65+
defp version() do
66+
case :application.get_key(:tableau, :vsn) do
67+
{:ok, version} -> to_string(version)
68+
_ -> "dev"
69+
end
70+
end
71+
end

0 commit comments

Comments
 (0)