bits&pieces by pekala

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

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 object
const 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 color
const baseStyles: SystemStyleObject = {
color: "code.default",
backgroundColor: "code.background",
};
// Loop through the allowed colors and add styles
// for the corresponding Prism class
const 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 classes
const 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/875e6ff75bc8ce171c758bf75f304707
const camelToKebab = (token: string) =>
token.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();

Example

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 theme
colors: {
// ...other default theme colors
...codeTheme.colors.light,
modes: {
dark: {
//...other dark theme colors
...codeTheme.colors.dark,
},
},
},
styles: {
//...other styled components
code: {
...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.

Brought to you by Maciek. Who dat?