M .vscode/settings.json => .vscode/settings.json +1 -1
@@ 1,5 1,5 @@
{
"files.associations": {
- "*.html": "mustache"
+ "*.thtml": "mustache"
}
}=
\ No newline at end of file
M kethel/lib/bricks.ex => kethel/lib/bricks.ex +5 -3
@@ 8,9 8,11 @@ defmodule Bricks do
@typedoc "A valid mustache template"
@type mustache() :: binary()
- @spec collect(relative_path()) :: %{ brick_name() => mustache() }
+ @type bricks() :: %{brick_name() => mustache()}
+
+ @spec collect(relative_path()) :: bricks()
def collect(folder) do
Path.wildcard(folder <> "/bricks/*.thtml")
- |> Map.new(fn x -> { FileX.trimmed_filename(x), File.read!(x) } end)
+ |> Map.new(fn x -> {FileX.trimmed_filename(x), File.read!(x)} end)
end
- end
+end
M kethel/lib/common.ex => kethel/lib/common.ex +1 -1
@@ 1,3 1,3 @@
defmodule Context do
- defstruct project_root: "/", bricks: %{}, fxg: "fxg"
+ defstruct project_root: "/", fxg: "fxg"
end
M kethel/lib/pages.ex => kethel/lib/pages.ex +37 -26
@@ 1,49 1,60 @@
require Rambo
defmodule Pages do
- defstruct [ :source_files, :main_template, :project_root, :bricks ]
+ defstruct [:source_files, :main_template, :project_root, :bricks]
- def trim_abs_path(path, context) do
- begin = byte_size(context.project_root) + byte_size("pages/") # ROOT + pages/
- binary_part(path, begin, byte_size(path) - begin - byte_size(".fxg")) # remove extension
+ defp trim_abs_path(path, project_root) do
+ # ROOT + pages/
+ begin = byte_size(project_root) + byte_size("pages/")
+ # remove extension
+ binary_part(path, begin, byte_size(path) - begin - byte_size(".fxg"))
end
- @spec collect(%Context{}) :: %Pages{}
- def collect(context) do
- pages_root = context.project_root <> "/pages"
+ @spec collect(%Context{}, Bricks.bricks()) :: %Pages{}
+ def collect(project_root, bricks) do
+ pages_root = project_root <> "/pages"
+
%Pages{
- project_root: context.project_root,
- source_files: Path.wildcard(pages_root <> "/**/*.fxg")
- |> Enum.map(fn path -> { path, File.read!(path) } end)
- |> Enum.map(fn { path, data } -> { trim_abs_path(path, context), data } end),
- main_template: File.read!(context.project_root <> "/templates/base.thtml"),
- bricks: context.bricks,
+ project_root: project_root,
+ source_files:
+ Path.wildcard(pages_root <> "/**/*.fxg")
+ |> Task.async_stream(fn path -> {trim_abs_path(path, project_root), File.read!(path)} end)
+ # we assume every file read succeeds :)) always be positive
+ |> Enum.map(fn {:ok, val} -> val end),
+ main_template: File.read!(project_root <> "/templates/base.thtml"),
+ bricks: bricks
}
end
@spec compile(%Pages{}) :: none()
def compile(pages) do
- Task.async_stream(pages.source_files, fn page -> compile_file(pages, page) end) |> Enum.to_list()
+ Task.async_stream(pages.source_files, fn page -> compile_file(pages, page) end)
+ |> Enum.to_list()
end
- @doc "compiles a file, already read into process memory. Does not return the binary, but instantly writes it to disk"
- @spec compile_file(%Pages{}, { }) :: none()
- defp compile_file(pages, { page_path, page_data }) do
+ # doc "compiles a file, already read into process memory. Does not return the binary, but instantly writes it to disk"
+ @spec compile_file(%Pages{}, {}) :: none()
+ defp compile_file(pages, {page_path, page_data}) do
output_path = "#{pages.project_root}/build#{page_path}.html"
- IO.puts("#{pages.project_root}/pages#{page_path}.fxg -> #{output_path}")
# check for a custom template
custom_template_path = "#{pages.project_root}/templates/pages#{page_path}.thtml"
- template = case File.read(custom_template_path) do
- {:ok, custom_template} -> custom_template
- {:error, :enoent} -> pages.main_template
- {:error, err } -> raise err
- end
- mustache_context = Map.merge(%{fxg_content: invoke_fxg(page_data), location: page_path}, pages.bricks)
+
+ template =
+ case File.read(custom_template_path) do
+ {:ok, custom_template} -> custom_template
+ {:error, :enoent} -> pages.main_template
+ {:error, err} -> raise err
+ end
+
+ mustache_context =
+ Map.merge(%{fxg_content: invoke_fxg(page_data), location: page_path}, pages.bricks)
+
page_rendered = Mustache.render(template, mustache_context)
File.write!(output_path, page_rendered)
+ IO.puts("[COMPILED] #{pages.project_root}/pages#{page_path}.fxg -> #{output_path}")
end
-
- @doc "Invokes the fxg compiler, a la `fxg -`, piping `data` into the process stdin, and reading the stdout. "
+
+ # doc "Invokes the fxg compiler, a la `fxg -`, piping `data` into the process stdin, and reading the stdout. "
@spec invoke_fxg(binary()) :: binary()
defp invoke_fxg(data) do
{:ok, res} = Rambo.run("fxg", ["-"], in: data)
A kethel/lib/scripts.ex => kethel/lib/scripts.ex +47 -0
@@ 0,0 1,47 @@
+require Rambo
+
+defmodule Scripts do
+ defstruct [:scripts, :project_root]
+
+ defp ensure_file_structure(project_root) do
+ File.mkdir_p!("#{project_root}/build/scripts")
+ end
+
+ defp trim_abs_path(path, project_root) do
+ # ROOT + scripts/
+ begin = byte_size(project_root) + byte_size("scripts/")
+ # remove extension
+ binary_part(path, begin, byte_size(path) - begin)
+ end
+
+ def collect(project_root) do
+ %Scripts{
+ scripts:
+ Path.wildcard(project_root <> "/scripts/**/*.js")
+ |> Task.async_stream(fn script ->
+ {trim_abs_path(script, project_root), File.read!(script)}
+ end)
+ |> Enum.map(fn {:ok, val} -> val end),
+ project_root: project_root
+ }
+ end
+
+ def compile(scripts) do
+ ensure_file_structure(scripts.project_root)
+
+ scripts.scripts
+ |> Task.async_stream(fn script -> compile_file(script, scripts.project_root) end)
+ |> Enum.map(fn {:ok, val} -> val end)
+
+ :ok
+ end
+
+ # data | npx lightningcss -m
+ defp compile_file({path, data}, project_root) do
+ {:ok, %Rambo{out: out}} = Rambo.run("npx", ["uglifyjs"], in: data)
+ output_path = "#{project_root}/build/scripts#{path}"
+ File.write!(output_path, out)
+
+ IO.puts("[COMPILED] #{project_root}/scripts#{path} -> #{output_path}")
+ end
+end
A kethel/lib/statics.ex => kethel/lib/statics.ex +31 -0
@@ 0,0 1,31 @@
+defmodule Statics do
+ @moduledoc """
+ Statics is an exception, because we only copy the files. Therefore, the performance gain from reading files into memory for processing isn't worth it. The compile fuction gets called at load-time for all other asset types.
+ """
+
+ @spec ensure_file_structure(binary()) :: :ok
+ defp ensure_file_structure(project_root) do
+ File.mkdir_p!("#{project_root}/build/static/")
+ end
+
+ defp trim_abs_path(path, project_root) do
+ # ROOT + static
+ begin = byte_size(project_root) + byte_size("static/")
+ # remove extension
+ binary_part(path, begin, byte_size(path) - begin)
+ end
+
+ # we dont async_stream here because copying is (relatively speaking) inexpensive, and we want to keep more threads open for other (more expensive) compilation processes
+ def compile(project_root) do
+ ensure_file_structure(project_root)
+
+ for path <- Path.wildcard("#{project_root}/static/**/*") do
+ rel_path = trim_abs_path(path, project_root)
+ output_path = "#{project_root}/build/static#{rel_path}"
+
+ File.copy!(path, output_path)
+
+ IO.puts("[ COPIED ] #{path} -> #{output_path}")
+ end
+ end
+end
A kethel/lib/styles.ex => kethel/lib/styles.ex +45 -0
@@ 0,0 1,45 @@
+require Rambo
+
+defmodule Styles do
+ defstruct [:styles, :project_root]
+
+ defp ensure_file_structure(project_root) do
+ File.mkdir_p!("#{project_root}/build/styles")
+ end
+
+ defp trim_abs_path(path, project_root) do
+ # ROOT + styles/
+ begin = byte_size(project_root) + byte_size("styles/")
+ # remove extension
+ binary_part(path, begin, byte_size(path) - begin)
+ end
+
+ def collect(project_root) do
+ %Styles{
+ styles:
+ Path.wildcard(project_root <> "/styles/**/*.css")
+ |> Task.async_stream(fn style ->
+ {trim_abs_path(style, project_root), File.read!(style)}
+ end)
+ |> Enum.map(fn {:ok, val} -> val end),
+ project_root: project_root
+ }
+ end
+
+ def compile(styles) do
+ ensure_file_structure(styles.project_root)
+
+ styles.styles
+ |> Task.async_stream(fn style -> compile_file(style, styles.project_root) end)
+ |> Enum.map(fn {:ok, val} -> val end)
+ end
+
+ # data | npx lightningcss -m
+ defp compile_file({path, data}, project_root) do
+ {:ok, %Rambo{out: out}} = Rambo.run("npx", ["lightningcss", "-m"], in: data)
+ output_path = "#{project_root}/build/styles#{path}"
+ File.write!(output_path, out)
+
+ IO.puts("[COMPILED] #{project_root}/styles#{path} -> #{output_path}")
+ end
+end
M kethel/main.exs => kethel/main.exs +21 -9
@@ 3,19 3,31 @@ args = System.argv()
folder = Enum.at(args, 1)
if folder != nil do
- project_root = Path.expand(folder)
- context = %Context{
- project_root: project_root,
- bricks: Bricks.collect(project_root),
- fxg: System.find_executable("fxg")
- }
-
+ project_root = Path.expand(folder)
+
File.rm_rf!("#{project_root}/build")
File.mkdir_p("#{project_root}/build")
- pages = Pages.collect(context)
+ # other contents depend on this, so we cant async it
+ bricks = Bricks.collect(project_root)
+
+ # special case, see module documentation
+ statics_task = Task.async(fn -> Statics.compile(project_root) end)
+
+ styles_task = Task.async(fn -> Styles.collect(project_root) end)
+ pages_task = Task.async(fn -> Pages.collect(project_root, bricks) end)
+ scripts_task = Task.async(fn -> Scripts.collect(project_root) end)
+
+ [styles, pages, scripts] = Task.await_many([styles_task, pages_task, scripts_task], :infinity)
+
+ pages_compile_task = Task.async(fn -> Pages.compile(pages) end)
+ styles_compile_task = Task.async(fn -> Styles.compile(styles) end)
+ scripts_compile_task = Task.async(fn -> Scripts.compile(scripts) end)
- Pages.compile(pages)
+ Task.await_many(
+ [pages_compile_task, styles_compile_task, statics_task, scripts_compile_task],
+ :infinity
+ )
else
IO.puts(:stderr, "The root project folder should be provided on the command line")
end
M kethel/mix.exs => kethel/mix.exs +2 -2
@@ 13,8 13,8 @@ defmodule Kethel.MixProject do
defp deps do
[
- { :mustache, "~> 0.5.0" },
- {:rambo, "~> 0.3.4" },
+ {:mustache, "~> 0.5.0"},
+ {:rambo, "~> 0.3.4"}
]
end
end
R templates/base.html => templates/base.thtml +0 -0
R templates/pages/colophon.html => templates/pages/colophon.thtml +0 -0
R templates/pages/index.html => templates/pages/index.thtml +0 -0
R templates/paste.html => templates/paste.thtml +0 -0
R templates/thought.html => templates/thought.thtml +0 -0
R templates/thoughts.html => templates/thoughts.thtml +0 -0