Theme-Aware Syntax Highlighting
Published on July 12th, 2020
TypeScript
Theme UI
Problem
Theme UI comes with an official plugin for code syntax highlighting using Prism. However, the presets that come with that package, even tough they sometimes have dark and light variants, are not responsive to the current color mode set in Theme UI.
Solution
To get the desired behaviour, we need to build a styling object that maps Prims's class names for different recognised code tokens to named colors Theme UI tokens. Those can then be defined in all color modes we declare in Theme UI.
The Bit
tsx
import { CSSSelectorObject, SystemStyleObject } from "@styled-system/css";export function getCodeTheme(colorSets: Record<string, Record<CodeColor, string>>,italics: Array<CodeColor>) {// Add a "code" namespace, so that the resulting object// can be safely spread in the theme objectconst namespacedColors: Record<keyof typeof colorSets,{ code: Record<CodeColor, string> }> = {};for (const theme of Object.keys(colorSets)) {namespacedColors[theme] = { code: colorSets[theme] };}// Styles object for the default background / text colorconst baseStyles: SystemStyleObject = {color: "code.default",backgroundColor: "code.background",};// Loop through the allowed colors and add styles// for the corresponding Prism classconst classStyles: CSSSelectorObject = {};for (const colorName of codeColors) {if (colorName === "default" || colorName === "background") {continue;}const className = `.${camelToKebab(colorName)}`;classStyles[className] = {color: `code.${colorName}`,fontStyle: italics.includes(colorName) ? "italic" : undefined,};}return {colors: namespacedColors,codeStyles: { ...baseStyles, ...classStyles },};}// Allowed color names, correspond to Prism classesconst codeColors = ["default", // fallback text color"background", // code block background color"comment","string","url","builtin","char","constant","function","variable","tag","attrName","inserted","boolean","keyword","operator","className","punctuation","namespace","number","property","selector","doctype","deleted","changed",] as const;type CodeColor = typeof codeColors[number];// Quick and dirty camelCase to kebab-case// https://gist.github.com/nblackburn/875e6ff75bc8ce171c758bf75f304707const camelToKebab = (token: string) =>token.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
Example
tsx
const codeColors = {dark: {default: "rgb(214, 222, 235)",background: "rgb(1, 22, 39)",function: "rgb(130, 170, 255)",variable: "rgb(214, 222, 235)",//... all the other colors} as const,light: {default: "rgb(64, 63, 83)",background: "rgb(251, 251, 251)",function: "rgb(153, 76, 195)",variable: "rgb(201, 103, 101)",//... all the other colors} as const,};const codeItalics = ["function", "attrName", "selector"] as const;const codeTheme = getCodeTheme(codeColors, codeItalics);const myTheme = {//... rest of your Theme UI themecolors: {// ...other default theme colors...codeTheme.colors.light,modes: {dark: {//...other dark theme colors...codeTheme.colors.dark,},},},styles: {//...other styled componentscode: {...codeTheme.codeStyles,},},};
Final words
You can see the example of how this works by switching between color modes on this page.
I'm not fully happy with how the quality of this highlighting though, so I'll be
exploring alternatives to @theme-ui/prism
in the future.