Skip to content

Commit 2278b90

Browse files
authored
Initial attempt at a code splitted syntax highligher (#20)
1 parent e7dbcdc commit 2278b90

9 files changed

+1207
-207
lines changed

Diff for: components/markdown/CodeBlock.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import dynamic from "next/dynamic";
2+
import React, { PropsWithChildren, useState, Suspense } from "react";
3+
4+
const SyntaxHighlighter = dynamic(
5+
() => import("../syntax-highlighter/syntax-highlighter"),
6+
{
7+
suspense: true,
8+
}
9+
);
10+
11+
interface CodeBlockProps {
12+
inline?: boolean;
13+
className?: string;
14+
}
15+
16+
export default function CodeBlock({
17+
children,
18+
className,
19+
inline,
20+
}: PropsWithChildren<CodeBlockProps>) {
21+
if (!children) {
22+
return null;
23+
}
24+
25+
if (inline) {
26+
return (
27+
<code data-inline="data-inline" className={className}>
28+
{children}
29+
</code>
30+
);
31+
}
32+
33+
const language = className.replace("language-", "");
34+
const codeToParse = String(children?.[0] || "");
35+
36+
return (
37+
<code className={className}>
38+
<Suspense fallback={codeToParse}>
39+
<SyntaxHighlighter language={language} code={codeToParse} />
40+
</Suspense>
41+
</code>
42+
);
43+
}

Diff for: components/markdown/Markdown.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ReactMarkdown from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { Link } from "../link";
4+
import CodeBlock from "./CodeBlock";
45

56
interface MarkdownProps {
67
document: string;
@@ -42,6 +43,7 @@ export default function Markdown({ document, className }: MarkdownProps) {
4243

4344
return <Link {...props} />;
4445
},
46+
code: CodeBlock,
4547
}}
4648
>
4749
{document}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
.codeBlock {
2+
--code-syntax-plain: #ffffff;
3+
--code-syntax-comment: #616e88;
4+
--code-syntax-keyword: #81a1c1;
5+
--code-syntax-tag: #d08770;
6+
--code-syntax-punctuation: #ffffff;
7+
--code-syntax-property: #81a1c1;
8+
--code-syntax-propertyname: #8fbcbb;
9+
--code-syntax-definition-property: #88c0d0;
10+
--code-syntax-variable: #d8dee9;
11+
--code-syntax-function-variable: #8fbcbb;
12+
--code-syntax-definition-variable: #d8dee9;
13+
--code-syntax-static: #b48ead;
14+
--code-syntax-string: #a3be8c;
15+
--code-syntax-special-string: #d08770;
16+
--code-syntax-literal: #d8dee9;
17+
--code-syntax-atom: #b48ead;
18+
--code-syntax-bracket: #e5e9f0;
19+
--code-syntax-quotes: #e5e9f0;
20+
--code-syntax-brace: #e5e9f0;
21+
}
22+
23+
.keyword,
24+
.moduleKeyword,
25+
.className {
26+
color: var(--code-syntax-keyword);
27+
font-weight: 500;
28+
}
29+
30+
.processingInstruction,
31+
.inserted,
32+
.string {
33+
color: var(--code-syntax-string);
34+
}
35+
36+
.variableName {
37+
color: var(--code-syntax-variable);
38+
}
39+
40+
.definition_variableName {
41+
color: var(--code-syntax-definition-variable);
42+
}
43+
44+
.function_variableName {
45+
color: var(--code-syntax-function-variable);
46+
}
47+
48+
.plain {
49+
color: var(--code-syntax-plain);
50+
}
51+
52+
.comment {
53+
color: var(--code-syntax-comment);
54+
}
55+
56+
.tag,
57+
.tagName {
58+
color: var(--code-syntax-tag);
59+
}
60+
61+
.punctuation {
62+
color: var(--code-syntax-punctuation);
63+
}
64+
65+
.number {
66+
color: var(--code-syntax-static);
67+
}
68+
69+
.atom,
70+
.bool,
71+
.special_variableName {
72+
color: var(--code-syntax-atom);
73+
}
74+
75+
.brace {
76+
color: var(--code-syntax-brace);
77+
}
78+
79+
.quote,
80+
.doubleQuote {
81+
color: var(--code-syntax-quotes);
82+
}
83+
84+
.angleBracket,
85+
.squareBracket,
86+
.paren {
87+
color: var(--code-syntax-bracket);
88+
}
89+
90+
.labelName,
91+
.typeName,
92+
.property {
93+
color: var(--code-syntax-property);
94+
}
95+
96+
.propertyName {
97+
color: var(--code-syntax-propertyname);
98+
}
99+
100+
.operator,
101+
.literal {
102+
color: var(--code-syntax-literal);
103+
}
104+
105+
.link,
106+
.regexp,
107+
.escape,
108+
.url,
109+
.special_string {
110+
color: var(--code-syntax-special-string);
111+
}
112+
113+
.definition_propertyName {
114+
color: var(--code-syntax-definition-property);
115+
}

Diff for: components/syntax-highlighter/syntax-highlighter.tsx

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import {
3+
EditorView,
4+
highlightSpecialChars,
5+
lineNumbers,
6+
} from "@codemirror/view";
7+
import {
8+
defaultHighlightStyle,
9+
syntaxHighlighting,
10+
} from "@codemirror/language";
11+
import { languages } from "@codemirror/language-data";
12+
import { EditorState } from "@codemirror/state";
13+
import { createHighlighterTokensFromStyles } from "./utils";
14+
import styles from "./syntax-highlighter.module.css";
15+
16+
export interface SyntaxHighlighterProps {
17+
code: string;
18+
language: string;
19+
}
20+
21+
const highlightStyle = createHighlighterTokensFromStyles(styles);
22+
23+
export default function SyntaxHighlighter({
24+
language,
25+
code = "",
26+
}: SyntaxHighlighterProps) {
27+
const editorViewRef = useRef<EditorView>();
28+
const languageConfig = languages.find((langConfig) =>
29+
langConfig.alias.includes(language)
30+
);
31+
const block = useRef();
32+
33+
useEffect(() => {
34+
let mounted = true;
35+
36+
(async function () {
37+
const languageSupport = await languageConfig.load();
38+
39+
if (block.current && mounted) {
40+
const extensions = [
41+
lineNumbers(),
42+
EditorView.editable.of(false),
43+
EditorView.theme(
44+
{
45+
".cm-gutters": {
46+
borderRight: "1px solid #739fee73",
47+
color: "#739fee",
48+
// Matching the `pre` tag's background since the editor
49+
// text sits below the gutter when scrolled horizontal.
50+
backgroundColor: "var(--tw-prose-pre-bg)",
51+
minWidth: "4ch",
52+
marginRight: "12px",
53+
backdropFilter: "blur(4px)",
54+
},
55+
},
56+
{ dark: true }
57+
),
58+
syntaxHighlighting(defaultHighlightStyle),
59+
syntaxHighlighting(highlightStyle),
60+
highlightSpecialChars(),
61+
languageSupport,
62+
EditorState.tabSize.of(2),
63+
];
64+
65+
let view = new EditorView({
66+
doc: code.trimEnd(),
67+
extensions,
68+
parent: block.current,
69+
});
70+
71+
editorViewRef.current = view;
72+
}
73+
})();
74+
75+
return () => {
76+
mounted = false;
77+
// If there's a view, destroy it.
78+
editorViewRef.current?.destroy();
79+
};
80+
}, [languageConfig, code]);
81+
82+
return <div ref={block} className={styles.codeBlock}></div>;
83+
}

Diff for: components/syntax-highlighter/utils.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { HighlightStyle, TagStyle } from "@codemirror/language";
2+
import { tags } from "@lezer/highlight";
3+
4+
export function createHighlighterTokensFromStyles(
5+
styles: Record<string, string>
6+
): HighlightStyle {
7+
const highlightConfig: TagStyle[] = Object.entries(styles).map(
8+
([key, className]) => {
9+
if (key.includes("_")) {
10+
return {
11+
tag: composeTagsFromString(key),
12+
class: className,
13+
};
14+
}
15+
16+
return {
17+
tag: tags[key],
18+
class: className,
19+
};
20+
}
21+
);
22+
23+
return HighlightStyle.define(
24+
highlightConfig.filter((config) => typeof config.tag !== "undefined")
25+
);
26+
}
27+
28+
const composeTagsFromString = (stringifiedTagName) =>
29+
stringifiedTagName.split("_").reduceRight((val, fn) => {
30+
if (!tags[fn]) {
31+
const error = Error(
32+
[
33+
`Unable to find a tag function named ${fn},`,
34+
`while parsing key ${stringifiedTagName}.`,
35+
"Key will be ignored in release build.",
36+
"Fix styles to remove this error.",
37+
].join(" ")
38+
);
39+
40+
// Don't want to break production app if something slips out,
41+
// but hopefully by breaking the dev app, a developer will fix
42+
// any issues before releasing to prod.
43+
if (process.env.NODE_ENV !== "development") {
44+
throw error;
45+
} else {
46+
console.error({ error });
47+
}
48+
}
49+
return tags[fn](typeof val === "string" ? tags[val] : val);
50+
});

Diff for: next.config.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module.exports = {
2-
swcMinify: true,
2+
// Leaving this off for now.
3+
// Codemirror does not minify correctly with this on.
4+
swcMinify: false,
35
reactStrictMode: true,
46
i18n: {
57
locales: ["en"],
@@ -8,10 +10,10 @@ module.exports = {
810
async redirects() {
911
return [
1012
{
11-
source: '/:username/:gist_id',
12-
destination: '/:gist_id',
13+
source: "/:username/:gist_id",
14+
destination: "/:gist_id",
1315
permanent: true,
1416
},
15-
]
16-
}
17-
}
17+
];
18+
},
19+
};

0 commit comments

Comments
 (0)