From 3a6b96458aaa637309a1ceef651158071bd4de84 Mon Sep 17 00:00:00 2001 From: m1kadev <67912368+m1kadev@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:39:16 +0100 Subject: [PATCH] Implement easy static content --- .vscode/settings.json | 2 +- kethel/lib/bricks.ex | 8 ++- kethel/lib/common.ex | 2 +- kethel/lib/pages.ex | 63 +++++++++++-------- kethel/lib/scripts.ex | 47 ++++++++++++++ kethel/lib/statics.ex | 31 +++++++++ kethel/lib/styles.ex | 45 +++++++++++++ kethel/main.exs | 30 ++++++--- kethel/mix.exs | 4 +- templates/{base.html => base.thtml} | 0 .../pages/{colophon.html => colophon.thtml} | 0 templates/pages/{index.html => index.thtml} | 0 templates/{paste.html => paste.thtml} | 0 templates/{thought.html => thought.thtml} | 0 templates/{thoughts.html => thoughts.thtml} | 0 15 files changed, 190 insertions(+), 42 deletions(-) create mode 100644 kethel/lib/scripts.ex create mode 100644 kethel/lib/statics.ex create mode 100644 kethel/lib/styles.ex rename templates/{base.html => base.thtml} (100%) rename templates/pages/{colophon.html => colophon.thtml} (100%) rename templates/pages/{index.html => index.thtml} (100%) rename templates/{paste.html => paste.thtml} (100%) rename templates/{thought.html => thought.thtml} (100%) rename templates/{thoughts.html => thoughts.thtml} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c5fb46dd2396e93217cea58771cb78edc49b6f6..a21f9518f13f9d20d1c470701f0e3b20938a901a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "files.associations": { - "*.html": "mustache" + "*.thtml": "mustache" } } \ No newline at end of file diff --git a/kethel/lib/bricks.ex b/kethel/lib/bricks.ex index da7440c3dbb1fe1b80a47d515bc6a6a86a3fbe67..04d3d9687abd8156ce554974b2a9377deeac0911 100644 --- a/kethel/lib/bricks.ex +++ b/kethel/lib/bricks.ex @@ -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 diff --git a/kethel/lib/common.ex b/kethel/lib/common.ex index 278247d42c5e37f9f7b601f5c05a8e9f950aa68b..ced880ec798fc7963e256cadc544c010c23faf4c 100644 --- a/kethel/lib/common.ex +++ b/kethel/lib/common.ex @@ -1,3 +1,3 @@ defmodule Context do - defstruct project_root: "/", bricks: %{}, fxg: "fxg" + defstruct project_root: "/", fxg: "fxg" end diff --git a/kethel/lib/pages.ex b/kethel/lib/pages.ex index b80b7b99af23279bfb18d45a4a63b1df4ce53eae..d211bc7e2bf0e10728ba46d71c63d5f4670be53c 100644 --- a/kethel/lib/pages.ex +++ b/kethel/lib/pages.ex @@ -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) diff --git a/kethel/lib/scripts.ex b/kethel/lib/scripts.ex new file mode 100644 index 0000000000000000000000000000000000000000..aafe0f4c520c28a4af3f979d1f6708d37fd943c0 --- /dev/null +++ b/kethel/lib/scripts.ex @@ -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 diff --git a/kethel/lib/statics.ex b/kethel/lib/statics.ex new file mode 100644 index 0000000000000000000000000000000000000000..26741e069d20f1cdbf609e02f35c47694706ab99 --- /dev/null +++ b/kethel/lib/statics.ex @@ -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 diff --git a/kethel/lib/styles.ex b/kethel/lib/styles.ex new file mode 100644 index 0000000000000000000000000000000000000000..793d41c79e5ff1e47c86cc5309db5a67cdbb3a0e --- /dev/null +++ b/kethel/lib/styles.ex @@ -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 diff --git a/kethel/main.exs b/kethel/main.exs index ba3b05782b109b04cb853731b0b69dfb2acd97b0..9645e111fc1c702ca09aef921f05eace84dd67b7 100644 --- a/kethel/main.exs +++ b/kethel/main.exs @@ -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 diff --git a/kethel/mix.exs b/kethel/mix.exs index 19ae81ac927c9c81fc10092239b18c418cebac5f..e1edd6f512c1197c3f4ce81e34c0e327e2b93fdb 100644 --- a/kethel/mix.exs +++ b/kethel/mix.exs @@ -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 diff --git a/templates/base.html b/templates/base.thtml similarity index 100% rename from templates/base.html rename to templates/base.thtml diff --git a/templates/pages/colophon.html b/templates/pages/colophon.thtml similarity index 100% rename from templates/pages/colophon.html rename to templates/pages/colophon.thtml diff --git a/templates/pages/index.html b/templates/pages/index.thtml similarity index 100% rename from templates/pages/index.html rename to templates/pages/index.thtml diff --git a/templates/paste.html b/templates/paste.thtml similarity index 100% rename from templates/paste.html rename to templates/paste.thtml diff --git a/templates/thought.html b/templates/thought.thtml similarity index 100% rename from templates/thought.html rename to templates/thought.thtml diff --git a/templates/thoughts.html b/templates/thoughts.thtml similarity index 100% rename from templates/thoughts.html rename to templates/thoughts.thtml