Syntax Highlighting

Syntax Highlighting! Good luck doing this in Trix!

As with everything in Rhino there are 2 parts:

  1. Add to our frontend editor
  2. 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.

shell
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.

JavaScript
// 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.

CSS
/*
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;
}
HTML
<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 repo which Lowlight uses internally. The link for the CSS themes is below:

https://github.com/highlightjs/highlight.js/tree/main/src/styles