~mika/www.m1kadev.nl

3a6b96458aaa637309a1ceef651158071bd4de84 — m1kadev 6 months ago 434a746
Implement easy static content
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