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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+25
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user