Syntax Highlighting
Syntax Highlighting! Good luck doing this in Trix!
As with everything in Rhino there are 2 parts:
- Add to our frontend editor
- Make sure we permit tags, attributes, styles, etc in ActionText.
Luckily for us, we don’t need to do the second part! The syntax highlighter we’ll be using from TipTap only uses <span>
, <pre>
, and <code>
which are already permitted by default in ActionText.
TipTap provides an official extension using Lowlight
https://tiptap.dev/api/nodes/code-block-lowlight
Installation
Assuming you have Rhino installed and working, let’s start by installing the additional dependencies we need.
yarn add lowlight @tiptap/extension-code-block-lowlight hast-util-to-html
Adding to RhinoEditor
The first step is to add JavaScript to enhance our editor. Also of note we need to disable
the built-in codeBlock
extension.
// app/javascript/application.js
import "rhino-editor/exports/styles/trix.css"
// This loads all languages
import {common, createLowlight} from 'lowlight'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
// This will important for storing fully syntax highlighted code.
import {toHtml} from 'hast-util-to-html'
const lowlight = createLowlight(common)
const syntaxHighlight = CodeBlockLowlight.configure({
lowlight,
})
// load specific languages only
// import { lowlight } from 'lowlight/lib/core'
// import javascript from 'highlight.js/lib/languages/javascript'
// lowlight.register({javascript})
function extendRhinoEditor (event) {
const rhinoEditor = event.target
if (rhinoEditor == null) return
// This is only for documentation site, feel free to modify this as needed.
if (rhinoEditor.getAttribute("id") !== "syntax-highlight-editor") return
rhinoEditor.starterKitOptions = {
...rhinoEditor.starterKitOptions,
// We disable codeBlock from the starterkit to be able to use CodeBlockLowlight's extension.
codeBlock: false
}
rhinoEditor.extensions = [syntaxHighlight]
rhinoEditor.rebuildEditor()
}
document.addEventListener("rhino-before-initialize", extendRhinoEditor)
// This next part is specifically for storing fully syntax highlighted markup in your database.
//
// On form submission, it will rewrite the value of the
const highlightCodeblocks = (content) => {
const doc = new DOMParser().parseFromString(content, 'text/html');
// If it has the "[has-highlighted]" attribute attached, we know it has already been syntax highlighted.
// This will get stripped from the editor.
doc.querySelectorAll('pre > code[has-highlighted]').forEach((el) => {
const html = toHtml(lowlight.highlightAuto(el.innerHTML).children)
el.setAttribute("has-highlighted", "")
el.innerHTML = html
});
let finalStr = doc.body.innerHTML;
return finalStr
};
document.addEventListener("submit", (e) => {
// find all rhino-editor inputs attached to this form and transform them.
const rhinoInputs = [...e.target.elements].filter((el) => el.classList.contains("rhino-editor-input"))
rhinoInputs.forEach((inputElement) => {
inputElement.value = highlightCodeblocks(inputElement.value)
})
})
The next step is to choose a theme. I went with the OneDark
theme, but feel free to choose any theme you wish.
/*
OneDark theme from here: https://github.com/highlightjs/highlight.js/blob/main/src/styles/atom-one-dark.css
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.trix-content .hljs {
color: #abb2bf;
background: #0d0d0d;
}
.trix-content pre {
color: #abb2bf;
background: #0d0d0d;
border-radius: 0.5rem;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
}
.trix-content code {
background: none;
color: #abb2bf;
background: #0d0d0d;
color: inherit;
font-size: 0.95em;
padding: 0;
}
.trix-content .hljs-comment,
.trix-content .hljs-quote {
color: #5c6370;
font-style: italic;
}
.trix-content .hljs-doctag,
.trix-content .hljs-keyword,
.trix-content .hljs-formula {
color: #c678dd;
}
.trix-content .hljs-section,
.trix-content .hljs-name,
.trix-content .hljs-selector-tag,
.trix-content .hljs-deletion,
.trix-content .hljs-subst {
color: #e06c75;
}
.trix-content .hljs-literal {
color: #56b6c2;
}
.trix-content .hljs-string,
.trix-content .hljs-regexp,
.trix-content .hljs-addition,
.trix-content .hljs-attribute,
.trix-content .hljs-meta .trix-content .hljs-string {
color: #98c379;
}
.trix-content .hljs-attr,
.trix-content .hljs-variable,
.trix-content .hljs-template-variable,
.trix-content .hljs-type,
.trix-content .hljs-selector-class,
.trix-content .hljs-selector-attr,
.trix-content .hljs-selector-pseudo,
.trix-content .hljs-number {
color: #d19a66;
}
.trix-content .hljs-symbol,
.trix-content .hljs-bullet,
.trix-content .hljs-link,
.trix-content .hljs-meta,
.trix-content .hljs-selector-id,
.trix-content .hljs-title {
color: #61aeee;
}
.trix-content .hljs-built_in,
.trix-content .hljs-title.class_,
.trix-content .hljs-class .trix-content .hljs-title {
color: #e6c07b;
}
.trix-content .hljs-emphasis {
font-style: italic;
}
.trix-content .hljs-strong {
font-weight: bold;
}
.trix-content .hljs-link {
text-decoration: underline;
}
<input type="hidden" class="rhino-editor-input" id="syntax-highlight-input" value="<pre><code class='highlight-js'>console.log('Hello World')</code></pre>">
<rhino-editor id="syntax-highlight-editor" input="syntax-highlight-input"></rhino-editor>
For additional themes, you can checkout the HighlightJS
https://github.com/highlightjs/highlight.js/tree/main/src/styles