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:
Using the code_snippet_template
, the Mix Task generates highlighted code and places it in a destination folder:
Snippet file naming convention:
- filename.lexer.theme.snip
- 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:
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.