feat: improve shiki code block renderer and show language
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
dd7042e462
commit
590d9cbb94
|
|
@ -0,0 +1,78 @@
|
|||
import type { SVGProps } from "react"
|
||||
|
||||
export function MdiLanguageTypescript(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 3h18v18H3zm10.71 14.86c.5.98 1.51 1.73 3.09 1.73c1.6 0 2.8-.83 2.8-2.36c0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02c0-.41.31-.73.81-.73c.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33c-1.51 0-2.48.96-2.48 2.23c0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13c0 .48-.45.83-1.15.83c-.83 0-1.31-.43-1.67-1.03zM13 11.25H8v1.5h1.5V20h1.75v-7.25H13z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UilReact(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11.103 10.438a1.786 1.786 0 1 0 2.44.654a1.786 1.786 0 0 0-2.44-.654m8.005 1.938q-.176-.201-.371-.403q.136-.144.264-.287c1.605-1.804 2.283-3.614 1.655-4.701c-.602-1.043-2.393-1.354-4.636-.918q-.331.065-.659.146q-.063-.216-.133-.43C14.467 3.49 13.238 1.999 11.982 2c-1.204 0-2.368 1.397-3.111 3.558q-.11.32-.203.644q-.219-.054-.44-.1c-2.366-.485-4.271-.165-4.898.924c-.601 1.043.027 2.75 1.528 4.472q.224.255.46.5c-.186.19-.361.381-.525.571c-1.465 1.698-2.057 3.376-1.457 4.415c.62 1.074 2.498 1.425 4.785.975q.278-.055.553-.124q.1.351.221.697C9.635 20.649 10.792 22 11.992 22c1.24 0 2.482-1.453 3.235-3.659c.06-.174.115-.355.169-.541q.355.088.715.156c2.203.417 3.952.09 4.551-.95c.619-1.075-.02-2.877-1.554-4.63M4.07 7.452c.386-.67 1.943-.932 3.986-.512q.196.04.399.09a20.464 20.464 0 0 0-.422 2.678A20.887 20.887 0 0 0 5.93 11.4q-.219-.227-.427-.465C4.216 9.461 3.708 8.081 4.07 7.452m3.887 5.728c-.51-.387-.985-.783-1.416-1.181c.43-.396.905-.79 1.415-1.176q-.028.589-.027 1.179q0 .59.028 1.178m0 3.94a7.237 7.237 0 0 1-2.64.094a1.766 1.766 0 0 1-1.241-.657c-.365-.63.111-1.978 1.364-3.43q.236-.273.488-.532a20.49 20.49 0 0 0 2.107 1.7a20.802 20.802 0 0 0 .426 2.712q-.25.063-.505.114m7.1-8.039q-.503-.317-1.018-.613q-.508-.292-1.027-.563a18.7 18.7 0 0 1 1.739-.635a18.218 18.218 0 0 1 .306 1.811M9.68 5.835c.636-1.85 1.578-2.98 2.304-2.98c.773-.001 1.777 1.218 2.434 3.197q.064.194.12.39a20.478 20.478 0 0 0-2.526.97a20.061 20.061 0 0 0-2.52-.981q.087-.3.188-.596m-.4 1.424a18.307 18.307 0 0 1 1.73.642q-1.052.542-2.048 1.181c.08-.638.187-1.249.318-1.823m-.317 7.66q.497.319 1.009.613q.522.3 1.057.576a18.196 18.196 0 0 1-1.744.665a19.144 19.144 0 0 1-.322-1.853m5.456 3.146a7.236 7.236 0 0 1-1.238 2.333a1.766 1.766 0 0 1-1.188.748c-.729 0-1.658-1.085-2.29-2.896q-.112-.321-.206-.648a20.109 20.109 0 0 0 2.53-1.01a20.8 20.8 0 0 0 2.547.979q-.072.249-.155.494m.362-1.324a19.267 19.267 0 0 1-1.762-.646q.509-.267 1.025-.565q.53-.306 1.032-.627a18.152 18.152 0 0 1-.295 1.838m.447-4.743q0 .911-.057 1.82c-.493.334-1.013.66-1.554.972c-.54.311-1.073.597-1.597.856q-.827-.396-1.622-.854q-.79-.455-1.544-.969q-.07-.91-.07-1.822q0-.911.068-1.821a24.168 24.168 0 0 1 3.158-1.823q.816.397 1.603.851q.79.454 1.55.959q.065.914.065 1.831m.956-5.093c1.922-.373 3.37-.122 3.733.507c.387.67-.167 2.148-1.554 3.706q-.115.129-.238.259a20.061 20.061 0 0 0-2.144-1.688a20.04 20.04 0 0 0-.405-2.649q.31-.076.608-.135m-.13 3.885a18.164 18.164 0 0 1 1.462 1.188a18.12 18.12 0 0 1-1.457 1.208q.023-.594.023-1.188q0-.604-.028-1.208m3.869 5.789c-.364.631-1.768.894-3.653.538q-.324-.061-.664-.146a20.069 20.069 0 0 0 .387-2.682a19.94 19.94 0 0 0 2.137-1.715q.177.183.336.364a7.234 7.234 0 0 1 1.403 2.238a1.766 1.766 0 0 1 .054 1.403"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLanguageJavascript(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 3h18v18H3zm4.73 15.04c.4.85 1.19 1.55 2.54 1.55c1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08c-.58 0-.82-.4-1.09-.87zm5.98-.18c.5.98 1.51 1.73 3.09 1.73c1.6 0 2.8-.83 2.8-2.36c0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02c0-.41.31-.73.81-.73c.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33c-1.51 0-2.48.96-2.48 2.23c0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13c0 .48-.45.83-1.15.83c-.83 0-1.31-.43-1.67-1.03z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiBash(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 9h2.31l.32-3h2l-.32 3h2l.32-3h2l-.32 3H15v2h-1.9l-.2 2H15v2h-2.31l-.32 3h-2l.32-3h-2l-.32 3h-2l.32-3H5v-2h1.9l.2-2H5zm4.1 2l-.2 2h2l.2-2M19 6h-2v8h2m0 2h-2v2h2Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLanguageCss3(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81l-5.48 1.81l-4.75-1.81l.33-1.64H2.85l-.79 4l7.85 3l9.05-3l1.2-6.03l.24-1.21L21.94 3z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MdiLanguageHtml5(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12 17.56l4.07-1.13l.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58l-2.22.6l-2.22-.6l-.14-1.66h-2l.29 3.19zM4.07 3h15.86L18.5 19.2L12 21l-6.5-1.8z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function RiMarkdownFill(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m4 12.5v-4l2 2l2-2v4h2v-7h-2l-2 2l-2-2H5v7zm11-3v-4h-2v4h-2l3 3l3-3z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import {
|
||||
MdiLanguageCss3,
|
||||
MdiLanguageHtml5,
|
||||
MdiLanguageJavascript,
|
||||
MdiLanguageTypescript,
|
||||
RiMarkdownFill,
|
||||
UilReact,
|
||||
} from "@renderer/components/icons/Language"
|
||||
|
||||
const LanguageAlias = {
|
||||
ts: "typescript",
|
||||
js: "javascript",
|
||||
|
||||
tsx: "typescriptreact",
|
||||
jsx: "javascriptreact",
|
||||
md: "markdown",
|
||||
}
|
||||
const LanguageToColorMap = {
|
||||
typescript: "#2b7489",
|
||||
javascript: "#f1e05a",
|
||||
javascriptreact: "#f1e05a",
|
||||
typescriptreact: "#2b7489",
|
||||
html: "#e34c26",
|
||||
java: "#b07219",
|
||||
go: "#00add8",
|
||||
vue: "#2c3e50",
|
||||
css: "#563d7c",
|
||||
yaml: "#cb171e",
|
||||
json: "#292929",
|
||||
markdown: "#083fa1",
|
||||
csharp: "#178600",
|
||||
"c#": "#178600",
|
||||
c: "#555555",
|
||||
cpp: "#f34b7d",
|
||||
"c++": "#f34b7d",
|
||||
python: "#3572a5",
|
||||
lua: "#000080",
|
||||
vimscript: "#199f4b",
|
||||
shell: "#89e051",
|
||||
dockerfile: "#384d54",
|
||||
ruby: "#701516",
|
||||
php: "#4f5d95",
|
||||
lisp: "#3fb68b",
|
||||
kotlin: "#F18E33",
|
||||
rust: "#dea584",
|
||||
dart: "#00B4AB",
|
||||
swift: "#ffac45",
|
||||
"objective-c": "#438eff",
|
||||
"objective-c++": "#6866fb",
|
||||
r: "#198ce7",
|
||||
matlab: "#e16737",
|
||||
scala: "#c22d40",
|
||||
sql: "#e38c00",
|
||||
perl: "#0298c3",
|
||||
}
|
||||
const languageToIconMap = {
|
||||
javascriptreact: <UilReact />,
|
||||
typescriptreact: <UilReact />,
|
||||
javascript: <MdiLanguageJavascript />,
|
||||
typescript: <MdiLanguageTypescript />,
|
||||
html: <MdiLanguageHtml5 />,
|
||||
css: <MdiLanguageCss3 />,
|
||||
markdown: <RiMarkdownFill />,
|
||||
}
|
||||
export const getLanguageColor = (language?: string) => {
|
||||
if (!language) return
|
||||
|
||||
const alias = LanguageAlias[language]
|
||||
if (alias) {
|
||||
return LanguageToColorMap[alias]
|
||||
}
|
||||
return LanguageToColorMap[language]
|
||||
}
|
||||
|
||||
export const getLanguageIcon = (language?: string) => {
|
||||
if (!language) return null
|
||||
|
||||
const alias = LanguageAlias[language]
|
||||
if (alias) {
|
||||
return languageToIconMap[alias]
|
||||
}
|
||||
|
||||
return languageToIconMap[language]
|
||||
}
|
||||
|
|
@ -23,7 +23,8 @@ const copyIconVariants: Variants = {
|
|||
|
||||
export const CopyButton: Component<{
|
||||
value: string
|
||||
}> = ({ value, className }) => {
|
||||
style?: React.CSSProperties
|
||||
}> = ({ value, className, style }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const copiedTimerRef = useRef<any>()
|
||||
const handleCopy = useCallback(() => {
|
||||
|
|
@ -45,6 +46,7 @@ export const CopyButton: Component<{
|
|||
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{copied ? (
|
||||
|
|
|
|||
|
|
@ -10,21 +10,13 @@ import type {
|
|||
BundledTheme,
|
||||
DynamicImportLanguageRegistration,
|
||||
DynamicImportThemeRegistration,
|
||||
HighlighterCore,
|
||||
} from "shiki"
|
||||
import { createHighlighterCoreSync, createJavaScriptRegexEngine } from "shiki/core"
|
||||
|
||||
import { getLanguageColor, getLanguageIcon } from "../constants"
|
||||
import { CopyButton } from "../copy-button"
|
||||
import { shikiTransformers } from "./shared"
|
||||
import { shiki, shikiTransformers } from "./shared"
|
||||
import styles from "./shiki.module.css"
|
||||
|
||||
const js = createJavaScriptRegexEngine()
|
||||
const shiki = createHighlighterCoreSync({
|
||||
themes: [],
|
||||
langs: [],
|
||||
engine: js,
|
||||
})
|
||||
|
||||
export interface ShikiProps {
|
||||
language: string | undefined
|
||||
code: string
|
||||
|
|
@ -42,24 +34,7 @@ let themeModule: Record<BundledTheme, DynamicImportThemeRegistration> | null = n
|
|||
let bundledLanguagesKeysSet: Set<string> | null = null
|
||||
|
||||
export const ShikiHighLighter: FC<ShikiProps> = (props) => {
|
||||
if (!shiki) {
|
||||
const { code, className } = props
|
||||
return (
|
||||
<pre className={className}>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return <ShikiHighLighterRender {...props} shiki={shiki} />
|
||||
}
|
||||
|
||||
const ShikiHighLighterRender: FC<
|
||||
ShikiProps & {
|
||||
shiki: HighlighterCore
|
||||
}
|
||||
> = (props) => {
|
||||
const { code, language, className, theme: overrideTheme, shiki } = props
|
||||
const { code, language, className, theme: overrideTheme } = props
|
||||
const [currentLanguage, setCurrentLanguage] = useState(language || "plaintext")
|
||||
|
||||
const guessCodeLanguage = useUISettingKey("guessCodeLanguage")
|
||||
|
|
@ -179,9 +154,8 @@ const ShikiHighLighterRender: FC<
|
|||
const ShikiCode: FC<
|
||||
ShikiProps & {
|
||||
codeTheme: string
|
||||
shiki: HighlighterCore
|
||||
}
|
||||
> = ({ code, language, codeTheme, className, transparent, shiki }) => {
|
||||
> = ({ code, language, codeTheme, className, transparent }) => {
|
||||
const rendered = useMemo(() => {
|
||||
try {
|
||||
return shiki.codeToHtml(code, {
|
||||
|
|
@ -193,10 +167,9 @@ const ShikiCode: FC<
|
|||
transformers: shikiTransformers,
|
||||
})
|
||||
} catch {
|
||||
// console.error(err)
|
||||
return null
|
||||
}
|
||||
}, [shiki, code, language, codeTheme])
|
||||
}, [code, language, codeTheme])
|
||||
|
||||
if (!rendered) {
|
||||
return (
|
||||
|
|
@ -205,6 +178,7 @@ const ShikiCode: FC<
|
|||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -217,8 +191,17 @@ const ShikiCode: FC<
|
|||
<div dangerouslySetInnerHTML={{ __html: rendered }} data-language={language} />
|
||||
<CopyButton
|
||||
value={code}
|
||||
className="absolute right-1 top-1 opacity-0 duration-200 group-hover:opacity-100"
|
||||
style={{
|
||||
backgroundColor: getLanguageColor(language),
|
||||
}}
|
||||
className={"absolute right-2 top-2 opacity-0 duration-200 group-hover:opacity-100"}
|
||||
/>
|
||||
{language !== "plaintext" && (
|
||||
<span className="center absolute bottom-2 right-2 flex gap-1 text-xs uppercase text-white opacity-80">
|
||||
<span className="center [&_svg]:size-4">{getLanguageIcon(language)}</span>
|
||||
<span>{language}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,17 @@ import {
|
|||
transformerNotationHighlight,
|
||||
} from "@shikijs/transformers"
|
||||
import type { ShikiTransformer } from "shiki"
|
||||
import { createHighlighterCoreSync, createJavaScriptRegexEngine } from "shiki"
|
||||
|
||||
export const shikiTransformers: ShikiTransformer[] = [
|
||||
transformerMetaHighlight(),
|
||||
transformerNotationDiff(),
|
||||
transformerNotationHighlight(),
|
||||
]
|
||||
|
||||
const js = createJavaScriptRegexEngine()
|
||||
export const shiki = createHighlighterCoreSync({
|
||||
themes: [],
|
||||
langs: [],
|
||||
engine: js,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
}
|
||||
|
||||
:global {
|
||||
.shiki {
|
||||
@apply !m-0 !px-0;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply !m-0 overflow-auto p-4;
|
||||
|
||||
|
|
@ -22,7 +26,7 @@
|
|||
}
|
||||
|
||||
.line {
|
||||
@apply block;
|
||||
@apply block px-5;
|
||||
|
||||
& > span:last-child {
|
||||
@apply mr-5;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function EntryHeaderImpl({
|
|||
"absolute left-5 top-0 flex h-full items-center gap-2 text-[13px] leading-none text-zinc-500",
|
||||
isAtTop ? "visible z-[11]" : "invisible z-[-99]",
|
||||
views[view].wideMode && "static",
|
||||
shouldShowMeta && "hidden",
|
||||
)}
|
||||
>
|
||||
<EntryReadHistory entryId={entryId} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue