-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use hljs to highlight code blocks (#33)
Partially addresses #6 Use the hljs library to perform syntax highlighting on code blocks. This library achieves clearer syntax highlighting than the default Docusaurus solution. This change adds the `rehype-hljs` plugin, copied from `rehype-hljs-var` in `gravitational/docs`. By copying this plugin, rather than using `rehype-highlight` (which this plugin uses), we can add functionality from `gravitational/docs` in which the docs engine renders `Var` components in syntax-highlighted code snippets. Implementation details: - Eject src/theme/MDXComponents/index.tsx - Add `Pre` from gravitational/docs (but don't include the `CommandLine`-handling logic, which depends on `remark-code-snippet`) - Set variables for `--font-ubunt` and `--font-lato`. - Remove conflicting default MDXComponents exports. - Fix `remark-update-tags`: Change `Var` to `var` to accommodate the new `remark-hljs` logic, which which `Var`s have lowercased tags. - Use attribute selectors for HLJS classes. This is because Docusaurus hashes `hljs-` class names in production but not in dev. There's probably a more elegant solution here, but this quick hack works for now.
- Loading branch information
Showing
13 changed files
with
518 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { unified, Transformer } from "unified"; | ||
import type { VFile } from "vfile"; | ||
import rehypeHighlight, { | ||
Options as RehypeHighlightOptions, | ||
} from "rehype-highlight"; | ||
import { common } from "lowlight"; | ||
import { visit, CONTINUE, SKIP } from "unist-util-visit"; | ||
import { v4 as uuid } from "uuid"; | ||
import remarkParse from "remark-parse"; | ||
import type { Text, Element, Node, Parent } from "hast"; | ||
import remarkMDX from "remark-mdx"; | ||
|
||
const makePlaceholder = (): string => { | ||
// UUID for uniqueness, but remove hyphens since these are often parsed | ||
// as operators or other non-identifier tokens. Make sure the placeholder | ||
// begins with a letter so it gets parsed as an identifier. | ||
return "var" + uuid().replaceAll("-", ""); | ||
}; | ||
|
||
const placeholderPattern = "var[a-z0-9]{32}"; | ||
|
||
// We only visit text nodes inside code snippets | ||
const isVarContainer = (node: Node) => { | ||
if (node.type === "text") { | ||
return node.value.includes("<Var"); | ||
} | ||
|
||
if ( | ||
node.type === "element" && | ||
node.children.length === 1 && | ||
node.children[0].type === "text" | ||
) { | ||
return node.children[0].value.includes("<Var"); | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
export const rehypeHLJS = (options?: RehypeHighlightOptions): Transformer => { | ||
return (root: Parent, file: VFile) => { | ||
options.languages = { ...options.languages, ...common }; | ||
const highlighter = rehypeHighlight(options); | ||
let placeholdersToVars: Record<string, Node> = {}; | ||
|
||
// In a code snippet, Var elements are parsed as text. Replace these with | ||
// UUID strings to ensure that the parser won't split these up and make | ||
// them unrecoverable. | ||
visit(root, isVarContainer, (node: Node, index: number, parent: Parent) => { | ||
const varPattern = new RegExp("<Var [^>]+/>", "g"); | ||
let txt: Text; | ||
if (node.type == "text") { | ||
txt = node; | ||
} else { | ||
// isVarContainer enforces having a single child text node | ||
txt = node.children[0]; | ||
} | ||
|
||
const newVal = txt.value.replace(varPattern, (match) => { | ||
const placeholder = makePlaceholder(); | ||
// Since the Var element was originally text, parse it so we can recover | ||
// its properties. The result should be a small HTML AST with a root | ||
// node and one child, the Var node. | ||
const varElement = unified() | ||
.use(remarkParse) | ||
.use(remarkMDX) | ||
.parse(match); | ||
|
||
placeholdersToVars[placeholder] = varElement.children[0]; | ||
return placeholder; | ||
}); | ||
if (node.type == "text") { | ||
node.value = newVal; | ||
} else { | ||
node.children[0].value = newVal; | ||
} | ||
}); | ||
|
||
// Apply syntax highlighting | ||
(highlighter as Function)(root, file); | ||
|
||
// After syntax highlighting, the content of the code snippet will be a | ||
// series of span elements with different "hljs-*" classes. Find the | ||
// placeholder UUIDs and replace them with their original Var elements, | ||
// inserting these as HTML AST nodes. | ||
visit(root, isVarContainer, (node: Node, index: number, parent: Parent) => { | ||
const el = node as Element | Text; | ||
let hljsSpanValue = ""; | ||
if (el.type === "text") { | ||
hljsSpanValue = el.value; | ||
} else { | ||
hljsSpanValue = (el.children[0] as Text).value; | ||
} | ||
|
||
// This is either a text node or an hljs span with only the placeholder as | ||
// its child. We don't need the node, so replace it with the original | ||
// Var. | ||
if (placeholdersToVars[hljsSpanValue]) { | ||
(parent as any).children[index] = placeholdersToVars[hljsSpanValue]; | ||
return [CONTINUE]; | ||
} | ||
|
||
const placeholders = Array.from( | ||
hljsSpanValue.matchAll(new RegExp(placeholderPattern, "g")) | ||
); | ||
|
||
// No placeholders to recover, so there's nothing more to do. | ||
if (placeholders.length == 0) { | ||
return [CONTINUE]; | ||
} | ||
|
||
// The element's text includes one or more Vars among other content, so we | ||
// need to replace the span (or text node) with a series of spans (or | ||
// text nodes) separated by Vars. | ||
let newChildren: Array<Text | Element> = []; | ||
|
||
// Assemble a map of indexes to their corresponding placeholders so we | ||
// can tell whether a given index falls within a placeholder. | ||
const placeholderIndices = new Map(); | ||
placeholders.forEach((p) => { | ||
placeholderIndices.set(p.index, p[0]); | ||
}); | ||
|
||
let valueIdx = 0; | ||
while (valueIdx < hljsSpanValue.length) { | ||
// The current index is in a placeholder, so add the original Var | ||
// component to newChildren. | ||
if (placeholderIndices.has(valueIdx)) { | ||
const placeholder = placeholderIndices.get(valueIdx); | ||
valueIdx += placeholder.length; | ||
newChildren.push(placeholdersToVars[placeholder] as Element); | ||
continue; | ||
} | ||
// The current index is outside a placeholder, so assemble a text or | ||
// span node and push that to newChildren. | ||
let textVal = ""; | ||
while ( | ||
!placeholderIndices.has(valueIdx) && | ||
valueIdx < hljsSpanValue.length | ||
) { | ||
textVal += hljsSpanValue[valueIdx]; | ||
valueIdx++; | ||
} | ||
if (el.type === "text") { | ||
newChildren.push({ | ||
type: "text", | ||
value: textVal, | ||
}); | ||
} else { | ||
newChildren.push({ | ||
tagName: "span", | ||
type: "element", | ||
properties: el.properties, | ||
children: [ | ||
{ | ||
type: "text", | ||
value: textVal, | ||
}, | ||
], | ||
}); | ||
} | ||
} | ||
|
||
// Delete the current span and replace it with the new children. | ||
(parent.children as Array<Text | Element>).splice( | ||
index, | ||
1, | ||
...newChildren | ||
); | ||
return [SKIP, index + newChildren.length]; | ||
}); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.wrapper { | ||
padding: 0 var(--m-0-5); | ||
border: 1px solid var(--color-light-gray); | ||
border-radius: var(--r-sm); | ||
font-size: var(--fs-text-md); | ||
word-break: break-word; | ||
background-color: var(--color-lightest-gray); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import cn from "classnames"; | ||
import { DetailedHTMLProps, HTMLAttributes } from "react"; | ||
import styles from "./Code.module.css"; | ||
|
||
export const CodeLine = (props: CodeLineProps) => { | ||
return <span className={styles.line} {...props} />; | ||
}; | ||
|
||
const isHLJSNode = (className?: string) => | ||
Boolean(className) && className.indexOf("hljs") !== -1; | ||
|
||
export default function ( | ||
props: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> | ||
) { | ||
if (isHLJSNode(props.className)) { | ||
return <code {...props} />; | ||
} | ||
|
||
return <code {...props} className={cn(styles.wrapper, props.className)} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* stylelint-disable selector-class-pattern */ | ||
/* stylelint-disable no-descending-specificity */ | ||
|
||
/* theme taken from https://github.com/highlightjs/highlight.js/blob/master/src/styles/monokai.css */ | ||
|
||
.wrapper { | ||
display: block; | ||
overflow-x: auto; | ||
margin: 0; | ||
padding: var(--m-1) var(--m-2); | ||
border-radius: var(--r-default); | ||
color: #ddd; | ||
font-family: var(--font-monospace); | ||
line-height: var(--lh-md); | ||
white-space: pre; | ||
background-color: var(--color-code); | ||
|
||
@media (--sm-scr) { | ||
font-size: var(--fs-text-sm); | ||
} | ||
|
||
@media (--md-scr) { | ||
font-size: var(--fs-text-md); | ||
} | ||
|
||
& code { | ||
font-family: inherit; | ||
} | ||
|
||
/* We use attribute selectors to select hljs- classes because Docusaurus | ||
* hashes them in production builds.*/ | ||
& :global { | ||
& [class^="hljs-tag"], | ||
& [class^="hljs-keyword"], | ||
& [class^="hljs-selector-tag"], | ||
& [class^="hljs-literal"], | ||
& [class^="hljs-strong"], | ||
& [class^="hljs-name"] { | ||
color: #f92672; | ||
} | ||
|
||
& [class^="hljs-code"] { | ||
color: #66d9ef; | ||
} | ||
|
||
& [class^="hljs-class"] [class^="hljs-title"] { | ||
color: white; | ||
} | ||
|
||
& [class^="hljs-attribute"], | ||
& [class^="hljs-symbol"], | ||
& [class^="hljs-regexp"], | ||
& [class^="hljs-link"] { | ||
color: #bf79db; | ||
} | ||
|
||
& [class^="hljs-string"], | ||
& [class^="hljs-bullet"], | ||
& [class^="hljs-subst"], | ||
& [class^="hljs-title"], | ||
& [class^="hljs-section"], | ||
& [class^="hljs-emphasis"], | ||
& [class^="hljs-type"], | ||
& [class^="hljs-built_in"], | ||
& [class^="hljs-builtin-name"], | ||
& [class^="hljs-selector-attr"], | ||
& [class^="hljs-selector-pseudo"], | ||
& [class^="hljs-addition"], | ||
& [class^="hljs-variable"], | ||
& [class^="hljs-template-tag"], | ||
& [class^="hljs-template-variable"] { | ||
color: #a6e22e; | ||
} | ||
|
||
& [class^="hljs-comment"], | ||
& [class^="hljs-quote"], | ||
& [class^="hljs-deletion"], | ||
& [class^="hljs-meta"] { | ||
color: #75715e; | ||
} | ||
|
||
& [class^="hljs-keyword"], | ||
& [class^="hljs-selector-tag"], | ||
& [class^="hljs-literal"], | ||
& [class^="hljs-doctag"], | ||
& [class^="hljs-title"], | ||
& [class^="hljs-section"], | ||
& [class^="hljs-type"], | ||
& [class^="hljs-selector-id"] { | ||
font-weight: bold; | ||
} | ||
} | ||
} | ||
|
||
.line { | ||
display: block; | ||
& :global { | ||
& .wrapper-input input { | ||
color: var(--color-light-blue); | ||
background-color: transparent; | ||
} | ||
|
||
& .wrapper-input input::placeholder { | ||
color: var(--color-light-gray); | ||
} | ||
|
||
& .wrapper-input svg { | ||
color: var(--color-light-blue); | ||
} | ||
} | ||
} |
Oops, something went wrong.