Created a Mix Task, to syntax highlights code snippets in bulk using Chroma

Hey Guys,

You might be aware of makeup, that does syntax highlighting. However it’s limited to only 7 languages.

And since my requirements are a bit broad, I found something better, called Chroma.

However, it was a manual and tedious process to generate syntax highlighted code using its playground.


So, I decided to automate using Mix.Task & Chroma CLI.

defmodule Mix.Tasks.Snippets.Heexify do
  @moduledoc """
  Visit: https://swapoff.org/chroma/playground/ to decide on lexer & theme.

  Snip files should have the following structure:
  1. filename.lexer.theme.snip
  2. filename.lexer.theme.(0,5).snip (With optional copyable lines for bash scripts)

  e.g. caddyfile.caddy.catppuccin-macchiato.snip

  Use: chroma --list to see all the themes & lexers.
  """
  require Logger
  @template Path.join(["assets", "code_snippet_template.html.heex"])
  @snippets_directory Path.join(["assets", "snippets"])
  @destination_directory Path.join(["lib", "derpy_tools_web", "components", "_snippets"])

  def run(_args) do
    @snippets_directory
    |> Path.join("**/*.snip")
    |> Path.wildcard()
    |> Enum.each(fn path ->
      [filename, lexer, theme, lines] = path |> extract_options()

      destination_sub_directory =
        path
        |> Path.relative_to(@snippets_directory)
        |> Path.dirname()

      target_snippet_file =
        if destination_sub_directory,
          do:
            Path.join([@destination_directory, destination_sub_directory, "#{filename}.html.heex"]),
          else: Path.join([@destination_directory, "#{filename}.html.heex"])

      if File.exists?(target_snippet_file) do
        Logger.info("Skipping: #{filename}")
      else
        Logger.info("Styling: #{filename}")
        File.rm_rf!(target_snippet_file)

        id =
          "i" <> (:crypto.strong_rand_bytes(7) |> Base.url_encode64() |> binary_part(0, 7))

        css =
          case System.cmd("chroma", [
                 "--lexer=#{lexer}",
                 "--formatter=html",
                 "--style=#{theme}",
                 "--html-styles",
                 path
               ]) do
            {css, 0} ->
              css
              |> String.replace("chroma", id)
              |> String.replace(~r"\/\*.*\*\/", "")

            _ ->
              nil
          end

        html =
          case System.cmd("chroma", [
                 "--lexer=#{lexer}",
                 "--formatter=html",
                 "--style=#{theme}",
                 "--html-only",
                 path
               ]) do
            {html, 0} ->
              html
              |> String.replace("<pre class=\"chroma\">", "<pre class=\"#{id}\">")
              |> String.replace(
                ~r/<span class="k">([\$❯])<\/span>/u,
                "<span class=\"select-none\">\\1<\/span>"
              )

            _ ->
              nil
          end

        Mix.Generator.copy_template(
          @template,
          target_snippet_file,
          %{
            id: id,
            css: css,
            html: html,
            lines: lines
          },
          force: true
        )
      end
    end)
  end

  defp extract_options(path) do
    path
    |> Path.basename()
    |> Path.rootname()
    |> String.split(".")
    |> options()
  end

  defp options([filename, lexer, theme] = list) when length(list) == 3,
    do: [filename, lexer, theme, nil]

  defp options([filename, lexer, theme, lines] = list) when length(list) == 4,
    do: [filename, lexer, theme, lines |> String.replace(~r/[\(\)]/, "")]
end

Here’s the result:


The Mix task, picks up files from the assets folder:

image


Using the code_snippet_template, the Mix Task generates highlighted code and places it in a destination folder:

image


Snippet file naming convention:

  1. filename.lexer.theme.snip
  2. filename.lexer.theme.(0,5).snip (With optional copyable lines for bash scripts within parenthesis)

Generated file structure

<style type="text/css" nonce={@style_nonce}>
</style>
<pre><code>...</code></pre>

Style nonce is what allows the generated CSS, to work even with a strict CSP policy!

default-src 'none';
style-src 'self' 'nonce-#{style_nonce}';
script-src 'self' 'nonce-#{script_nonce}';

Selective copying:

Source Code Copy

Finally, while copying, we can say which lines to copy, and instead of copying the whole text only selective part gets copied. i.e:

croc send --text "Alooga Looga"
croc 3420-angry-ferral-cat

Usage

<DerpyToolsWeb.Snippets.render
    snippet={:caddyfile}
    style_nonce={@style_nonce}
    name="Caddyfile"
    caption="Config for Caddyserver"
    class="green"
/>
<DerpyToolsWeb.Snippets.render
    snippet={:install_croc}
    style_nonce={@style_nonce}
    name="Bash"
    caption="Install croc with Homebrew"
    class="purple"
/>

defmodule DerpyToolsWeb.Snippets do
  @moduledoc """
  Embeds all the code snippets generated by Chroma.
  """
  use Phoenix.Component

  embed_templates "_snippets/**/*"

  attr :snippet, :atom
  attr :style_nonce, :string
  attr :name, :string, default: "Filename"
  attr :caption, :string, default: "Description"
  attr :class, :string, default: ""

  def render(assigns) do
    ~H"""
    <figure class="code-snippet relative">
      <div class={["label", @class]}><%= @name %></div>
      <%= apply(__MODULE__, @snippet, [assigns]) %>
      <figcaption class="flex justify-center"><%= @caption %></figcaption>
    </figure>
    """
  end
end

P.S. There’s a lot of moving parts, but code is open source, so take a look:

Perhaps will write a blog post about this in future.

1 Like