feat: migrate to shiki, support custom code theme (#1692)

* refactor: migrate to shiki

* read code theme from character metadata

* update

* remove refractor

* save code theme

* update

* fix post editor

* update

* update

* update
This commit is contained in:
Stephen Zhou 2024-04-11 10:41:59 +08:00 committed by GitHub
parent 5c9214634d
commit ce6e64ff3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 244 additions and 160 deletions

View File

@ -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",

View File

@ -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:

View File

@ -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({
/>
)}
</div>
<MarkdownContent
<MarkdownContentServer
className="mt-10"
content={page?.metadata?.content?.content}
page={page}
site={site}
withActions={false}
onlyContent={onlyContent}
codeTheme={site.metadata.content.code_theme}
/>
</>
)}

View File

@ -500,6 +500,7 @@ export default function PostEditor() {
onCreateEditor={onCreateEditor}
isRendering={isRendering}
setIsRendering={setIsRendering}
codeTheme={site.data?.metadata?.content?.code_theme}
/>
</div>
</div>

View File

@ -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")}
/>
</div>
<div className="mt-5 flex gap-3">
<Input
label="Code Theme Light"
options={bundledThemesInfo
.filter((i) => i.type === "light")
.map((i) => i.id)}
id="code_theme_light"
{...form.register("code_theme_light")}
/>
<Input
label="Code Theme Dark"
options={bundledThemesInfo
.filter((i) => i.type === "dark")
.map((i) => i.id)}
id="code_theme_dark"
{...form.register("code_theme_dark")}
/>
</div>
<div className="mt-5">
<Input
id="ga"

View File

@ -4,7 +4,7 @@ import serialize from "serialize-javascript"
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 { OIAButton } from "~/components/site/OIAButton"
import { PostFooter } from "~/components/site/PostFooter"
@ -200,7 +200,7 @@ export default async function SitePagePage({
)}
</>
)}
<MarkdownContent
<MarkdownContentServer
className="mt-10"
content={page?.metadata?.content?.content}
withToc={true}
@ -208,6 +208,7 @@ export default async function SitePagePage({
site={site}
withActions={true}
onlyContent={onlyContent}
codeTheme={site.metadata.content.code_theme}
/>
</>
)}

View File

@ -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 = ({
<MarkdownContent
content={comment.metadata?.content?.content}
strictMode={true}
highlighter={highlighter}
></MarkdownContent>
<div className="mt-1 flex items-center">
<div

View File

@ -1,5 +1,7 @@
import type { Result as TocResult } from "mdast-util-toc"
import { memo, type MutableRefObject } from "react"
import { Highlighter } from "shiki"
import type { BundledTheme } from "shiki/themes"
import PostActions from "~/components/site/PostActions"
import PostToc from "~/components/site/PostToc"
@ -22,6 +24,8 @@ const MarkdownContent = memo(function PageContent({
site,
withActions,
onlyContent,
highlighter,
codeTheme,
}: {
content?: string
className?: string
@ -35,12 +39,22 @@ const MarkdownContent = memo(function PageContent({
site?: ExpandedCharacter
withActions?: boolean
onlyContent?: boolean
highlighter?: Highlighter
codeTheme?: {
light?: BundledTheme
dark?: BundledTheme
}
}) {
let inParsedContent
if (parsedContent) {
inParsedContent = parsedContent
} else if (content) {
inParsedContent = renderPageContent(content, strictMode)
inParsedContent = renderPageContent({
content,
strictMode,
codeTheme,
highlighter,
})
}
let toc: TocResult | undefined = undefined

View File

@ -0,0 +1,15 @@
import { createHighlighter } from "~/lib/highlighter"
import MarkdownContent from "./MarkdownContent"
type MarkdownContentServerProps = Omit<
React.ComponentProps<typeof MarkdownContent>,
"highlighter"
>
const MarkdownContentServer = async (props: MarkdownContentServerProps) => {
const highlighter = await createHighlighter()
return <MarkdownContent {...props} highlighter={highlighter} />
}
export default MarkdownContentServer

View File

@ -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}
/>
)}
</div>

View File

@ -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 (
<article className="border rounded-xl p-6 mt-4">
@ -60,6 +62,7 @@ export const ImportPreview = ({ note }: { note: NoteMetadata }) => {
<DynamicMarkdownContent
className="mt-4"
content={note?.content}
highlighter={highlighter}
></DynamicMarkdownContent>
</div>
</article>

View File

@ -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({
<footer className="text-zinc-500 border-t">
<div className="max-w-screen-lg mx-auto px-5 py-10">
{site?.metadata?.content?.footer && (
<MarkdownContent
<MarkdownContentServer
className="[&>.prose]:text-sm"
content={site.metadata.content.footer}
strictMode={true}

View File

@ -22,3 +22,13 @@ pre {
pre.mermaid {
@apply bg-transparent flex justify-center;
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}

View File

@ -4,5 +4,4 @@
@import "./tailwind.css";
@import "./prose.css";
@import "./code.css";
@import "./prism-dark.css";
@import "./print.css";

View File

@ -1,81 +0,0 @@
/** based on prism tomorrow */
pre,
code {
--code-bg: theme("colors.slate.50");
--code-fg: theme("colors.slate.600");
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #e2777a;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}

View File

@ -0,0 +1,18 @@
import { useEffect, useState } from "react"
import type { Highlighter } from "shiki"
import { createHighlighter } from "~/lib/highlighter"
export function useHighlighter() {
const [highlighter, setHighlighter] = useState<Highlighter | undefined>()
useEffect(() => {
createHighlighter()
.then((h) => {
setHighlighter(h)
})
.catch((e) => {
console.error(e)
})
}, [])
return highlighter
}

View File

@ -95,7 +95,9 @@ export const expandCrossbellNote = async ({
}
}
const rendered = renderPageContent(expandedNote.metadata.content.content)
const rendered = renderPageContent({
content: expandedNote.metadata.content.content,
})
renderedMetadata = rendered.toMetadata()
if (keyword) {
const position = expandedNote.metadata.content.content
@ -266,6 +268,15 @@ export const expandCrossbellCharacter = async (site: CharacterEntity) => {
)?.value as string) || "null",
) || [{ id: nanoid(), label: "Archives", url: "/archives" }]
expandedCharacter.metadata.content.code_theme = JSON.parse(
(expandedCharacter.metadata?.content?.attributes?.find(
(a: any) => a.trait_type === "xlog_code_theme",
)?.value as string) || "null",
) || {
light: "github-light-default",
dark: "github-dark-default",
}
const getArribute = (outputKey: string, typeKey: string) => {
;(expandedCharacter.metadata.content as any)[outputKey] =
expandedCharacter.metadata?.content?.attributes?.find(

18
src/lib/highlighter.ts Normal file
View File

@ -0,0 +1,18 @@
import {
bundledLanguages,
bundledThemes,
getHighlighter,
type Highlighter,
} from "shiki"
let highlighter: Highlighter | undefined
export const createHighlighter = async () => {
if (!highlighter) {
highlighter = await getHighlighter({
themes: Object.keys(bundledThemes),
langs: Object.keys(bundledLanguages),
})
}
return highlighter
}

View File

@ -30,7 +30,9 @@ export const readFiles = async (files: FileList) => {
) => {
const reader = new FileReader()
reader.onload = () => {
const pageContent = renderPageContent(reader.result as string)
const pageContent = renderPageContent({
content: reader.result as string,
})
const metadata = pageContent.toMetadata()
resolve({
title:

View File

@ -1,4 +1,5 @@
import type { CharacterEntity, NoteEntity } from "crossbell"
import type { BundledTheme } from "shiki/themes"
export type Language = Readonly<"en" | "zh" | "zh-TW" | "ja">
@ -127,6 +128,10 @@ export type ExpandedCharacter = CharacterEntity & {
uh?: string
custom_domain?: string
site_name?: string
code_theme?: {
light?: BundledTheme
dark?: BundledTheme
}
}
}
}

View File

@ -8,14 +8,9 @@ import dynamic from "next/dynamic"
import { toast } from "react-hot-toast"
// @ts-expect-error: untyped.
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
import { refractor } from "refractor"
import langJsx from "refractor/lang/jsx"
import langSolidity from "refractor/lang/solidity"
import langTsx from "refractor/lang/tsx"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import rehypeInferDescriptionMeta from "rehype-infer-description-meta"
import rehypeKatex from "rehype-katex"
import rehypePrismGenerator from "rehype-prism-plus/generator"
import rehypeRaw from "rehype-raw"
import rehypeSanitize from "rehype-sanitize"
import rehypeSlug from "rehype-slug"
@ -29,12 +24,16 @@ import remarkGithubAlerts from "remark-github-alerts"
import remarkMath from "remark-math"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import type { Highlighter } from "shiki"
import type { BundledTheme } from "shiki/themes"
import { unified } from "unified"
import { visit } from "unist-util-visit"
import { VFile } from "vfile"
// @ts-expect-error
import remarkCalloutDirectives from "@microflash/remark-callout-directives"
import rehypeShikiFromHighlighter from "@shikijs/rehype/core"
import { transformerMetaHighlight } from "@shikijs/transformers"
import AdvancedImage from "~/components/ui/AdvancedImage"
import { isServerSide } from "~/lib/utils"
@ -62,13 +61,20 @@ const APlayer = dynamic(() => import("~/components/ui/APlayer"))
const DPlayer = dynamic(() => import("~/components/ui/DPlayer"))
const RSS = dynamic(() => import("~/components/ui/RSS"))
refractor.alias("html", ["svelte", "vue"])
refractor.register(langTsx)
refractor.register(langJsx)
refractor.register(langSolidity)
const rehypePrism = rehypePrismGenerator(refractor)
export const renderPageContent = (content: string, strictMode?: boolean) => {
export const renderPageContent = ({
content,
highlighter,
strictMode,
codeTheme,
}: {
content: string
highlighter?: Highlighter
strictMode?: boolean
codeTheme?: {
light?: BundledTheme
dark?: BundledTheme
}
}) => {
let hastTree: HashRoot | undefined = undefined
let mdastTree: MdashRoot | undefined = undefined
@ -122,10 +128,20 @@ export const renderPageContent = (content: string, strictMode?: boolean) => {
transformers,
})
.use(rehypeRemoveH1)
.use(rehypePrism, {
ignoreMissing: true,
showLineNumbers: true,
if (highlighter) {
pipeline.use(rehypeShikiFromHighlighter, highlighter, {
themes: codeTheme ?? {
light: "github-light-default",
dark: "github-dark-default",
},
onError: (e) => {
console.error(e)
},
transformers: [transformerMetaHighlight()],
})
}
pipeline
.use(rehypeKatex, {
strict: false,
})

View File

@ -148,6 +148,10 @@ export function useUpdateSite() {
platform: string
url?: string | undefined
}[]
code_theme?: {
light?: string
dark?: string
}
}) => {
if (!input.characterId) {
throw new Error("characterId are required")
@ -217,6 +221,7 @@ export function useUpdateSite() {
}
}
setAttribute("navigation", "navigation", true)
setAttribute("code_theme", "code_theme", true)
setAttribute("css", "css")
setAttribute("ga", "ga")
setAttribute("ua", "ua")