264 lines
6.8 KiB
Elixir
264 lines
6.8 KiB
Elixir
defmodule Lain do
|
|
@moduledoc """
|
|
Documentation for `Lain`.
|
|
"""
|
|
|
|
use Phoenix.Component
|
|
|
|
require Logger
|
|
|
|
import Phoenix.LiveViewTest, only: [rendered_to_string: 1]
|
|
|
|
alias Lain.Frontmatter
|
|
alias Lain.Markdown
|
|
|
|
@base_caller __ENV__
|
|
|
|
def read(path) do
|
|
path
|
|
|> File.read!()
|
|
|> Frontmatter.front_matter_split()
|
|
|> then(fn {frontmatter_text, body_text} ->
|
|
{Frontmatter.make_frontmatter(path, frontmatter_text), body_text}
|
|
end)
|
|
|> then(fn {frontmatter, body_text} ->
|
|
body = Markdown.render(body_text)
|
|
|
|
Map.merge(frontmatter, %{"body" => body, "path" => path})
|
|
end)
|
|
end
|
|
|
|
def validate(%{"slug" => nil, "path" => path}) do
|
|
{:error, "slug missing from #{path}"}
|
|
end
|
|
|
|
def validate(%{"slug" => "", "path" => path}) do
|
|
{:error, "slug missing from #{path}"}
|
|
end
|
|
|
|
def validate(other), do: {:ok, other}
|
|
|
|
def clean(base_path) do
|
|
base_path
|
|
|> Path.join("**")
|
|
|> Path.wildcard()
|
|
|> Enum.each(fn path ->
|
|
if File.dir?(path) do
|
|
if !File.exists?(Path.join(path, ".keep")) do
|
|
File.exists?(path) && File.rm_rf!(path)
|
|
end
|
|
else
|
|
File.exists?(path) && File.rm!(path)
|
|
end
|
|
end)
|
|
end
|
|
|
|
def run(path) do
|
|
TreeSitter.install_and_run(["-V"])
|
|
|
|
posts =
|
|
path
|
|
|> Path.join("**/*.md")
|
|
|> Path.wildcard()
|
|
|> Enum.map(&read/1)
|
|
|> Enum.map(&validate/1)
|
|
|> Enum.filter(fn
|
|
{:error, error} ->
|
|
Logger.warning(error)
|
|
|
|
false
|
|
|
|
{:ok, _other} ->
|
|
true
|
|
end)
|
|
|> Enum.map(&elem(&1, 1))
|
|
|> Enum.sort_by(&Map.get(&1, "date"), :desc)
|
|
|> Enum.map(&atomize_keys/1)
|
|
|
|
make_feed(posts, path)
|
|
make_sitemap(posts, path)
|
|
index = make_index(posts)
|
|
|
|
Lain.LinkLog.run(path)
|
|
Lain.Static.run(path)
|
|
|
|
Enum.map([index | posts], &write_page(&1, path))
|
|
end
|
|
|
|
def atomize_keys(page) do
|
|
page
|
|
|> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)
|
|
|> Enum.into(%{})
|
|
end
|
|
|
|
def make_index(posts) do
|
|
assigns = %{posts: posts}
|
|
|
|
body =
|
|
~H"""
|
|
<%= for post <- @posts do %>
|
|
<div :if={!post.hidden} style="margin-bottom: 1rem">
|
|
<span class="text-yellow align-top"><%= post.date %></span>
|
|
<a class="text-ellipsis overflow-hidden inline-block" style="max-width: 40ch" href={"/#{post.slug}/"}><%= post.title %></a>
|
|
</div>
|
|
<% end %>
|
|
"""
|
|
|> rendered_to_string()
|
|
|
|
%{
|
|
title: "Blog",
|
|
slug: "blog",
|
|
date: "2023-09-23",
|
|
body: body
|
|
}
|
|
end
|
|
|
|
def make_feed(posts, path) do
|
|
posts = posts |> Enum.reject(& &1.hidden) |> Enum.take(10)
|
|
base = Application.get_env(:lain, :base, "/")
|
|
|
|
assigns = %{base: base, posts: posts}
|
|
|
|
body =
|
|
EEx.eval_string(
|
|
"""
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<feed version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
<title>pre.hn</title>
|
|
<subtitle>Robert Prehn's personal blog.</subtitle>
|
|
<link rel="alternative" type="text/html">https://pre.hn</link>
|
|
<link rel="self" type="application/rss+xml">https://pre.hn/feed.rss</link>
|
|
|
|
<%= for post <- @posts do %>
|
|
<entry>
|
|
<title><%= HtmlSanitizeEx.strip_tags(post.title) %></title>
|
|
<content type="xhtml" xml:lang="en"
|
|
xml:base="https://pre.hn/"><![CDATA[<html><body>
|
|
<%= post.body %>
|
|
</body></html>]]>
|
|
</description>
|
|
<published><%= rss_date_format.(post.date) %></published>
|
|
<id><%= @base %><%= post.slug %>/</id>
|
|
<link rel="alternate" type="text/html"><%= @base %><%= post.slug %>/</link>
|
|
</entry>
|
|
<% end %>
|
|
</feed>
|
|
""",
|
|
assigns: assigns,
|
|
rss_date_format: &rss_date_format/1
|
|
)
|
|
|
|
File.mkdir_p!(Path.join([path, "build"]))
|
|
path = Path.join([path, "build", "feed.rss"])
|
|
|
|
File.write!(path, body)
|
|
end
|
|
|
|
@days_of_week %{
|
|
1 => "Mon",
|
|
2 => "Tue",
|
|
3 => "Wed",
|
|
4 => "Thu",
|
|
5 => "Fri",
|
|
6 => "Sat",
|
|
7 => "Sun"
|
|
}
|
|
@months %{
|
|
1 => "Jan",
|
|
2 => "Feb",
|
|
3 => "Mar",
|
|
4 => "Apr",
|
|
5 => "May",
|
|
6 => "Jun",
|
|
7 => "Jul",
|
|
8 => "Aug",
|
|
9 => "Sep",
|
|
10 => "Oct",
|
|
11 => "Nov",
|
|
12 => "Dec"
|
|
}
|
|
def rss_date_format(date_string) do
|
|
date = Date.from_iso8601!(date_string)
|
|
|
|
day = Map.get(@days_of_week, Date.day_of_week(date))
|
|
month = Map.get(@months, date.month)
|
|
|
|
"#{day}, #{date.day} #{month} #{date.year} 00:00:00 -0600"
|
|
end
|
|
|
|
def make_sitemap(posts, path) do
|
|
posts
|
|
|> Stream.reject(fn %{hidden: hidden} ->
|
|
hidden
|
|
end)
|
|
|> Stream.map(
|
|
&%Sitemapper.URL{lastmod: &1[:updated_at] || &1.date, loc: "https://pre.hn/#{&1.slug}/"}
|
|
)
|
|
|> Sitemapper.generate(sitemap_url: "https://pre.hn/")
|
|
|> Sitemapper.persist(
|
|
store: Sitemapper.FileStore,
|
|
store_config: [path: Path.join(path, "build")]
|
|
)
|
|
|> Stream.run()
|
|
end
|
|
|
|
attr(:path, :string, required: true)
|
|
attr(:title, :string, required: true)
|
|
attr(:slug, :string, required: true)
|
|
attr(:description, :string)
|
|
slot(:inner_block, required: true)
|
|
|
|
def layout(assigns) do
|
|
%{path: path} = assigns
|
|
path = Path.join(path, "templates/layout.html.heex")
|
|
|
|
inner_block = ~H"""
|
|
<%= render_slot(@inner_block) %>
|
|
"""
|
|
|
|
assigns = %{assigns | inner_block: inner_block}
|
|
|
|
EEx.eval_file(path, [assigns: assigns],
|
|
engine: Phoenix.LiveView.TagEngine,
|
|
tag_handler: Phoenix.LiveView.HTMLEngine,
|
|
caller: __ENV__,
|
|
source: File.read!(path)
|
|
)
|
|
end
|
|
|
|
def menu(assigns) do
|
|
~H"""
|
|
<nav class="text-light flex justify-center">
|
|
<div class="container flex">
|
|
<a class={"box rounded-tr !mt-[28px] !-mb-[2px] !border-b-light #{if @slug == "index", do: "current"}"} href="/">About Me</a>
|
|
<a class={"box rounded-tr !mt-[28px] !-mb-[2px] !border-b-light #{if @slug == "blog", do: "current"}"} href="/blog">Blog</a>
|
|
<a class={"box rounded-tr !mt-[28px] !-mb-[2px] !border-b-light #{if @slug == "link-log", do: "current"}"} href="/link-log">Link Log</a>
|
|
</div>
|
|
</nav>
|
|
"""
|
|
end
|
|
|
|
def write_page(%{slug: slug} = assigns, root_path) do
|
|
path =
|
|
if slug == "index" do
|
|
Path.join([root_path, "build", "index.html"])
|
|
else
|
|
directory = Path.join([root_path, "build", slug])
|
|
File.mkdir_p!(directory)
|
|
Path.join([root_path, "build", slug, "index.html"])
|
|
end
|
|
|
|
~H"""
|
|
<.layout path={root_path} title={@title} slug={@slug} description={assigns[:description]}>
|
|
<main class="container mx-auto z-10 relative">
|
|
<section class="box border border-light">
|
|
<h1><%= @title %></h1>
|
|
<%= {:safe, @body} %>
|
|
</section>
|
|
</main>
|
|
</.layout>
|
|
"""
|
|
|> rendered_to_string()
|
|
|> then(&File.write!(path, &1))
|
|
end
|
|
end
|