Skip to content

Commit a8a819e

Browse files
authored
feat(extensions)!: pre_render phase (#138)
The pre_render phase is ran after the pre_build phase but before the graph is traversed and every page is rendered to HTML. Extensions are also now able to mount to multiple phases in the build cycle. This allows extensions like the Page and Post extension to separate the construction of their pages/posts into separate phases. Doing so means that other extensions are able to modify the data structures in the token before they are rendered. For example, you can implement a Table of Contents extension that loops through each post, creates a TOC structure from markdown headings, then put that structure into the post data in the token. This data is then in the token and available to use in layouts. BREAKING CHANGE: Extensions no longer implement a `run/1` callback. They can implement a callback named after the build phase: `pre_build/1`, `pre_render/1`, etc fix!: correctly slugify permalinks BREAKING CHANGE: This switches to using the [slugify](https://hex.pm/packages/slugify) package for creating slugs. This leads to better slugs, but also means your existing page's permalinks might change, which might break backlinks. When upgrading, please check for permalink changes so that you can set up the proper redirects. You can easily do this by building your site before the update, running `find _site/ | sort > before.txt`, then updating, deleting and rebuilding your site, then run `find _site/ | sort > after.txt` and then comparing the files to look for differences.
1 parent fa8636b commit a8a819e

24 files changed

+169
-1371
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
include:
1919
- elixir: 1.17.x
2020
otp: 27.x
21+
- elixir: 1.18.x
22+
otp: 28.x
2123

2224
steps:
2325
- uses: actions/checkout@v4

.github/workflows/lint-commit.yaml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Lint Commit
22
on:
3-
pull_request:
3+
pull_request_target:
44
types:
55
- opened
66
- reopened
@@ -14,9 +14,8 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v4
17-
- name: Install Deps
18-
run: yarn install
19-
- name: Lint PR Title
20-
run: echo "${PULL_REQUEST_TITLE}" | yarn commitlint
17+
- uses: amannn/action-semantic-pull-request@v5
2118
env:
22-
PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
with:
21+
subjectPattern: ^(?![A-Z]).+$

commitlint.config.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/mix/tasks/tableau.build.ex

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ defmodule Mix.Tasks.Tableau.Build do
2828
{:ok, config} = Tableau.Config.get()
2929
token = Map.put(token, :extensions, %{})
3030

31-
token = mods |> extensions_for(:pre_build) |> run_extensions(token)
31+
token = mods |> extensions_for(:pre_build) |> run_extensions(:pre_build, token)
3232

33+
token = mods |> extensions_for(:pre_render) |> run_extensions(:pre_render, token)
3334
graph = Tableau.Graph.insert(token.graph, mods)
3435

3536
File.mkdir_p!(out)
3637

3738
pages =
38-
for mod <- Graph.vertices(graph), {:ok, :page} == Nodable.type(mod) do
39-
{mod, Map.new(Nodable.opts(mod) || [])}
39+
for page <- Graph.vertices(graph), {:ok, :page} == Nodable.type(page) do
40+
{page, Map.new(Nodable.opts(page) || [])}
4041
end
4142

4243
token = put_in(token.site[:pages], Enum.map(pages, fn {_mod, page} -> page end))
@@ -62,7 +63,7 @@ defmodule Mix.Tasks.Tableau.Build do
6263

6364
token = put_in(token.site[:pages], pages)
6465

65-
token = mods |> extensions_for(:pre_write) |> run_extensions(token)
66+
token = mods |> extensions_for(:pre_write) |> run_extensions(:pre_write, token)
6667

6768
for %{body: body, permalink: permalink} <- pages do
6869
file_path = build_file_path(out, permalink)
@@ -77,7 +78,7 @@ defmodule Mix.Tasks.Tableau.Build do
7778
File.cp_r!(config.include_dir, out)
7879
end
7980

80-
token = mods |> extensions_for(:post_write) |> run_extensions(token)
81+
token = mods |> extensions_for(:post_write) |> run_extensions(:post_write, token)
8182

8283
token
8384
end
@@ -101,14 +102,14 @@ defmodule Mix.Tasks.Tableau.Build do
101102

102103
defp extensions_for(modules, type) do
103104
extensions =
104-
for mod <- modules, Code.ensure_loaded?(mod), {:ok, type} == Tableau.Extension.type(mod) do
105+
for mod <- modules, Code.ensure_loaded?(mod), function_exported?(mod, type, 1) do
105106
mod
106107
end
107108

108109
Enum.sort_by(extensions, & &1.__tableau_extension_priority__())
109110
end
110111

111-
defp run_extensions(extensions, token) do
112+
defp run_extensions(extensions, type, token) do
112113
for module <- extensions, reduce: token do
113114
token ->
114115
raw_config =
@@ -124,7 +125,7 @@ defmodule Mix.Tasks.Tableau.Build do
124125

125126
token = put_in(token.extensions[key], %{config: config})
126127

127-
case module.run(token) do
128+
case apply(module, type, [token]) do
128129
{:ok, token} ->
129130
token
130131

lib/tableau/converter.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Tableau.Converter do
66
@doc """
77
Converts content into HTML.
88
9-
Is given the file path, the content of the files (sans front matter), the front matter, and a list of options.
9+
Is given the file path, the frontmatter, the content of the files (sans front matter), and a list of options.
1010
"""
1111
@callback convert(filepath :: String.t(), front_matter :: map(), content :: String.t(), opts :: Keyword.t()) ::
1212
String.t()

lib/tableau/extension.ex

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ defmodule Tableau.Extension do
77
## Options
88
99
* `:key` - The key in which the extensions configuration and data is loaded.
10-
* `:type` - The type of extension. See below for a description.
1110
* `:priority` - An integer used for ordering extensions of the same type.
1211
* `:enabled` - Whether or not to enable the extension. Defaults to true, and can be configured differently based on the extension.
1312
14-
## Types
13+
## Callbacks
1514
16-
There are currently the following extension types:
15+
There are currently the following extension callbacks:
1716
1817
- `:pre_build` - executed before tableau builds your site and writes anything to disk.
1918
- `:pre_write` - executed after tableau builds your site but before it writes anything to disk.
@@ -33,9 +32,10 @@ defmodule Tableau.Extension do
3332
3433
```elixir
3534
defmodule MySite.PostsExtension do
36-
use Tableau.Extension, key: :posts, type: :pre_build, priority: 300
35+
use Tableau.Extension, key: :posts, priority: 300
3736
38-
def run(token) do
37+
@impl Tableau.Extension
38+
def pre_build(token) do
3939
posts =
4040
for post <- Path.wildcard("_posts/**/*.md") do
4141
%Tableau.Page{
@@ -57,15 +57,35 @@ defmodule Tableau.Extension do
5757
```
5858
'''
5959

60-
@typep extension_type :: :pre_build | :post_write
6160
@type token :: map()
6261

6362
@doc """
64-
The extension entry point.
63+
Called in the pre_build phase.
6564
6665
The function is passed a token and can return a new token with new data loaded into it.
6766
"""
68-
@callback run(token()) :: {:ok, token()} | :error
67+
@callback pre_build(token()) :: {:ok, token()} | :error
68+
69+
@doc """
70+
Called in the pre_render phase.
71+
72+
The function is passed a token and can return a new token with new data loaded into it.
73+
"""
74+
@callback pre_render(token()) :: {:ok, token()} | :error
75+
76+
@doc """
77+
Called in the pre_write phase.
78+
79+
The function is passed a token and can return a new token with new data loaded into it.
80+
"""
81+
@callback pre_write(token()) :: {:ok, token()} | :error
82+
83+
@doc """
84+
Called in the post_write phase.
85+
86+
The function is passed a token and can return a new token with new data loaded into it.
87+
"""
88+
@callback post_write(token()) :: {:ok, token()} | :error
6989

7090
@doc """
7191
Optional callback to validate the config for an extension. Useful for
@@ -74,15 +94,18 @@ defmodule Tableau.Extension do
7494
@callback config(Keyword.t() | map()) :: {:ok, map()} | {:error, any()}
7595

7696
@optional_callbacks [
77-
config: 1
97+
config: 1,
98+
pre_build: 1,
99+
pre_render: 1,
100+
pre_write: 1,
101+
post_write: 1
78102
]
79103

80104
defmacro __using__(opts) do
81-
opts = Keyword.validate!(opts, [:key, :enabled, :type, :priority])
105+
opts = Keyword.validate!(opts, [:key, :enabled, :priority])
82106

83107
prelude =
84108
quote do
85-
def __tableau_extension_type__, do: unquote(opts)[:type]
86109
def __tableau_extension_key__, do: unquote(opts)[:key]
87110
def __tableau_extension_enabled__, do: Keyword.get(unquote(opts), :enabled, true)
88111
def __tableau_extension_priority__, do: unquote(opts)[:priority] || 0
@@ -97,17 +120,7 @@ defmodule Tableau.Extension do
97120
end
98121

99122
@doc false
100-
@spec type(module()) :: extension_type()
101-
def type(module) do
102-
if function_exported?(module, :__tableau_extension_type__, 0) do
103-
{:ok, module.__tableau_extension_type__()}
104-
else
105-
:error
106-
end
107-
end
108-
109-
@doc false
110-
@spec key(module()) :: extension_type()
123+
@spec key(module()) :: atom()
111124
def key(module) do
112125
if function_exported?(module, :__tableau_extension_key__, 0) do
113126
{:ok, module.__tableau_extension_key__()}

lib/tableau/extensions/common.ex

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,8 @@ defmodule Tableau.Extension.Common do
7272
end
7373
end)
7474

75-
path
76-
|> String.replace(Map.keys(vars), &to_string(Map.fetch!(vars, &1)))
77-
|> String.replace(" ", "-")
78-
|> String.replace("_", "-")
79-
|> String.replace(~r/[^[:alnum:]\/\-.]/, "")
80-
|> String.downcase()
81-
|> URI.encode()
75+
String.replace(path, Map.keys(vars), fn key ->
76+
vars |> Map.fetch!(key) |> to_string() |> Slug.slugify(ignore: ["."])
77+
end)
8278
end
8379
end

lib/tableau/extensions/data_extension.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ defmodule Tableau.DataExtension do
5555
5656
<!-- tabs-close -->
5757
"""
58-
use Tableau.Extension, key: :data, type: :pre_build, priority: 200
58+
use Tableau.Extension, key: :data, priority: 200
5959

6060
import Schematic
6161

62+
@impl Tableau.Extension
6263
def config(config) do
6364
unify(
6465
map(%{
@@ -69,7 +70,8 @@ defmodule Tableau.DataExtension do
6970
)
7071
end
7172

72-
def run(token) do
73+
@impl Tableau.Extension
74+
def pre_build(token) do
7375
data =
7476
for file <- Path.wildcard(Path.join(token.extensions.data.config.dir, "**/*.{yml,yaml,exs}")), into: %{} do
7577
case Path.extname(file) do

lib/tableau/extensions/page_extension.ex

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@ defmodule Tableau.PageExtension do
6565
As noted above, a converter can be overridden on a specific page, using the frontmatter `:converter` key.
6666
"""
6767

68-
use Tableau.Extension, key: :pages, type: :pre_build, priority: 100
68+
use Tableau.Extension, key: :pages, priority: 100
6969

7070
import Schematic
7171

7272
alias Tableau.Extension.Common
7373

74+
@impl Tableau.Extension
7475
def config(input) do
7576
unify(
7677
map(%{
@@ -83,7 +84,8 @@ defmodule Tableau.PageExtension do
8384
)
8485
end
8586

86-
def run(token) do
87+
@impl Tableau.Extension
88+
def pre_build(token) do
8789
%{site: %{config: %{converters: converters}}, extensions: %{pages: %{config: config}}} = token
8890

8991
exts = Enum.map_join(converters, ",", fn {ext, _} -> to_string(ext) end)
@@ -96,40 +98,47 @@ defmodule Tableau.PageExtension do
9698
|> Path.join("**/*.{#{exts}}")
9799
|> Common.paths()
98100
end)
99-
|> Common.entries(fn %{path: path, front_matter: front_matter, pre_convert_body: body, ext: ext} ->
100-
{
101-
build(path, front_matter, body, config),
102-
fn assigns ->
103-
converter =
104-
case front_matter[:converter] do
105-
nil -> converters[ext]
106-
converter -> Module.concat([converter])
107-
end
108-
109-
converter.convert(path, front_matter, body, assigns)
110-
end
111-
}
101+
|> Common.entries(fn entry ->
102+
%{
103+
path: path,
104+
front_matter: front_matter,
105+
pre_convert_body: body,
106+
ext: ext
107+
} = entry
108+
109+
build(path, front_matter, body, config, fn assigns ->
110+
converter =
111+
case front_matter[:converter] do
112+
nil -> converters[ext]
113+
converter -> Module.concat([converter])
114+
end
115+
116+
converter.convert(path, front_matter, body, assigns)
117+
end)
112118
end)
113119

120+
{:ok, Map.put(token, :pages, pages)}
121+
end
122+
123+
@impl Tableau.Extension
124+
def pre_render(token) do
114125
graph =
115126
Tableau.Graph.insert(
116127
token.graph,
117-
Enum.map(pages, fn {page, renderer} ->
118-
%Tableau.Page{parent: page.layout, permalink: page.permalink, template: renderer, opts: page}
128+
Enum.map(token.pages, fn page ->
129+
%Tableau.Page{parent: page.layout, permalink: page.permalink, template: page.renderer, opts: page}
119130
end)
120131
)
121132

122-
{:ok,
123-
token
124-
|> Map.put(:pages, pages |> Enum.unzip() |> elem(0))
125-
|> Map.put(:graph, graph)}
133+
{:ok, Map.put(token, :graph, graph)}
126134
end
127135

128-
defp build(filename, front_matter, body, pages_config) do
136+
defp build(filename, front_matter, body, pages_config, renderer) do
129137
front_matter
130138
|> Map.put(:__tableau_page_extension__, true)
131139
|> Map.put(:body, body)
132140
|> Map.put(:file, filename)
141+
|> Map.put(:renderer, renderer)
133142
|> Map.put(:layout, Module.concat([front_matter[:layout] || pages_config.layout]))
134143
|> Common.build_permalink(pages_config)
135144
end

lib/tableau/extensions/post_extension.ex

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ defmodule Tableau.PostExtension do
7171
As noted above, a converter can be overridden on a specific page, using the frontmatter `:converter` key.
7272
"""
7373

74-
use Tableau.Extension, key: :posts, type: :pre_build, priority: 100
74+
use Tableau.Extension, key: :posts, priority: 100
7575

7676
import Schematic
7777

@@ -100,7 +100,7 @@ defmodule Tableau.PostExtension do
100100
end
101101

102102
@impl Tableau.Extension
103-
def run(token) do
103+
def pre_build(token) do
104104
%{site: %{config: %{converters: converters}}, extensions: %{posts: %{config: config}}} = token
105105
exts = Enum.map_join(converters, ",", fn {ext, _} -> to_string(ext) end)
106106

@@ -132,18 +132,20 @@ defmodule Tableau.PostExtension do
132132
end
133133
end)
134134

135+
{:ok, Map.put(token, :posts, posts)}
136+
end
137+
138+
@impl Tableau.Extension
139+
def pre_render(token) do
135140
graph =
136141
Tableau.Graph.insert(
137142
token.graph,
138-
Enum.map(posts, fn post ->
143+
Enum.map(token.posts, fn post ->
139144
%Tableau.Page{parent: post.layout, permalink: post.permalink, template: post.renderer, opts: post}
140145
end)
141146
)
142147

143-
{:ok,
144-
token
145-
|> Map.put(:posts, posts)
146-
|> Map.put(:graph, graph)}
148+
{:ok, Map.put(token, :graph, graph)}
147149
end
148150

149151
defp build(filename, attrs, body, posts_config, renderer) do

0 commit comments

Comments
 (0)