pollen #12
50 changed files with 59 additions and 1262 deletions
|
@ -1,5 +0,0 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
import_deps: [:typed_struct, :plug]
|
||||
]
|
37
.gitignore
vendored
37
.gitignore
vendored
|
@ -1,32 +1,7 @@
|
|||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
sloane_sh-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
|
||||
/.elixir_ls/
|
||||
|
||||
/priv/output/
|
||||
|
||||
.DS_Store
|
||||
|
||||
compiled/
|
||||
/build/
|
||||
|
||||
*.html
|
||||
*.css
|
||||
|
|
2
.mise.toml
Normal file
2
.mise.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[tools]
|
||||
racket = "8.15"
|
|
@ -1,3 +0,0 @@
|
|||
erlang 26.2.2
|
||||
elixir 1.16.1-otp-26
|
||||
nodejs 20.11.1
|
24
README.md
24
README.md
|
@ -1,10 +1,22 @@
|
|||
# SloaneSH
|
||||
|
||||
_Sloane's personal static site generator powering [sloane.sh](https://sloane.sh)._
|
||||
# sloane.sh
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repo
|
||||
2. Install Erlang and Elixir: `asdf install`
|
||||
3. Install dependencies: `mix deps.get`
|
||||
```sh
|
||||
brew install --cask racket
|
||||
raco pkg install pollen
|
||||
```
|
||||
|
||||
## Working on the site
|
||||
|
||||
```sh
|
||||
raco pollen start site
|
||||
```
|
||||
|
||||
## Building the site
|
||||
|
||||
```sh
|
||||
mkdir -p build
|
||||
raco pollen render site
|
||||
raco pollen publish site build
|
||||
```
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
body {
|
||||
--pagefind-ui-border-width: 1px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#search {
|
||||
--pagefind-ui-primary: #eeeeee;
|
||||
--pagefind-ui-text: #eeeeee;
|
||||
--pagefind-ui-background: rgb(64, 64, 64);
|
||||
--pagefind-ui-border: rgb(163, 163, 163);
|
||||
--pagefind-ui-tag: #152028;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// See the Tailwind configuration guide for advanced usage
|
||||
// https://tailwindcss.com/docs/configuration
|
||||
|
||||
let plugin = require('tailwindcss/plugin')
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'./js/**/*.js',
|
||||
'../lib/sloane_sh/layouts/*.ex',
|
||||
'../priv/site/**/*.*ex',
|
||||
'../priv/site/**/*.md',
|
||||
'../priv/site/**/*.html',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
|
||||
plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
|
||||
plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
|
||||
plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
|
||||
]
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import Config
|
||||
|
||||
config :logger, :default_formatter, format: "$time $metadata[$level] $message\n"
|
||||
|
||||
config :tailwind,
|
||||
version: "3.4.1",
|
||||
default: [
|
||||
args: ~w(
|
||||
--config=tailwind.config.js
|
||||
--input=css/app.css
|
||||
--output=../priv/output/assets/css/app.css
|
||||
),
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
|
||||
config :esbuild,
|
||||
version: "0.20.1",
|
||||
default: [
|
||||
args: ~w(js/app.js --bundle --outdir=../priv/output/assets/js),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
]
|
|
@ -1,15 +0,0 @@
|
|||
defmodule Mix.Tasks.Site.Build do
|
||||
@moduledoc "Build and output the site as HTML"
|
||||
@shortdoc "build the site"
|
||||
use Mix.Task
|
||||
require Logger
|
||||
|
||||
alias SloaneSH.Format
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_args) do
|
||||
Mix.Task.run("app.start")
|
||||
{micro, :ok} = :timer.tc(&SloaneSH.build/0)
|
||||
Logger.info("Built site in #{Format.time(micro)}")
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
defmodule Mix.Tasks.Site.Dev do
|
||||
@moduledoc "Build the site, watch for changes, and serve the built site"
|
||||
@shortdoc "run site.watch and site.serve"
|
||||
use Mix.Task
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_args) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
{:ok, watch_pid} =
|
||||
Task.start_link(fn ->
|
||||
Mix.Task.run("site.watch")
|
||||
end)
|
||||
|
||||
{:ok, serve_pid} =
|
||||
Task.start_link(fn ->
|
||||
Mix.Task.run("site.serve")
|
||||
end)
|
||||
|
||||
unless iex_running?() do
|
||||
watch_ref = Process.monitor(watch_pid)
|
||||
serve_ref = Process.monitor(serve_pid)
|
||||
|
||||
receive do
|
||||
{:DOWN, ^watch_ref, _, _, _} ->
|
||||
:ok
|
||||
|
||||
{:DOWN, ^serve_ref, _, _, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp iex_running? do
|
||||
Code.ensure_loaded?(IEx) and IEx.started?()
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
defmodule Mix.Tasks.Site.Serve do
|
||||
@moduledoc "Serve the built site. Only for use in development"
|
||||
@shortdoc "serve the built site"
|
||||
use Mix.Task
|
||||
require Logger
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_args) do
|
||||
Mix.Task.run("app.start")
|
||||
Logger.info("Starting development server...")
|
||||
|
||||
{:ok, pid} = SloaneSH.serve()
|
||||
|
||||
unless iex_running?() do
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} ->
|
||||
Logger.info("Development server terminated")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp iex_running? do
|
||||
Code.ensure_loaded?(IEx) and IEx.started?()
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
defmodule Mix.Tasks.Site.Watch do
|
||||
@moduledoc "Build and output the site as HTML watching for changes"
|
||||
@shortdoc "build the site and watch for changes"
|
||||
use Mix.Task
|
||||
require Logger
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_args) do
|
||||
Mix.Task.run("app.start")
|
||||
Logger.info("Starting site.watch...")
|
||||
|
||||
{:ok, pid} = SloaneSH.watch()
|
||||
|
||||
unless iex_running?() do
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} ->
|
||||
Logger.info("site.watch terminated")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp iex_running? do
|
||||
Code.ensure_loaded?(IEx) and IEx.started?()
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
defmodule SloaneSH do
|
||||
@moduledoc """
|
||||
Sloane's personal static site generator powering [sloane.sh](https://sloane.sh).
|
||||
"""
|
||||
|
||||
alias SloaneSH.Build
|
||||
alias SloaneSH.Context
|
||||
alias SloaneSH.Serve
|
||||
alias SloaneSH.Watch
|
||||
|
||||
def build(_opts \\ []) do
|
||||
context()
|
||||
|> Build.run()
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def watch(_opts \\ []) do
|
||||
context()
|
||||
|> Watch.start_link()
|
||||
end
|
||||
|
||||
def serve do
|
||||
context()
|
||||
|> Serve.start_link()
|
||||
end
|
||||
|
||||
def context do
|
||||
Context.new()
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
defmodule SloaneSH.Asset do
|
||||
use TypedStruct
|
||||
alias SloaneSH.Config
|
||||
alias SloaneSH.Context
|
||||
|
||||
typedstruct do
|
||||
field :mod, module(), enforce: true
|
||||
field :src, String.t(), enforce: true
|
||||
field :src_contents, binary(), enforce: true
|
||||
field :attrs, map(), enforce: true
|
||||
end
|
||||
|
||||
@callback extensions(cfg :: Config.t()) :: [String.t()]
|
||||
|
||||
@callback attrs(cfg :: Config.t(), path :: String.t(), data :: binary()) ::
|
||||
{:ok, map(), without_attrs :: term()} | {:ok, map()} | :error | {:error, term()}
|
||||
|
||||
@callback render(
|
||||
cfg :: Config.t(),
|
||||
ctx :: Context.t(),
|
||||
path :: String.t(),
|
||||
data :: binary(),
|
||||
attrs :: map()
|
||||
) ::
|
||||
{:ok, [{dest :: String.t(), binary()}]} | :error | {:error, term()}
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
defmodule SloaneSH.Assets.Image do
|
||||
alias SloaneSH.Asset
|
||||
alias SloaneSH.OutputDirs
|
||||
|
||||
@behaviour Asset
|
||||
|
||||
@impl Asset
|
||||
def extensions(_cfg), do: ~w[.jpg .jpeg .png .webp .gif]
|
||||
|
||||
@impl Asset
|
||||
def attrs(_cfg, _path, data) do
|
||||
{:ok, image} = Image.from_binary(data)
|
||||
|
||||
aspect = Image.aspect(image)
|
||||
{width, height, _} = Image.shape(image)
|
||||
|
||||
{:ok, %{aspect: aspect, width: width, height: height}, image}
|
||||
end
|
||||
|
||||
@impl Asset
|
||||
def render(cfg, _ctx, path, data, _attrs) do
|
||||
formats = ~w[.webp .png .jpg]
|
||||
|
||||
outputs =
|
||||
for format <- formats do
|
||||
format_path = OutputDirs.replace_ext(path, format)
|
||||
output_path = OutputDirs.image(cfg, format_path)
|
||||
|
||||
{:ok, converted} =
|
||||
Image.write(data, :memory,
|
||||
suffix: format,
|
||||
strip_metadata: true,
|
||||
minimize_file_size: true,
|
||||
quality: 80
|
||||
)
|
||||
|
||||
{output_path, converted}
|
||||
end
|
||||
|
||||
{:ok, outputs}
|
||||
end
|
||||
end
|
|
@ -1,88 +0,0 @@
|
|||
defmodule SloaneSH.Assets.Markdown do
|
||||
@moduledoc """
|
||||
Helper to define markdown, html, and eex templating for pages and posts
|
||||
"""
|
||||
defmacro __using__(opts) do
|
||||
type = Keyword.fetch!(opts, :type)
|
||||
|
||||
quote do
|
||||
import SloaneSH.Layouts.Helpers, warn: false
|
||||
alias SloaneSH.Asset
|
||||
alias SloaneSH.FrontMatter
|
||||
alias SloaneSH.Layouts
|
||||
alias SloaneSH.OutputDirs
|
||||
|
||||
@behaviour Asset
|
||||
|
||||
@impl Asset
|
||||
def extensions(_cfg), do: ~w[.md .html .md.eex .html.eex]
|
||||
|
||||
@impl Asset
|
||||
def attrs(cfg, path, data) do
|
||||
{:ok, attrs, without_attrs} = FrontMatter.parse(data)
|
||||
|
||||
attrs =
|
||||
Map.put_new_lazy(attrs, :permalink, fn ->
|
||||
output = apply(OutputDirs, unquote(type), [cfg, path])
|
||||
permalink = OutputDirs.to_permalink(cfg, output)
|
||||
end)
|
||||
|
||||
attrs = handle_attrs(cfg, path, without_attrs, attrs)
|
||||
|
||||
{:ok, attrs, without_attrs}
|
||||
end
|
||||
|
||||
@impl Asset
|
||||
def render(cfg, ctx, path, data, attrs) do
|
||||
output_path =
|
||||
if attrs[:permalink] do
|
||||
OutputDirs.from_permalink(cfg, attrs[:permalink])
|
||||
else
|
||||
apply(OutputDirs, unquote(type), [cfg, path])
|
||||
end
|
||||
|
||||
output = {output_path, do_render(ctx, path, data, attrs)}
|
||||
{:ok, [output]}
|
||||
end
|
||||
|
||||
defp do_render(ctx, path, data, attrs) when is_binary(path) do
|
||||
do_render(ctx, base_and_ext(path), data, attrs)
|
||||
end
|
||||
|
||||
defp do_render(ctx, {path, ".eex"}, data, attrs) do
|
||||
eexed = eval_eex(data, "#{path}.eex", ctx, attrs)
|
||||
do_render(ctx, base_and_ext(path), eexed, attrs)
|
||||
end
|
||||
|
||||
defp do_render(ctx, {path, ".md"}, data, attrs) do
|
||||
html = Earmark.as_html!(data)
|
||||
do_render(ctx, {path, ".html"}, html, attrs)
|
||||
end
|
||||
|
||||
defp do_render(ctx, {_path, ".html"}, data, attrs) do
|
||||
apply(Layouts, unquote(type), [data, ctx, attrs])
|
||||
end
|
||||
|
||||
defp base_and_ext(path) do
|
||||
ext = Path.extname(path)
|
||||
base = Path.basename(path, ext)
|
||||
{base, ext}
|
||||
end
|
||||
|
||||
defp eval_eex(template, file, ctx, attrs) do
|
||||
{result, _binding} =
|
||||
template
|
||||
|> EEx.compile_string(file: file)
|
||||
|> Code.eval_quoted([ctx: ctx, attrs: attrs], __ENV__)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def handle_attrs(cfg, path, data, attrs) do
|
||||
attrs
|
||||
end
|
||||
|
||||
defoverridable handle_attrs: 4
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule SloaneSH.Assets.Page do
|
||||
use SloaneSH.Assets.Markdown, type: :page
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
defmodule SloaneSH.Assets.Post do
|
||||
use SloaneSH.Assets.Markdown, type: :post
|
||||
require Logger
|
||||
|
||||
def handle_attrs(_cfg, path, _data, attrs) do
|
||||
unless Map.has_key?(attrs, :date) do
|
||||
Logger.warning("Post missing date property: #{inspect(path)}")
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
defmodule SloaneSH.Build do
|
||||
require Logger
|
||||
|
||||
alias SloaneSH.Context
|
||||
|
||||
def run(%Context{} = ctx) do
|
||||
assets = ctx.posts ++ ctx.pages ++ ctx.images
|
||||
|
||||
File.mkdir_p!(ctx.config.output_dir)
|
||||
|
||||
for asset <- assets do
|
||||
case asset.mod.render(ctx.config, ctx, asset.src, asset.src_contents, asset.attrs) do
|
||||
{:ok, output_files} ->
|
||||
for {dest, content} <- output_files do
|
||||
with :ok <- dest |> Path.dirname() |> File.mkdir_p(),
|
||||
:ok <- File.write(dest, content) do
|
||||
Logger.info("Wrote #{inspect(dest)}.")
|
||||
else
|
||||
{:error, err} ->
|
||||
Logger.error("Failed to write #{inspect(dest)}, #{inspect(err)}")
|
||||
end
|
||||
end
|
||||
|
||||
err ->
|
||||
Logger.error("Failed to render #{inspect(asset.src)}, #{inspect(err)}")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
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 :images_dir, String.t(), enforce: true
|
||||
field :output_dir, 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"),
|
||||
images_dir: Path.join(priv, "site/images"),
|
||||
output_dir: Path.join(priv, "output")
|
||||
}
|
||||
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
|
|
@ -1,59 +0,0 @@
|
|||
defmodule SloaneSH.Context do
|
||||
@moduledoc """
|
||||
A SloaneSH build context containing configuration and reference to content
|
||||
files.
|
||||
"""
|
||||
use TypedStruct
|
||||
require Logger
|
||||
|
||||
alias SloaneSH.Config
|
||||
alias SloaneSH.Asset
|
||||
alias SloaneSH.Assets.Page
|
||||
alias SloaneSH.Assets.Post
|
||||
alias SloaneSH.Assets.Image
|
||||
alias __MODULE__
|
||||
|
||||
typedstruct do
|
||||
field :config, Config.t(), enforce: true
|
||||
field :pages, [Asset.t()], default: []
|
||||
field :posts, [Asset.t()], default: []
|
||||
field :images, [Asset.t()], default: []
|
||||
end
|
||||
|
||||
def new(cfg \\ Config.default()) do
|
||||
pages = load_assets(cfg, Page, cfg.pages_dir)
|
||||
posts = load_assets(cfg, Post, cfg.posts_dir)
|
||||
images = load_assets(cfg, Image, cfg.images_dir)
|
||||
|
||||
%Context{config: cfg, pages: pages, posts: posts, images: images}
|
||||
end
|
||||
|
||||
defp load_assets(cfg, mod, src_dir) do
|
||||
exts = mod.extensions(cfg)
|
||||
|
||||
for src <- collect_src_files(src_dir, exts) do
|
||||
contents = File.read!(src)
|
||||
|
||||
case mod.attrs(cfg, src, contents) do
|
||||
{:ok, attrs, src_contents} ->
|
||||
%Asset{mod: mod, src: src, src_contents: src_contents, attrs: attrs}
|
||||
|
||||
{:ok, attrs} ->
|
||||
%Asset{mod: mod, src: src, src_contents: contents, attrs: attrs}
|
||||
|
||||
_ ->
|
||||
Logger.warning("Failed to parse attrs for #{inspect(src)}")
|
||||
%Asset{mod: mod, src: src, src_contents: contents, attrs: %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_src_files(src_dir, exts) do
|
||||
files = src_dir |> File.ls!() |> Enum.map(&Path.join(src_dir, &1))
|
||||
{src_files, rest} = Enum.split_with(files, &String.ends_with?(&1, exts))
|
||||
|
||||
other_dirs = Enum.filter(rest, &File.dir?/1)
|
||||
|
||||
src_files ++ Enum.flat_map(other_dirs, &collect_src_files(&1, exts))
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
defmodule SloaneSH.Format do
|
||||
@moduledoc """
|
||||
Functions to format various literals into human readable forms.
|
||||
"""
|
||||
|
||||
@millisecond 1000
|
||||
@second @millisecond ** 2
|
||||
@minute 60 * @second
|
||||
@hour 60 * @minute
|
||||
|
||||
@max_depth 2
|
||||
|
||||
def time(micro, depth \\ 0)
|
||||
def time(_micro, depth) when depth >= @max_depth, do: []
|
||||
|
||||
time_units = [
|
||||
{@hour, "h"},
|
||||
{@minute, "m"},
|
||||
{@second, "s"},
|
||||
{@millisecond, "ms"}
|
||||
]
|
||||
|
||||
for {division, unit} <- time_units do
|
||||
def time(micro, depth) when micro >= unquote(division) do
|
||||
count = "#{div(micro, unquote(division))}#{unquote(unit)}"
|
||||
rem = rem(micro, unquote(division))
|
||||
[count, time(rem, depth + 1)]
|
||||
end
|
||||
end
|
||||
|
||||
def time(micro, _depth) do
|
||||
"#{micro}μs"
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
defmodule SloaneSH.FrontMatter do
|
||||
@moduledoc """
|
||||
Parses TOML front matter out put files
|
||||
"""
|
||||
|
||||
def parse("+++" <> rest) do
|
||||
[toml, body] = String.split(rest, ["+++\n", "+++\r\n"], parts: 2)
|
||||
|
||||
with {:ok, attrs} <- Toml.decode(toml, keys: :atoms) do
|
||||
{:ok, attrs, body}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(body) do
|
||||
{:ok, %{}, body}
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
defmodule SloaneSH.Layouts do
|
||||
@moduledoc """
|
||||
`EEx` based layouts
|
||||
"""
|
||||
require EEx
|
||||
import SloaneSH.Layouts.Partials, warn: false
|
||||
import SloaneSH.Layouts.Helpers, warn: false
|
||||
|
||||
@layouts_dir Path.join(:code.priv_dir(:sloane_sh), "site/layouts")
|
||||
|
||||
EEx.function_from_file(:def, :root, Path.join(@layouts_dir, "root.html.eex"), [
|
||||
:inner_content,
|
||||
:ctx,
|
||||
:attrs
|
||||
])
|
||||
|
||||
EEx.function_from_file(:defp, :page_layout, Path.join(@layouts_dir, "page.html.eex"), [
|
||||
:inner_content,
|
||||
:ctx,
|
||||
:attrs
|
||||
])
|
||||
|
||||
EEx.function_from_file(:defp, :post_layout, Path.join(@layouts_dir, "post.html.eex"), [
|
||||
:inner_content,
|
||||
:ctx,
|
||||
:attrs
|
||||
])
|
||||
|
||||
def page(inner_content, ctx, attrs) do
|
||||
inner_content
|
||||
|> page_layout(ctx, attrs)
|
||||
|> root(ctx, attrs)
|
||||
end
|
||||
|
||||
def post(inner_content, ctx, attrs) do
|
||||
inner_content
|
||||
|> post_layout(ctx, attrs)
|
||||
|> root(ctx, attrs)
|
||||
end
|
||||
|
||||
defp prefix_title(prefix, nil), do: prefix
|
||||
defp prefix_title(prefix, page_title), do: [prefix, " | ", page_title]
|
||||
end
|
|
@ -1,67 +0,0 @@
|
|||
defmodule SloaneSH.Layouts.Helpers do
|
||||
require Logger
|
||||
alias SloaneSH.Context
|
||||
alias SloaneSH.OutputDirs
|
||||
|
||||
def cx(classes) do
|
||||
classes
|
||||
|> Enum.map(fn
|
||||
{_, _} = t -> t
|
||||
c -> {c, true}
|
||||
end)
|
||||
|> Enum.filter(fn {_, v} -> !!v end)
|
||||
|> Enum.map_join(" ", fn {class, _} -> class end)
|
||||
end
|
||||
|
||||
def sorted_post_attrs(%Context{} = ctx) do
|
||||
{drafts, others} =
|
||||
ctx.posts
|
||||
|> Enum.map(& &1.attrs)
|
||||
|> Enum.split_with(&is_nil(&1[:date]))
|
||||
|
||||
others = Enum.sort_by(others, & &1[:date], {:desc, Date})
|
||||
|
||||
drafts ++ others
|
||||
end
|
||||
|
||||
def fmt_date(nil), do: "Draft"
|
||||
def fmt_date(date), do: Timex.format!(date, "{Mfull} {D}, {YYYY}")
|
||||
|
||||
def picture(ctx, src, alt \\ "", class \\ "") do
|
||||
image =
|
||||
Enum.find(ctx.images, fn i ->
|
||||
output = OutputDirs.image(ctx.config, i.src)
|
||||
src == OutputDirs.to_permalink(ctx.config, output)
|
||||
end)
|
||||
|
||||
if is_nil(image) do
|
||||
Logger.warning("Could not find #{inspect(src)} to make picture element")
|
||||
~s|<img src="#{src}" alt="#{alt}" class="#{class}">|
|
||||
else
|
||||
[{_, src} | srcsets] =
|
||||
[
|
||||
{"image/jpg", ".jpg"},
|
||||
{"image/webp", ".webp"},
|
||||
{"image/png", ".png"}
|
||||
]
|
||||
|> Enum.map(fn {type, ext} ->
|
||||
{type, OutputDirs.replace_ext(src, ext)}
|
||||
end)
|
||||
|
||||
EEx.eval_string(
|
||||
~S"""
|
||||
<picture class="<%= class %>">
|
||||
<%= for {type, srcset} <- srcsets do %>
|
||||
<source srcset="<%= srcset %>" type="<%= type %>" />
|
||||
<% end %>
|
||||
<img src="<%= src %>" alt="<%= alt %>" />
|
||||
</picture>
|
||||
""",
|
||||
src: src,
|
||||
srcsets: srcsets,
|
||||
alt: alt,
|
||||
class: class
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
defmodule SloaneSH.Layouts.Partials do
|
||||
@moduledoc """
|
||||
HTML partials for use in HTML layouts
|
||||
"""
|
||||
require EEx
|
||||
import SloaneSH.Layouts.Helpers, warn: false
|
||||
|
||||
EEx.function_from_string(
|
||||
:def,
|
||||
:header,
|
||||
~S"""
|
||||
<header class="flex flex-row justify-between gap-2 pb-4 mb-2" data-pagefind-ignore>
|
||||
<div class="flex flex-col justify-end">
|
||||
<%= if attrs[:title] do %>
|
||||
<h1 class="text-3xl font-extrabold"><%= attrs[:title] %></h1>
|
||||
<% end %>
|
||||
<%= if attrs[:date] do %>
|
||||
<small><%= fmt_date(attrs[:date]) %></small>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<a href="/"><span class="text-lg font-bold">sloane.sh</span></a>
|
||||
<%= picture(ctx, "/assets/images/heart.png", "a purple heart emoji", "w-4 h-4") %>
|
||||
</div>
|
||||
<nav class="flex flex-row gap-2">
|
||||
<a href="/" class="<%= cx(underline: attrs[:permalink] == "/") %>">home</a>
|
||||
<a href="/posts" class="<%= cx(underline: Map.get(attrs, :permalink, "") =~ ~r[^/posts]) %>">posts</a>
|
||||
<a href="/search" class="<%= cx(underline: attrs[:permalink] == "/search") %>">search</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
""",
|
||||
[:ctx, :attrs]
|
||||
)
|
||||
end
|
|
@ -1,55 +0,0 @@
|
|||
defmodule SloaneSH.OutputDirs do
|
||||
def page(cfg, src) do
|
||||
path = Path.relative_to(src, cfg.pages_dir)
|
||||
|
||||
cfg.output_dir |> Path.join(path) |> prettify_html_path()
|
||||
end
|
||||
|
||||
def post(cfg, src) do
|
||||
path = Path.relative_to(src, cfg.posts_dir)
|
||||
path = Path.join("post", path)
|
||||
|
||||
cfg.output_dir |> Path.join(path) |> prettify_html_path()
|
||||
end
|
||||
|
||||
def image(cfg, src) do
|
||||
path = Path.relative_to(src, cfg.images_dir)
|
||||
path = Path.join("assets/images", path)
|
||||
|
||||
cfg.output_dir |> Path.join(path)
|
||||
end
|
||||
|
||||
def prettify_html_path(path) do
|
||||
file = Path.basename(path)
|
||||
[without_extension | _] = String.split(file, ".", parts: 2)
|
||||
|
||||
suffix =
|
||||
if without_extension == "index" do
|
||||
"index.html"
|
||||
else
|
||||
Path.join(without_extension, "index.html")
|
||||
end
|
||||
|
||||
String.replace_suffix(path, file, suffix)
|
||||
end
|
||||
|
||||
def to_permalink(cfg, output_path) do
|
||||
output_path
|
||||
|> Path.relative_to(cfg.output_dir)
|
||||
|> String.trim_trailing("index.html")
|
||||
|> String.replace_prefix("", "/")
|
||||
|> String.trim_trailing("/")
|
||||
end
|
||||
|
||||
def from_permalink(cfg, permalink) do
|
||||
Path.join([cfg.output_dir, permalink, "/index.html"])
|
||||
end
|
||||
|
||||
def replace_ext(path, new_ext) do
|
||||
ext = Path.extname(path)
|
||||
base = Path.basename(path, ext)
|
||||
dir = Path.dirname(path)
|
||||
|
||||
Path.join(dir, base <> new_ext)
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
defmodule SloaneSH.Serve do
|
||||
@moduledoc """
|
||||
Task to use `Bandit` to start `SloaneSH.Serve.Plug`
|
||||
"""
|
||||
use Supervisor
|
||||
|
||||
alias __MODULE__
|
||||
|
||||
def start_link(ctx) do
|
||||
Supervisor.start_link(__MODULE__, ctx, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl Supervisor
|
||||
def init(_ctx) do
|
||||
children = [
|
||||
{Bandit, plug: Serve.Plug}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
defmodule SloaneSH.Serve.Plug do
|
||||
@moduledoc """
|
||||
Basic HTTP server for testing and local development.
|
||||
|
||||
Inspired by https://github.com/mbuhot/plug_static_index_html
|
||||
"""
|
||||
use Plug.Builder
|
||||
|
||||
plug Plug.Logger
|
||||
plug :rewrite_for_index_html
|
||||
plug Plug.Static, at: "/", from: {:sloane_sh, "priv/output"}
|
||||
plug :not_found
|
||||
|
||||
def rewrite_for_index_html(conn, _) do
|
||||
if String.match?(conn.request_path, ~r[.+\..+$]) do
|
||||
conn
|
||||
else
|
||||
%{
|
||||
conn
|
||||
| request_path: Path.join(conn.request_path, "index.html"),
|
||||
path_info: conn.path_info ++ ["index.html"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def not_found(conn, _) do
|
||||
send_resp(conn, 404, "Not found")
|
||||
end
|
||||
end
|
|
@ -1,81 +0,0 @@
|
|||
defmodule SloaneSH.Watch do
|
||||
use GenServer
|
||||
use TypedStruct
|
||||
require Logger
|
||||
|
||||
alias SloaneSH.Build
|
||||
alias SloaneSH.Context
|
||||
alias SloaneSH.Layouts
|
||||
|
||||
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
|
||||
|
||||
@impl GenServer
|
||||
def init(%Context{} = ctx) do
|
||||
{:ok, watcher_pid} =
|
||||
FileSystem.start_link(
|
||||
dirs: [
|
||||
# ctx.config.layouts_dir,
|
||||
"priv/site/layouts",
|
||||
"lib/sloane_sh/layouts",
|
||||
ctx.config.pages_dir,
|
||||
ctx.config.posts_dir
|
||||
]
|
||||
)
|
||||
|
||||
:ok = FileSystem.subscribe(watcher_pid)
|
||||
|
||||
Task.start_link(fn ->
|
||||
Tailwind.install_and_run(:default, ~w[--watch])
|
||||
end)
|
||||
|
||||
Task.start_link(fn ->
|
||||
Esbuild.install_and_run(:default, ~w[--watch])
|
||||
end)
|
||||
|
||||
state = %__MODULE__{ctx: ctx, watcher_pid: watcher_pid}
|
||||
|
||||
{:ok, state, {:continue, :build}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_continue(:build, %{ctx: ctx} = state) do
|
||||
Build.run(ctx)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info({:file_event, pid, {path, _events}}, %{watcher_pid: pid} = state) do
|
||||
if String.match?(path, ~r/layouts/) do
|
||||
recompile_layouts()
|
||||
end
|
||||
|
||||
ctx = Context.new()
|
||||
|
||||
{:noreply, %{state | ctx: ctx}, {:continue, :build}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info({:file_event, pid, :stop}, %{watcher_pid: pid}) do
|
||||
Logger.warning("File watcher stopped")
|
||||
{:stop, :watcher_stopped, pid}
|
||||
end
|
||||
|
||||
defp recompile_layouts do
|
||||
helpers_source = Layouts.Helpers.module_info(:compile)[:source] |> List.to_string()
|
||||
partials_source = Layouts.Partials.module_info(:compile)[:source] |> List.to_string()
|
||||
layouts_source = Layouts.module_info(:compile)[:source] |> List.to_string()
|
||||
|
||||
{:ok, _, _} =
|
||||
Kernel.ParallelCompiler.compile([helpers_source, partials_source, layouts_source])
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
53
mix.exs
53
mix.exs
|
@ -1,53 +0,0 @@
|
|||
defmodule SloaneSH.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :sloane_sh,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.15",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
aliases: aliases()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:file_system, "~> 1.0.0"},
|
||||
{:typed_struct, "~> 0.3.0"},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:earmark_parser, "~> 1.4"},
|
||||
{:plug, "~> 1.15"},
|
||||
{:bandit, "~> 1.2"},
|
||||
{:tailwind, "~> 0.2"},
|
||||
{:toml, "~> 0.7"},
|
||||
{:esbuild, "~> 0.8"},
|
||||
{:timex, "~> 3.7"},
|
||||
{:image, "~> 0.42"}
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[
|
||||
"assets.deploy": [
|
||||
"tailwind default --minify",
|
||||
"esbuild default --minify --sourcemap --target=chrome58,firefox57,safari11,edge16"
|
||||
],
|
||||
"site.index": "cmd npx -y pagefind --site priv/output/",
|
||||
"site.deploy": [
|
||||
"site.build",
|
||||
"site.index",
|
||||
"assets.deploy"
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
39
mix.lock
39
mix.lock
|
@ -1,39 +0,0 @@
|
|||
%{
|
||||
"bandit": {:hex, :bandit, "1.2.2", "569fe5d0efb107c9af37a1e37e25ce2ceec293101a2d4bc512876fc3207192b5", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "2f89adb7281c78d4e75733e0a9e1b24f46f84d2993963d6fa57d0eafadec5f03"},
|
||||
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
||||
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
|
||||
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"floki": {:hex, :floki, "0.35.4", "cc947b446024732c07274ac656600c5c4dc014caa1f8fb2dfff93d275b83890d", [:mix], [], "hexpm", "27fa185d3469bd8fc5947ef0f8d5c4e47f0af02eb6b070b63c868f69e3af0204"},
|
||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"image": {:hex, :image, "0.42.0", "aa561f15b53c40ac571e7880083cecf1419ff405fc45dc95675c58aa308eaa22", [:mix], [{:bumblebee, "~> 0.3", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.5", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.23", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "19972043abadc40e2d77dc38fc57f52382859791f89a962b0f1425ae64262f7d"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.0.0", "4857ec2edaccd0934a923c2b0ba526c44a173c86b847e8db725172e9e51d11d6", [:mix], [], "hexpm", "cee794a052f243291d92fa3ccabcb4c29bb8d236f655fb03bcbdc3a8214b8d13"},
|
||||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
|
||||
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 889 B |
|
@ -1,4 +0,0 @@
|
|||
<%= header(ctx, attrs) %>
|
||||
<main class="prose-neutral prose max-w-none dark:prose-invert" data-pagefind-body>
|
||||
<%= inner_content %>
|
||||
</main>
|
|
@ -1,5 +0,0 @@
|
|||
<%= header(ctx, attrs) %>
|
||||
|
||||
<article class="prose-neutral prose max-w-none dark:prose-invert" data-pagefind-body>
|
||||
<%= inner_content %>
|
||||
</article>
|
|
@ -1,15 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="m-4 flex flex-row justify-center" lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="/assets/images/heart.webp">
|
||||
<title><%= prefix_title("sloane.sh", attrs[:page_title]) %></title>
|
||||
<link rel="stylesheet" href="/assets/css/app.css" />
|
||||
</head>
|
||||
<body class="w-full max-w-3xl bg-white text-neutral-900 dark:bg-neutral-900 dark:text-neutral-200 pb-8">
|
||||
<%= inner_content %>
|
||||
<script defer src="/assets/js/app.js"></script>
|
||||
<!-- generated by sloane_sh at <%= DateTime.utc_now() |> DateTime.to_iso8601() %> -->
|
||||
</body>
|
||||
</html>
|
|
@ -1,29 +0,0 @@
|
|||
+++
|
||||
page_title = "home"
|
||||
permalink = "/"
|
||||
+++
|
||||
|
||||
hey, i'm sloane! i'm a professional software engineer and an amateur musician, photographer, wife, chef, and pet mom.
|
||||
|
||||
### around the web
|
||||
|
||||
<ul>
|
||||
<li><a rel="me" href="https://tech.lgbt/@sloane" target="_blank">@sloane</a> on the fediverse</li>
|
||||
<li><a href="https://github.com/sloanelybutsurely" target="_blank">@sloanelybutsurely</a> on github</li>
|
||||
<li><a href="https://instagram.com/sloane_of_arc" target="_blank">@sloane_of_arc</a> on instagram</li>
|
||||
</ul>
|
||||
|
||||
### writing
|
||||
|
||||
i write more than i share but here are some things that have made it out of my drafts folder recently
|
||||
|
||||
<%= for post <- Enum.take(sorted_post_attrs(ctx), 3) do %>
|
||||
- <%= fmt_date(post[:date]) %>: [<%= post[:title] %>](<%= post[:permalink] %>)
|
||||
<% end %>
|
||||
|
||||
|
||||
### projects
|
||||
|
||||
<h4>🌱 <a href="https://screen.garden" target="_blank">screen.garden</a></h4>
|
||||
|
||||
real-time collaboration across obsidian and the web. currently in closed beta.
|
|
@ -1,11 +0,0 @@
|
|||
+++
|
||||
page_title = "posts"
|
||||
+++
|
||||
|
||||
<ul data-pagefind-ignore>
|
||||
<%= for post <- sorted_post_attrs(ctx) do %>
|
||||
<li>
|
||||
<%= fmt_date(post[:date]) %>: <a href="<%= post[:permalink] %>"><%= post[:title] %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
|
@ -1,16 +0,0 @@
|
|||
</main>
|
||||
<div class="mt-6 mx-auto" id="search"></div>
|
||||
|
||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
||||
<script src="/pagefind/pagefind-ui.js"></script>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new PagefindUI({
|
||||
element: '#search',
|
||||
showSubResults: true,
|
||||
showImages: false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
|
@ -1,33 +0,0 @@
|
|||
+++
|
||||
date = 2023-07-23
|
||||
title = "obsidianrc - a programmer's approach to obsidian"
|
||||
page_title = "obsidian"
|
||||
+++
|
||||
|
||||
my biggest hangup with obsidian has always been that it isn't vim. as a longtime vim user i'm used to being able to fully customize and extend my editing experience.
|
||||
|
||||
there are many existing community plugins that make it easy to customize some parts of obsidian but whenever i would have an idea or a desired workflow that didn't fit into one of those existing boxes i was stuck.
|
||||
|
||||
i had three options, roughly in order of immediate work required:
|
||||
|
||||
1. adapt my own workflows to fit the existing options
|
||||
1. contribute to an existing community plugin and hope that the original author has time to review, merge in, and publish my change
|
||||
1. build my own plugin to do exactly what i want
|
||||
|
||||
so today i had an idea for another, hopefully easier option: obsidianrc
|
||||
|
||||
an obsidianrc is a personal, custom plugin that resembles an "[rc](https://superuser.com/questions/144339/vimrc-screenrc-bashrc-kshrc-etc-what-does-the-rc-mean)" file like a `.vimrc`. it has no one purpose but instead is a grab-bag of whatever the author needed at the time. an obsidian user's obsidianrc evolves with her as she comes up with new workflows or changes her existing workflows.
|
||||
|
||||
the freedom of the obsidianrc is that it never needs to be used by anyone but its author. it's a workbench covered in jigs. if any bit of functionality ever sticks out as generally useful the code is easily (famous last words perhaps...) extracted into its own plugin and published in the community plugin repo.
|
||||
|
||||
## create your own obsidianrc
|
||||
|
||||
1. use the [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin) template
|
||||
1. name your repo `obsidianrc`
|
||||
1. install your obsidianrc in your vault
|
||||
1. fill your obsidianrc with whatever need whenever you need it
|
||||
1. (optional) use [BRAT](https://github.com/TfTHacker/obsidian42-brat) to install your obsidianrc and keep it up-to-date
|
||||
|
||||
## what's in my obsidianrc?
|
||||
|
||||
well at time of writing, not much. but you can take a look for yourself: [sloanelybutsurely/obsidianrc](https://github.com/sloanelybutsurely/obsidianrc)
|
|
@ -1,19 +0,0 @@
|
|||
+++
|
||||
date = 2023-12-28
|
||||
title = "in review: 2023"
|
||||
page_title = "in review: 2023"
|
||||
+++
|
||||
|
||||
- **january**: visited family in florida
|
||||
- **february**: switched to injections (dolls, i implore you to do the same)
|
||||
- **march**: back in florida for an elastic on-site (and an evening at disney world)
|
||||
- **april**: saw caroline polachek live
|
||||
- **may**: visited a friend in kentucky
|
||||
- **june**: was in a friend’s wedding, had brunch with friends passing through town, performed with the columbus gay men’s chorus, pride!! 🏳️🌈
|
||||
- **july**: vacationed in northern michigan, went to a minor league baseball game, saw an airshow (hayley’s first!), started a new job
|
||||
- **august**: went to the state fair
|
||||
- **september**: just enjoyed the last bits of summer
|
||||
- **october**: saw kim petras live in DC, started working towards my private pilot certificate
|
||||
- **november**: celebrated my 30th birthday
|
||||
- **december**: celebrated the holidays and rang in the new year with family and friends
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
+++
|
||||
title = "\"Uses This\""
|
||||
page_title = "uses"
|
||||
permalink = "/uses"
|
||||
date = 2024-03-09
|
||||
+++
|
||||
|
||||
> **Have you considered adding a `/uses/` page to your own site, answering the same questions?**
|
||||
> \- [Daniel](https://wafer.baby/@d) of [usesthis.com](https://usesthis.com/)
|
||||
|
||||
## Who are you, and what do you do?
|
||||
|
||||
I'm a professional software engineer and once engineering manager. I work a day job but I'm also working with a good friend and former boss to bring [screen.garden](https://screen.garden), a real-time collaboration tool for PKMs and the web, to life.
|
||||
|
||||
In my free time I sing with a local queer TTBB chorus, play table-top RPGs, watch Formula 1, and play video games.
|
||||
|
||||
## What hardware do you use?
|
||||
|
||||
I work atop a sit-stand desk I bought when I first started working remotely in 2017. It stays in the "sit" position 99% of the time. For work I use whatever machine my employer provides. Right now that's a 14in M3 MacBook Pro. Personally, I have an M1 MacBook Air which I love. A single thunderbolt cable runs from either of those machines to a [CalDigit TS4](https://www.caldigit.com/thunderbolt-station-4/) which connects it to power, ethernet, a USB hub, and my display.
|
||||
|
||||
I use just the one display, a [GIGABYTE M32U](https://www.gigabyte.com/Monitor/M32U), which is a 32 inch, 4k, 144Hz monitor. Whenever someone is talking about replacing their monitor I always bring up refresh rate. It's one of those things that sounds like you wouldn't notice but it actually makes looking at a screen for most of your day a lot more pleasant. I've sat a no-name-brand monitor light and a Logitech webcam atop it.
|
||||
|
||||
I have a collection of mechanical keyboards ([ErgoDox EZ](https://ergodox-ez.com/), [Keyboardio Atreus](https://shop.keyboard.io/products/keyboardio-atreus), to name a couple) which all live in a drawer while I type away on my [Glorious GMMK Pro](https://www.gloriousgaming.com/products/glorious-gmmk-pro-75-barebone-black) with [Glorious Panda tactile switches](https://www.gloriousgaming.com/products/glorious-panda-mechanical-switches?variant=37691905933487). I think Glorious's branding is a bit "cringe" to say the least but they were the only custom keyboard option I could get same-day at the nearby Micro Center when I needed to replace my [Pok3r](https://drop.com/buy/vortex-poker-iii-compact-keyboard) following a coffee spill incident.
|
||||
|
||||
I talk to my coworkers and friends through a [Blue Yeti mic](https://www.bestbuy.com/site/blue-microphones-blue-yeti-professional-multi-pattern-usb-condenser-microphone/9737441.p?skuId=9737441) that I bought when a former employer gave everyone a couple hundred dollars for work-from-home equipment in early 2020 (despite my having already worked from my home my entire tenure there).
|
||||
|
||||
At the edges of my desk are piles of scrunchies, a couple hair clips, my AirPods Pros, a pair of Sennheiser HD 600s, my iPhone 14 Pro Max (I always go "Max" or "Plus" for the extra battery life), an [Aquaphor lip balm stick](https://www.aquaphorus.com/products/lip-care/lip-repair-stick), some hand lotion, and a nice candle.
|
||||
|
||||
Away from my desk I have a collection of cameras but the one I use the most is my Leica M6 which I usually shoot with a Voigtlander Nokton Classic 35mm f/1.4. I digitize my negatives with a beat-up Sony a6000, a cheap macro lens, and a [Valoi easy35](https://www.valoi.co/easy35).
|
||||
|
||||
I have a couple Apple TV 4ks to stream content from the cloud and also the Plex Media Server running on a Synology NAS. I have a couple TVs in different rooms but the Xbox Series X stays connected to the 65 inch LG C1 OLED (once again which a high refresh rate).
|
||||
|
||||
Finally, currently sitting on my nightstand wrapped in some FiiO IEMs is a 5th iPod Classic (aka an iPod Video) whose hard disk I've [replaced with a 512gb microSD card](https://www.iflash.xyz/). It's really incredible how well it still works.
|
||||
|
||||
## And what software?
|
||||
|
||||
These days I'm macOS all-the-way. I'm fully integrated into the ecosystem and the ergonomics and reliability of development on the platform is unparalleled in my opinion. Obviously I use a ton of software so I'll limit (mostly) to things I keep pinned to my dock (although most of the time I'm launching things from [Alfred](https://www.alfredapp.com/)):
|
||||
|
||||
- [Firefox](https://www.mozilla.org/en-US/firefox/new/) to browse the web
|
||||
- [Fantastical](https://flexibits.com/fantastical) to manage several calendar accounts. I could just use Calendar.app but there are few features (like travel time and automatic event merging) that keep me renewing my subscription.
|
||||
- Mail.app for emails...
|
||||
- [Things 3](https://culturedcode.com/things/) makes sure I get things done. I switched from an Android phone to an iPhone many years ago just so I could use Things while I was away from my computer.
|
||||
- [Kitty](https://sw.kovidgoyal.net/kitty/) to run all of my command line apps. I always work within a `tmux` session so my terminal emulator doesn't really matter all that much (because I'm never using tabs or splits or whatever) but Kitty is quick and the [alternative icon](https://github.com/DinkDonk/kitty-icon) I use for it is really cute. I'm a vim user (neovim really) and have been since 2015. My neovim setup could be its own post...
|
||||
- Music - I switch between two libraries: 1. My local library which I sync with my iPod and 2. My iCloud, Apple Music backed library
|
||||
- [Dash 6](https://kapeli.com/dash) (usually via Alfred) to quickly reference documentation. Elixir / Hex package docs support is incredible
|
||||
- [Obsidian](https://obsidian.md) for personal, work, and TTRPG notes. I keep my plugins list slip with just Templater, DataView, Tasks, Periodic Notes, and of course screen.garden.
|
||||
- [Readwise Reader](https://readwise.io/read) as my read-later service
|
||||
|
||||
I have to shout-out Lightroom with [Negative Lab Pro](https://www.negativelabpro.com/) for converting scans/photos of film negatives.
|
||||
|
||||
|
||||
## What would be your dream setup?
|
||||
|
||||
I've obviously spoiled myself already so I'd keep most things the same but...
|
||||
|
||||
I'd love a thunderbolt KVM of some kind that would let me swap _quickly_ between machines at the press of a button. I also feel like I'd benefit from a larger desk.
|
||||
|
||||
I think about replacing my webcam with the Sony a6000 and replacing that with a newer, higher resolution mirrorless camera.
|
||||
|
||||
I'm really hoping the ARM desktop / server market continues to become more accessible to the consumer market because the Synology NAS is looking a little worse-for-wear these days. I've thought about replacing it with a custom build x64 machine but the additional power consumption and heat keep me from doing it (I'm spoiled by these Apple ARM machines...).
|
3
site/index.html.pm
Normal file
3
site/index.html.pm
Normal file
|
@ -0,0 +1,3 @@
|
|||
#lang pollen
|
||||
|
||||
hey, i'm sloane! i'm a professional software engineer and an amateur musician, photographer, wife, chef, and pet mom.
|
3
site/index.ptree
Normal file
3
site/index.ptree
Normal file
|
@ -0,0 +1,3 @@
|
|||
#lang pollen
|
||||
|
||||
index.html
|
17
site/pollen.rkt
Normal file
17
site/pollen.rkt
Normal file
|
@ -0,0 +1,17 @@
|
|||
#lang racket/base
|
||||
(require pollen/decode pollen/misc/tutorial txexpr)
|
||||
(provide (all-defined-out))
|
||||
|
||||
(define site-name "sloane.sh")
|
||||
(define email "sloane@fastmail.com")
|
||||
|
||||
(define txexpr-elements-proc decode-paragraphs)
|
||||
(define string-proc (compose1 smart-quotes smart-dashes))
|
||||
|
||||
(define (root . elements)
|
||||
(txexpr 'root empty (decode-elements elements
|
||||
#:txexpr-elements-proc txexpr-elements-proc
|
||||
#:string-proc string-proc)))
|
||||
|
||||
(module setup racket/base
|
||||
(provide (all-defined-out)))
|
10
site/template.html.p
Normal file
10
site/template.html.p
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>◊|site-name|</title>
|
||||
</head>
|
||||
<body>
|
||||
◊(->html doc)
|
||||
</body>
|
||||
</html>
|
|
@ -1,42 +0,0 @@
|
|||
defmodule SloaneSH.FrontMatterTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias SloaneSH.FrontMatter
|
||||
|
||||
test "parses TOML front matter" do
|
||||
document = ~S"""
|
||||
+++
|
||||
foo = "bar"
|
||||
+++
|
||||
# Hello, World!
|
||||
|
||||
This is a document with front matter.
|
||||
"""
|
||||
|
||||
assert {:ok, %{foo: "bar"}, "# Hello, World!" <> _} = FrontMatter.parse(document, %{})
|
||||
end
|
||||
|
||||
test "returns an empty map is the document doesn't have front matter" do
|
||||
document = ~S"""
|
||||
# Hello, World!
|
||||
|
||||
This is a document with front matter.
|
||||
"""
|
||||
|
||||
assert {:ok, %{} = map, document} = FrontMatter.parse(document, %{})
|
||||
assert %{} = map
|
||||
end
|
||||
|
||||
test "errors in TOML front matter produce an error" do
|
||||
document = ~S"""
|
||||
+++
|
||||
foo = "bar
|
||||
+++
|
||||
# Hello, World!
|
||||
|
||||
This is a document with front matter.
|
||||
"""
|
||||
|
||||
assert {:error, _} = FrontMatter.parse(document, %{})
|
||||
end
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule SloaneSHTest do
|
||||
use ExUnit.Case
|
||||
end
|
|
@ -1 +0,0 @@
|
|||
ExUnit.start()
|
Loading…
Reference in a new issue