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({