Skip to content

Commit 4276514

Browse files
committed
Refactor MDX, add diff-highlight aricle
1 parent cd1a98d commit 4276514

File tree

9 files changed

+301
-56
lines changed

9 files changed

+301
-56
lines changed

app.config.ts

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import tsconfig from "./tsconfig.json";
1919
// import { ssrBabelPlugin } from "./vite/ssrBabelPlugin";
2020
import SSPreloadBabel from "solid-start-preload/babel";
2121
import { viteImagePlugin } from "./vite/viteImagePlugin";
22+
import { viteMDPlugin } from "./vite/viteMDPlugin";
2223

2324
// import devtools from "solid-devtools/vite";
2425

25-
const { default: mdx } = _mdx;
26-
2726
const root = process.cwd();
2827

2928
const babelPluginLabels = [
@@ -79,6 +78,7 @@ export default defineConfig({
7978
viteImagePlugin(),
8079
compileTime(),
8180
solidSvg(),
81+
viteMDPlugin(),
8282
// devtools({
8383
// autoname: true,
8484
// locator: {
@@ -87,54 +87,6 @@ export default defineConfig({
8787
// jsxLocation: true,
8888
// } as any,
8989
// }),
90-
mdx.withImports({})({
91-
jsx: true,
92-
jsxImportSource: "solid-js",
93-
providerImportSource: "solid-mdx",
94-
remarkPlugins: [
95-
remarkFrontmatter,
96-
remarkGfm,
97-
[
98-
remarkCaptions,
99-
{
100-
external: {
101-
table: "Table:",
102-
},
103-
},
104-
],
105-
],
106-
rehypePlugins: [
107-
[
108-
rehypeShiki,
109-
{
110-
theme: "github-dark",
111-
transformers: [
112-
transformerNotationDiff(),
113-
(() => {
114-
let meta: any;
115-
return {
116-
preprocess(raw: any, options: any) {
117-
meta = {};
118-
const rawMeta = options.meta.__raw;
119-
if (!rawMeta) return;
120-
meta = Object.fromEntries(
121-
Object.entries(
122-
parseDelimitedString(rawMeta, " ")
123-
).map(([k, v]) => [k, JSON.parse(v)])
124-
);
125-
},
126-
code(node: any) {
127-
node.properties = { ...node.properties, ...meta };
128-
node.meta = meta;
129-
},
130-
};
131-
})(),
132-
],
133-
},
134-
],
135-
[rehypeRaw, { passThrough: nodeTypes }],
136-
],
137-
}),
13890
linariaVitePlugin({
13991
include: [/\/src\//],
14092
exclude: [/solid-refresh/, /\/@babel\/runtime\//, /\.import\./],
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { ShikiTransformer } from "@shikijs/core";
2+
import type { Element } from "hast";
3+
4+
export interface shikiDiffNotationOptions {
5+
// class for added lines
6+
classLineAdd?: string;
7+
// class for removed lines
8+
classLineRemove?: string;
9+
// class added to the <pre> element when the current code has diff
10+
classActivePre?: string;
11+
}
12+
13+
type MetaNode = Element & { meta?: Record<string, any> };
14+
15+
export function shikiDiffNotation(
16+
options: shikiDiffNotationOptions = {}
17+
): ShikiTransformer {
18+
const {
19+
classLineAdd = "add",
20+
classLineRemove = "remove",
21+
classActivePre = "diff",
22+
} = options;
23+
24+
return {
25+
name: "shiki-diff-notation",
26+
code(node: MetaNode) {
27+
if (!node.meta?.diff) return;
28+
this.addClassToHast(this.pre, classActivePre);
29+
30+
const lines = node.children.filter(
31+
(node) => node.type === "element"
32+
) as Element[];
33+
34+
lines.forEach((line) => {
35+
for (const child of line.children) {
36+
if (child.type !== "element") continue;
37+
const text = child.children[0];
38+
if (text.type !== "text") continue;
39+
40+
if (text.value.startsWith("+")) {
41+
text.value = text.value.slice(1);
42+
this.addClassToHast(line, classLineAdd);
43+
}
44+
if (text.value.startsWith("-")) {
45+
text.value = text.value.slice(1);
46+
this.addClassToHast(line, classLineRemove);
47+
}
48+
}
49+
});
50+
},
51+
};
52+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"node": ">=18"
4040
},
4141
"devDependencies": {
42+
"@shikijs/core": "1.3.0",
4243
"@shikijs/rehype": "1.3.0",
4344
"@shikijs/transformers": "1.3.0",
4445
"@shikijs/twoslash": "1.3.0",

src/Article.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const Article: Component<Props> = (props) => {
7878
);
7979
},
8080
code: (props: any) => {
81-
const { children: _c, title, ...rest } = $destructure(props);
81+
const { children: _c, title, class: $class } = $destructure(props);
8282
const c = children(() => _c);
8383
return (
8484
<>
@@ -89,12 +89,12 @@ const Article: Component<Props> = (props) => {
8989
</div>
9090
</Show>
9191
<code
92-
{...rest}
93-
class={
92+
class={cn(
93+
$class,
9494
typeof c() == "string"
9595
? "rounded bg-colors-primary-300 pl-2 pr-2"
9696
: ""
97-
}>
97+
)}>
9898
{c()}
9999
</code>
100100
</>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
title: Adding diff highlighting to Markdown using Shiki
3+
---
4+
5+
import DialogImage from "~/DialogImage";
6+
import p1 from "./github.jpg?lazy";
7+
import t1 from "./github.jpg?lazy&size=900x";
8+
import p2 from "./shiki-diff.jpg?lazy";
9+
import t2 from "./shiki-diff.jpg?lazy&size=900x";
10+
11+
12+
Shiki is a great library for adding some color to markdown codeblocks, so let's add some
13+
awesome diff highlighting to it.
14+
15+
16+
For anyone unaware, [Shiki](https://shiki.style) is a neat little library that adds
17+
syntax highlihgting to your code blocks in markdown. There's a [wide list of languages
18+
supported](https://shiki.style/languages) as well as support for adding
19+
custom languages.
20+
The installation process is very quick and supports various markdown environments
21+
such as [MDX](https://mdxjs.com/), which I'll be using today.
22+
23+
# The quest for existing solutions
24+
25+
## Github
26+
27+
First, let's look at how *not* to do diff highlighting - by looking at what Github
28+
does. At the time of writing, Github does not support syntax highlighting
29+
of languages in conjunction with diff highlighting - it's either one or the other.
30+
This means that, if you want to show any sort of diff highlighting, you loose all
31+
other syntax highlighting.
32+
33+
<Img
34+
src={p1}
35+
thumbnail={t1}
36+
caption="Github's Markdown diff formatting"
37+
/>
38+
39+
Yeah... doesn't look that great (in my opinion).
40+
Another big issue is, that selecting text also selects the diff `+` and `-` markers,
41+
which is very annoying since the markers need to be manually removed when pasted
42+
into an editor.
43+
44+
Let's look at how the markup looks like:
45+
46+
````txt
47+
```diff
48+
-console.log('hewwo')
49+
+console.log('hello')
50+
console.log('goodbye')
51+
```
52+
````
53+
54+
Alright, at least the markup looks pretty good in my opinion - simple and easy to understand, just
55+
add a `+` or `-` as the first character in a line and it gets highlighted!
56+
57+
## Shiki/transformers
58+
59+
Lucky for us, there seems to be a first-party plugin for adding all sorts of notions, including
60+
diff highlighting! I highly encourage everyone to look at the awesome
61+
[@shiki/transformers](https://shiki.style/packages/transformers#transformernotationdiff)
62+
package which seems to have just what we need, and it's just 1 line to add the plugin.
63+
64+
Here's the example output from the documentation:
65+
66+
<Img
67+
src={p2}
68+
thumbnail={t2}
69+
caption="Shiki diff transformer"
70+
/>
71+
72+
It looks great, doesn't select the marker symbols, and it's not opinionated about the styling -
73+
lines just get some CSS classes assigned and we can style them however we want, neat!
74+
Now let's look at the markup:
75+
76+
````txt
77+
```ts
78+
console.log('hewwo') // [!code --]
79+
console.log('hello') // [!code ++]
80+
console.log('goodbye')
81+
```
82+
````
83+
84+
Ok... I have to say it's not exactly my cup of tea. The same parsing logic is used for all sorts
85+
of notations the package supports, so it makes sense for it to be more complex so that it supports
86+
passing in additional arguments, etc. but it's a bit overkill for our use-case.
87+
88+
Most people would just accept the notation and carry on with their day, but if you're like me,
89+
follow along as we make our own implementation.
90+
91+
# A better notation
92+
93+
I decided to work off the existing code and just change the necessary parts, so the first step
94+
was to copy the [original
95+
implementation](https://github.com/shikijs/shiki/blob/main/packages/transformers/src/transformers/notation-diff.ts) and
96+
it's dependencies.
97+
The next step was to decide on a notation, the Github one seems pretty good so I'll go with that.
98+
99+
````txt
100+
```ts diff
101+
-console.log('hewwo')
102+
+console.log('hello')
103+
console.log('goodbye')
104+
```
105+
````
106+
107+
108+
Implementing a shiki highlighter is incredibly easy, since the library provides all the hooks
109+
we need and more - there's even helpers for adding CSS classes to nodes.
110+
Here's what a simple implementation of the above looks like:
111+
112+
```ts title="shikiDiffNotation.ts"
113+
export function shikiDiffNotation(
114+
options: shikiDiffNotationOptions = {}
115+
): ShikiTransformer {
116+
const {
117+
classLineAdd = "add",
118+
classLineRemove = "remove",
119+
classActivePre = "diff",
120+
} = options;
121+
122+
return {
123+
name: "shiki-diff-notation",
124+
code(node: MetaNode) {
125+
if (!node.meta?.diff) return;
126+
this.addClassToHast(this.pre, classActivePre);
127+
128+
const lines = node.children.filter(
129+
(node) => node.type === "element"
130+
) as Element[];
131+
132+
lines.forEach((line) => {
133+
for (const child of line.children) {
134+
if (child.type !== "element") continue;
135+
const text = child.children[0];
136+
if (text.type !== "text") continue;
137+
138+
if (text.value.startsWith("+")) {
139+
text.value = text.value.slice(1);
140+
this.addClassToHast(line, classLineAdd);
141+
}
142+
if (text.value.startsWith("-")) {
143+
text.value = text.value.slice(1);
144+
this.addClassToHast(line, classLineRemove);
145+
}
146+
}
147+
});
148+
},
149+
};
150+
}
151+
```
152+
153+
I also added a couple of CSS classes to add highlighting and marker symbols that
154+
won't get included in user selections.
155+
After adding our plugin, we can now enjoy the new, superior, notation as it
156+
was originally intended:
157+
158+
```ts diff
159+
-console.log('hewwo')
160+
+console.log('hello')
161+
console.log('goodbye')
162+
```
163+
164+
Perfection.
82.7 KB
Loading
37.9 KB
Loading

src/style/global.style.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,26 @@ export const globals = css`
218218
.language-id {
219219
display: none;
220220
}
221-
.diff.add {
222-
background: rgba(53, 117, 42, 0.2);
221+
&.diff {
222+
.line {
223+
&::before {
224+
content: " ";
225+
padding: 0 8px;
226+
user-select: none;
227+
}
228+
&.add {
229+
background: rgba(53, 117, 42, 0.15);
230+
&::before {
231+
content: "+";
232+
}
233+
}
234+
&.remove {
235+
background: rgba(193, 34, 34, 0.15);
236+
&::before {
237+
content: "-";
238+
}
239+
}
240+
}
223241
}
224242
}
225243
.theme-light .shiki.github-dark {

0 commit comments

Comments
 (0)