Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/tableau.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Tableau do
* `:converters` - mapping of file extensions to converter module. Defaults to `[md: Tableau.MDExConverter]`
* `:markdown` - keyword
* `:mdex` - keyword - Options to pass to `MDEx.to_html/2`
* `:slug` - keyword - Options to pass to `Slug.slugify/2`

### Example

Expand All @@ -25,6 +26,9 @@ defmodule Tableau do
md: Tableau.MDExConverter,
dj: MySite.DjotConverter
],
slug: [
lowercase: false
],
markdown: [
mdex: [
extension: [
Expand Down
8 changes: 8 additions & 0 deletions lib/tableau/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Tableau.Config do
out_dir: "_site",
timezone: "Etc/UTC",
reload_log: false,
slug: [],
converters: [md: Tableau.MDExConverter],
markdown: [mdex: []]
]
Expand All @@ -32,6 +33,13 @@ defmodule Tableau.Config do
optional(:reload_log) => bool(),
optional(:converters) => keyword(values: atom()),
optional(:markdown) => keyword(values: list()),
optional(:slug) =>
keyword(%{
optional(:separator) => oneof([str(), int()]),
optional(:lowercase) => bool(),
optional(:truncate) => int(),
optional(:ignore) => oneof([str(), list(oneof([str(), int()]))])
}),
optional(:base_path) => str(),
url: str()
},
Expand Down
21 changes: 21 additions & 0 deletions lib/tableau/extensions/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ defmodule Tableau.Extension.Common do
wildcard |> Path.wildcard() |> Enum.sort()
end

@doc """
Transform strings from any language into slugs using `Slug.slugify/1`.

Returns the original string if the slug cannot be generated.
"""
def slugify(string, overrides \\ [])

def slugify(string, %{site: %{config: config}}) do
Slug.slugify(string, config.slug) || string
end

def slugify(string, config) when is_map(config) do
slugify(string)
end

def slugify(string, overrides) do
{:ok, config} = Tableau.Config.get()

Slug.slugify(string, Keyword.merge(config.slug, overrides)) || string
end

@doc """
Build content entries from a list of paths.

Expand Down
37 changes: 33 additions & 4 deletions lib/tableau/extensions/tag_extension.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,29 @@ defmodule Tableau.TagExtension do

The `@page` assign passed to the `layout` provided in the configuration is described by `t:page/0`.

Unless a tag has a `slug` defined in the plugin `tags` map, tag names will be converted to slugs using `Slug.slugify/2` with options provided in Tableau configuration. These slugs will be used to build the permalink.

## Configuration

- `:enabled` - boolean - Extension is active or not.
* `:layout` - module - The `Tableau.Layout` implementation to use.
* `:permalink` - string - The permalink prefix to use for the tag page, will be joined with the tag name.
* `:tags` - map - A map of tag display values to slug options. Supported options:
* `:slug` - string - The slug to use for the displayed tag


### Configuring Manual Tag Slugs

```elixir
config :tableau, Tableau.TagExtension,
enabled: true,
tags: %{
"C++" => [slug: "c-plus-plus"]
}
```

With this configuration, the tag `C++` will be have a permalink slug of `c-plus-plus`,
`Eixir` will be `elixir`, and `Bun.sh` will be `bun-sh`.


## Layout and Page
Expand Down Expand Up @@ -79,6 +97,8 @@ defmodule Tableau.TagExtension do

import Schematic

alias Tableau.Extension.Common

@type page :: %{
title: String.t(),
tag: String.t(),
Expand All @@ -89,7 +109,8 @@ defmodule Tableau.TagExtension do
@type tag :: %{
title: String.t(),
tag: String.t(),
permalink: String.t()
permalink: String.t(),
slug: String.t()
}

@type tags :: %{
Expand All @@ -102,6 +123,7 @@ defmodule Tableau.TagExtension do
oneof([
map(%{enabled: false}),
map(%{
optional(:tags, %{}) => map(keys: str(), values: keyword(%{slug: str()})),
enabled: true,
layout: atom(),
permalink: str()
Expand All @@ -115,14 +137,21 @@ defmodule Tableau.TagExtension do
def pre_build(token) do
posts = token.posts
permalink = token.extensions.tag.config.permalink
defs = token.extensions.tag.config.tags

tags =
for post <- posts, tag <- post |> Map.get(:tags, []) |> Enum.uniq(), reduce: Map.new() do
acc ->
permalink = Path.join(permalink, tag)
slug = get_in(defs, [tag, :slug]) || Common.slugify(tag, token)
permalink = Path.join(permalink, slug)

tag = %{title: tag, permalink: permalink, tag: tag}
Map.update(acc, tag, [post], &[post | &1])
tag = %{title: tag, permalink: permalink, tag: tag, slug: slug}
Map.update(acc, slug, %{tag: tag, posts: [post]}, &%{tag: tag, posts: [post | &1.posts]})
end

tags =
for {_slug, %{tag: tag, posts: posts}} <- tags, into: %{} do
{tag, posts}
end

{:ok, Map.put(token, :tags, tags)}
Expand Down
39 changes: 34 additions & 5 deletions test/tableau/extensions/tag_extension_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,57 @@ defmodule Tableau.TagExtensionTest do
alias Tableau.TagExtension
alias Tableau.TagExtensionTest.Layout

describe "config" do
test "handles tag slugs correctly" do
config =
%{
enabled: true,
layout: Layout,
permalink: "/tags",
tags: %{"C++" => [slug: "c-plus-plus"]}
}

assert {:ok, ^config} = TagExtension.config(config)
end
end

describe "run" do
test "creates tag pages and tags key" do
posts = [
# dedups tags
post(1, tags: ["post", "post"]),
# post can have multiple tags, includes posts from same tag
post(2, tags: ["til", "post"]),
# tags will be converted to slugs for linking
post(2, tags: ["til", "post", "Today I Learned", "C++"]),
post(3, tags: ["recipe"])
]

token = %{
posts: posts,
graph: Graph.new(),
extensions: %{tag: %{config: %{layout: Layout, permalink: "/tags"}}}
extensions: %{tag: %{config: %{layout: Layout, permalink: "/tags", tags: %{"C++" => [slug: "c-plus-plus"]}}}}
}

assert {:ok, token} = TagExtension.pre_build(token)
assert {:ok, token} = TagExtension.pre_render(token)

assert %{
tags: %{
%{tag: "post", title: "post", permalink: "/tags/post"} => [%{title: "Post 2"}, %{title: "Post 1"}],
%{tag: "recipe", title: "recipe", permalink: "/tags/recipe"} => [%{title: "Post 3"}],
%{tag: "til", title: "til", permalink: "/tags/til"} => [%{title: "Post 2"}]
%{tag: "post", title: "post", permalink: "/tags/post", slug: "post"} => [
%{title: "Post 2"},
%{title: "Post 1"}
],
%{tag: "recipe", title: "recipe", permalink: "/tags/recipe", slug: "recipe"} => [%{title: "Post 3"}],
%{tag: "til", title: "til", permalink: "/tags/til", slug: "til"} => [%{title: "Post 2"}],
%{
tag: "Today I Learned",
title: "Today I Learned",
permalink: "/tags/today-i-learned",
slug: "today-i-learned"
} => [
%{title: "Post 2"}
],
%{tag: "C++", title: "C++", permalink: "/tags/c-plus-plus", slug: "c-plus-plus"} => [%{title: "Post 2"}]
},
graph: graph
} = token
Expand All @@ -39,6 +66,8 @@ defmodule Tableau.TagExtensionTest do
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/post"))
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/recipe"))
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/til"))
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/today-i-learned"))
assert Enum.any?(vertices, &page_with_permalink?(&1, "/tags/c-plus-plus"))

assert Layout in vertices
end
Expand Down