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""" """ 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"""
<%= @content %>