Skip to content

Commit f272a7e

Browse files
committed
feat: Refactor code highlighting implementation by introducing a custom highlighter, update dependencies, and enhance sitemap configuration
1 parent 29d6c38 commit f272a7e

File tree

6 files changed

+102
-42
lines changed

6 files changed

+102
-42
lines changed

bun.lockb

13.1 KB
Binary file not shown.

next-sitemap.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ const sitemapConfig = {
1010
"/twitter-image.jpg",
1111
"/robots.txt",
1212
"/_not-found",
13-
// Exclude any numeric entity URLs (shouldn't exist anymore, but safety net)
14-
/^\/[a-z]+\/\d+$/,
1513
],
1614
transform: async (config, path) => {
1715
// Priority based on page type

package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@
1010
"prepare": "husky"
1111
},
1212
"dependencies": {
13-
"@tailwindcss/postcss": "^4.1.17",
13+
"@tailwindcss/postcss": "^4.1.18",
1414
"@tailwindcss/typography": "^0.5.19",
1515
"clsx": "^2.1.1",
16-
"next": "^16.0.7",
16+
"next": "^16.1.3",
1717
"next-sitemap": "^4.2.3",
1818
"next-themes": "^0.4.6",
19-
"posthog-js": "^1.302.2",
20-
"react": "19.2.1",
21-
"react-dom": "19.2.1",
19+
"posthog-js": "^1.327.0",
20+
"react": "19.2.3",
21+
"react-dom": "19.2.3",
2222
"tailwind-merge": "^3.4.0",
2323
"tailwindcss-animate": "^1.0.7",
2424
"use-clipboard-copy": "^0.2.0",
25-
"zod": "^4.1.13"
25+
"zod": "^4.3.5"
2626
},
2727
"devDependencies": {
28-
"@biomejs/biome": "^2.3.8",
29-
"@types/bun": "^1.3.4",
30-
"@types/node": "^24.10.1",
31-
"@types/react": "^19.2.7",
28+
"@biomejs/biome": "^2.3.11",
29+
"@types/bun": "^1.3.6",
30+
"@types/node": "^25.0.9",
31+
"@types/react": "^19.2.8",
3232
"@types/react-dom": "^19.2.3",
3333
"husky": "^9.1.7",
3434
"lint-staged": "^16.2.7",
35-
"shiki": "^3.19.0",
36-
"tailwindcss": "^4.1.17",
35+
"shiki": "^3.21.0",
36+
"tailwindcss": "^4.1.18",
3737
"typescript": "5.9.3"
3838
},
3939
"lint-staged": {

src/components/RequestDisplayElement.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import type { JSX } from "react"
44
import { useEffect, useMemo, useState } from "react"
5-
import { codeToHtml } from "shiki"
6-
// Adjust path as necessary if you create the lib folder
5+
import { highlightCode } from "../lib/highlighter"
76
import { defaultLanguageId, languageOptions } from "../lib/language-snippets"
87
import { CopyButton } from "./copy-button"
98

109
interface RequestDisplayElementProps {
1110
slug: string
12-
wrapText?: boolean
1311
}
1412

1513
/**
@@ -26,7 +24,6 @@ interface ProcessedLanguageOption {
2624

2725
const RequestDisplayElement = ({
2826
slug,
29-
wrapText = false,
3027
}: RequestDisplayElementProps): JSX.Element => {
3128
const [selectedLangId, setSelectedLangId] =
3229
useState<string>(defaultLanguageId)
@@ -49,15 +46,11 @@ const RequestDisplayElement = ({
4946
// Effect to update highlighted HTML when the current snippet changes
5047
useEffect(() => {
5148
if (currentSnippet) {
52-
codeToHtml(currentSnippet.code, {
53-
lang: currentSnippet.id, // Use the id from LanguageOption
54-
theme: "solarized-dark", // Or your preferred theme
55-
})
49+
highlightCode(currentSnippet.code, currentSnippet.id)
5650
.then((html) => setHighlightedCodeHtml(html))
5751
.catch((error) => {
5852
console.error("Error highlighting code:", error)
5953
// Fallback to plain text if shiki fails
60-
// Ensure the fallback is also wrapped in pre/code for consistent styling by the parent div
6154
const escapedCode = currentSnippet.code
6255
.replace(/</g, "&lt;")
6356
.replace(/>/g, "&gt;")
@@ -118,11 +111,7 @@ const RequestDisplayElement = ({
118111
role="tabpanel"
119112
id={`panel-${currentSnippet?.id}`}
120113
aria-labelledby={`tab-${currentSnippet?.id}`}
121-
className={`border-x border-b border-[#FFE81F11] overflow-x-auto rounded-b-lg ${processedOptions.length > 0 ? "rounded-t-none" : "rounded-t-lg"} [&>pre]:rounded-none [&>pre]:border-0 ${
122-
wrapText
123-
? "![&>pre]:text-nowrap overflow-y-auto"
124-
: "![&>pre]:text-wrap overflow-y-hidden"
125-
}`}
114+
className={`border-x border-b border-[#FFE81F11] overflow-auto rounded-b-lg ${processedOptions.length > 0 ? "rounded-t-none" : "rounded-t-lg"} [&>pre]:rounded-none [&>pre]:border-0`}
126115
style={{ fontSize: 15 }} // Minor functional change: adjusted fontSize from 14 to 15
127116
/* biome-ignore lint/security/noDangerouslySetInnerHtml: <Shiki output is sanitized HTML> */
128117
dangerouslySetInnerHTML={{ __html: highlightedCodeHtml }}

src/components/ResponseDisplayElement.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSX } from "react"
2-
import { codeToHtml } from "shiki/bundle-web.mjs"
2+
import { highlightCode } from "../lib/highlighter"
33
import { CopyButton } from "./copy-button"
44

55
/**
@@ -9,7 +9,6 @@ import { CopyButton } from "./copy-button"
99
interface ResponseDisplayElementProps {
1010
/** The content to display, typically a JSON string. */
1111
children: string
12-
wrapText?: boolean
1312
}
1413

1514
/**
@@ -19,12 +18,8 @@ interface ResponseDisplayElementProps {
1918
*/
2019
const ResponseDisplayElement = async ({
2120
children,
22-
wrapText = false,
2321
}: ResponseDisplayElementProps): Promise<JSX.Element> => {
24-
const html = await codeToHtml(children, {
25-
lang: "json",
26-
theme: "solarized-dark",
27-
})
22+
const html = await highlightCode(children, "json")
2823

2924
return (
3025
<div className="response-display-element w-full max-w-(--breakpoint-lg) not-prose rounded-lg shadow-md overflow-hidden my-4 relative border border-[#FFE81F11]">
@@ -33,18 +28,14 @@ const ResponseDisplayElement = async ({
3328
</div>
3429
<div className="relative">
3530
<div
36-
className={`overflow-auto max-h-screen border-x border-b border-[#FFE81F11] rounded-b-lg ${
37-
wrapText
38-
? "[&>pre]:text-nowrap overflow-y-auto"
39-
: "[&>pre]:text-wrap overflow-y-hidden"
40-
}`}
31+
className="overflow-auto max-h-[50vh] border-x border-b border-[#FFE81F11] rounded-b-lg"
4132
style={{
4233
fontSize: 16,
4334
}}
44-
/* biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> */
35+
/* biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki output is sanitized HTML */
4536
dangerouslySetInnerHTML={{ __html: html }}
4637
/>
47-
<div className="absolute top-1.5 right-1.5">
38+
<div className="absolute top-2 right-7">
4839
<CopyButton text={children} />
4940
</div>
5041
</div>

src/lib/highlighter.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Fine-grained Shiki highlighter with singleton pattern.
3+
* Only loads the languages and theme we actually use for optimal bundle size.
4+
*/
5+
import type { HighlighterCore } from "shiki/core"
6+
import { createHighlighterCore } from "shiki/core"
7+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
8+
9+
/**
10+
* Languages used in the application for code highlighting.
11+
* These are dynamically imported for tree-shaking.
12+
*/
13+
const LANGUAGES = [
14+
import("shiki/langs/bash.mjs"),
15+
import("shiki/langs/javascript.mjs"),
16+
import("shiki/langs/python.mjs"),
17+
import("shiki/langs/java.mjs"),
18+
import("shiki/langs/csharp.mjs"),
19+
import("shiki/langs/php.mjs"),
20+
import("shiki/langs/ruby.mjs"),
21+
import("shiki/langs/go.mjs"),
22+
import("shiki/langs/swift.mjs"),
23+
import("shiki/langs/kotlin.mjs"),
24+
import("shiki/langs/dart.mjs"),
25+
import("shiki/langs/rust.mjs"),
26+
import("shiki/langs/json.mjs"),
27+
]
28+
29+
/**
30+
* Theme used throughout the application.
31+
*/
32+
const THEME = import("shiki/themes/solarized-dark.mjs")
33+
34+
/**
35+
* Singleton highlighter instance promise.
36+
* Creating a highlighter is expensive, so we cache it.
37+
*/
38+
let highlighterPromise: Promise<HighlighterCore> | null = null
39+
40+
/**
41+
* Gets or creates the singleton highlighter instance.
42+
* Uses the JavaScript RegExp engine for smaller bundle size (no WASM).
43+
* @returns Promise resolving to the highlighter instance
44+
*/
45+
export async function getHighlighter(): Promise<HighlighterCore> {
46+
if (!highlighterPromise) {
47+
highlighterPromise = createHighlighterCore({
48+
themes: [THEME],
49+
langs: LANGUAGES,
50+
engine: createJavaScriptRegexEngine(),
51+
})
52+
}
53+
return highlighterPromise
54+
}
55+
56+
/**
57+
* Highlights code and returns HTML string.
58+
* @param code - The code to highlight
59+
* @param lang - The language identifier (e.g., 'javascript', 'python')
60+
* @returns Promise resolving to highlighted HTML string
61+
*/
62+
export async function highlightCode(
63+
code: string,
64+
lang: string,
65+
): Promise<string> {
66+
const highlighter = await getHighlighter()
67+
return highlighter.codeToHtml(code, {
68+
lang,
69+
theme: "solarized-dark",
70+
})
71+
}
72+
73+
/**
74+
* Disposes the highlighter instance to free resources.
75+
* Call this when the highlighter is no longer needed.
76+
*/
77+
export function disposeHighlighter(): void {
78+
if (highlighterPromise) {
79+
highlighterPromise.then((h) => h.dispose())
80+
highlighterPromise = null
81+
}
82+
}

0 commit comments

Comments
 (0)