Add highlight.js syntax highlighting to source viewer

- Install highlight.js via npm with 8 language definitions
- SourceViewer hook applies syntax highlighting on mount/update
- Atom One Dark theme for dark mode, Atom One Light for light mode
- Add hooks directory, package.json, and package-lock.json
- Add demo document and screenshots
- Add rodney-docker.md documentation
- Ignore .rodney/ chrome data directory
This commit is contained in:
2026-05-14 13:17:10 +01:00
parent fa4825d76d
commit 54651d2349
12 changed files with 531 additions and 1 deletions
+172
View File
@@ -235,3 +235,175 @@ body { font-family: 'Source Sans 3', sans-serif; }
.blogex-pagination a:hover {
text-decoration: underline;
}
/* Source Viewer — line-numbered code display */
.source-viewer {
--sv-bg: oklch(20.15% 0.012 254.09);
--sv-text: oklch(90% 0.01 240);
--sv-line-num: oklch(55% 0.02 240);
--sv-highlight: oklch(30% 0.02 240);
}
[data-theme="dark"] .source-viewer {
--sv-bg: oklch(16% 0.01 254);
--sv-text: oklch(88% 0.01 240);
--sv-line-num: oklch(50% 0.02 240);
--sv-highlight: oklch(25% 0.02 240);
}
.sv-lines {
display: flex;
flex-direction: column;
}
.sv-line {
display: flex;
align-items: baseline;
min-height: 1.25rem;
padding: 0 0.25rem;
transition: background-color 0.2s ease;
}
.sv-line-highlighted {
background-color: var(--sv-highlight);
}
.sv-line-number {
display: inline-block;
width: 2.5rem;
text-align: right;
padding-right: 0.75rem;
color: var(--sv-line-num);
user-select: none;
opacity: 0.6;
flex-shrink: 0;
}
.sv-line-content {
white-space: pre;
flex: 1;
min-width: 0;
}
/* highlight.js — Atom One Dark (dark theme) */
.source-viewer code.hljs {
color: #abb2bf;
background: transparent;
padding: 0;
}
.source-viewer code.hljs .hljs-comment,
.source-viewer code.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 {
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 {
color: #e06c75;
}
.source-viewer code.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 {
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 {
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 {
color: #61aeee;
}
.source-viewer code.hljs .hljs-built_in,
.source-viewer code.hljs .hljs-title.class_,
.source-viewer code.hljs .hljs-class .hljs-title {
color: #e6c07b;
}
.source-viewer code.hljs .hljs-emphasis {
font-style: italic;
}
.source-viewer code.hljs .hljs-strong {
font-weight: bold;
}
/* highlight.js — Atom One Light (light theme) */
[data-theme="light"] .source-viewer code.hljs {
color: #383a42;
background: transparent;
}
[data-theme="light"] .source-viewer code.hljs .hljs-comment,
[data-theme="light"] .source-viewer code.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 {
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 {
color: #e45649;
}
[data-theme="light"] .source-viewer code.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 {
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 {
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 {
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 {
color: #c18401;
}
[data-theme="light"] .source-viewer code.hljs .hljs-emphasis {
font-style: italic;
}
[data-theme="light"] .source-viewer code.hljs .hljs-strong {
font-weight: bold;
}
+22 -1
View File
@@ -23,13 +23,34 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/firehose"
import {SourceViewer} from "./hooks/source_viewer"
import topbar from "../vendor/topbar"
// Syntax highlighting via highlight.js
import hljs from "highlight.js/lib/core"
import elixir from "highlight.js/lib/languages/elixir"
import javascript from "highlight.js/lib/languages/javascript"
import typescript from "highlight.js/lib/languages/typescript"
import markdown from "highlight.js/lib/languages/markdown"
import python from "highlight.js/lib/languages/python"
import ruby from "highlight.js/lib/languages/ruby"
import bash from "highlight.js/lib/languages/bash"
import sql from "highlight.js/lib/languages/sql"
hljs.registerLanguage("elixir", elixir)
hljs.registerLanguage("javascript", javascript)
hljs.registerLanguage("typescript", typescript)
hljs.registerLanguage("markdown", markdown)
hljs.registerLanguage("python", python)
hljs.registerLanguage("ruby", ruby)
hljs.registerLanguage("bash", bash)
hljs.registerLanguage("sql", sql)
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks},
hooks: {...colocatedHooks, SourceViewer},
})
// Show progress bar on live navigation and form submits
+38
View File
@@ -0,0 +1,38 @@
/**
* SourceViewer hook — scrolls to and highlights the selected line.
*
* 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.
*/
import hljs from "highlight.js/lib/core"
export const SourceViewer = {
mounted() {
this.applySyntaxHighlighting()
this.scrollToHighlighted()
},
updated() {
this.applySyntaxHighlighting()
this.scrollToHighlighted()
},
applySyntaxHighlighting() {
const codeEl = this.el.querySelector("code")
if (codeEl && !codeEl.querySelector(".hljs")) {
hljs.highlightElement(codeEl)
}
},
scrollToHighlighted() {
const lineNum = this.el.dataset.highlightedLine
if (lineNum !== undefined && lineNum !== "") {
const lineEl = document.getElementById(`line-${lineNum}`)
if (lineEl) {
lineEl.scrollIntoView({ behavior: "smooth", block: "center" })
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "assets",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assets",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"highlight.js": "^11.11.1"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "assets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"highlight.js": "^11.11.1"
}
}