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:
parent
5c9214634d
commit
ce6e64ff3c
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -500,6 +500,7 @@ export default function PostEditor() {
|
|||
onCreateEditor={onCreateEditor}
|
||||
isRendering={isRendering}
|
||||
setIsRendering={setIsRendering}
|
||||
codeTheme={site.data?.metadata?.content?.code_theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@
|
|||
@import "./tailwind.css";
|
||||
@import "./prose.css";
|
||||
@import "./code.css";
|
||||
@import "./prism-dark.css";
|
||||
@import "./print.css";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue