basic build and watch commands
This commit is contained in:
parent
aa01ee6967
commit
7c0a72d775
13 changed files with 291 additions and 15 deletions
|
@ -1,4 +1,5 @@
|
||||||
# Used by "mix format"
|
# Used by "mix format"
|
||||||
[
|
[
|
||||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||||
|
import_deps: [:typed_struct]
|
||||||
]
|
]
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -26,3 +26,5 @@ sloane_sh-*.tar
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
/.elixir_ls/
|
/.elixir_ls/
|
||||||
|
|
||||||
|
/priv/output/
|
||||||
|
|
|
@ -3,11 +3,24 @@ defmodule SloaneSH do
|
||||||
Sloane's personal static site generator powering [sloane.sh](https://sloane.sh).
|
Sloane's personal static site generator powering [sloane.sh](https://sloane.sh).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias SloaneSH.Build
|
||||||
|
alias SloaneSH.Watch
|
||||||
|
alias SloaneSH.Context
|
||||||
|
|
||||||
def build(_opts \\ []) do
|
def build(_opts \\ []) do
|
||||||
|
context()
|
||||||
|
|> Build.run()
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def watch(_opts \\ []) do
|
def watch(_opts \\ []) do
|
||||||
SloaneSH.Watch.start_link()
|
context()
|
||||||
|
|> Watch.start_link()
|
||||||
|
end
|
||||||
|
|
||||||
|
def context do
|
||||||
|
Context.new()
|
||||||
|
|> Context.init()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
51
lib/sloane_sh/build.ex
Normal file
51
lib/sloane_sh/build.ex
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
defmodule SloaneSH.Build do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias SloaneSH.Context
|
||||||
|
alias SloaneSH.Markdown
|
||||||
|
alias SloaneSH.Write
|
||||||
|
|
||||||
|
def run(%Context{} = ctx) do
|
||||||
|
ctx
|
||||||
|
|> build_pages()
|
||||||
|
|> build_posts()
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_pages(%Context{} = ctx) do
|
||||||
|
Logger.info("Building pages...")
|
||||||
|
for page <- ctx.pages, do: build_page(ctx, page)
|
||||||
|
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_posts(%Context{} = ctx) do
|
||||||
|
Logger.info("Building posts...")
|
||||||
|
for post <- ctx.posts, do: build_post(ctx, post)
|
||||||
|
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_page(%Context{} = ctx, page) do
|
||||||
|
path = Path.join(ctx.config.pages_dir, page)
|
||||||
|
|
||||||
|
with {:ok, data} <- File.read(path),
|
||||||
|
{:ok, html} <- Markdown.transform(ctx, data),
|
||||||
|
:ok <- Write.page(ctx, page, html) do
|
||||||
|
Logger.info("Built page: #{page}")
|
||||||
|
else
|
||||||
|
err -> Logger.error("Failed to build page #{page}: #{inspect(err)}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_post(%Context{} = ctx, post) do
|
||||||
|
path = Path.join(ctx.config.posts_dir, post)
|
||||||
|
|
||||||
|
with {:ok, data} <- File.read(path),
|
||||||
|
{:ok, html} <- Markdown.transform(ctx, data),
|
||||||
|
:ok <- Write.post(ctx, post, html) do
|
||||||
|
Logger.info("Built post: #{post}")
|
||||||
|
else
|
||||||
|
err -> Logger.error("Failed to build post #{post}: #{inspect(err)}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
lib/sloane_sh/config.ex
Normal file
40
lib/sloane_sh/config.ex
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
defmodule SloaneSH.Config do
|
||||||
|
@moduledoc """
|
||||||
|
SloaneSH configuration
|
||||||
|
"""
|
||||||
|
use TypedStruct
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
|
|
||||||
|
typedstruct do
|
||||||
|
field :pages_dir, String.t(), enforce: true
|
||||||
|
field :posts_dir, String.t(), enforce: true
|
||||||
|
field :output, String.t(), enforce: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def default do
|
||||||
|
priv = :code.priv_dir(:sloane_sh) |> resolve_link()
|
||||||
|
|
||||||
|
%Config{
|
||||||
|
pages_dir: Path.join(priv, "site/pages"),
|
||||||
|
posts_dir: Path.join(priv, "site/posts"),
|
||||||
|
output: Path.join(priv, "output")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_config?(%Config{} = cfg, path) do
|
||||||
|
Enum.any?([cfg.pages_dir, cfg.posts_dir], &String.starts_with?(path, &1)) and
|
||||||
|
Path.extname(path) == ".md"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolve_link(path) do
|
||||||
|
case File.read_link(path) do
|
||||||
|
{:ok, link} ->
|
||||||
|
dir = Path.dirname(path)
|
||||||
|
Path.expand(link, dir)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
67
lib/sloane_sh/context.ex
Normal file
67
lib/sloane_sh/context.ex
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
defmodule SloaneSH.Context do
|
||||||
|
@moduledoc """
|
||||||
|
A SloaneSH build context containing configuration and reference to content
|
||||||
|
files.
|
||||||
|
"""
|
||||||
|
use TypedStruct
|
||||||
|
|
||||||
|
alias SloaneSH.Config
|
||||||
|
alias __MODULE__
|
||||||
|
|
||||||
|
typedstruct do
|
||||||
|
field :config, Config.t(), enforce: true
|
||||||
|
field :pages, [String.t()], default: []
|
||||||
|
field :posts, [String.t()], default: []
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(config \\ Config.default()) do
|
||||||
|
%Context{config: config}
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(%Context{config: config} = context) do
|
||||||
|
with {:ok, pages_contents} <- File.ls(config.pages_dir),
|
||||||
|
{:ok, posts_contents} <- File.ls(config.posts_dir) do
|
||||||
|
pages = Enum.filter(pages_contents, &String.match?(&1, ~r/.*\.md$/))
|
||||||
|
posts = Enum.filter(posts_contents, &String.match?(&1, ~r/.*\.md$/))
|
||||||
|
%Context{context | pages: pages, posts: posts}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_add(%Context{config: config} = ctx, path) do
|
||||||
|
if Config.in_config?(config, path) do
|
||||||
|
cond do
|
||||||
|
String.starts_with?(path, config.pages_dir) ->
|
||||||
|
page = Path.relative_to(path, config.pages_dir)
|
||||||
|
%{ctx | pages: Enum.uniq([page | ctx.pages])}
|
||||||
|
|
||||||
|
String.starts_with?(path, config.posts_dir) ->
|
||||||
|
post = Path.relative_to(path, config.posts_dir)
|
||||||
|
%{ctx | posts: Enum.uniq([post | ctx.posts])}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
else
|
||||||
|
ctx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_context?(%Context{config: config, pages: pages, posts: posts}, path) do
|
||||||
|
with true <- Config.in_config?(config, path) do
|
||||||
|
cond do
|
||||||
|
String.starts_with?(path, config.pages_dir) ->
|
||||||
|
page = Path.relative_to(path, config.pages_dir)
|
||||||
|
|
||||||
|
[page in pages]
|
||||||
|
|
||||||
|
String.starts_with?(path, config.posts_dir) ->
|
||||||
|
post = Path.relative_to(path, config.posts_dir)
|
||||||
|
|
||||||
|
[post in posts]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
lib/sloane_sh/markdown.ex
Normal file
21
lib/sloane_sh/markdown.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule SloaneSH.Markdown do
|
||||||
|
@moduledoc """
|
||||||
|
Markdown parsing using `Earmark` and `Earmark.Parser`
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias SloaneSH.Context
|
||||||
|
|
||||||
|
def transform(%Context{} = _ctx, data) when is_binary(data) do
|
||||||
|
case Earmark.as_html(data) do
|
||||||
|
{:ok, html_doc, deprecation_messages} ->
|
||||||
|
for msg <- deprecation_messages, do: Logger.warning(msg)
|
||||||
|
|
||||||
|
{:ok, html_doc}
|
||||||
|
|
||||||
|
{:error, html_doc, error_messages} ->
|
||||||
|
for msg <- error_messages, do: Logger.error(msg)
|
||||||
|
{:error, html_doc}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,29 +1,62 @@
|
||||||
defmodule SloaneSH.Watch do
|
defmodule SloaneSH.Watch do
|
||||||
use GenServer
|
use GenServer
|
||||||
|
use TypedStruct
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def start_link(init_arg \\ [], opts \\ []) do
|
alias SloaneSH.Build
|
||||||
GenServer.start_link(__MODULE__, init_arg, opts)
|
alias SloaneSH.Context
|
||||||
|
|
||||||
|
typedstruct do
|
||||||
|
field :ctx, Context.t(), enforce: true
|
||||||
|
field :watcher_pid, pid(), enforce: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link(%Context{} = ctx, opts \\ []) do
|
||||||
|
GenServer.start_link(__MODULE__, ctx, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def init([]) do
|
def init(%Context{} = ctx) do
|
||||||
dirs = [Path.join(:code.priv_dir(:sloane_sh), "site")]
|
{:ok, watcher_pid} =
|
||||||
Logger.info("Watching #{inspect(dirs)} for changes")
|
FileSystem.start_link(
|
||||||
{:ok, pid} = FileSystem.start_link(dirs: dirs)
|
dirs:
|
||||||
FileSystem.subscribe(pid)
|
dbg([
|
||||||
|
ctx.config.pages_dir,
|
||||||
|
ctx.config.posts_dir
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, pid}
|
FileSystem.subscribe(watcher_pid)
|
||||||
|
|
||||||
|
state = %__MODULE__{ctx: ctx, watcher_pid: watcher_pid}
|
||||||
|
|
||||||
|
{:ok, state, {:continue, :build}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def handle_info({:file_event, pid, {path, events}}, pid) do
|
def handle_continue(:build, %{ctx: ctx} = state) do
|
||||||
Logger.info("File event: #{inspect(path)} #{inspect(events)}")
|
Build.run(ctx)
|
||||||
{:noreply, pid}
|
|
||||||
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def handle_info({:file_event, pid, :stop}, pid) do
|
def handle_info({:file_event, pid, {path, events}}, %{ctx: ctx, watcher_pid: pid} = state) do
|
||||||
|
ctx = Context.maybe_add(ctx, path)
|
||||||
|
|
||||||
|
if Context.in_context?(ctx, path) do
|
||||||
|
path = Path.relative_to(path, ctx.config.pages_dir)
|
||||||
|
Logger.info("File changed: #{path}")
|
||||||
|
Build.build_page(ctx, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
%{state | ctx: ctx}
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info({:file_event, pid, :stop}, %{watcher_pid: pid}) do
|
||||||
Logger.warning("File watcher stopped")
|
Logger.warning("File watcher stopped")
|
||||||
{:stop, :watcher_stopped, pid}
|
{:stop, :watcher_stopped, pid}
|
||||||
end
|
end
|
||||||
|
|
33
lib/sloane_sh/write.ex
Normal file
33
lib/sloane_sh/write.ex
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
defmodule SloaneSH.Write do
|
||||||
|
def page(ctx, src, data) do
|
||||||
|
path = md_to_html(src)
|
||||||
|
|
||||||
|
write(ctx, path, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(ctx, src, data) do
|
||||||
|
path = Path.join("posts", md_to_html(src))
|
||||||
|
|
||||||
|
write(ctx, path, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(ctx, name, data) do
|
||||||
|
path = Path.join(ctx.config.output, name)
|
||||||
|
|
||||||
|
with :ok <- File.mkdir_p(Path.dirname(path)) do
|
||||||
|
File.write(path, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def md_to_html("index.md"), do: "index.html"
|
||||||
|
|
||||||
|
def md_to_html(path) do
|
||||||
|
dir = Path.dirname(path)
|
||||||
|
base = Path.basename(path, ".md")
|
||||||
|
|
||||||
|
case dir do
|
||||||
|
"." -> Path.join(base, "index.html")
|
||||||
|
_ -> Path.join([dir, base, "index.html"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
mix.exs
5
mix.exs
|
@ -21,7 +21,10 @@ defmodule SloaneSH.MixProject do
|
||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:file_system, "~> 1.0.0"}
|
{:file_system, "~> 1.0.0"},
|
||||||
|
{:typed_struct, "~> 0.3.0"},
|
||||||
|
{:earmark, "~> 1.4"},
|
||||||
|
{:earmark_parser, "~> 1.4"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
mix.lock
3
mix.lock
|
@ -1,3 +1,6 @@
|
||||||
%{
|
%{
|
||||||
|
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||||
|
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||||
|
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
|
||||||
}
|
}
|
||||||
|
|
6
priv/site/pages/index.md
Normal file
6
priv/site/pages/index.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Hello, World!
|
||||||
|
|
||||||
|
my name is sloane. i am a software engineer.
|
||||||
|
|
||||||
|
|
||||||
|
i'm on the fediverse [@sloane@tech.lgbt](https://tech.lgbt/@sloane)
|
3
priv/site/posts/2024-02-16-test-post.md
Normal file
3
priv/site/posts/2024-02-16-test-post.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Test Post
|
||||||
|
|
||||||
|
this is just a test of the posts functionality
|
Loading…
Reference in a new issue