WIP microprints source now showing
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.
This commit is contained in:
parent
061787e897
commit
7a50f2d4e7
@ -12,6 +12,7 @@ defmodule Firehose.Application do
|
|||||||
Firehose.Repo,
|
Firehose.Repo,
|
||||||
{DNSCluster, query: Application.get_env(:firehose, :dns_cluster_query) || :ignore},
|
{DNSCluster, query: Application.get_env(:firehose, :dns_cluster_query) || :ignore},
|
||||||
{Phoenix.PubSub, name: Firehose.PubSub},
|
{Phoenix.PubSub, name: Firehose.PubSub},
|
||||||
|
{Microprints.MicroprintCache, pubsub: Firehose.PubSub},
|
||||||
# Start a worker by calling: Firehose.Worker.start_link(arg)
|
# Start a worker by calling: Firehose.Worker.start_link(arg)
|
||||||
# {Firehose.Worker, arg},
|
# {Firehose.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
|
|||||||
228
app/lib/firehose_web/live/microprints_live.ex
Normal file
228
app/lib/firehose_web/live/microprints_live.ex
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
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
|
||||||
@ -33,6 +33,12 @@ defmodule FirehoseWeb.Router do
|
|||||||
get "/:blog_id/:slug", BlogController, :show
|
get "/:blog_id/:slug", BlogController, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", FirehoseWeb do
|
||||||
|
pipe_through :browser
|
||||||
|
|
||||||
|
live "/microprints", MicroprintsLive
|
||||||
|
end
|
||||||
|
|
||||||
# JSON API + feeds (no Phoenix layout)
|
# JSON API + feeds (no Phoenix layout)
|
||||||
scope "/api/blog" do
|
scope "/api/blog" do
|
||||||
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
|
forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog
|
||||||
|
|||||||
@ -70,6 +70,9 @@ defmodule Firehose.MixProject do
|
|||||||
{:dns_cluster, "~> 0.2.0"},
|
{:dns_cluster, "~> 0.2.0"},
|
||||||
{:bandit, "~> 1.5"},
|
{:bandit, "~> 1.5"},
|
||||||
{:blogex, path: "../blogex"},
|
{:blogex, path: "../blogex"},
|
||||||
|
{:microprints,
|
||||||
|
git: "ssh://git@gitea.apps.sustainabledelivery.com:3022/QWAN/microprints-phoenix.git",
|
||||||
|
branch: "main"},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
||||||
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
||||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
|
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
|
||||||
|
"microprints": {:git, "ssh://git@gitea.apps.sustainabledelivery.com:3022/QWAN/microprints-phoenix.git", "29ef59ff6eb41853b6f91872d8fffdfba4d85a62", [branch: "main"]},
|
||||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
|
|||||||
@ -103,7 +103,8 @@ defmodule FirehoseWeb.BlogControllerTest do
|
|||||||
test "meta tags have correct og_url", %{conn: conn} do
|
test "meta tags have correct og_url", %{conn: conn} do
|
||||||
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200)
|
||||||
|
|
||||||
assert response =~ ~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world")
|
assert response =~
|
||||||
|
~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
56
app/test/firehose_web/live/microprints_live_test.exs
Normal file
56
app/test/firehose_web/live/microprints_live_test.exs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
defmodule FirehoseWeb.MicroprintsLiveTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
describe "scan_source_files/0" do
|
||||||
|
test "returns only .ex files from app/ and blogex/ directories" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
# Should include app/lib files
|
||||||
|
assert "lib/firehose.ex" in files
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include files from _build/ directory" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
refute Enum.any?(files, &String.starts_with?(&1, "_build/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include files from deps/ directory" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
refute Enum.any?(files, &String.starts_with?(&1, "deps/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include files from examples/ directory" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
refute Enum.any?(files, &String.contains?(&1, "/examples/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include test files" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
refute Enum.any?(files, &String.contains?(&1, "/test/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paths are relative (no leading slash or absolute path)" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
refute Enum.any?(files, &String.starts_with?(&1, "/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "app paths do not contain source dir prefix" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
app_files = Enum.filter(files, &String.starts_with?(&1, "lib/"))
|
||||||
|
refute Enum.any?(app_files, &String.starts_with?(&1, "app/"))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blogex paths start with ../blogex/" do
|
||||||
|
files = FirehoseWeb.MicroprintsLive.scan_source_files()
|
||||||
|
|
||||||
|
blogex_files = Enum.filter(files, &String.starts_with?(&1, "../blogex/"))
|
||||||
|
refute blogex_files == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user