Extracted from qwan-tracker
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
defmodule Microprints.MicroprintComponent do
|
||||
@moduledoc """
|
||||
Phoenix Component for rendering microprint SVG visualizations.
|
||||
|
||||
Renders each line of code as a colored horizontal stripe,
|
||||
creating a compact visual fingerprint of the file's structure.
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
alias Microprints.Microprint
|
||||
|
||||
# Pixels per indent level (1 space = 1 indent unit)
|
||||
@indent_scale 2
|
||||
# Maximum indent in pixels (cap at 40% of width)
|
||||
@max_indent_ratio 0.4
|
||||
|
||||
@doc """
|
||||
Renders a microprint visualization as an inline SVG.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `:microprint` - The microprint map with `:lines` (list of line info maps)
|
||||
* `:width` - SVG width in pixels (default: 200)
|
||||
* `:max_height` - Maximum SVG height in pixels (default: 100)
|
||||
* `:clickable` - Whether lines are clickable to highlight (default: false)
|
||||
* `:file_path` - File path for click events (required if clickable)
|
||||
* `:highlighted_line` - Currently highlighted line number (0-indexed)
|
||||
"""
|
||||
attr :microprint, :map, required: true
|
||||
attr :width, :integer, default: 200
|
||||
attr :max_height, :integer, default: 100
|
||||
attr :clickable, :boolean, default: false
|
||||
attr :file_path, :string, default: nil
|
||||
attr :highlighted_line, :integer, default: nil
|
||||
|
||||
def microprint(assigns) do
|
||||
lines = assigns.microprint.lines
|
||||
line_count = length(lines)
|
||||
|
||||
# Each line is 1px, but cap at max_height
|
||||
height = min(line_count, assigns.max_height)
|
||||
|
||||
# If we have more lines than max_height, we need to sample
|
||||
{displayed_lines, line_mapping} =
|
||||
if line_count > assigns.max_height do
|
||||
{sample_lines(lines, assigns.max_height),
|
||||
sample_line_mapping(line_count, assigns.max_height)}
|
||||
else
|
||||
{lines, Enum.to_list(0..(line_count - 1))}
|
||||
end
|
||||
|
||||
max_indent = trunc(assigns.width * @max_indent_ratio)
|
||||
|
||||
# Find max line length for proportional scaling
|
||||
max_length = displayed_lines |> Enum.map(& &1.length) |> Enum.max(fn -> 1 end) |> max(1)
|
||||
|
||||
# Calculate x offset and width for each line
|
||||
rendered_lines =
|
||||
displayed_lines
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {line_info, index} ->
|
||||
base_x = min(line_info.indent * @indent_scale, max_indent)
|
||||
available_width = assigns.width - base_x
|
||||
|
||||
# Map display index to actual line number
|
||||
actual_line = Enum.at(line_mapping, index, index)
|
||||
highlighted = assigns.highlighted_line == actual_line
|
||||
|
||||
# Build segments for this line
|
||||
segments =
|
||||
if line_info[:segments] && line_info.segments != [] do
|
||||
# Multi-color: render each segment
|
||||
scale = if line_info.length > 0, do: available_width / line_info.length, else: 1
|
||||
|
||||
line_info.segments
|
||||
|> Enum.map(fn seg ->
|
||||
seg_x = base_x + trunc(seg.start * scale)
|
||||
seg_width = max(1, trunc(seg.length * scale))
|
||||
|
||||
%{
|
||||
x: seg_x,
|
||||
width: seg_width,
|
||||
color: seg.color,
|
||||
y: index,
|
||||
line_number: actual_line,
|
||||
highlighted: highlighted
|
||||
}
|
||||
end)
|
||||
else
|
||||
# Single color fallback
|
||||
width =
|
||||
if line_info.length == 0 do
|
||||
2
|
||||
else
|
||||
max(2, trunc(line_info.length / max_length * available_width))
|
||||
end
|
||||
|
||||
[
|
||||
%{
|
||||
x: base_x,
|
||||
width: width,
|
||||
color: line_info.color,
|
||||
y: index,
|
||||
line_number: actual_line,
|
||||
highlighted: highlighted
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
%{y: index, line_number: actual_line, highlighted: highlighted, segments: segments}
|
||||
end)
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:height, height)
|
||||
|> assign(:rendered_lines, rendered_lines)
|
||||
|
||||
~H"""
|
||||
<svg
|
||||
width={@width}
|
||||
height={@height}
|
||||
viewBox={"0 0 #{@width} #{@height}"}
|
||||
class={"block mt-1 #{if @clickable, do: "cursor-pointer", else: ""}"}
|
||||
>
|
||||
<%= for line <- @rendered_lines do %>
|
||||
<%= for segment <- line.segments do %>
|
||||
<rect
|
||||
x={segment.x}
|
||||
y={segment.y}
|
||||
width={segment.width}
|
||||
height="1"
|
||||
fill={segment.color}
|
||||
opacity={if segment.highlighted, do: "1", else: "0.8"}
|
||||
phx-click={if @clickable, do: "highlight_line"}
|
||||
phx-value-line={if @clickable, do: segment.line_number}
|
||||
phx-value-path={if @clickable, do: @file_path}
|
||||
/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= if @highlighted_line do %>
|
||||
<rect
|
||||
x="0"
|
||||
y={highlight_y(@rendered_lines, @highlighted_line)}
|
||||
width={@width}
|
||||
height="3"
|
||||
fill="none"
|
||||
stroke="#000"
|
||||
stroke-width="1"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<% end %>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
defp highlight_y(rendered_lines, highlighted_line) do
|
||||
case Enum.find(rendered_lines, &(&1.line_number == highlighted_line)) do
|
||||
nil -> 0
|
||||
line -> max(0, line.y - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp sample_line_mapping(line_count, max_count) do
|
||||
step = line_count / max_count
|
||||
|
||||
0..(max_count - 1)
|
||||
|> Enum.map(fn i -> trunc(i * step) end)
|
||||
end
|
||||
|
||||
# Sample lines evenly when we have more lines than pixels available
|
||||
defp sample_lines(lines, max_count) do
|
||||
line_count = length(lines)
|
||||
step = line_count / max_count
|
||||
|
||||
0..(max_count - 1)
|
||||
|> Enum.map(fn i ->
|
||||
index = trunc(i * step)
|
||||
Enum.at(lines, index)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a color legend for microprint visualizations.
|
||||
|
||||
Shows each syntax type with its corresponding color.
|
||||
"""
|
||||
def microprint_legend(assigns) do
|
||||
assigns = assign(assigns, :legend, Microprint.color_legend())
|
||||
|
||||
~H"""
|
||||
<div class="flex flex-wrap gap-3 text-xs">
|
||||
<%= for {label, color} <- @legend do %>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class="w-3 h-3 inline-block rounded-sm"
|
||||
style={"background: #{color};"}
|
||||
>
|
||||
</span>
|
||||
<span class="text-base-content/60">{label}</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders source code with line numbers and optional highlighting.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `:content` - The source code content as a string
|
||||
* `:highlighted_line` - Line number to highlight (0-indexed, optional)
|
||||
* `:language` - Language for syntax highlighting (e.g., "elixir", "javascript", "typescript", "markdown")
|
||||
"""
|
||||
attr :content, :string, required: true
|
||||
attr :highlighted_line, :integer, default: nil
|
||||
attr :language, :string, default: nil
|
||||
|
||||
def source_viewer(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id="source-viewer"
|
||||
class="source-viewer mt-2 max-h-96 overflow-auto rounded font-mono text-xs"
|
||||
phx-hook="SourceViewer"
|
||||
data-highlighted-line={@highlighted_line}
|
||||
data-language={@language}
|
||||
style="background: var(--sv-bg); color: var(--sv-text);"
|
||||
>
|
||||
<pre id="source-viewer-pre" phx-update="ignore" class="m-0 p-2"><code class={"language-#{@language || "plaintext"}"}><%= @content %></code></pre>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user