also added go to install showboat and rodney for the next steps. microprints work in qwan tracker, but not in firehose. some of the rendering is in the project, maybe the library should provide sample webpages.
229 lines
5.9 KiB
Elixir
229 lines
5.9 KiB
Elixir
defmodule FirehoseWeb.MicroprintsLive do
|
|
use FirehoseWeb, :live_view
|
|
|
|
alias Microprints.MicroprintCache
|
|
alias Microprints.MicroprintComponent
|
|
|
|
@source_dirs ["app", "blogex"]
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
files = scan_source_files()
|
|
|
|
microprints =
|
|
files
|
|
|> Enum.map(&process_file/1)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Microprints")
|
|
|> assign(:microprints, microprints)
|
|
|> assign(:expanded_path, nil)
|
|
|> assign(:highlighted_path, nil)
|
|
|> assign(:highlighted_line, nil)}
|
|
rescue
|
|
e ->
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Microprints")
|
|
|> assign(:microprints, [])
|
|
|> assign(:expanded_path, nil)
|
|
|> assign(:highlighted_path, nil)
|
|
|> assign(:highlighted_line, nil)
|
|
|> put_flash(:error, "Error loading microprints: #{inspect(e)}")}
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div class="max-w-6xl mx-auto">
|
|
<h1 class="text-2xl font-bold mb-2">Microprints</h1>
|
|
<p class="text-sm text-zinc-500 mb-6">
|
|
Visual fingerprints of source code files. Click a line to highlight it.
|
|
Click a card to expand and view the source.
|
|
</p>
|
|
|
|
<.microprint_legend />
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
|
|
<%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %>
|
|
{error = item[:error]}
|
|
<div class="card bg-base-100 shadow-sm border border-zinc-200">
|
|
<div class="card-body p-4">
|
|
<h3 class="text-sm font-mono font-medium truncate" title={path}>
|
|
{path}
|
|
</h3>
|
|
|
|
<%= if microprint do %>
|
|
<.microprint
|
|
microprint={microprint}
|
|
width={200}
|
|
max_height={100}
|
|
clickable={true}
|
|
file_path={path}
|
|
highlighted_line={@highlighted_line}
|
|
/>
|
|
|
|
<button
|
|
phx-click="toggle_expand"
|
|
phx-value-path={path}
|
|
class="btn btn-xs btn-ghost mt-2 w-full"
|
|
>
|
|
<%= if @expanded_path == path do %>
|
|
Collapse
|
|
<% else %>
|
|
Expand
|
|
<% end %>
|
|
</button>
|
|
|
|
<%= if @expanded_path == path and source do %>
|
|
<.source_viewer
|
|
content={source}
|
|
highlighted_line={@highlighted_line}
|
|
language="elixir"
|
|
/>
|
|
<% end %>
|
|
<% else %>
|
|
<div class="text-xs text-red-500 mt-1">
|
|
Error: {inspect(error)}
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("highlight_line", %{"line" => line, "path" => path}, socket) do
|
|
highlighted =
|
|
case socket.assigns.highlighted_path do
|
|
^path -> nil
|
|
_ -> String.to_integer(line)
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:highlighted_path, path)
|
|
|> assign(:highlighted_line, highlighted)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("toggle_expand", %{"path" => path}, socket) do
|
|
expanded =
|
|
case socket.assigns.expanded_path do
|
|
^path -> nil
|
|
_ -> path
|
|
end
|
|
|
|
{:noreply, assign(socket, :expanded_path, expanded)}
|
|
end
|
|
|
|
# Private helpers
|
|
|
|
@doc false
|
|
def scan_source_files do
|
|
@source_dirs
|
|
|> Enum.flat_map(&collect_elixir_files/1)
|
|
|> Enum.uniq()
|
|
|> Enum.sort()
|
|
end
|
|
|
|
defp resolve_absolute_paths(files) do
|
|
app_root = Mix.Project.project_file() |> Path.dirname()
|
|
|
|
Enum.map(files, fn path ->
|
|
case Path.split(path) do
|
|
[".." | _rest] ->
|
|
Path.join(app_root, path)
|
|
|
|
_ ->
|
|
Path.expand(path)
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp excluded_path?(path) do
|
|
path =~ "/_build/" or
|
|
path =~ "/deps/" or
|
|
path =~ "/examples/" or
|
|
path =~ "/test/" or
|
|
path =~ "/lib_dev/"
|
|
end
|
|
|
|
defp collect_elixir_files(dir) do
|
|
app_root = Mix.Project.project_file() |> Path.dirname()
|
|
monorepo_root = app_root |> Path.dirname()
|
|
base = Path.join(monorepo_root, dir)
|
|
|
|
case File.dir?(base) do
|
|
true ->
|
|
base
|
|
|> Path.join("**/*.ex")
|
|
|> Path.wildcard()
|
|
|> Enum.filter(&File.regular?(&1))
|
|
|> Enum.reject(&excluded_path?/1)
|
|
|> Enum.map(&format_path(&1, monorepo_root, dir))
|
|
|
|
false ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp format_path(path, monorepo_root, dir) do
|
|
relative = Path.relative_to(path, monorepo_root)
|
|
|
|
case dir do
|
|
"app" ->
|
|
String.replace_prefix(relative, "app/", "")
|
|
|
|
_ ->
|
|
"../" <> relative
|
|
end
|
|
end
|
|
|
|
defp process_file(rel_path) do
|
|
abs_path = resolve_absolute_paths([rel_path]) |> List.first()
|
|
|
|
case MicroprintCache.get_microprint(abs_path) do
|
|
{:ok, microprint} ->
|
|
# Add line numbers to each line for highlighting
|
|
lines_with_numbers =
|
|
microprint.lines
|
|
|> Enum.with_index(1)
|
|
|> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end)
|
|
|
|
# Read source code for expand/contract
|
|
source = read_source(abs_path)
|
|
|
|
%{
|
|
path: rel_path,
|
|
microprint: Map.put(microprint, :lines, lines_with_numbers),
|
|
source: source
|
|
}
|
|
|
|
{:error, reason} ->
|
|
%{path: rel_path, microprint: nil, error: reason}
|
|
end
|
|
end
|
|
|
|
defp read_source(abs_path) do
|
|
case File.read(abs_path) do
|
|
{:ok, content} ->
|
|
content
|
|
|> String.replace("<", "<")
|
|
|> String.replace(">", ">")
|
|
|
|
{:error, _} ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Delegate to MicroprintComponent
|
|
defdelegate microprint(assigns), to: MicroprintComponent
|
|
defdelegate microprint_legend(assigns), to: MicroprintComponent
|
|
defdelegate source_viewer(assigns), to: MicroprintComponent
|
|
end
|