Skip to content

Initial attempt at a code splitted syntax highligher #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions components/markdown/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import dynamic from "next/dynamic";
import React, { PropsWithChildren, useState, Suspense } from "react";

const SyntaxHighlighter = dynamic(
() => import("../syntax-highlighter/syntax-highlighter"),
{
suspense: true,
}
);

interface CodeBlockProps {
inline?: boolean;
className?: string;
}

export default function CodeBlock({
children,
className,
inline,
}: PropsWithChildren<CodeBlockProps>) {
if (!children) {
return null;
}

if (inline) {
return (
<code data-inline="data-inline" className={className}>
{children}
</code>
);
}

const language = className.replace("language-", "");
const codeToParse = String(children?.[0] || "");

return (
<code className={className}>
<Suspense fallback={codeToParse}>
<SyntaxHighlighter language={language} code={codeToParse} />
</Suspense>
</code>
);
}
2 changes: 2 additions & 0 deletions components/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Link } from "../link";
import CodeBlock from "./CodeBlock";

interface MarkdownProps {
document: string;
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function Markdown({ document, className }: MarkdownProps) {

return <Link {...props} />;
},
code: CodeBlock,
}}
>
{document}
Expand Down
115 changes: 115 additions & 0 deletions components/syntax-highlighter/syntax-highlighter.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
.codeBlock {
--code-syntax-plain: #ffffff;
--code-syntax-comment: #616e88;
--code-syntax-keyword: #81a1c1;
--code-syntax-tag: #d08770;
--code-syntax-punctuation: #ffffff;
--code-syntax-property: #81a1c1;
--code-syntax-propertyname: #8fbcbb;
--code-syntax-definition-property: #88c0d0;
--code-syntax-variable: #d8dee9;
--code-syntax-function-variable: #8fbcbb;
--code-syntax-definition-variable: #d8dee9;
--code-syntax-static: #b48ead;
--code-syntax-string: #a3be8c;
--code-syntax-special-string: #d08770;
--code-syntax-literal: #d8dee9;
--code-syntax-atom: #b48ead;
--code-syntax-bracket: #e5e9f0;
--code-syntax-quotes: #e5e9f0;
--code-syntax-brace: #e5e9f0;
}

.keyword,
.moduleKeyword,
.className {
color: var(--code-syntax-keyword);
font-weight: 500;
}

.processingInstruction,
.inserted,
.string {
color: var(--code-syntax-string);
}

.variableName {
color: var(--code-syntax-variable);
}

.definition_variableName {
color: var(--code-syntax-definition-variable);
}

.function_variableName {
color: var(--code-syntax-function-variable);
}

.plain {
color: var(--code-syntax-plain);
}

.comment {
color: var(--code-syntax-comment);
}

.tag,
.tagName {
color: var(--code-syntax-tag);
}

.punctuation {
color: var(--code-syntax-punctuation);
}

.number {
color: var(--code-syntax-static);
}

.atom,
.bool,
.special_variableName {
color: var(--code-syntax-atom);
}

.brace {
color: var(--code-syntax-brace);
}

.quote,
.doubleQuote {
color: var(--code-syntax-quotes);
}

.angleBracket,
.squareBracket,
.paren {
color: var(--code-syntax-bracket);
}

.labelName,
.typeName,
.property {
color: var(--code-syntax-property);
}

.propertyName {
color: var(--code-syntax-propertyname);
}

.operator,
.literal {
color: var(--code-syntax-literal);
}

.link,
.regexp,
.escape,
.url,
.special_string {
color: var(--code-syntax-special-string);
}

.definition_propertyName {
color: var(--code-syntax-definition-property);
}
83 changes: 83 additions & 0 deletions components/syntax-highlighter/syntax-highlighter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useEffect, useRef, useState } from "react";
import {
EditorView,
highlightSpecialChars,
lineNumbers,
} from "@codemirror/view";
import {
defaultHighlightStyle,
syntaxHighlighting,
} from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import { EditorState } from "@codemirror/state";
import { createHighlighterTokensFromStyles } from "./utils";
import styles from "./syntax-highlighter.module.css";

export interface SyntaxHighlighterProps {
code: string;
language: string;
}

const highlightStyle = createHighlighterTokensFromStyles(styles);

export default function SyntaxHighlighter({
language,
code = "",
}: SyntaxHighlighterProps) {
const editorViewRef = useRef<EditorView>();
const languageConfig = languages.find((langConfig) =>
langConfig.alias.includes(language)
);
const block = useRef();

useEffect(() => {
let mounted = true;

(async function () {
const languageSupport = await languageConfig.load();

if (block.current && mounted) {
const extensions = [
lineNumbers(),
EditorView.editable.of(false),
EditorView.theme(
{
".cm-gutters": {
borderRight: "1px solid #739fee73",
color: "#739fee",
// Matching the `pre` tag's background since the editor
// text sits below the gutter when scrolled horizontal.
backgroundColor: "var(--tw-prose-pre-bg)",
minWidth: "4ch",
marginRight: "12px",
backdropFilter: "blur(4px)",
},
},
{ dark: true }
),
syntaxHighlighting(defaultHighlightStyle),
syntaxHighlighting(highlightStyle),
highlightSpecialChars(),
languageSupport,
EditorState.tabSize.of(2),
];

let view = new EditorView({
doc: code.trimEnd(),
extensions,
parent: block.current,
});

editorViewRef.current = view;
}
})();

return () => {
mounted = false;
// If there's a view, destroy it.
editorViewRef.current?.destroy();
};
}, [languageConfig, code]);

return <div ref={block} className={styles.codeBlock}></div>;
}
50 changes: 50 additions & 0 deletions components/syntax-highlighter/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HighlightStyle, TagStyle } from "@codemirror/language";
import { tags } from "@lezer/highlight";

export function createHighlighterTokensFromStyles(
styles: Record<string, string>
): HighlightStyle {
const highlightConfig: TagStyle[] = Object.entries(styles).map(
([key, className]) => {
if (key.includes("_")) {
return {
tag: composeTagsFromString(key),
class: className,
};
}

return {
tag: tags[key],
class: className,
};
}
);

return HighlightStyle.define(
highlightConfig.filter((config) => typeof config.tag !== "undefined")
);
}

const composeTagsFromString = (stringifiedTagName) =>
stringifiedTagName.split("_").reduceRight((val, fn) => {
if (!tags[fn]) {
const error = Error(
[
`Unable to find a tag function named ${fn},`,
`while parsing key ${stringifiedTagName}.`,
"Key will be ignored in release build.",
"Fix styles to remove this error.",
].join(" ")
);

// Don't want to break production app if something slips out,
// but hopefully by breaking the dev app, a developer will fix
// any issues before releasing to prod.
if (process.env.NODE_ENV !== "development") {
throw error;
} else {
console.error({ error });
}
}
return tags[fn](typeof val === "string" ? tags[val] : val);
});
14 changes: 8 additions & 6 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module.exports = {
swcMinify: true,
// Leaving this off for now.
// Codemirror does not minify correctly with this on.
swcMinify: false,
reactStrictMode: true,
i18n: {
locales: ["en"],
Expand All @@ -8,10 +10,10 @@ module.exports = {
async redirects() {
return [
{
source: '/:username/:gist_id',
destination: '/:gist_id',
source: "/:username/:gist_id",
destination: "/:gist_id",
permanent: true,
},
]
}
}
];
},
};
Loading