~mika/www.m1kadev.nl

4d656d6d3c0d08b651351dd9aa938794e7a481b6 — m1kadev 6 months ago 1a4d481
Start refactoring build.exs into kethel
7 files changed, 87 insertions(+), 371 deletions(-)

D build.exs
A kethel/.formatter.exs
A kethel/.gitignore
A kethel/lib/bricks.ex
A kethel/main.exs
A kethel/mix.exs
A kethel/mix.lock
D build.exs => build.exs +0 -371
@@ 1,371 0,0 @@
require Mix
require EEx

Mix.install([
  {:tzdata, "~> 1.1"},
  {:mustache, "~> 0.5.0"}
])

Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)

defmodule Git do
  def get_file_commit(path) do
    {ts_string, 0} = System.cmd("git", ["log", "--format=%ct", path])
    String.to_integer(String.trim_trailing(ts_string))
  end
end

defmodule Templates do
  binfo_template = """
  commit=<%= commit %>lightningcss=<%= lightningcss %>html_minifier_next=<%= html_minifier_next %>uglifyjs=<%= uglifyjs %>build_time=<%= build_time %>
  """

  pastelink_template = """
  <a href="/<%= location %>.html"><%= name %></a> | <%= date %>
  """

  EEx.function_from_string(
    :def,
    :build_info,
    binfo_template,
    [
      :commit,
      :lightningcss,
      :html_minifier_next,
      :uglifyjs,
      :build_time
    ]
  )

  EEx.function_from_string(
    :def,
    :pastelink,
    pastelink_template,
    [
      :location,
      :name,
      :date
    ]
  )

  def ogp(title, url, description) do
    """
      <meta property="og:type" content="website">
      <meta property="og:title" content="#{title}">
      <meta property="og:url" content="#{url}">
      <meta property="og:image" content="https://m1kadev.nl/static/logo.png">
      <meta property="og:description" content="#{description}">
      <meta property="og:locale" content="en_GB" />
      <meta property="og:site_name" content="m1kadev.nl" />
    """
  end
end

defmodule Builders do
  def html(input_path) do
    output_path = String.replace_prefix(input_path, "src", "build")

    {_, 0} =
      System.cmd("html-minifier-next", [
        "--preset",
        "conservative",
        "-o",
        output_path,
        input_path
      ])
  end

  def css(input_path) do
    output_path = "build/" <> input_path

    {_, 0} =
      System.cmd("lightningcss", [
        "-m",
        "-o",
        output_path,
        input_path
      ])
  end

  def js(input_path) do
    output_path = "build/" <> input_path

    {_, 0} =
      System.cmd("uglifyjs", [
        "-c",
        "-o",
        output_path,
        input_path
      ])
  end

  def fxg(input_path, template, bricks) do
    own_template =
      input_path
      |> String.replace_prefix("pages", "templates/pages")
      |> String.replace_suffix("fxg", "html")

    case File.read(own_template) do
      {:ok, ctemplate} -> _fxg(input_path, ctemplate, bricks)
      _ -> _fxg(input_path, template, bricks)
    end
  end

  def paste(input_path, template, bricks) do
    {:ok, code} = File.read(input_path)
    IO.inspect(code)

    output = "build/" <> input_path <> ".html"

    lang = Path.extname(input_path) |> String.slice(1..-1//1)
    file = Path.basename(input_path)
    time = FileX.createdf(input_path)

    result =
      Mustache.render(
        template,
        Map.merge(
          %{
            filename: file,
            about_paste: "Created on " <> time,
            language: "<script src=\"static/" <> lang <> ".min.js\"></script>",
            codetag: "<pre><code class=\"language-" <> lang <> "\">",
            code: code,
            ogp:
              Templates.ogp(file, "https://m1kadev.nl/pastes/#{file}.html", "Created on " <> time)
          },
          bricks
        )
      )

    IO.inspect(input_path)

    :ok = File.write(output, result)
  end

  defp _fxg(input_path, template, bricks) do
    output_path =
      String.replace_prefix(input_path, "pages", "build")
      |> String.replace_suffix("fxg", "html")

    name =
      case Path.basename(input_path) |> Path.rootname() do
        "index" -> "home"
        x -> x
      end

    {content, 0} = System.cmd("fxg", [input_path])

    ogp =
      Templates.ogp(
        name,
        "https://m1kadev.nl/#{String.replace_prefix(output_path, "build/", "")}",
        ""
      )

    output =
      Mustache.render(
        template,
        Map.merge(
          %{
            fxg_content: content,
            location: name,
            ogp: ogp
          },
          bricks
        )
      )

    IO.inspect(output_path)
    :ok = File.write(output_path, output)
  end

  def thought(path, unix) do
    thought = File.read!(path)
    name = String.replace_prefix(path, "thoughts/", "")

    date = DateTime.from_unix!(unix) |> Calendar.strftime("%d/%m/%y (%H:%M)")

    """
    <div class="thought">
    <h3><a href="/thoughts/#{FileX.sanitised_name(name)}.html">#{name}</a></h3>
    <span>#{thought}</span>
    <small> -mika, #{date}</small>
    </div>
    """
  end
end

defmodule FileX do
  def createdf(path) do
    {:ok, %File.Stat{mtime: {{y, m, d}, _}}} = File.stat(path, time: :local)
    "#{d}/#{m}/#{y}"
  end

  def created(path) do
    {:ok, %File.Stat{mtime: x}} = File.stat(path, time: :posix)
    x
  end

  def sanitised_name(path) do
    Path.basename(path)
    |> String.trim_trailing(".html")
    |> String.replace("-", "_")
    |> String.replace(" ", "_")
  end
end

{commit, 0} = System.cmd("git", ["rev-parse", "HEAD"])

{lightningcss, 0} = System.cmd("lightningcss", ["-V"])

{html_minifier_next, 0} = System.cmd("html-minifier-next", ["-V"])

{uglifyjs, 0} = System.cmd("uglifyjs", ["-V"])

File.rm_rf("build")

File.mkdir_p!("build")

bricks =
  Path.wildcard("bricks/*.html")
  |> Map.new(fn x -> {FileX.sanitised_name(x), File.read!(x)} end)

{:ok, main_template} = File.read("templates/base.html")

Path.wildcard("pages/**/*.fxg")
|> Task.async_stream(fn file -> Builders.fxg(file, main_template, bricks) end)
|> Enum.to_list()

File.mkdir_p!("build/static")

Path.wildcard("static/**/*")
|> Task.async_stream(fn file -> File.cp(file, "build/" <> file) end)
|> Enum.to_list()

File.mkdir_p!("build/scripts")

Path.wildcard("scripts/**/*.js")
|> Task.async_stream(fn file -> Builders.js(file) end)
|> Enum.to_list()

File.mkdir_p!("build/styles")

Path.wildcard("styles/**/*.css")
|> Task.async_stream(fn file -> Builders.css(file) end)
|> Enum.to_list()

File.mkdir_p("build/pastes")

{:ok, paste_template} = File.read("templates/paste.html")

pastes =
  Path.wildcard("pastes/**/*")
  |> Enum.map(fn path -> {path, Git.get_file_commit(path)} end)
  |> Enum.sort_by(fn {_, t} -> t end, :desc)
  |> Enum.map(fn {path, _} -> path end)

pastes
|> Task.async_stream(fn file -> Builders.paste(file, paste_template, bricks) end)
|> Enum.to_list()

paste_data =
  Map.new(pastes, fn x -> {x, FileX.createdf(x)} end)
  |> Enum.map(fn {x, y} -> Templates.pastelink(x, x, y) end)
  |> Enum.join("<br>")

pastes_header = """
<h1>my pastes</h1>
<p>random code snippets i found useful or wanted to save :))</p>
<hr>
"""

# main_template
# |> String.replace("<content />", pastes_header <> "<p>" <> paste_data <> "</p>")
paste_index =
  Mustache.render(
    main_template,
    Map.merge(bricks, %{
      fxg_content: pastes_header <> "<p>" <> paste_data <> "</p>",
      location: "pastes",
      ogp:
        Templates.ogp(
          "thoughts",
          "https://m1kadev.nl/pastes",
          "random code snippets i found useful or wanted to save :))"
        )
    })
  )

File.write("build/pastes/index.html", paste_index)

File.mkdir_p("build/thoughts")

thoughts_template = File.read!("templates/thoughts.html")
thought_template = File.read!("templates/thought.html")

thought_paths =
  Path.wildcard("thoughts/**/*")
  |> Enum.map(fn path -> {path, Git.get_file_commit(path)} end)
  |> Enum.sort_by(fn {_, t} -> t end, :desc)

thoughts = Enum.map(thought_paths, fn {path, date} -> Builders.thought(path, date) end)

thoughts_html =
  Mustache.render(
    thoughts_template,
    Map.merge(bricks, %{
      fxg_content: Enum.join(thoughts, "<hr>"),
      location: "thoughts",
      ogp:
        Templates.ogp(
          "thoughts",
          "https://m1kadev.nl/thoughts",
          "important thoughts that needed publishing"
        )
    })
  )

for {thought, {path, time}} <- Enum.zip(thoughts, thought_paths) do
  thought = FileX.sanitised_name(path)
  output_path = "build/thoughts/" <> thought <> ".html"

  when_said = DateTime.from_unix!(time) |> Calendar.strftime("%H:%M, %d/%m/%y")
  thought_txt = File.read!(path)

  thought_html =
    Mustache.render(
      thought_template,
      Map.merge(bricks, %{
        fxg_content: thought_txt,
        thought: thought,
        date: when_said,
        ogp:
          Templates.ogp(
            thought,
            "https://m1kadev.nl/#{String.replace_prefix(output_path, "build/", "")}",
            String.replace(thought_txt, "\"", "&quot;")
          )
      })
    )

  File.write(output_path, thought_html)
end

File.write("build/thoughts/index.html", thoughts_html)

File.copy("favicon.ico", "build/favicon.ico")

{:ok, date} = DateTime.now("Europe/Amsterdam")
build_time = Calendar.strftime(date, "%H:%M:%S %d/%m/%y")

File.write(
  "build/info.txt",
  Templates.build_info(
    commit,
    lightningcss |> String.split_at(13) |> elem(1),
    html_minifier_next,
    uglifyjs |> String.split_at(10) |> elem(1),
    build_time
  )
)

A kethel/.formatter.exs => kethel/.formatter.exs +4 -0
@@ 0,0 1,4 @@
# Used by "mix format"
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

A kethel/.gitignore => kethel/.gitignore +23 -0
@@ 0,0 1,23 @@
# 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/

# 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").
kethel-*.tar

# Temporary files, for example, from tests.
/tmp/

A kethel/lib/bricks.ex => kethel/lib/bricks.ex +17 -0
@@ 0,0 1,17 @@
defmodule Bricks do
  @typedoc "A path, relative to mix.exs"
  @type relative_path() :: String.t()

  @typedoc "The name of the brick"
  @type brick_name() :: String.t()

  @typedoc "A valid mustache template"
  @type mustache() :: String.t()

  @spec collect(relative_path()) :: %{brick_name() => mustache()}
  def collect(folder) do
    Path.wildcard(folder <> "/bricks/*.thtml")
      |> Enum.map(fn brick -> { brick, File.read!(brick) } end) 
      |> Enum.map(fn { brick, contents } -> { Path.basename(brick) |> Path.rootname(), contents } end) # "../bricks/base_header.html" would beccome just "base_header"
  end
 end 

A kethel/main.exs => kethel/main.exs +11 -0
@@ 0,0 1,11 @@
args = System.argv()

folder = Enum.at(args, 1)

if folder != nil do
  IO.inspect(folder)
  bricks = Bricks.collect(folder)
  IO.inspect(bricks)
else
  IO.puts(:stderr, "The root project folder should be provided on the command line")
end

A kethel/mix.exs => kethel/mix.exs +20 -0
@@ 0,0 1,20 @@
defmodule Kethel.MixProject do
  use Mix.Project

  def project do
    [
      app: :kethel,
      version: "0.1.0",
      elixir: "~> 1.18",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  defp deps do
    [
      { :tzdata, "~> 1.1" },
      { :mustache, "~> 0.5.0" }
    ]
  end
end

A kethel/mix.lock => kethel/mix.lock +12 -0
@@ 0,0 1,12 @@
%{
  "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
  "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.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.4", [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.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
  "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
  "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
  "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
  "mustache": {:hex, :mustache, "0.5.1", "1bfee8a68a8e72d47616541a93a632ae1684c1abc17c9eb5f7bfecb78d7496e5", [:mix], [], "hexpm", "524c9bbb6080a52d7b6806436b4e269e0224c785a228faf3293ef30a75016bfa"},
  "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
  "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
  "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
  "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
}