Skip to content

Commit f1365fa

Browse files
committed
Add code blocks and math support
1 parent 914c66d commit f1365fa

File tree

17 files changed

+601
-147
lines changed

17 files changed

+601
-147
lines changed

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
<meta name="viewport" content="width=device-width,initial-scale=1" />
66
<!--metaTags-->
77
<link rel="icon" href="/favicon.ico" />
8+
<link
9+
href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css"
10+
rel="stylesheet" />
811
</head>
912
<body>
1013
<div id="app"><!--body--></div>

markdown/remark-plugin-transform-markdown.ts

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,91 @@ import type { Plugin } from "unified";
22
import type { Root, Heading, Literal } from "mdast";
33
import { toText } from "hast-util-to-text";
44

5-
const populateFrontMatter: Plugin = () => (ast) => {
5+
const transformMarkdown: Plugin = () => (ast, vFile) => {
66
const { children } = ast as Root;
77
if (!children.length) return;
88
const firstHeading = children.findIndex(
99
(node): node is Heading => node.type === "heading" && node.depth === 1,
1010
);
1111
if (!firstHeading) return;
12-
const title = toText(children[firstHeading]);
12+
const title = children[firstHeading];
13+
const titleText = title ? toText(title) : "";
1314
const frontMatter = children.find((node) => node.type === "yaml");
1415
if (!frontMatter) {
1516
children.unshift({
1617
type: "yaml",
17-
value: `title: ${title}`,
18+
value: `title: ${titleText}`,
1819
});
1920
} else {
20-
(frontMatter as Literal).value += `\ntitle: ${title}`;
21+
(frontMatter as Literal).value += `\ntitle: ${titleText}`;
2122
}
22-
children.splice(
23-
firstHeading + 1,
24-
0,
25-
{
26-
type: "mdxjsEsm",
27-
value: 'import PostData from "@/components/PostData";',
28-
data: {
29-
estree: {
30-
type: "Program",
31-
body: [
32-
{
33-
type: "ImportDeclaration",
34-
specifiers: [
35-
{
36-
type: "ImportDefaultSpecifier",
37-
local: {
38-
type: "Identifier",
39-
name: "PostData",
23+
if (vFile.dirname?.includes("blog")) {
24+
children.splice(
25+
firstHeading + 1,
26+
0,
27+
{
28+
type: "mdxjsEsm",
29+
value: 'import PostData from "@/components/PostData";',
30+
data: {
31+
estree: {
32+
type: "Program",
33+
body: [
34+
{
35+
type: "ImportDeclaration",
36+
specifiers: [
37+
{
38+
type: "ImportDefaultSpecifier",
39+
local: {
40+
type: "Identifier",
41+
name: "PostData",
42+
},
4043
},
44+
],
45+
source: {
46+
type: "Literal",
47+
value: "@/components/PostData",
48+
raw: '"@/components/PostData"',
4149
},
42-
],
43-
source: {
44-
type: "Literal",
45-
value: "@/components/PostData",
46-
raw: '"@/components/PostData"',
4750
},
48-
},
49-
],
50-
sourceType: "module",
51-
comments: [],
51+
],
52+
sourceType: "module",
53+
comments: [],
54+
},
5255
},
5356
},
54-
},
55-
{
56-
type: "mdxJsxFlowElement",
57-
name: "PostData",
58-
attributes: [
59-
{
60-
type: "mdxJsxAttribute",
61-
name: "frontMatter",
62-
value: {
63-
type: "mdxJsxAttributeValueExpression",
64-
value: "frontMatter",
65-
data: {
66-
estree: {
67-
type: "Program",
68-
body: [
69-
{
70-
type: "ExpressionStatement",
71-
expression: {
72-
type: "Identifier",
73-
name: "frontMatter",
57+
{
58+
type: "mdxJsxFlowElement",
59+
name: "PostData",
60+
attributes: [
61+
{
62+
type: "mdxJsxAttribute",
63+
name: "frontMatter",
64+
value: {
65+
type: "mdxJsxAttributeValueExpression",
66+
value: "frontMatter",
67+
data: {
68+
estree: {
69+
type: "Program",
70+
body: [
71+
{
72+
type: "ExpressionStatement",
73+
expression: {
74+
type: "Identifier",
75+
name: "frontMatter",
76+
},
7477
},
75-
},
76-
],
77-
sourceType: "module",
78-
comments: [],
78+
],
79+
sourceType: "module",
80+
comments: [],
81+
},
7982
},
8083
},
8184
},
82-
},
83-
],
84-
children: [],
85-
},
86-
);
85+
],
86+
children: [],
87+
},
88+
);
89+
}
8790
};
8891

89-
export default populateFrontMatter;
92+
export default transformMarkdown;

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,16 @@
5656
"copy-text-to-clipboard": "^3.2.0",
5757
"github-slugger": "^2.0.0",
5858
"hast-util-to-text": "^4.0.0",
59+
"prism-react-renderer": "^2.1.0",
5960
"react": "^18.2.0",
6061
"react-dom": "^18.2.0",
6162
"react-helmet-async": "^1.3.0",
6263
"react-router-dom": "^6.17.0",
6364
"react-tooltip": "^5.22.0",
65+
"rehype-katex": "^7.0.0",
6466
"remark-frontmatter": "^5.0.0",
6567
"remark-gfm": "^4.0.0",
68+
"remark-math": "^6.0.0",
6669
"remark-mdx-frontmatter": "^4.0.0",
6770
"vite": "^4.5.0",
6871
"vite-plugin-svgr": "^4.1.0"

project-words.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ copiable
55
coursetable
66
estree
77
huayu
8+
infty
89
joshcena
10+
katex
911
luo
12+
mathcal
1013
mdast
1114
mdxjs
15+
metastring
1216
noninteractive
1317
noreply
1418
peaceiris
1519
pipeable
1620
posix
21+
rehype
1722
saturationl
1823
scrolly
1924
sida

src/Layout/Command/commandHandler.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ import parseBash from "bash-parser";
33
import { paths } from "@/routes";
44

55
export function cd(url: string, pwd: string): string | null {
6-
const newPath = new URL(url.endsWith("/") ? url : `${url}/`, pwd);
7-
let pwdTree = pathTree;
8-
for (const part of newPath.pathname.split("/").filter(Boolean)) {
9-
if (!(part in pwdTree)) return null;
10-
pwdTree = pwdTree[part]!;
6+
try {
7+
const newPath = new URL(url.endsWith("/") ? url : `${url}/`, pwd);
8+
let pwdTree = pathTree;
9+
for (const part of newPath.pathname.split("/").filter(Boolean)) {
10+
if (!(part in pwdTree)) return null;
11+
pwdTree = pwdTree[part]!;
12+
}
13+
return String(newPath);
14+
} catch (e) {
15+
console.error(`Cannot cd to ${url} from ${pwd}`);
16+
return null;
1117
}
12-
return String(newPath);
1318
}
1419

1520
function ls(relative: string, pwd: string): [number, string] {

src/Layout/Command/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export default function Command(): JSX.Element {
7878
const timeoutRef = useRef<number | null>(null);
7979
const [env, setEnv] = useState<Record<string, string>>({
8080
__proto__: null as never,
81-
PWD: cd(location.pathname, "https://joshcena.com")!,
81+
// If current location is not found
82+
PWD:
83+
cd(location.pathname, "https://joshcena.com") ?? "https://joshcena.com",
8284
});
8385
const [lastExit, setLastExit] = useState(0);
8486
const [children, setChildren] = useState<(readonly [number, ReactNode])[]>(
@@ -89,7 +91,7 @@ export default function Command(): JSX.Element {
8991
setEnv((e) => ({
9092
__proto__: null as never,
9193
...e,
92-
PWD: cd(location.pathname, e.PWD!)!,
94+
PWD: cd(location.pathname, e.PWD!) ?? e.PWD!,
9395
}));
9496
}, [location]);
9597
useEffect(() => {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.codeBlockContainer:hover .language {
2+
display: none;
3+
}
4+
5+
.codeBlockTitle {
6+
}
7+
8+
.codeBlockContent {
9+
position: relative;
10+
line-height: 1.5;
11+
}
12+
13+
.codeBlock {
14+
padding: 1em;
15+
}
16+
17+
.copyButton {
18+
position: absolute;
19+
top: 1em;
20+
right: 1em;
21+
}
22+
23+
.language {
24+
position: absolute;
25+
top: 1em;
26+
right: 1em;
27+
font-size: small;
28+
font-weight: bold;
29+
text-transform: uppercase;
30+
color: var(--color-text-subdued);
31+
padding: 0.2em 0.5em;
32+
border-radius: 0.2em;
33+
}

src/components/CodeBlock/index.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { Children, type ReactNode } from "react";
2+
import clsx from "clsx";
3+
import { Highlight, themes, type Language } from "prism-react-renderer";
4+
import { useColorMode } from "@/context/ColorMode";
5+
import CopyButton from "@/components/CopyButton";
6+
import styles from "./index.module.css";
7+
8+
export default function CodeBlock({
9+
children,
10+
title,
11+
language: languageProp,
12+
className: classNameProp,
13+
}: {
14+
readonly children?: ReactNode;
15+
readonly metastring?: string;
16+
readonly title?: string | undefined;
17+
readonly language?: string;
18+
readonly className?: string | undefined;
19+
}): JSX.Element {
20+
const language =
21+
languageProp ?? classNameProp?.match(/language-(?<lang>[^ ]+)/)?.[1];
22+
const childrenArray = Children.toArray(children);
23+
const { colorMode } = useColorMode();
24+
const theme = colorMode === "dark" ? themes.vsDark : themes.github;
25+
if (
26+
childrenArray.length === 1 &&
27+
React.isValidElement(childrenArray[0]) &&
28+
childrenArray[0].type === "code"
29+
)
30+
return <CodeBlock {...childrenArray[0].props} />;
31+
const code = childrenArray
32+
.map((child) => {
33+
if (typeof child === "string") return child;
34+
if (typeof child === "number") return String(child);
35+
if (React.isValidElement(child) && child.type === "br") return "\n";
36+
return "";
37+
})
38+
.join("")
39+
.trim();
40+
return (
41+
<div
42+
className={clsx(
43+
classNameProp,
44+
styles.codeBlockContainer,
45+
"show-copy-button",
46+
)}>
47+
{title && <div className={styles.codeBlockTitle}>{title}</div>}
48+
<div className={styles.codeBlockContent}>
49+
<Highlight
50+
theme={theme}
51+
code={code}
52+
language={(language ?? "text") as Language}>
53+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
54+
<pre
55+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
56+
tabIndex={0}
57+
className={clsx(className, styles.codeBlock)}
58+
style={style}>
59+
<code>
60+
{tokens.map((line, i) => {
61+
if (line.length === 1 && line[0]!.content === "\n")
62+
line[0]!.content = "";
63+
64+
const lineProps = getLineProps({
65+
line,
66+
});
67+
return (
68+
// eslint-disable-next-line react/no-array-index-key
69+
<span key={i} {...lineProps}>
70+
{line.map((token, j) => (
71+
<span
72+
// eslint-disable-next-line react/no-array-index-key
73+
key={j}
74+
{...getTokenProps({ token, key: j })}
75+
/>
76+
))}
77+
<br />
78+
</span>
79+
);
80+
})}
81+
</code>
82+
</pre>
83+
)}
84+
</Highlight>
85+
<span className={styles.language}>{language}</span>
86+
<CopyButton className={styles.copyButton} string={code} />
87+
</div>
88+
</div>
89+
);
90+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.copyButton {
2+
background-color: #0000;
3+
border: none;
4+
padding: 0;
5+
margin: 0;
6+
cursor: pointer;
7+
white-space: nowrap;
8+
overflow: visible;
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
}
13+
14+
.copyIcon {
15+
height: 1.2em;
16+
position: relative;
17+
display: none;
18+
}
19+
20+
:global(.show-copy-button):is(:hover, :focus-visible) .copyIcon {
21+
display: inline;
22+
}

0 commit comments

Comments
 (0)