Skip to content

Commit 2b38b43

Browse files
committed
feat: arbitrary frontmatter keys in permalink
1 parent 0dfcfbf commit 2b38b43

File tree

5 files changed

+163
-110
lines changed

5 files changed

+163
-110
lines changed

lib/tableau/extensions/post_extension.ex

+27-110
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,3 @@
1-
defmodule Tableau.PostExtension.Config do
2-
@moduledoc """
3-
Configuration for PostExtension.
4-
5-
## Options
6-
7-
- `:enabled` - boolean - Extension is active or not.
8-
- `:dir` - string - Directory to scan for markdown files. Defaults to `_posts`
9-
- `:future` - boolean - Show posts that have dates later than the current timestamp, or time at which the site is generated.
10-
- `:permalink` - string - Default output path for posts. Accepts `:title` as a replacement keyword, replaced with the post's provided title. If a post has a `:permalink` provided, that will override this value _for that post_.
11-
- `:layout` - string - Elixir module providing page layout for posts. Default is nil
12-
"""
13-
14-
import Schematic
15-
16-
defstruct enabled: true, dir: "_posts", future: false, permalink: nil, layout: nil
17-
18-
def new(input), do: unify(schematic(), input)
19-
20-
def schematic do
21-
schema(
22-
__MODULE__,
23-
%{
24-
optional(:enabled) => bool(),
25-
optional(:dir) => str(),
26-
optional(:future) => bool(),
27-
optional(:permalink) => str(),
28-
optional(:layout) => str()
29-
},
30-
convert: false
31-
)
32-
end
33-
end
34-
35-
defmodule Tableau.PostExtension.Posts.HTMLConverter do
36-
def convert(_filepath, body, _attrs, _opts) do
37-
{:ok, config} = Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{})))
38-
39-
body |> MDEx.to_html(config.markdown[:mdex])
40-
end
41-
end
42-
43-
defmodule Tableau.PostExtension.Posts.Post do
44-
def build(filename, attrs, body) do
45-
{:ok, config} = Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{})))
46-
47-
{:ok, post_config} =
48-
Tableau.PostExtension.Config.new(
49-
Map.new(Application.get_env(:tableau, Tableau.PostExtension, %{}))
50-
)
51-
52-
attrs
53-
|> Map.put(:body, body)
54-
|> Map.put(:file, filename)
55-
|> Map.put(:layout, Module.concat([attrs.layout || post_config.layout]))
56-
|> Map.put(
57-
:date,
58-
DateTime.from_naive!(
59-
Code.eval_string(attrs.date) |> elem(0),
60-
config.timezone
61-
)
62-
)
63-
|> build_permalink(post_config)
64-
end
65-
66-
def parse(_file_path, content) do
67-
Tableau.YamlFrontMatter.parse!(content, atoms: true)
68-
end
69-
70-
defp build_permalink(%{permalink: permalink} = attrs, _config) do
71-
permalink
72-
|> transform_permalink(attrs)
73-
|> then(&Map.put(attrs, :permalink, &1))
74-
end
75-
76-
defp build_permalink(%{title: _title} = attrs, %{permalink: permalink})
77-
when not is_nil(permalink) do
78-
permalink
79-
|> transform_permalink(attrs)
80-
|> then(&Map.put(attrs, :permalink, &1))
81-
end
82-
83-
defp build_permalink(%{file: filename} = attrs, _) do
84-
filename
85-
|> Path.rootname()
86-
|> transform_permalink(attrs)
87-
|> then(&Map.put(attrs, :permalink, &1))
88-
end
89-
90-
defp transform_permalink(path, attrs) do
91-
path
92-
|> String.replace(":title", attrs.title)
93-
|> String.replace(" ", "-")
94-
|> String.replace(~r/[^[:alnum:]\/\-]/, "")
95-
|> String.downcase()
96-
end
97-
end
98-
991
defmodule Tableau.PostExtension do
1002
@moduledoc """
1013
Markdown files (with YAML frontmatter) in the configured posts directory will be automatically compiled into Tableau pages.
@@ -104,35 +6,50 @@ defmodule Tableau.PostExtension do
1046
1057
## Options
1068
107-
Frontmatter is compiled with `yaml_elixir` and supports atom keys by prefixing a key with a colon `:title:`. Keys are all converted to atoms.
9+
Frontmatter is compiled with `yaml_elixir` and all keys are converted to atoms.
10810
109-
* `:id` - An Elixir module to be used when compiling the backing `Tableau.Page`
11+
* `:id` - An Elixir module to be used when compiling the backing `Tableau.Page`. A leaking implementation detail that should be fixed eventually.
11012
* `:title` - The title of the post
11113
* `:permalink` - The permalink of the post. `:title` will be replaced with the posts title and non alphanumeric characters removed. Optional.
112-
* `:date` - An Elixir `NaiveDateTime`, often presented as a `sigil_N`
113-
* `:layout` - A Tableau layout module.
14+
* `:date` - A string representation of an Elixir `NaiveDateTime`, often presented as a `sigil_N`. This will be converted to your configured timezone.
15+
* `:layout` - A string representation of a Tableau layout module.
11416
11517
## Example
11618
117-
```markdown
118-
---
19+
```yaml
11920
id: "Update.Volume3"
12021
title: "The elixir-tools Update Vol. 3"
12122
permalink: "/news/:title"
12223
date: "~N[2023-09-19 01:00:00]"
12324
layout: "ElixirTools.PostLayout"
124-
---
12525
```
12626
127-
## URL generation
27+
## Permalink
12828
129-
If a `:permalink` is specified in the front matter, whatever is there _will_ be the post's permalink.
29+
The permalink is a string with colon prefixed template variables.
13030
131-
If a global `:permalink` is set, it's rules will be used. See `Tableau.PostExtension.Config` for details.
31+
These variables will be swapped with the corresponding YAML Frontmatter key, with the result being piped through `to_string/1`.
13232
133-
If permalink is set in either location, the file's name and path will be used
33+
In addition, there are `:year`, `:month`, and `:day` template variables.
13434
135-
In all cases, permalinks are stripped of non-alphanumeric characters.
35+
## Configuration
36+
37+
- `:enabled` - boolean - Extension is active or not.
38+
- `:dir` - string - Directory to scan for markdown files. Defaults to `_posts`
39+
- `:future` - boolean - Show posts that have dates later than the current timestamp, or time at which the site is generated.
40+
- `:permalink` - string - Default output path for posts. Accepts `:title` as a replacement keyword, replaced with the post's provided title. If a post has a `:permalink` provided, that will override this value _for that post_.
41+
- `:layout` - string - Elixir module providing page layout for posts. Default is nil
42+
43+
### Example
44+
45+
```elixir
46+
config :tableau, Tableau.PostExtension,
47+
enabled: true,
48+
dir: "_articles",
49+
future: true,
50+
permalink: "/articles/:year/:month/:day/:title",
51+
layout: "MyApp.PostLayout"
52+
```
13653
"""
13754

13855
{:ok, config} =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Tableau.PostExtension.Config do
2+
@moduledoc false
3+
4+
import Schematic
5+
6+
defstruct enabled: true, dir: "_posts", future: false, permalink: nil, layout: nil
7+
8+
def new(input), do: unify(schematic(), input)
9+
10+
def schematic do
11+
schema(
12+
__MODULE__,
13+
%{
14+
optional(:enabled) => bool(),
15+
optional(:dir) => str(),
16+
optional(:future) => bool(),
17+
optional(:permalink) => str(),
18+
optional(:layout) => str()
19+
},
20+
convert: false
21+
)
22+
end
23+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule Tableau.PostExtension.Posts.HTMLConverter do
2+
@moduledoc false
3+
def convert(_filepath, body, _attrs, _opts) do
4+
{:ok, config} = Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{})))
5+
6+
body |> MDEx.to_html(config.markdown[:mdex])
7+
end
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule Tableau.PostExtension.Posts.Post do
2+
@moduledoc false
3+
def build(filename, attrs, body) do
4+
{:ok, config} = Tableau.Config.new(Map.new(Application.get_env(:tableau, :config, %{})))
5+
6+
{:ok, post_config} =
7+
Tableau.PostExtension.Config.new(
8+
Map.new(Application.get_env(:tableau, Tableau.PostExtension, %{}))
9+
)
10+
11+
attrs
12+
|> Map.put(:body, body)
13+
|> Map.put(:file, filename)
14+
|> Map.put(:layout, Module.concat([attrs.layout || post_config.layout]))
15+
|> Map.put(
16+
:date,
17+
DateTime.from_naive!(
18+
Code.eval_string(attrs.date) |> elem(0),
19+
config.timezone
20+
)
21+
)
22+
|> build_permalink(post_config)
23+
end
24+
25+
def parse(_file_path, content) do
26+
Tableau.YamlFrontMatter.parse!(content, atoms: true)
27+
end
28+
29+
defp build_permalink(%{permalink: permalink} = attrs, _config) do
30+
permalink
31+
|> transform_permalink(attrs)
32+
|> then(&Map.put(attrs, :permalink, &1))
33+
end
34+
35+
defp build_permalink(attrs, %{permalink: permalink}) when not is_nil(permalink) do
36+
permalink
37+
|> transform_permalink(attrs)
38+
|> then(&Map.put(attrs, :permalink, &1))
39+
end
40+
41+
defp build_permalink(%{file: filename} = attrs, _) do
42+
filename
43+
|> Path.rootname()
44+
|> transform_permalink(attrs)
45+
|> then(&Map.put(attrs, :permalink, &1))
46+
end
47+
48+
defp transform_permalink(path, attrs) do
49+
vars =
50+
attrs
51+
|> Map.new(fn {k, v} -> {":#{k}", v} end)
52+
|> Map.merge(%{
53+
":day" => attrs.date.day,
54+
":month" => attrs.date.month,
55+
":year" => attrs.date.year
56+
})
57+
58+
path
59+
|> String.replace(Map.keys(vars), &to_string(Map.fetch!(vars, &1)))
60+
|> String.replace(" ", "-")
61+
|> String.replace(~r/[^[:alnum:]\/\-]/, "")
62+
|> String.downcase()
63+
end
64+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule Tableau.PostExtension.Posts.PostTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Tableau.PostExtension.Posts.Post
5+
6+
describe "build/3" do
7+
test "substitutes arbitrary front matter into permalink" do
8+
actual =
9+
Post.build(
10+
"some/file/name.md",
11+
%{
12+
title: "foo man chu",
13+
type: "articles",
14+
permalink: "/blog/:type/:title",
15+
layout: Some.Layout,
16+
date: inspect(DateTime.utc_now())
17+
},
18+
"hi"
19+
)
20+
21+
assert %{permalink: "/blog/articles/foo-man-chu"} = actual
22+
end
23+
24+
test "substitutes date pieces into permalink" do
25+
actual =
26+
Post.build(
27+
"some/file/name.md",
28+
%{
29+
title: "foo man chu",
30+
type: "articles",
31+
permalink: "/:year/:month/:day/:title",
32+
layout: Some.Layout,
33+
date: "~N[2023-10-31 00:01:00]"
34+
},
35+
"hi"
36+
)
37+
38+
assert %{permalink: "/2023/10/31/foo-man-chu"} = actual
39+
end
40+
end
41+
end

0 commit comments

Comments
 (0)