diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 405cd2b..5425fd3 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -286,124 +286,123 @@ body { font-family: 'Source Sans 3', sans-serif; } } /* highlight.js — Atom One Dark (dark theme) */ -.source-viewer code.hljs { +.source-viewer .hljs { color: #abb2bf; background: transparent; - padding: 0; } -.source-viewer code.hljs .hljs-comment, -.source-viewer code.hljs .hljs-quote { +.source-viewer .hljs .hljs-comment, +.source-viewer .hljs .hljs-quote { color: #5c6370; font-style: italic; } -.source-viewer code.hljs .hljs-doctag, -.source-viewer code.hljs .hljs-keyword, -.source-viewer code.hljs .hljs-formula { +.source-viewer .hljs .hljs-doctag, +.source-viewer .hljs .hljs-keyword, +.source-viewer .hljs .hljs-formula { color: #c678dd; } -.source-viewer code.hljs .hljs-section, -.source-viewer code.hljs .hljs-name, -.source-viewer code.hljs .hljs-selector-tag, -.source-viewer code.hljs .hljs-deletion, -.source-viewer code.hljs .hljs-subst { +.source-viewer .hljs .hljs-section, +.source-viewer .hljs .hljs-name, +.source-viewer .hljs .hljs-selector-tag, +.source-viewer .hljs .hljs-deletion, +.source-viewer .hljs .hljs-subst { color: #e06c75; } -.source-viewer code.hljs .hljs-literal { +.source-viewer .hljs .hljs-literal { color: #56b6c2; } -.source-viewer code.hljs .hljs-string, -.source-viewer code.hljs .hljs-regexp, -.source-viewer code.hljs .hljs-addition, -.source-viewer code.hljs .hljs-attribute, -.source-viewer code.hljs .hljs-meta .hljs-string { +.source-viewer .hljs .hljs-string, +.source-viewer .hljs .hljs-regexp, +.source-viewer .hljs .hljs-addition, +.source-viewer .hljs .hljs-attribute, +.source-viewer .hljs .hljs-meta .hljs-string { color: #98c379; } -.source-viewer code.hljs .hljs-attr, -.source-viewer code.hljs .hljs-variable, -.source-viewer code.hljs .hljs-template-variable, -.source-viewer code.hljs .hljs-type, -.source-viewer code.hljs .hljs-selector-class, -.source-viewer code.hljs .hljs-selector-attr, -.source-viewer code.hljs .hljs-selector-pseudo, -.source-viewer code.hljs .hljs-number { +.source-viewer .hljs .hljs-attr, +.source-viewer .hljs .hljs-variable, +.source-viewer .hljs .hljs-template-variable, +.source-viewer .hljs .hljs-type, +.source-viewer .hljs .hljs-selector-class, +.source-viewer .hljs .hljs-selector-attr, +.source-viewer .hljs .hljs-selector-pseudo, +.source-viewer .hljs .hljs-number { color: #d19a66; } -.source-viewer code.hljs .hljs-symbol, -.source-viewer code.hljs .hljs-link, -.source-viewer code.hljs .hljs-meta, -.source-viewer code.hljs .hljs-selector-id, -.source-viewer code.hljs .hljs-title { +.source-viewer .hljs .hljs-symbol, +.source-viewer .hljs .hljs-link, +.source-viewer .hljs .hljs-meta, +.source-viewer .hljs .hljs-selector-id, +.source-viewer .hljs .hljs-title { color: #61aeee; } -.source-viewer code.hljs .hljs-built_in, -.source-viewer code.hljs .hljs-title.class_, -.source-viewer code.hljs .hljs-class .hljs-title { +.source-viewer .hljs .hljs-built_in, +.source-viewer .hljs .hljs-title.class_, +.source-viewer .hljs .hljs-class .hljs-title { color: #e6c07b; } -.source-viewer code.hljs .hljs-emphasis { +.source-viewer .hljs .hljs-emphasis { font-style: italic; } -.source-viewer code.hljs .hljs-strong { +.source-viewer .hljs .hljs-strong { font-weight: bold; } /* highlight.js — Atom One Light (light theme) */ -[data-theme="light"] .source-viewer code.hljs { +[data-theme="light"] .source-viewer .hljs { color: #383a42; background: transparent; } -[data-theme="light"] .source-viewer code.hljs .hljs-comment, -[data-theme="light"] .source-viewer code.hljs .hljs-quote { +[data-theme="light"] .source-viewer .hljs .hljs-comment, +[data-theme="light"] .source-viewer .hljs .hljs-quote { color: #a0a1a7; font-style: italic; } -[data-theme="light"] .source-viewer code.hljs .hljs-doctag, -[data-theme="light"] .source-viewer code.hljs .hljs-keyword, -[data-theme="light"] .source-viewer code.hljs .hljs-formula { +[data-theme="light"] .source-viewer .hljs .hljs-doctag, +[data-theme="light"] .source-viewer .hljs .hljs-keyword, +[data-theme="light"] .source-viewer .hljs .hljs-formula { color: #a626a4; } -[data-theme="light"] .source-viewer code.hljs .hljs-section, -[data-theme="light"] .source-viewer code.hljs .hljs-name, -[data-theme="light"] .source-viewer code.hljs .hljs-selector-tag, -[data-theme="light"] .source-viewer code.hljs .hljs-deletion, -[data-theme="light"] .source-viewer code.hljs .hljs-subst { +[data-theme="light"] .source-viewer .hljs .hljs-section, +[data-theme="light"] .source-viewer .hljs .hljs-name, +[data-theme="light"] .source-viewer .hljs .hljs-selector-tag, +[data-theme="light"] .source-viewer .hljs .hljs-deletion, +[data-theme="light"] .source-viewer .hljs .hljs-subst { color: #e45649; } -[data-theme="light"] .source-viewer code.hljs .hljs-literal { +[data-theme="light"] .source-viewer .hljs .hljs-literal { color: #0184bc; } -[data-theme="light"] .source-viewer code.hljs .hljs-string, -[data-theme="light"] .source-viewer code.hljs .hljs-regexp, -[data-theme="light"] .source-viewer code.hljs .hljs-addition, -[data-theme="light"] .source-viewer code.hljs .hljs-attribute, -[data-theme="light"] .source-viewer code.hljs .hljs-meta .hljs-string { +[data-theme="light"] .source-viewer .hljs .hljs-string, +[data-theme="light"] .source-viewer .hljs .hljs-regexp, +[data-theme="light"] .source-viewer .hljs .hljs-addition, +[data-theme="light"] .source-viewer .hljs .hljs-attribute, +[data-theme="light"] .source-viewer .hljs .hljs-meta .hljs-string { color: #50a14f; } -[data-theme="light"] .source-viewer code.hljs .hljs-attr, -[data-theme="light"] .source-viewer code.hljs .hljs-variable, -[data-theme="light"] .source-viewer code.hljs .hljs-template-variable, -[data-theme="light"] .source-viewer code.hljs .hljs-type, -[data-theme="light"] .source-viewer code.hljs .hljs-selector-class, -[data-theme="light"] .source-viewer code.hljs .hljs-selector-attr, -[data-theme="light"] .source-viewer code.hljs .hljs-selector-pseudo, -[data-theme="light"] .source-viewer code.hljs .hljs-number { +[data-theme="light"] .source-viewer .hljs .hljs-attr, +[data-theme="light"] .source-viewer .hljs .hljs-variable, +[data-theme="light"] .source-viewer .hljs .hljs-template-variable, +[data-theme="light"] .source-viewer .hljs .hljs-type, +[data-theme="light"] .source-viewer .hljs .hljs-selector-class, +[data-theme="light"] .source-viewer .hljs .hljs-selector-attr, +[data-theme="light"] .source-viewer .hljs .hljs-selector-pseudo, +[data-theme="light"] .source-viewer .hljs .hljs-number { color: #986801; } -[data-theme="light"] .source-viewer code.hljs .hljs-symbol, -[data-theme="light"] .source-viewer code.hljs .hljs-link, -[data-theme="light"] .source-viewer code.hljs .hljs-meta, -[data-theme="light"] .source-viewer code.hljs .hljs-selector-id, -[data-theme="light"] .source-viewer code.hljs .hljs-title { +[data-theme="light"] .source-viewer .hljs .hljs-symbol, +[data-theme="light"] .source-viewer .hljs .hljs-link, +[data-theme="light"] .source-viewer .hljs .hljs-meta, +[data-theme="light"] .source-viewer .hljs .hljs-selector-id, +[data-theme="light"] .source-viewer .hljs .hljs-title { color: #4078f2; } -[data-theme="light"] .source-viewer code.hljs .hljs-built_in, -[data-theme="light"] .source-viewer code.hljs .hljs-title.class_, -[data-theme="light"] .source-viewer code.hljs .hljs-class .hljs-title { +[data-theme="light"] .source-viewer .hljs .hljs-built_in, +[data-theme="light"] .source-viewer .hljs .hljs-title.class_, +[data-theme="light"] .source-viewer .hljs .hljs-class .hljs-title { color: #c18401; } -[data-theme="light"] .source-viewer code.hljs .hljs-emphasis { +[data-theme="light"] .source-viewer .hljs .hljs-emphasis { font-style: italic; } -[data-theme="light"] .source-viewer code.hljs .hljs-strong { +[data-theme="light"] .source-viewer .hljs .hljs-strong { font-weight: bold; } diff --git a/app/assets/js/hooks/source_viewer.js b/app/assets/js/hooks/source_viewer.js index cf426e1..89b2009 100644 --- a/app/assets/js/hooks/source_viewer.js +++ b/app/assets/js/hooks/source_viewer.js @@ -4,7 +4,7 @@ * Attached to the `#source-viewer` div via `phx-hook="SourceViewer"`. * Reads `data-highlighted-line` to find the target line element and * scrolls it into view with a smooth animation. - * Also applies highlight.js syntax highlighting to code blocks. + * Applies highlight.js syntax highlighting to individual line content elements. */ import hljs from "highlight.js/lib/core" @@ -20,10 +20,33 @@ export const SourceViewer = { }, applySyntaxHighlighting() { - const codeEl = this.el.querySelector("code") - if (codeEl && !codeEl.querySelector(".hljs")) { - hljs.highlightElement(codeEl) - } + const container = this.el.querySelector(".sv-lines") + if (!container || container.querySelector(".hljs-on")) return; + + // Collect the raw text from all line content spans + const lineContentEls = container.querySelectorAll(".sv-line-content") + if (lineContentEls.length === 0) return; + + const rawText = Array.from(lineContentEls).map(el => el.textContent).join("\n") + + // Create a temp element for highlight.js to process + const temp = document.createElement("code") + temp.textContent = rawText + hljs.highlightElement(temp) + + // Split the highlighted HTML back into lines + // highlight.js innerHTML uses actual newlines between lines of code + const highlightedLines = temp.innerHTML.split("\n") + + // Apply each highlighted line to the corresponding .sv-line-content + lineContentEls.forEach((el, i) => { + if (highlightedLines[i]) { + el.innerHTML = highlightedLines[i] + } + }) + + // Mark as highlighted so we don't re-apply on subsequent updates + container.classList.add("hljs", "hljs-on") }, scrollToHighlighted() { @@ -35,4 +58,4 @@ export const SourceViewer = { } } } -} +} \ No newline at end of file diff --git a/app/lib/firehose_web/live/microprints_live.ex b/app/lib/firehose_web/live/microprints_live.ex index 6dab3a5..84e95cf 100644 --- a/app/lib/firehose_web/live/microprints_live.ex +++ b/app/lib/firehose_web/live/microprints_live.ex @@ -26,7 +26,7 @@ defmodule FirehoseWeb.MicroprintsLive do |> assign(:page_title, "Microprints") |> assign(:microprints, []) |> assign(:expanded_path, nil) - |> assign(:source_content, nil) + |> assign(:source_lines, nil) |> assign(:highlighted_path, nil) |> assign(:highlighted_line, nil) |> put_flash(:error, "Error loading microprints: #{inspect(e)}")} @@ -80,8 +80,8 @@ defmodule FirehoseWeb.MicroprintsLive do highlighted_line={@highlighted_line} /> - <%= if @expanded_path == path and @source_content do %> <.source_viewer - source_content={@source_content} + <%= if @expanded_path == path and @source_lines do %> <.source_viewer + source_lines={@source_lines} highlighted_line={@highlighted_line} language="elixir" file_path={path} @@ -120,7 +120,7 @@ defmodule FirehoseWeb.MicroprintsLive do |> assign(:highlighted_path, path) |> assign(:highlighted_line, highlighted) |> assign(:expanded_path, expanded) - |> assign(:source_content, nil) + |> assign(:source_lines, nil) {:noreply, push_patch(socket, to: build_params(socket))} end @@ -133,7 +133,7 @@ defmodule FirehoseWeb.MicroprintsLive do _ -> path end - source_content = + source_lines = case expanded do nil -> nil ^path -> @@ -146,7 +146,7 @@ defmodule FirehoseWeb.MicroprintsLive do end end - socket = socket |> assign(:expanded_path, expanded) |> assign(:source_content, source_content) + socket = socket |> assign(:expanded_path, expanded) |> assign(:source_lines, source_lines) {:noreply, push_patch(socket, to: build_params(socket))} end @@ -158,7 +158,7 @@ defmodule FirehoseWeb.MicroprintsLive do highlighted_path = params["highlighted"] highlighted_line = if params["line"], do: String.to_integer(params["line"]), else: nil - source_content = + source_lines = if expanded_path do item = Enum.find(socket.assigns.microprints, &(&1.path == expanded_path)) @@ -170,7 +170,7 @@ defmodule FirehoseWeb.MicroprintsLive do socket |> assign(:expanded_path, expanded_path) - |> assign(:source_content, source_content) + |> assign(:source_lines, source_lines) |> assign(:highlighted_path, highlighted_path) |> assign(:highlighted_line, highlighted_line) end @@ -325,13 +325,8 @@ defmodule FirehoseWeb.MicroprintsLive do defp read_source(abs_path) do case File.read(abs_path) do - {:ok, content} -> - content - |> String.replace("<", "<") - |> String.replace(">", ">") - - {:error, _} -> - nil + {:ok, content} -> String.split(content, "\n") + {:error, _} -> nil end end @@ -353,7 +348,17 @@ defmodule FirehoseWeb.MicroprintsLive do data-language={@language} style="background: var(--sv-bg); color: var(--sv-text);" > -
"-pre"} phx-update="ignore" class="m-0 p-2"><%= @source_content %>
+