From 8fe534c88856693bf5c4d0ea45d4f50b269bfe44 Mon Sep 17 00:00:00 2001 From: "wkylin.w@gmail.com" Date: Mon, 30 Dec 2024 16:06:07 +0800 Subject: [PATCH] feat: shiki code highlighter --- package-lock.json | 131 ++++++++++++++++++ package.json | 1 + .../stateless/AdvancedCodeBlock/index.tsx | 50 +++++++ .../stateless/CodeHighlighter/index.tsx | 38 +++++ .../stateless/CopyToClipboard/index.tsx | 37 +++++ src/pages/home/index.jsx | 15 +- 6 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 src/components/stateless/AdvancedCodeBlock/index.tsx create mode 100644 src/components/stateless/CodeHighlighter/index.tsx create mode 100644 src/components/stateless/CopyToClipboard/index.tsx diff --git a/package-lock.json b/package-lock.json index 16ce0cec..1571c9eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,7 @@ "resize-observer-polyfill": "^1.5.1", "sanitize.css": "^13.0.0", "screenfull": "^6.0.2", + "shiki": "^1.24.4", "sse.js": "^2.5.0", "styled-components": "^6.1.13", "three": "^0.171.0", @@ -7321,6 +7322,57 @@ "dev": true, "license": "MIT" }, + "node_modules/@shikijs/core": { + "version": "1.24.4", + "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-1.24.4.tgz", + "integrity": "sha512-jjLsld+xEEGYlxAXDyGwWsKJ1sw5Pc1pnp4ai2ORpjx2UX08YYTC0NNqQYO1PaghYaR+PvgMOGuvzw2he9sk0Q==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.24.4", + "@shikijs/engine-oniguruma": "1.24.4", + "@shikijs/types": "1.24.4", + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.24.4", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-1.24.4.tgz", + "integrity": "sha512-TClaQOLvo9WEMJv6GoUsykQ6QdynuKszuORFWCke8qvi6PeLm7FcD9+7y45UenysxEWYpDL5KJaVXTngTE+2BA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.24.4", + "@shikijs/vscode-textmate": "^9.3.1", + "oniguruma-to-es": "0.8.1" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.24.4", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.4.tgz", + "integrity": "sha512-Do2ry6flp2HWdvpj2XOwwa0ljZBRy15HKZITzPcNIBOGSeprnA8gOooA/bLsSPuy8aJBa+Q/r34dMmC3KNL/zw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.24.4", + "@shikijs/vscode-textmate": "^9.3.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.24.4", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-1.24.4.tgz", + "integrity": "sha512-0r0XU7Eaow0PuDxuWC1bVqmWCgm3XqizIaT7SM42K03vc69LGooT0U8ccSR44xP/hGlNx4FKhtYpV+BU6aaKAA==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.1.tgz", + "integrity": "sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -16400,6 +16452,12 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz", @@ -20999,6 +21057,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.4", + "resolved": "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.2", "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", @@ -29220,6 +29301,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-es": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-0.8.1.tgz", + "integrity": "sha512-dekySTEvCxCj0IgKcA2uUCO/e4ArsqpucDPcX26w9ajx+DvMWLc5eZeJaRQkd7oC/+rwif5gnT900tA34uN9Zw==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.0.2", + "regex-recursion": "^5.0.0" + } + }, "node_modules/only": { "version": "0.0.2", "resolved": "https://registry.npmmirror.com/only/-/only-0.0.2.tgz", @@ -34722,6 +34814,31 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmmirror.com/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -36294,6 +36411,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/shiki": { + "version": "1.24.4", + "resolved": "https://registry.npmmirror.com/shiki/-/shiki-1.24.4.tgz", + "integrity": "sha512-aVGSFAOAr1v26Hh/+GBIsRVDWJ583XYV7CuNURKRWh9gpGv4OdbisZGq96B9arMYTZhTQkmRF5BrShOSTvNqhw==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.24.4", + "@shikijs/engine-javascript": "1.24.4", + "@shikijs/engine-oniguruma": "1.24.4", + "@shikijs/types": "1.24.4", + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", diff --git a/package.json b/package.json index aaa4761a..6b4c2cba 100644 --- a/package.json +++ b/package.json @@ -310,6 +310,7 @@ "resize-observer-polyfill": "^1.5.1", "sanitize.css": "^13.0.0", "screenfull": "^6.0.2", + "shiki": "^1.24.4", "sse.js": "^2.5.0", "styled-components": "^6.1.13", "three": "^0.171.0", diff --git a/src/components/stateless/AdvancedCodeBlock/index.tsx b/src/components/stateless/AdvancedCodeBlock/index.tsx new file mode 100644 index 00000000..3600c780 --- /dev/null +++ b/src/components/stateless/AdvancedCodeBlock/index.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import type { HTMLAttributes } from 'react' +import type { BundledLanguage, BundledTheme } from 'shiki' +import ShikiCode from '@stateless/CodeHighlighter' +import CopyToClipboard from '@stateless/CopyToClipboard' +import clsx from 'clsx' + +type AdvancedBlockProps = { + code: string + fileName?: string + lang?: BundledLanguage + theme?: BundledTheme + className?: string +} + +export const AdvancedCodeBlock = ({ + code, + fileName, + lang = 'typescript', + theme = 'github-light', + className, + ...props +}: AdvancedBlockProps & HTMLAttributes) => { + return ( +
+
+
+ {fileName ??
}
+
+ +
+
+
+
+            
+          
+
+
+
+ ) +} diff --git a/src/components/stateless/CodeHighlighter/index.tsx b/src/components/stateless/CodeHighlighter/index.tsx new file mode 100644 index 00000000..7aa9f54f --- /dev/null +++ b/src/components/stateless/CodeHighlighter/index.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from 'react' +import { type BundledLanguage, type BundledTheme, codeToHtml } from 'shiki' +import clsx from 'clsx' +import type { HTMLAttributes } from 'react' + +type ShikiProps = { + code: string + lang: BundledLanguage + theme: BundledTheme + className?: string +} + +export default function ShikiCode({ + code, + lang, + theme, + className, + ...props +}: Readonly & HTMLAttributes) { + const [html, setHtml] = useState('') + + useEffect(() => { + const loadHtml = async () => { + const result = await codeToHtml(code, { lang, theme }) + setHtml(result) + } + + loadHtml() + }, [code, lang, theme]) + + return ( +
+ ) +} diff --git a/src/components/stateless/CopyToClipboard/index.tsx b/src/components/stateless/CopyToClipboard/index.tsx new file mode 100644 index 00000000..22c230bc --- /dev/null +++ b/src/components/stateless/CopyToClipboard/index.tsx @@ -0,0 +1,37 @@ +'use client' +import React from 'react' +import { CheckIcon, CopyIcon } from 'lucide-react' +import { useState } from 'react' + +const CopyToClipboard = ({ code }: Readonly<{ code: string }>) => { + const [isCopied, setIsCopied] = useState(false) + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(code) + setIsCopied(true) + } catch (_error) { + setIsCopied(false) + } finally { + setTimeout(() => { + setIsCopied(false) + }, 2000) + } + } + + return ( + + ) +} + +export default CopyToClipboard diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx index 19b7eacd..02c2e006 100644 --- a/src/pages/home/index.jsx +++ b/src/pages/home/index.jsx @@ -37,6 +37,7 @@ import DynamicBackground from '@stateless/DynamicBackground' import ContentPlaceholder from '@stateless/ContentPlaceholder' import SkeletonFix from '@stateless/SkeletonFix' import { ReactSignature } from '@stateless/ReactSignature' +// import AdvancedCodeBlock from '@stateless/AdvancedCodeBlock' import { oneApiChat, prettyObject, randomNum } from '@utils/aidFn' import { fireConfetti } from '@utils/confetti' @@ -44,6 +45,15 @@ import styles from './index.module.less' const boxCount = Array.apply(null, Array(10)) +const code = { + fileName: './explanations.ts', + code: `export const = explanations = { +main : "This component needs more than the default code block to be displayed" +detailed : "For now, if you want the exact same behaviour, please check the github" +}`, + lang: 'typescript', +} + const preCode = ` const GroceryItem: React.FC = ({ item }) => { return ( @@ -399,7 +409,10 @@ const Home = () => {
-
+ {/*
+ +
*/} +