diff --git a/package.json b/package.json
index e0a0191a..cd564a2f 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,8 @@
"@monaco-editor/react": "4.6.0",
"@popperjs/core": "2.11.8",
"@prisma/client": "5.9.1",
+ "@shikijs/rehype": "^1.3.0",
+ "@shikijs/transformers": "^1.3.0",
"@supercharge/request-ip": "1.2.0",
"@tanstack/react-query": "4.36.1",
"@uiw/codemirror-extensions-events": "4.21.22",
@@ -120,11 +122,9 @@
"react-string-replace": "1.1.1",
"react-tweet": "3.2.0",
"react-virtuoso": "4.7.0",
- "refractor": "4.8.1",
"rehype-autolink-headings": "7.1.0",
"rehype-infer-description-meta": "2.0.0",
"rehype-katex": "7.0.0",
- "rehype-prism-plus": "2.0.0",
"rehype-raw": "7.0.0",
"rehype-sanitize": "6.0.0",
"rehype-slug": "6.0.0",
@@ -142,6 +142,7 @@
"rss-parser": "3.13.0",
"serialize-javascript": "6.0.2",
"sharp": "0.32.6",
+ "shiki": "^1.3.0",
"sortablejs": "1.15.2",
"swiper": "11.0.6",
"tailwind-merge": "2.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index deec4c13..666fb98d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,6 +93,12 @@ dependencies:
'@prisma/client':
specifier: 5.9.1
version: 5.9.1(prisma@5.9.1)
+ '@shikijs/rehype':
+ specifier: ^1.3.0
+ version: 1.3.0
+ '@shikijs/transformers':
+ specifier: ^1.3.0
+ version: 1.3.0
'@supercharge/request-ip':
specifier: 1.2.0
version: 1.2.0
@@ -279,9 +285,6 @@ dependencies:
react-virtuoso:
specifier: 4.7.0
version: 4.7.0(react-dom@18.2.0)(react@18.2.0)
- refractor:
- specifier: 4.8.1
- version: 4.8.1
rehype-autolink-headings:
specifier: 7.1.0
version: 7.1.0
@@ -291,9 +294,6 @@ dependencies:
rehype-katex:
specifier: 7.0.0
version: 7.0.0
- rehype-prism-plus:
- specifier: 2.0.0
- version: 2.0.0
rehype-raw:
specifier: 7.0.0
version: 7.0.0
@@ -345,6 +345,9 @@ dependencies:
sharp:
specifier: 0.32.6
version: 0.32.6
+ shiki:
+ specifier: ^1.3.0
+ version: 1.3.0
sortablejs:
specifier: 1.15.2
version: 1.15.2
@@ -3621,6 +3624,27 @@ packages:
'@scure/base': 1.1.5
dev: false
+ /@shikijs/core@1.3.0:
+ resolution: {integrity: sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==}
+ dev: false
+
+ /@shikijs/rehype@1.3.0:
+ resolution: {integrity: sha512-CknEidx0ZTg3TeYAPU4ah8cr31a16neBbMyQ5kwAVdkloCe65uhQp+C/FEFs8NRir4eU5XCDA/+w2v5wnN6zgQ==}
+ dependencies:
+ '@shikijs/transformers': 1.3.0
+ '@types/hast': 3.0.4
+ hast-util-to-string: 3.0.0
+ shiki: 1.3.0
+ unified: 11.0.4
+ unist-util-visit: 5.0.0
+ dev: false
+
+ /@shikijs/transformers@1.3.0:
+ resolution: {integrity: sha512-3mlpg2I9CjhjE96dEWQOGeCWoPcyTov3s4aAsHmgvnTHa8MBknEnCQy8/xivJPSpD+olqOqIEoHnLfbNJK29AA==}
+ dependencies:
+ shiki: 1.3.0
+ dev: false
+
/@sindresorhus/is@3.1.2:
resolution: {integrity: sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==}
engines: {node: '>=10'}
@@ -3979,10 +4003,6 @@ packages:
/@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
- /@types/prismjs@1.26.3:
- resolution: {integrity: sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==}
- dev: false
-
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@@ -10470,10 +10490,6 @@ packages:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
- /parse-numeric-range@1.3.0:
- resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
- dev: false
-
/parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
dependencies:
@@ -11305,15 +11321,6 @@ packages:
which-builtin-type: 1.1.3
dev: true
- /refractor@4.8.1:
- resolution: {integrity: sha512-/fk5sI0iTgFYlmVGYVew90AoYnNMP6pooClx/XKqyeeCQXrL0Kvgn8V0VEht5ccdljbzzF1i3Q213gcntkRExg==}
- dependencies:
- '@types/hast': 2.3.10
- '@types/prismjs': 1.26.3
- hastscript: 7.2.0
- parse-entities: 4.0.1
- dev: false
-
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -11362,25 +11369,6 @@ packages:
vfile: 6.0.1
dev: false
- /rehype-parse@9.0.0:
- resolution: {integrity: sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==}
- dependencies:
- '@types/hast': 3.0.4
- hast-util-from-html: 2.0.1
- unified: 11.0.4
- dev: false
-
- /rehype-prism-plus@2.0.0:
- resolution: {integrity: sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==}
- dependencies:
- hast-util-to-string: 3.0.0
- parse-numeric-range: 1.3.0
- refractor: 4.8.1
- rehype-parse: 9.0.0
- unist-util-filter: 5.0.1
- unist-util-visit: 5.0.0
- dev: false
-
/rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
dependencies:
@@ -11814,6 +11802,12 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ /shiki@1.3.0:
+ resolution: {integrity: sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==}
+ dependencies:
+ '@shikijs/core': 1.3.0
+ dev: false
+
/shimmer@1.2.1:
resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
dev: false
@@ -12594,14 +12588,6 @@ packages:
'@types/unist': 3.0.2
dev: false
- /unist-util-filter@5.0.1:
- resolution: {integrity: sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==}
- dependencies:
- '@types/unist': 3.0.2
- unist-util-is: 6.0.0
- unist-util-visit-parents: 6.0.1
- dev: false
-
/unist-util-find-after@5.0.0:
resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
dependencies:
diff --git a/src/app/[locale]/@modal/(.)post/[site]/[slug]/page.tsx b/src/app/[locale]/@modal/(.)post/[site]/[slug]/page.tsx
index 30899349..b4b9aa9c 100644
--- a/src/app/[locale]/@modal/(.)post/[site]/[slug]/page.tsx
+++ b/src/app/[locale]/@modal/(.)post/[site]/[slug]/page.tsx
@@ -3,7 +3,7 @@ import { notFound } from "next/navigation"
import { dehydrate, Hydrate } from "@tanstack/react-query"
-import MarkdownContent from "~/components/common/MarkdownContent"
+import MarkdownContentServer from "~/components/common/MarkdownContentServer"
import PostCover from "~/components/home/PostCover"
import PostModal from "~/components/home/PostModal"
import { PostFooter } from "~/components/site/PostFooter"
@@ -119,13 +119,14 @@ export default async function SiteModal({
/>
)}
-
>
)}
diff --git a/src/app/[locale]/dashboard/[subdomain]/editor/post-editor.tsx b/src/app/[locale]/dashboard/[subdomain]/editor/post-editor.tsx
index 8a18e742..850f28c2 100644
--- a/src/app/[locale]/dashboard/[subdomain]/editor/post-editor.tsx
+++ b/src/app/[locale]/dashboard/[subdomain]/editor/post-editor.tsx
@@ -500,6 +500,7 @@ export default function PostEditor() {
onCreateEditor={onCreateEditor}
isRendering={isRendering}
setIsRendering={setIsRendering}
+ codeTheme={site.data?.metadata?.content?.code_theme}
/>
diff --git a/src/app/[locale]/dashboard/[subdomain]/settings/general/page-component.tsx b/src/app/[locale]/dashboard/[subdomain]/settings/general/page-component.tsx
index 20875f1f..898a0a7c 100644
--- a/src/app/[locale]/dashboard/[subdomain]/settings/general/page-component.tsx
+++ b/src/app/[locale]/dashboard/[subdomain]/settings/general/page-component.tsx
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import toast from "react-hot-toast"
+import { bundledThemesInfo } from "shiki/themes"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
@@ -33,6 +34,8 @@ export default function SiteSettingsGeneralPage() {
ga: "",
ua: "",
uh: "",
+ code_theme_light: "",
+ code_theme_dark: "",
} as {
icon?: string
banner?: {
@@ -46,6 +49,8 @@ export default function SiteSettingsGeneralPage() {
ga: string
ua: string
uh: string
+ code_theme_light: string
+ code_theme_dark: string
},
})
@@ -61,6 +66,10 @@ export default function SiteSettingsGeneralPage() {
ga: values.ga,
ua: values.ua,
uh: values.uh,
+ code_theme: {
+ light: values.code_theme_light,
+ dark: values.code_theme_dark,
+ },
})
})
@@ -105,6 +114,18 @@ export default function SiteSettingsGeneralPage() {
form.setValue("ua", site.data.metadata?.content?.ua || "")
!form.getValues("uh") &&
form.setValue("uh", site.data.metadata?.content?.uh || "")
+ !form.getValues("code_theme_light") &&
+ form.setValue(
+ "code_theme_light",
+ site.data.metadata?.content?.code_theme?.light ||
+ "github-light-default",
+ )
+ !form.getValues("code_theme_dark") &&
+ form.setValue(
+ "code_theme_dark",
+ site.data.metadata?.content?.code_theme?.dark ||
+ "github-dark-default",
+ )
}
}, [site.data, form])
@@ -209,6 +230,24 @@ export default function SiteSettingsGeneralPage() {
help={t("Support Markdown")}
/>
+
+ i.type === "light")
+ .map((i) => i.id)}
+ id="code_theme_light"
+ {...form.register("code_theme_light")}
+ />
+ i.type === "dark")
+ .map((i) => i.id)}
+ id="code_theme_dark"
+ {...form.register("code_theme_dark")}
+ />
+
)}
-
>
)}
diff --git a/src/components/common/CommentItem.tsx b/src/components/common/CommentItem.tsx
index d72b24e4..498b2a2f 100644
--- a/src/components/common/CommentItem.tsx
+++ b/src/components/common/CommentItem.tsx
@@ -18,6 +18,7 @@ import { Avatar } from "~/components/ui/Avatar"
import { Button } from "~/components/ui/Button"
import { UniLink } from "~/components/ui/UniLink"
import { useDate } from "~/hooks/useDate"
+import { useHighlighter } from "~/hooks/useHighlighter"
import { CSB_SCAN } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
import { cn } from "~/lib/utils"
@@ -57,6 +58,8 @@ export const CommentItem = ({
])
}, [deletePage.isSuccess, originalCharacterId, originalNoteId, queryClient])
+ const highlighter = useHighlighter()
+
if (!comment.metadata?.content?.content) {
return null
}
@@ -139,6 +142,7 @@ export const CommentItem = ({
,
+ "highlighter"
+>
+
+const MarkdownContentServer = async (props: MarkdownContentServerProps) => {
+ const highlighter = await createHighlighter()
+ return
+}
+
+export default MarkdownContentServer
diff --git a/src/components/dashboard/DualColumnEditor.tsx b/src/components/dashboard/DualColumnEditor.tsx
index 89bc729d..bf83a382 100644
--- a/src/components/dashboard/DualColumnEditor.tsx
+++ b/src/components/dashboard/DualColumnEditor.tsx
@@ -2,6 +2,7 @@ import { useDebounceEffect } from "ahooks"
import type { Root } from "hast"
import { useTranslations } from "next-intl"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import type { BundledTheme } from "shiki/themes"
import { EditorView } from "@codemirror/view"
@@ -9,6 +10,7 @@ import MarkdownContent from "~/components/common/MarkdownContent"
import { toolbarShortcuts } from "~/components/dashboard/toolbars"
import { editorUpload } from "~/components/dashboard/toolbars/Multimedia"
import CodeMirror from "~/components/ui/CodeMirror"
+import { useHighlighter } from "~/hooks/useHighlighter"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
import { cn } from "~/lib/utils"
import { renderPageContent } from "~/markdown"
@@ -19,13 +21,19 @@ export default function DualColumnEditor({
onCreateEditor,
isRendering,
setIsRendering,
+ codeTheme,
}: {
initialContent: string
onChange: (value: string) => void
onCreateEditor: (view: EditorView) => void
isRendering: boolean
setIsRendering: (value: boolean) => void
+ codeTheme?: {
+ light?: BundledTheme
+ dark?: BundledTheme
+ }
}) {
+ const highlighter = useHighlighter()
const isMobileLayout = useIsMobileLayout()
const t = useTranslations()
@@ -45,11 +53,17 @@ export default function DualColumnEditor({
useDebounceEffect(
() => {
- const result = renderPageContent(values)
+ if (!highlighter) return
+ const result = renderPageContent({
+ content: values,
+ highlighter,
+ strictMode: true,
+ codeTheme,
+ })
setTree(result.tree)
setParsedContent(result)
},
- [values],
+ [values, codeTheme, highlighter],
{
wait: 500,
},
@@ -234,6 +248,8 @@ export default function DualColumnEditor({
onMouseEnter={() => {
setCurrentScrollArea("preview")
}}
+ highlighter={highlighter}
+ codeTheme={codeTheme}
/>
)}
diff --git a/src/components/dashboard/ImportPreview.tsx b/src/components/dashboard/ImportPreview.tsx
index 056caae8..ac0e46ff 100644
--- a/src/components/dashboard/ImportPreview.tsx
+++ b/src/components/dashboard/ImportPreview.tsx
@@ -4,6 +4,7 @@ import dynamic from "next/dynamic"
import { useState } from "react"
import { useDate } from "~/hooks/useDate"
+import { useHighlighter } from "~/hooks/useHighlighter"
import { RESERVED_TAGS } from "~/lib/constants"
import { cn } from "~/lib/utils"
@@ -18,6 +19,7 @@ export const ImportPreview = ({ note }: { note: NoteMetadata }) => {
const date = useDate()
const [showcaseMore, setShowcaseMore] = useState(false)
const t = useTranslations()
+ const highlighter = useHighlighter()
return (
@@ -60,6 +62,7 @@ export const ImportPreview = ({ note }: { note: NoteMetadata }) => {
diff --git a/src/components/site/SiteFooter.tsx b/src/components/site/SiteFooter.tsx
index 8167b06d..a188780c 100644
--- a/src/components/site/SiteFooter.tsx
+++ b/src/components/site/SiteFooter.tsx
@@ -2,7 +2,7 @@ import { getTranslations } from "next-intl/server"
import Script from "next/script"
import { Logo } from "~/components/common/Logo"
-import MarkdownContent from "~/components/common/MarkdownContent"
+import MarkdownContentServer from "~/components/common/MarkdownContentServer"
import { SITE_URL } from "~/lib/env"
import { ExpandedCharacter } from "~/lib/types"
@@ -35,7 +35,7 @@ export default async function SiteFooter({