Skip to content

Commit 282d9c6

Browse files
refactor(details): (re)write diff marking for syntax highlighting, simplifies code and removes dep
1 parent ba6fd0e commit 282d9c6

File tree

4 files changed

+100
-69
lines changed

4 files changed

+100
-69
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"@shikijs/langs": "^3.7.0",
2626
"@shikijs/rehype": "^3.7.0",
2727
"@shikijs/themes": "^3.7.0",
28-
"@shikijs/transformers": "^3.7.0",
2928
"@sveltejs/adapter-vercel": "^5.7.2",
3029
"@sveltejs/kit": "^2.22.2",
3130
"@sveltejs/vite-plugin-svelte": "^5.1.0",

pnpm-lock.yaml

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/routes/[pid=pid]/[org]/[repo]/[id=number]/PageRenderer.svelte

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,9 @@
3636
Tag
3737
} from "@lucide/svelte";
3838
import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
39-
import { transformerNotationDiff } from "@shikijs/transformers";
40-
import posthog from "posthog-js";
4139
import rehypeSlug from "rehype-slug";
4240
import remarkGemoji from "remark-gemoji";
4341
import remarkGithub from "remark-github";
44-
import type { SpecialLanguage } from "shiki";
4542
import type { Plugin } from "svelte-exmarkdown";
4643
import type {
4744
DiscussionDetails,
@@ -62,71 +59,19 @@
6259
import Step from "$lib/components/Step.svelte";
6360
import Steps from "$lib/components/Steps.svelte";
6461
import BottomCollapsible from "./BottomCollapsible.svelte";
62+
import { transformerDiffMarking, transformerLanguageDetection } from "./syntax-highlighting";
6563
6664
const shikiPlugin: Plugin = {
6765
rehypePlugin: [
6866
rehypeShikiFromHighlighter,
6967
highlighter,
7068
{
7169
themes: { light: "github-light-default", dark: "github-dark-default" },
72-
transformers: [
73-
{
74-
preprocess(code, options) {
75-
if (options.lang === "diff") {
76-
const cleanedCode = code
77-
.split("\n")
78-
.map(line => line.replace(/^[+-]/, ""))
79-
.join("\n");
80-
const detectedLanguage = detectLanguage(cleanedCode);
81-
if (!detectedLanguage) {
82-
if (browser)
83-
posthog.captureException(new Error("Failed to determine diff language"), {
84-
code
85-
});
86-
return;
87-
}
88-
options.lang = detectedLanguage;
89-
return code
90-
.split("\n")
91-
.map(line =>
92-
line.replace(
93-
/^([+-])(.*)/,
94-
(_, sign, rest) => `${rest} // [!code ${sign}${sign}]`
95-
)
96-
)
97-
.join("\n");
98-
}
99-
},
100-
pre(node) {
101-
node.properties["data-language"] = this.options.lang
102-
.toLowerCase()
103-
.replace(/^js$/, "javascript")
104-
.replace(/^ts$/, "typescript");
105-
}
106-
},
107-
transformerNotationDiff()
108-
]
70+
transformers: [transformerLanguageDetection, transformerDiffMarking]
10971
} satisfies Parameters<typeof rehypeShikiFromHighlighter>[1]
11072
]
11173
};
11274
113-
// Utils
114-
function detectLanguage(code: string): (SpecialLanguage | (string & {})) | undefined {
115-
const match = code
116-
.split("\n", 1)[0]
117-
?.trim()
118-
?.match(/^(?:\/\/|#) ?[^ !]+?\.([A-Za-z0-9]{1,10})$/);
119-
if (match) return match[1];
120-
121-
const hasHTML = /<\/[a-zA-Z0-9-]+>/.test(code);
122-
const hasJS = / (let|var|const|=) /.test(code);
123-
124-
if (hasHTML && hasJS) return "svelte";
125-
if (hasHTML) return "html";
126-
if (hasJS) return /(: [A-Z]|type |interface )/.test(code) ? "ts" : "js";
127-
if (/[a-z-]+: \S+/.test(code)) return "css";
128-
}
129-
13075
function formatToDateTime(date: string) {
13176
return new Intl.DateTimeFormat("en", {
13277
dateStyle: "medium",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { browser } from "$app/environment";
2+
import posthog from "posthog-js";
3+
import type { ShikiTransformer, SpecialLanguage } from "shiki";
4+
5+
/**
6+
* Detects the programming or markup language based on the given code snippet.
7+
*
8+
* @param code the code snippet to analyze and detect the language from.
9+
* @returns The detected language as a string, or undefined if no language
10+
* could be determined.
11+
*/
12+
export function detectLanguage(code: string): (SpecialLanguage | (string & {})) | undefined {
13+
const match = code
14+
.split("\n", 1)[0]
15+
?.trim()
16+
?.match(/^(?:\/\/|#) ?[^ !]+?\.([A-Za-z0-9]{1,10})$/);
17+
if (match) return match[1];
18+
19+
const hasHTML = /<\/[a-zA-Z0-9-]+>/.test(code);
20+
const hasJS = / (let|var|const|=) /.test(code);
21+
22+
if (hasHTML && hasJS) return "svelte";
23+
if (hasHTML) return "html";
24+
if (hasJS) return /(: [A-Z]|type |interface )/.test(code) ? "ts" : "js";
25+
if (/[a-z-]+: \S+/.test(code)) return "css";
26+
}
27+
28+
/**
29+
* A Shiki transformer used for language detection and setting the appropriate language metadata
30+
* in code blocks. Useful for handling code snippets with "diff" language and converting them
31+
* to a detected programming language.
32+
*/
33+
export const transformerLanguageDetection: ShikiTransformer = {
34+
preprocess(code, options) {
35+
if (options.lang === "diff") {
36+
const cleanedCode = code
37+
.split("\n")
38+
.map(line => line.replace(/^[+-]/, ""))
39+
.join("\n");
40+
const detectedLanguage = detectLanguage(cleanedCode);
41+
if (!detectedLanguage) {
42+
if (browser)
43+
posthog.captureException(new Error("Failed to determine diff language"), {
44+
code
45+
});
46+
return;
47+
}
48+
options.lang = detectedLanguage;
49+
return code;
50+
}
51+
},
52+
pre(node) {
53+
node.properties["data-language"] = this.options.lang
54+
.toLowerCase()
55+
.replace(/^js$/, "javascript")
56+
.replace(/^ts$/, "typescript");
57+
}
58+
};
59+
60+
/**
61+
* Replicate the behavior of Shiki's `transformerNotationDiff`,
62+
* but in-house and without needing any code transformation.
63+
*
64+
* @see https://shiki.style/packages/transformers#transformernotationdiff
65+
*/
66+
export const transformerDiffMarking: ShikiTransformer = {
67+
pre(node) {
68+
const isDiff = this.tokens.some(line => {
69+
return line.some(token => {
70+
if (token.offset > 0) return false;
71+
const content = token.content.trim();
72+
return content === "+" || content === "-";
73+
});
74+
});
75+
if (isDiff) this.addClassToHast(node, "has-diff");
76+
},
77+
line(node) {
78+
const firstChild = node.children[0];
79+
if (!firstChild || firstChild.type !== "element") return;
80+
const firstToken = firstChild.children[0];
81+
if (!firstToken || firstToken.type !== "text") return;
82+
if (firstToken.value.startsWith("+")) {
83+
this.addClassToHast(node, ["diff", "add"]);
84+
if (firstToken.value.length === 1) {
85+
node.children.shift();
86+
} else {
87+
firstToken.value = firstToken.value.slice(1);
88+
}
89+
} else if (firstToken.value.startsWith("-")) {
90+
this.addClassToHast(node, ["diff", "remove"]);
91+
if (firstToken.value.length === 1) {
92+
node.children.shift();
93+
} else {
94+
firstToken.value = firstToken.value.slice(1);
95+
}
96+
}
97+
}
98+
};

0 commit comments

Comments
 (0)