diff --git a/lib/sloane_sh.ex b/lib/sloane_sh.ex index 8b4582f..3e90d5d 100644 --- a/lib/sloane_sh.ex +++ b/lib/sloane_sh.ex @@ -27,6 +27,5 @@ defmodule SloaneSH do def context do Context.new() - |> Context.init() end end diff --git a/lib/sloane_sh/asset.ex b/lib/sloane_sh/asset.ex new file mode 100644 index 0000000..c66749f --- /dev/null +++ b/lib/sloane_sh/asset.ex @@ -0,0 +1,26 @@ +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 :: binary()} | {: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 diff --git a/lib/sloane_sh/assets/image.ex b/lib/sloane_sh/assets/image.ex new file mode 100644 index 0000000..e69de29 diff --git a/lib/sloane_sh/assets/markdown.ex b/lib/sloane_sh/assets/markdown.ex new file mode 100644 index 0000000..be33c15 --- /dev/null +++ b/lib/sloane_sh/assets/markdown.ex @@ -0,0 +1,78 @@ +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 + 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 = EEx.eval_string(data, ctx: ctx, attrs: 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 + + def handle_attrs(cfg, path, data, attrs) do + attrs + end + + defoverridable handle_attrs: 4 + end + end +end diff --git a/lib/sloane_sh/assets/page.ex b/lib/sloane_sh/assets/page.ex new file mode 100644 index 0000000..84065b7 --- /dev/null +++ b/lib/sloane_sh/assets/page.ex @@ -0,0 +1,3 @@ +defmodule SloaneSH.Assets.Page do + use SloaneSH.Assets.Markdown, type: :page +end diff --git a/lib/sloane_sh/assets/post.ex b/lib/sloane_sh/assets/post.ex new file mode 100644 index 0000000..e09c69c --- /dev/null +++ b/lib/sloane_sh/assets/post.ex @@ -0,0 +1,12 @@ +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 diff --git a/lib/sloane_sh/build.ex b/lib/sloane_sh/build.ex index ff62201..d96d351 100644 --- a/lib/sloane_sh/build.ex +++ b/lib/sloane_sh/build.ex @@ -2,60 +2,28 @@ defmodule SloaneSH.Build do require Logger alias SloaneSH.Context - alias SloaneSH.Layouts - alias SloaneSH.Markdown - alias SloaneSH.Write def run(%Context{} = ctx) do - ctx - |> build_pages() - |> build_posts() - |> copy_img() - end + assets = ctx.posts ++ ctx.pages - def build_pages(%Context{} = ctx) do - Logger.info("Building pages...") - for page <- ctx.pages, do: build_page(ctx, page) + File.mkdir_p!(ctx.config.output_dir) - ctx - end + 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 - 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, md} <- Markdown.transform(ctx, data), - contents = Layouts.page_layout(ctx, md.attrs, md.html), - html = Layouts.root_layout(ctx, md.attrs, contents), - :ok <- Write.page(ctx, page, html) do - Logger.info("Built page: #{page}") - else - err -> Logger.error("Failed to build page #{page}: #{inspect(err)}") + err -> + Logger.error("Failed to render #{inspect(asset.src)}, #{inspect(err)}") + end end end - - def build_post(%Context{} = ctx, post) do - path = Path.join(ctx.config.posts_dir, post) - - with {:ok, data} <- File.read(path), - {:ok, md} <- Markdown.transform(ctx, data), - contents = Layouts.post_layout(ctx, md.attrs, md.html), - html = Layouts.root_layout(ctx, md.attrs, contents), - :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 - - def copy_img(%Context{} = ctx) do - ctx - end end diff --git a/lib/sloane_sh/config.ex b/lib/sloane_sh/config.ex index 9995fc7..7c59a48 100644 --- a/lib/sloane_sh/config.ex +++ b/lib/sloane_sh/config.ex @@ -22,11 +22,6 @@ defmodule SloaneSH.Config do } 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} -> diff --git a/lib/sloane_sh/context.ex b/lib/sloane_sh/context.ex index af0e4aa..9404f45 100644 --- a/lib/sloane_sh/context.ex +++ b/lib/sloane_sh/context.ex @@ -4,64 +4,53 @@ defmodule SloaneSH.Context do files. """ use TypedStruct + require Logger alias SloaneSH.Config + alias SloaneSH.Asset + alias SloaneSH.Assets.Page + alias SloaneSH.Assets.Post alias __MODULE__ typedstruct do field :config, Config.t(), enforce: true - field :pages, [String.t()], default: [] - field :posts, [String.t()], default: [] + field :pages, [Asset.t()], default: [] + field :posts, [Asset.t()], default: [] end - def new(config \\ Config.default()) do - %Context{config: config} + def new(cfg \\ Config.default()) do + pages = load_assets(cfg, Page, cfg.pages_dir) + posts = load_assets(cfg, Post, cfg.posts_dir) + + %Context{config: cfg, pages: pages, posts: posts} 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 + defp load_assets(cfg, mod, src_dir) do + exts = mod.extensions(cfg) - 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])} + for src <- collect_src_files(src_dir, exts) do + contents = File.read!(src) - String.starts_with?(path, config.posts_dir) -> - post = Path.relative_to(path, config.posts_dir) - %{ctx | posts: Enum.uniq([post | ctx.posts])} + case mod.attrs(cfg, src, contents) do + {:ok, attrs, src_contents} -> + %Asset{mod: mod, src: src, src_contents: src_contents, attrs: attrs} - true -> - ctx - end - else - ctx - end - end + {:ok, attrs} -> + %Asset{mod: mod, src: src, src_contents: contents, attrs: attrs} - 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 + _ -> + 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 diff --git a/lib/sloane_sh/front_matter.ex b/lib/sloane_sh/front_matter.ex index c761b9e..c4bca00 100644 --- a/lib/sloane_sh/front_matter.ex +++ b/lib/sloane_sh/front_matter.ex @@ -3,7 +3,7 @@ defmodule SloaneSH.FrontMatter do Parses TOML front matter out put files """ - def parse("+++" <> rest, _ctx) do + def parse("+++" <> rest) do [toml, body] = String.split(rest, ["+++\n", "+++\r\n"], parts: 2) with {:ok, attrs} <- Toml.decode(toml, keys: :atoms) do @@ -11,7 +11,7 @@ defmodule SloaneSH.FrontMatter do end end - def parse(body, _ctx) do + def parse(body) do {:ok, %{}, body} end end diff --git a/lib/sloane_sh/layouts.ex b/lib/sloane_sh/layouts.ex index 8af41a1..1f1563c 100644 --- a/lib/sloane_sh/layouts.ex +++ b/lib/sloane_sh/layouts.ex @@ -7,24 +7,36 @@ defmodule SloaneSH.Layouts do @layouts_dir Path.join(:code.priv_dir(:sloane_sh), "site/layouts") - EEx.function_from_file(:def, :root_layout, Path.join(@layouts_dir, "root.html.eex"), [ + EEx.function_from_file(:def, :root, Path.join(@layouts_dir, "root.html.eex"), [ + :inner_content, :ctx, - :attrs, - :inner_content + :attrs ]) - EEx.function_from_file(:def, :page_layout, Path.join(@layouts_dir, "page.html.eex"), [ + EEx.function_from_file(:defp, :page_layout, Path.join(@layouts_dir, "page.html.eex"), [ + :inner_content, :ctx, - :attrs, - :inner_content + :attrs ]) - EEx.function_from_file(:def, :post_layout, Path.join(@layouts_dir, "post.html.eex"), [ + EEx.function_from_file(:defp, :post_layout, Path.join(@layouts_dir, "post.html.eex"), [ + :inner_content, :ctx, - :attrs, - :inner_content + :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 diff --git a/lib/sloane_sh/output_dirs.ex b/lib/sloane_sh/output_dirs.ex new file mode 100644 index 0000000..6caf30f --- /dev/null +++ b/lib/sloane_sh/output_dirs.ex @@ -0,0 +1,40 @@ +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 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 +end diff --git a/lib/sloane_sh/write.ex b/lib/sloane_sh/write.ex deleted file mode 100644 index 48da04a..0000000 --- a/lib/sloane_sh/write.ex +++ /dev/null @@ -1,33 +0,0 @@ -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_dir, 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 diff --git a/mix.exs b/mix.exs index 66f38cb..35c09a4 100644 --- a/mix.exs +++ b/mix.exs @@ -41,7 +41,12 @@ defmodule SloaneSH.MixProject do "tailwind default --minify", "esbuild default --minify --sourcemap --target=chrome58,firefox57,safari11,edge16" ], - "site.index": "cmd npx -y pagefind --site priv/output/" + "site.index": "cmd npx -y pagefind --site priv/output/", + "site.deploy": [ + "site.build", + "site.index", + "assets.deploy" + ] ] end end diff --git a/priv/site/pages/index.md b/priv/site/pages/index.md index 0d33d77..624975a 100644 --- a/priv/site/pages/index.md +++ b/priv/site/pages/index.md @@ -1,5 +1,4 @@ +++ -permalink = "/" page_title = "home" +++ diff --git a/priv/site/pages/search.md b/priv/site/pages/search.html similarity index 90% rename from priv/site/pages/search.md rename to priv/site/pages/search.html index 45fb6f7..39c91da 100644 --- a/priv/site/pages/search.md +++ b/priv/site/pages/search.html @@ -1,7 +1,3 @@ -+++ -permalink = "/search" -+++ -
diff --git a/priv/site/posts/2024-02-16-test-post.md b/priv/site/posts/2024-02-16-test-post.md index 361ca99..c83884e 100644 --- a/priv/site/posts/2024-02-16-test-post.md +++ b/priv/site/posts/2024-02-16-test-post.md @@ -1,6 +1,7 @@ +++ title = "Test Post" page_title = "Test Post" +date = 2024-02-16 +++ # Test Post