add 代码块主题
This commit is contained in:
parent
d2f05c7b71
commit
ce91021879
@ -27,12 +27,14 @@
|
||||
"@tiptap/react": "^2.2.4",
|
||||
"@tiptap/starter-kit": "^2.2.4",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
"marked": "^15.0.6",
|
||||
"next": "14.1.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
|
@ -62,6 +62,9 @@ importers:
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/prismjs':
|
||||
specifier: ^1.26.5
|
||||
version: 1.26.5
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -80,6 +83,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
prismjs:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
@ -829,6 +835,9 @@ packages:
|
||||
'@types/node@20.17.16':
|
||||
resolution: {integrity: sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/prop-types@15.7.14':
|
||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||
|
||||
@ -1394,6 +1403,10 @@ packages:
|
||||
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -2441,6 +2454,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/prop-types@15.7.14': {}
|
||||
|
||||
'@types/react-dom@18.3.5(@types/react@18.3.18)':
|
||||
@ -2955,6 +2970,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prismjs@1.29.0: {}
|
||||
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
|
@ -1,14 +1,16 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import '@/styles/code-themes.css'
|
||||
import { ThemeProvider } from '@/components/theme/ThemeProvider'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: 'NeuraPress - AI Enhanced Article Editor',
|
||||
description: 'An intelligent article editor for creating and formatting content',
|
||||
export const metadata: Metadata = {
|
||||
title: 'NeuraPress',
|
||||
description: 'Markdown 转微信公众号内容神器',
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
|
30
src/components/editor/CodeThemeSelector.tsx
Normal file
30
src/components/editor/CodeThemeSelector.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import { codeThemes, type CodeThemeId } from '@/config/code-themes'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
interface CodeThemeSelectorProps {
|
||||
value: CodeThemeId
|
||||
onChange: (value: CodeThemeId) => void
|
||||
}
|
||||
|
||||
export function CodeThemeSelector({ value, onChange }: CodeThemeSelectorProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Label>代码主题</Label>
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="选择代码主题" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeThemes.map((theme) => (
|
||||
<SelectItem key={theme.id} value={theme.id}>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -6,7 +6,7 @@ import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { convertToWechat } from '@/lib/markdown'
|
||||
import { type RendererOptions } from '@/lib/markdown'
|
||||
import { type RendererOptions } from '@/lib/types'
|
||||
import { useEditorSync } from './hooks/useEditorSync'
|
||||
import { useAutoSave } from './hooks/useAutoSave'
|
||||
import { EditorToolbar } from './components/EditorToolbar'
|
||||
@ -16,6 +16,8 @@ import { type PreviewSize } from './constants'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { WechatStylePicker } from '@/components/template/WechatStylePicker'
|
||||
import { Copy, Clock, Type, Trash2 } from 'lucide-react'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { codeThemes, type CodeThemeId } from '@/config/code-themes'
|
||||
|
||||
// 计算阅读时间(假设每分钟阅读300字)
|
||||
const calculateReadingTime = (text: string): string => {
|
||||
@ -49,6 +51,9 @@ export default function WechatEditor() {
|
||||
const [wordCount, setWordCount] = useState('0')
|
||||
const [readingTime, setReadingTime] = useState('1 分钟')
|
||||
|
||||
// 添加 codeTheme 状态
|
||||
const [codeTheme] = useLocalStorage<CodeThemeId>('code-theme', codeThemes[0].id)
|
||||
|
||||
// 使用自定义 hooks
|
||||
const { handleScroll } = useEditorSync(editorRef)
|
||||
const { handleEditorChange } = useAutoSave(value, setIsDraft)
|
||||
@ -151,7 +156,8 @@ export default function WechatEditor() {
|
||||
...(styleOptions.inline?.listitem || {}),
|
||||
fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
|
||||
}
|
||||
}
|
||||
},
|
||||
codeTheme
|
||||
}
|
||||
|
||||
const html = convertToWechat(value, mergedOptions)
|
||||
@ -171,7 +177,7 @@ export default function WechatEditor() {
|
||||
console.error('Template transformation error:', error)
|
||||
return html
|
||||
}
|
||||
}, [value, selectedTemplate, styleOptions])
|
||||
}, [value, selectedTemplate, styleOptions, codeTheme])
|
||||
|
||||
// 处理复制
|
||||
const handleCopy = useCallback(async () => {
|
||||
@ -249,21 +255,33 @@ export default function WechatEditor() {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [handleSave])
|
||||
|
||||
// 延迟更新预览内容
|
||||
// 更新预览内容
|
||||
useEffect(() => {
|
||||
if (!showPreview) return
|
||||
|
||||
setIsConverting(true)
|
||||
const updatePreview = async () => {
|
||||
if (!value) {
|
||||
setPreviewContent('')
|
||||
return
|
||||
}
|
||||
|
||||
setIsConverting(true)
|
||||
try {
|
||||
const content = getPreviewContent()
|
||||
setPreviewContent(content)
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "预览更新失败",
|
||||
description: "生成预览内容时发生错误",
|
||||
})
|
||||
} finally {
|
||||
setIsConverting(false)
|
||||
}
|
||||
}
|
||||
updatePreview()
|
||||
}, [value, selectedTemplate, styleOptions, showPreview, getPreviewContent])
|
||||
|
||||
const timeoutId = setTimeout(updatePreview, 100)
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [value, selectedTemplate, styleOptions, codeTheme, getPreviewContent, toast])
|
||||
|
||||
// 加载已保存的内容
|
||||
useEffect(() => {
|
||||
@ -285,14 +303,13 @@ export default function WechatEditor() {
|
||||
|
||||
// 渲染预览内容
|
||||
const renderPreview = useCallback(() => {
|
||||
const content = getPreviewContent()
|
||||
return (
|
||||
<div
|
||||
className="preview-content"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
dangerouslySetInnerHTML={{ __html: previewContent }}
|
||||
/>
|
||||
)
|
||||
}, [getPreviewContent])
|
||||
}, [previewContent])
|
||||
|
||||
// 检测是否为移动设备
|
||||
const isMobile = useCallback(() => {
|
||||
|
@ -2,7 +2,10 @@ import { cn } from '@/lib/utils'
|
||||
import { PREVIEW_SIZES, type PreviewSize } from '../constants'
|
||||
import { Loader2, ZoomIn, ZoomOut, Maximize2, Minimize2 } from 'lucide-react'
|
||||
import { templates } from '@/config/wechat-templates'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { codeThemes, type CodeThemeId } from '@/config/code-themes'
|
||||
import '@/styles/code-themes.css'
|
||||
|
||||
interface EditorPreviewProps {
|
||||
previewRef: React.RefObject<HTMLDivElement>
|
||||
@ -24,6 +27,7 @@ export function EditorPreview({
|
||||
const [zoom, setZoom] = useState(100)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const isScrolling = useRef<boolean>(false)
|
||||
const [codeTheme] = useLocalStorage<CodeThemeId>('code-theme', codeThemes[0].id)
|
||||
|
||||
const handleZoomIn = () => {
|
||||
setZoom(prev => Math.min(prev + 10, 200))
|
||||
@ -50,7 +54,8 @@ export function EditorPreview({
|
||||
"preview-container bg-background transition-all duration-300 ease-in-out flex flex-col",
|
||||
"h-full sm:w-1/2",
|
||||
"markdown-body relative",
|
||||
selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
|
||||
selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles,
|
||||
`code-theme-${codeTheme}`
|
||||
)}
|
||||
>
|
||||
<div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b flex items-center justify-between z-10 sticky top-0 left-0 right-0">
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { Copy, Plus, Save, Smartphone } from 'lucide-react'
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Copy, Plus, Save, Smartphone, Settings } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WechatStylePicker } from '../../template/WechatStylePicker'
|
||||
import { TemplateManager } from '../../template/TemplateManager'
|
||||
@ -12,6 +15,9 @@ import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { CodeThemeSelector } from '../CodeThemeSelector'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { codeThemes, type CodeThemeId } from '@/config/code-themes'
|
||||
|
||||
interface EditorToolbarProps {
|
||||
value: string
|
||||
@ -47,6 +53,7 @@ export function EditorToolbar({
|
||||
styleOptions
|
||||
}: EditorToolbarProps) {
|
||||
const { toast } = useToast()
|
||||
const [codeTheme, setCodeTheme] = useLocalStorage<CodeThemeId>('code-theme', codeThemes[0].id)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
@ -117,16 +124,16 @@ export function EditorToolbar({
|
||||
currentContent={value}
|
||||
onNew={onNewArticle}
|
||||
/>
|
||||
|
||||
<WechatStylePicker
|
||||
value={selectedTemplate}
|
||||
onSelect={onTemplateSelect}
|
||||
/>
|
||||
<TemplateManager onTemplateChange={onTemplateChange} />
|
||||
/>
|
||||
<CodeThemeSelector value={codeTheme} onChange={setCodeTheme} />
|
||||
<StyleConfigDialog
|
||||
value={styleOptions}
|
||||
onChangeAction={onStyleOptionsChange}
|
||||
/>
|
||||
<TemplateManager onTemplateChange={onTemplateChange} />
|
||||
<button
|
||||
onClick={onPreviewToggle}
|
||||
className={cn(
|
||||
|
90
src/config/code-themes.ts
Normal file
90
src/config/code-themes.ts
Normal file
@ -0,0 +1,90 @@
|
||||
export const codeThemes = [
|
||||
{
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
description: 'GitHub 风格代码主题',
|
||||
theme: {
|
||||
background: '#f6f8fa',
|
||||
text: '#24292e',
|
||||
comment: '#6a737d',
|
||||
keyword: '#d73a49',
|
||||
string: '#032f62',
|
||||
number: '#005cc5',
|
||||
function: '#6f42c1',
|
||||
class: '#22863a',
|
||||
variable: '#24292e',
|
||||
operator: '#d73a49'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dracula',
|
||||
name: 'Dracula',
|
||||
description: '暗黑风格代码主题',
|
||||
theme: {
|
||||
background: '#282a36',
|
||||
text: '#f8f8f2',
|
||||
comment: '#6272a4',
|
||||
keyword: '#ff79c6',
|
||||
string: '#f1fa8c',
|
||||
number: '#bd93f9',
|
||||
function: '#50fa7b',
|
||||
class: '#8be9fd',
|
||||
variable: '#f8f8f2',
|
||||
operator: '#ff79c6'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'monokai',
|
||||
name: 'Monokai',
|
||||
description: '经典 Sublime Text 主题',
|
||||
theme: {
|
||||
background: '#272822',
|
||||
text: '#f8f8f2',
|
||||
comment: '#75715e',
|
||||
keyword: '#f92672',
|
||||
string: '#e6db74',
|
||||
number: '#ae81ff',
|
||||
function: '#a6e22e',
|
||||
class: '#66d9ef',
|
||||
variable: '#f8f8f2',
|
||||
operator: '#f92672'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'solarized-light',
|
||||
name: 'Solarized Light',
|
||||
description: '护眼浅色主题',
|
||||
theme: {
|
||||
background: '#fdf6e3',
|
||||
text: '#657b83',
|
||||
comment: '#93a1a1',
|
||||
keyword: '#859900',
|
||||
string: '#2aa198',
|
||||
number: '#d33682',
|
||||
function: '#268bd2',
|
||||
class: '#b58900',
|
||||
variable: '#657b83',
|
||||
operator: '#859900'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'nord',
|
||||
name: 'Nord',
|
||||
description: '北欧风格主题',
|
||||
theme: {
|
||||
background: '#2e3440',
|
||||
text: '#d8dee9',
|
||||
comment: '#4c566a',
|
||||
keyword: '#81a1c1',
|
||||
string: '#a3be8c',
|
||||
number: '#b48ead',
|
||||
function: '#88c0d0',
|
||||
class: '#8fbcbb',
|
||||
variable: '#d8dee9',
|
||||
operator: '#81a1c1'
|
||||
}
|
||||
}
|
||||
] as const
|
||||
|
||||
export type CodeTheme = typeof codeThemes[number]
|
||||
export type CodeThemeId = CodeTheme['id']
|
@ -3,11 +3,13 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
// 获取初始值
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return initialValue
|
||||
}
|
||||
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
@ -34,9 +36,11 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
return () => window.removeEventListener('storage', handleStorageChange)
|
||||
}, [key, initialValue])
|
||||
|
||||
// 更新存储的值
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
if (typeof window !== 'undefined') {
|
||||
|
@ -1,21 +1,57 @@
|
||||
import { Marked } from 'marked'
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { Tokens } from 'marked'
|
||||
import { codeThemes, type CodeThemeId } from '@/config/code-themes'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import 'prismjs/components/prism-jsx'
|
||||
import 'prismjs/components/prism-tsx'
|
||||
import 'prismjs/components/prism-css'
|
||||
import 'prismjs/components/prism-scss'
|
||||
import 'prismjs/components/prism-json'
|
||||
import 'prismjs/components/prism-yaml'
|
||||
import 'prismjs/components/prism-markdown'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-java'
|
||||
import 'prismjs/components/prism-go'
|
||||
import 'prismjs/components/prism-rust'
|
||||
import 'prismjs/components/prism-sql'
|
||||
import 'prismjs/components/prism-docker'
|
||||
import 'prismjs/components/prism-nginx'
|
||||
import type { StyleOptions, RendererOptions } from '@/lib/types'
|
||||
|
||||
// 将样式对象转换为 CSS 字符串
|
||||
function cssPropertiesToString(style: StyleOptions = {}): string {
|
||||
if (!style) return ''
|
||||
|
||||
return Object.entries(style)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
// 处理媒体查询
|
||||
if (key === '@media (max-width: 768px)') {
|
||||
return '' // 我们不在内联样式中包含媒体查询
|
||||
}
|
||||
|
||||
// 转换驼峰命名为连字符命名
|
||||
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
|
||||
// 处理数字值
|
||||
if (typeof value === 'number' && !cssKey.includes('line-height')) {
|
||||
value = `${value}px`
|
||||
}
|
||||
|
||||
return `${cssKey}: ${value}`
|
||||
})
|
||||
.filter(Boolean) // 移除空字符串
|
||||
.join(';')
|
||||
}
|
||||
|
||||
// 将基础样式选项转换为 CSS 字符串
|
||||
function baseStylesToString(base: RendererOptions['base'] = {}): string {
|
||||
if (!base) return ''
|
||||
|
||||
const styles: string[] = []
|
||||
|
||||
if (base.lineHeight) {
|
||||
@ -24,6 +60,12 @@ function baseStylesToString(base: RendererOptions['base'] = {}): string {
|
||||
if (base.fontSize) {
|
||||
styles.push(`font-size: ${base.fontSize}`)
|
||||
}
|
||||
if (base.textAlign) {
|
||||
styles.push(`text-align: ${base.textAlign}`)
|
||||
}
|
||||
if (base.themeColor) {
|
||||
styles.push(`--theme-color: ${base.themeColor}`)
|
||||
}
|
||||
|
||||
return styles.join(';')
|
||||
}
|
||||
@ -63,19 +105,20 @@ marked.use({
|
||||
|
||||
const defaultOptions: RendererOptions = {
|
||||
base: {
|
||||
primaryColor: '#333333',
|
||||
textAlign: 'left',
|
||||
lineHeight: '1.75',
|
||||
themeColor: '#1a1a1a',
|
||||
fontSize: '15px',
|
||||
themeColor: '#1a1a1a'
|
||||
lineHeight: '1.75',
|
||||
textAlign: 'left'
|
||||
},
|
||||
block: {
|
||||
h1: { fontSize: '24px' },
|
||||
h2: { fontSize: '20px' },
|
||||
h3: { fontSize: '18px' },
|
||||
p: { fontSize: '15px', color: '#333333' },
|
||||
code_pre: { fontSize: '14px', color: '#333333' },
|
||||
blockquote: { fontSize: '15px', color: '#666666' }
|
||||
code_pre: {
|
||||
fontSize: '14px',
|
||||
overflowX: 'auto',
|
||||
borderRadius: '8px',
|
||||
padding: '1em',
|
||||
lineHeight: '1.5',
|
||||
margin: '10px 8px'
|
||||
}
|
||||
},
|
||||
inline: {
|
||||
link: { color: '#576b95' },
|
||||
@ -84,6 +127,28 @@ const defaultOptions: RendererOptions = {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代码主题的样式
|
||||
function getCodeThemeStyles(theme: CodeThemeId): StyleOptions {
|
||||
const themeConfig = codeThemes.find(t => t.id === theme)
|
||||
if (!themeConfig) return {}
|
||||
|
||||
return {
|
||||
background: themeConfig.theme.background,
|
||||
color: themeConfig.theme.text,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代码token的样式
|
||||
function getTokenStyles(theme: CodeThemeId, tokenType: string): string {
|
||||
const themeConfig = codeThemes.find(t => t.id === theme)
|
||||
if (!themeConfig) return ''
|
||||
|
||||
const tokenColor = themeConfig.theme[tokenType as keyof typeof themeConfig.theme]
|
||||
if (!tokenColor) return ''
|
||||
|
||||
return `color: ${tokenColor};`
|
||||
}
|
||||
|
||||
export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string {
|
||||
const renderer = new marked.Renderer()
|
||||
|
||||
@ -91,239 +156,194 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
||||
const mergedOptions = {
|
||||
base: { ...defaultOptions.base, ...options.base },
|
||||
block: { ...defaultOptions.block, ...options.block },
|
||||
inline: { ...defaultOptions.inline, ...options.inline }
|
||||
inline: { ...defaultOptions.inline, ...options.inline },
|
||||
codeTheme: options.codeTheme || codeThemes[0].id
|
||||
}
|
||||
|
||||
// 重写 heading 方法
|
||||
renderer.heading = function({ text, depth }: Tokens.Heading) {
|
||||
const style = {
|
||||
...mergedOptions.block?.[`h${depth}`],
|
||||
color: mergedOptions.base?.themeColor, // 使用主题颜色
|
||||
textAlign: mergedOptions.base?.textAlign // 添加文本对齐
|
||||
const headingKey = `h${depth}` as keyof RendererOptions['block']
|
||||
const headingStyle = (mergedOptions.block?.[headingKey] || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...headingStyle,
|
||||
color: mergedOptions.base?.themeColor
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const tokens = marked.Lexer.lexInline(text)
|
||||
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||
return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${content}</h${depth}>`
|
||||
return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${text}</h${depth}>`
|
||||
}
|
||||
|
||||
// 重写 paragraph 方法
|
||||
renderer.paragraph = function({ text }: Tokens.Paragraph) {
|
||||
const style = mergedOptions.block?.p || {}
|
||||
const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...paragraphStyle,
|
||||
fontSize: mergedOptions.base?.fontSize,
|
||||
lineHeight: mergedOptions.base?.lineHeight
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const tokens = marked.Lexer.lexInline(text)
|
||||
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||
return `<p style="${styleStr}">${content}</p>`
|
||||
return `<p${styleStr ? ` style="${styleStr}"` : ''}>${text}</p>`
|
||||
}
|
||||
|
||||
// 重写 blockquote 方法
|
||||
renderer.blockquote = function({ text }: Tokens.Blockquote) {
|
||||
const style = mergedOptions.block?.blockquote
|
||||
const blockquoteStyle = (mergedOptions.block?.blockquote || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...blockquoteStyle,
|
||||
borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}`
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const tokens = marked.Lexer.lexInline(text)
|
||||
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||
|
||||
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${content}</blockquote>`
|
||||
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>`
|
||||
}
|
||||
|
||||
// 重写 code 方法
|
||||
renderer.code = function({ text, lang }: Tokens.Code) {
|
||||
const style = mergedOptions.block?.code_pre
|
||||
const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...codeStyle,
|
||||
...getCodeThemeStyles(mergedOptions.codeTheme)
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<pre${styleStr ? ` style="${styleStr}"` : ''}><code class="language-${lang || ''}">${text}</code></pre>`
|
||||
|
||||
// 代码高亮处理
|
||||
let highlighted = text
|
||||
if (lang && Prism.languages[lang]) {
|
||||
try {
|
||||
const grammar = Prism.languages[lang]
|
||||
const tokens = Prism.tokenize(text, grammar)
|
||||
|
||||
highlighted = tokens.map(token => {
|
||||
if (typeof token === 'string') {
|
||||
return token
|
||||
}
|
||||
|
||||
const tokenStyle = getTokenStyles(mergedOptions.codeTheme, token.type)
|
||||
return `<span style="${tokenStyle}">${token.content}</span>`
|
||||
}).join('')
|
||||
} catch (error) {
|
||||
console.error(`Error highlighting code: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
return `<pre${styleStr ? ` style="${styleStr}"` : ''}><code class="language-${lang || ''}">${highlighted}</code></pre>`
|
||||
}
|
||||
|
||||
// 重写 codespan 方法
|
||||
renderer.codespan = function({ text }: Tokens.Codespan) {
|
||||
const style = mergedOptions.inline?.codespan
|
||||
const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...codespanStyle
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<code${styleStr ? ` style="${styleStr}"` : ''}>${text}</code>`
|
||||
}
|
||||
|
||||
// 重写 em 方法
|
||||
renderer.em = function({ text }: Tokens.Em) {
|
||||
const style = mergedOptions.inline?.em
|
||||
const emStyle = (mergedOptions.inline?.em || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...emStyle,
|
||||
fontStyle: 'italic'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>`
|
||||
}
|
||||
|
||||
// 重写 strong 方法
|
||||
renderer.strong = function({ text }: Tokens.Strong) {
|
||||
const style = mergedOptions.inline?.strong
|
||||
const strongStyle = (mergedOptions.inline?.strong || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...strongStyle,
|
||||
color: mergedOptions.base?.themeColor,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>`
|
||||
}
|
||||
|
||||
// 重写 link 方法
|
||||
renderer.link = function({ href, title, text }: Tokens.Link) {
|
||||
const style = mergedOptions.inline?.link
|
||||
const linkStyle = (mergedOptions.inline?.link || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...linkStyle
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>`
|
||||
}
|
||||
|
||||
// 重写 image 方法
|
||||
renderer.image = function({ href, title, text }: Tokens.Image) {
|
||||
const style = mergedOptions.block?.image
|
||||
const imageStyle = (mergedOptions.block?.image || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...imageStyle,
|
||||
maxWidth: '100%',
|
||||
display: 'block',
|
||||
margin: '0.5em auto'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''} />`
|
||||
return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''}>`
|
||||
}
|
||||
|
||||
// 重写 list 方法
|
||||
renderer.list = function(token: Tokens.List) {
|
||||
// 重写 list 方法
|
||||
renderer.list = function(token: Tokens.List) {
|
||||
const tag = token.ordered ? 'ol' : 'ul'
|
||||
try {
|
||||
const style = {
|
||||
...(mergedOptions.block?.[token.ordered ? 'ol' : 'ul'] || {}),
|
||||
listStyle: token.ordered ? 'decimal' : 'disc',
|
||||
paddingLeft: '2em',
|
||||
marginBottom: '16px'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''
|
||||
|
||||
const items = token.items.map(item => {
|
||||
let itemText = item.text
|
||||
if (item.task) {
|
||||
const checkbox = `<input type="checkbox"${item.checked ? ' checked=""' : ''} disabled="" /> `
|
||||
itemText = checkbox + itemText
|
||||
}
|
||||
return renderer.listitem({ ...item, text: itemText })
|
||||
}).join('')
|
||||
|
||||
return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${items}</${tag}>`
|
||||
} catch (error) {
|
||||
console.error(`Error rendering list: ${error}`)
|
||||
return `<${tag}>${token.items.map(item => renderer.listitem(item)).join('')}</${tag}>`
|
||||
const listStyle = (mergedOptions.block?.[tag] || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...listStyle,
|
||||
listStyle: token.ordered ? 'decimal' : 'disc',
|
||||
paddingLeft: '2em',
|
||||
marginBottom: '16px'
|
||||
}
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''
|
||||
|
||||
const items = token.items.map(item => {
|
||||
let itemText = item.text
|
||||
if (item.task) {
|
||||
const checkbox = `<input type="checkbox"${item.checked ? ' checked=""' : ''} disabled="" /> `
|
||||
itemText = checkbox + itemText
|
||||
}
|
||||
return renderer.listitem({ ...item, text: itemText })
|
||||
}).join('')
|
||||
|
||||
return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${items}</${tag}>`
|
||||
}
|
||||
|
||||
// 重写 listitem 方法
|
||||
renderer.listitem = function(item: Tokens.ListItem) {
|
||||
try {
|
||||
const style = {
|
||||
...(mergedOptions.inline?.listitem || {}),
|
||||
marginBottom: '8px',
|
||||
display: 'list-item'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
|
||||
// 处理嵌套列表
|
||||
let content = item.text
|
||||
if (item.tokens) {
|
||||
content = item.tokens.map(token => {
|
||||
if (token.type === 'list') {
|
||||
// 递归处理嵌套列表
|
||||
return renderer.list(token as Tokens.List)
|
||||
} else {
|
||||
// 处理其他类型的 token
|
||||
const tokens = marked.Lexer.lexInline(token.raw)
|
||||
return marked.Parser.parseInline(tokens, { renderer })
|
||||
}
|
||||
}).join('')
|
||||
} else {
|
||||
// 如果没有 tokens,则按普通文本处理
|
||||
const tokens = marked.Lexer.lexInline(content)
|
||||
content = marked.Parser.parseInline(tokens, { renderer })
|
||||
}
|
||||
|
||||
return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
|
||||
} catch (error) {
|
||||
console.error(`Error rendering list item: ${error}`)
|
||||
return `<li>${item.text}</li>`
|
||||
// 重写 listitem 方法
|
||||
renderer.listitem = function(item: Tokens.ListItem) {
|
||||
const listitemStyle = (mergedOptions.inline?.listitem || {}) as StyleOptions
|
||||
const style: StyleOptions = {
|
||||
...listitemStyle,
|
||||
marginBottom: '8px',
|
||||
display: 'list-item'
|
||||
}
|
||||
}
|
||||
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
|
||||
// 处理嵌套列表
|
||||
let content = item.text
|
||||
if (item.tokens) {
|
||||
content = item.tokens.map(token => {
|
||||
if (token.type === 'list') {
|
||||
// 递归处理嵌套列表
|
||||
return renderer.list(token as Tokens.List)
|
||||
} else {
|
||||
// 处理其他类型的 token
|
||||
const tokens = marked.Lexer.lexInline(token.raw)
|
||||
return marked.Parser.parseInline(tokens, { renderer })
|
||||
}
|
||||
}).join('')
|
||||
} else {
|
||||
// 如果没有 tokens,则按普通文本处理
|
||||
const tokens = marked.Lexer.lexInline(content)
|
||||
content = marked.Parser.parseInline(tokens, { renderer })
|
||||
}
|
||||
|
||||
return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
|
||||
}
|
||||
|
||||
// Convert Markdown to HTML using the custom renderer
|
||||
const html = marked.parse(markdown, { renderer }) as string
|
||||
|
||||
// Apply base styles
|
||||
const baseStyles = baseStylesToString(mergedOptions.base)
|
||||
return baseStyles ? `<section style="${baseStyles}">${html}</section>` : html
|
||||
}
|
||||
|
||||
export function convertToXiaohongshu(markdown: string): string {
|
||||
// 预处理 markdown
|
||||
markdown = preprocessMarkdown(markdown)
|
||||
|
||||
const renderer = new marked.Renderer()
|
||||
|
||||
// 自定义渲染规则
|
||||
renderer.heading = function({ text, depth }: Tokens.Heading) {
|
||||
const fontSize = {
|
||||
[1]: '20px',
|
||||
[2]: '18px',
|
||||
[3]: '16px',
|
||||
[4]: '15px',
|
||||
[5]: '14px',
|
||||
[6]: '14px'
|
||||
}[depth] || '14px'
|
||||
|
||||
return `<h${depth} style="margin-top: 25px; margin-bottom: 12px; font-weight: bold; font-size: ${fontSize}; color: #222;">${text}</h${depth}>`
|
||||
}
|
||||
|
||||
renderer.paragraph = function({ text }: Tokens.Paragraph) {
|
||||
return `<p style="margin-bottom: 16px; line-height: 1.6; font-size: 15px; color: #222;">${text}</p>`
|
||||
}
|
||||
|
||||
// 使用自定义渲染器转换 Markdown
|
||||
return marked.parse(markdown, { renderer }) as string
|
||||
}
|
||||
|
||||
export interface StyleOptions {
|
||||
fontSize?: string
|
||||
color?: string
|
||||
margin?: string
|
||||
padding?: string
|
||||
border?: string
|
||||
borderLeft?: string
|
||||
borderBottom?: string
|
||||
borderRadius?: string
|
||||
background?: string
|
||||
fontWeight?: string
|
||||
fontStyle?: string
|
||||
textDecoration?: string
|
||||
display?: string
|
||||
lineHeight?: string | number
|
||||
textAlign?: string
|
||||
paddingLeft?: string
|
||||
overflowX?: string
|
||||
width?: string
|
||||
letterSpacing?: string
|
||||
fontFamily?: string
|
||||
WebkitBackgroundClip?: string
|
||||
WebkitTextFillColor?: string
|
||||
listStyle?: string
|
||||
'@media (max-width: 768px)'?: {
|
||||
margin?: string
|
||||
padding?: string
|
||||
fontSize?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RendererOptions {
|
||||
base?: {
|
||||
primaryColor?: string
|
||||
textAlign?: string
|
||||
lineHeight?: string | number
|
||||
fontSize?: string
|
||||
themeColor?: string
|
||||
padding?: string
|
||||
maxWidth?: string
|
||||
margin?: string
|
||||
wordBreak?: string
|
||||
whiteSpace?: string
|
||||
color?: string
|
||||
fontFamily?: string
|
||||
}
|
||||
block?: {
|
||||
[key: string]: StyleOptions
|
||||
}
|
||||
inline?: {
|
||||
[key: string]: StyleOptions
|
||||
}
|
||||
dark?: {
|
||||
base?: {
|
||||
color?: string
|
||||
}
|
||||
block?: {
|
||||
[key: string]: {
|
||||
color?: string
|
||||
background?: string
|
||||
border?: string
|
||||
borderLeftColor?: string
|
||||
boxShadow?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { CodeThemeId } from '@/config/code-themes'
|
||||
|
||||
export interface RendererOptions {
|
||||
base?: {
|
||||
themeColor?: string
|
||||
textAlign?: string
|
||||
fontSize?: string
|
||||
lineHeight?: string
|
||||
textAlign?: string
|
||||
}
|
||||
block?: {
|
||||
h1?: StyleOptions
|
||||
@ -28,6 +30,7 @@ export interface RendererOptions {
|
||||
link?: StyleOptions
|
||||
listitem?: StyleOptions
|
||||
}
|
||||
codeTheme?: CodeThemeId
|
||||
}
|
||||
|
||||
export interface StyleOptions {
|
||||
|
254
src/styles/code-themes.css
Normal file
254
src/styles/code-themes.css
Normal file
@ -0,0 +1,254 @@
|
||||
/* GitHub Theme */
|
||||
.code-theme-github pre {
|
||||
background-color: #f6f8fa;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.code-theme-github .token.comment {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.code-theme-github .token.keyword {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.code-theme-github .token.string {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.code-theme-github .token.number {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.code-theme-github .token.function {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.code-theme-github .token.class-name {
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.code-theme-github .token.operator {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
/* Dracula Theme */
|
||||
.code-theme-dracula pre {
|
||||
background-color: #282a36;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.comment {
|
||||
color: #6272a4;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.keyword {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.string {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.number {
|
||||
color: #bd93f9;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.function {
|
||||
color: #50fa7b;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.class-name {
|
||||
color: #8be9fd;
|
||||
}
|
||||
|
||||
.code-theme-dracula .token.operator {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
/* Monokai Theme */
|
||||
.code-theme-monokai pre {
|
||||
background-color: #272822;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.comment {
|
||||
color: #75715e;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.keyword {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.string {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.number {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.function {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.class-name {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.code-theme-monokai .token.operator {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
/* Solarized Light Theme */
|
||||
.code-theme-solarized-light pre {
|
||||
background-color: #fdf6e3;
|
||||
color: #657b83;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.comment {
|
||||
color: #93a1a1;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.keyword {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.string {
|
||||
color: #2aa198;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.number {
|
||||
color: #d33682;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.function {
|
||||
color: #268bd2;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.class-name {
|
||||
color: #b58900;
|
||||
}
|
||||
|
||||
.code-theme-solarized-light .token.operator {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
/* Nord Theme */
|
||||
.code-theme-nord pre {
|
||||
background-color: #2e3440;
|
||||
color: #d8dee9;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.comment {
|
||||
color: #4c566a;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.keyword {
|
||||
color: #81a1c1;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.string {
|
||||
color: #a3be8c;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.number {
|
||||
color: #b48ead;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.function {
|
||||
color: #88c0d0;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.class-name {
|
||||
color: #8fbcbb;
|
||||
}
|
||||
|
||||
.code-theme-nord .token.operator {
|
||||
color: #81a1c1;
|
||||
}
|
||||
|
||||
/* Common styles for all themes */
|
||||
pre {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
font-family: "Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Line Numbers */
|
||||
.line-numbers .line-numbers-rows {
|
||||
border-right: 1px solid #999;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
/* Code Block Header */
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5em 1em;
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.code-header .language {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code-header .copy-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 0.2em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.code-header .copy-button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.code-header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.code-header .copy-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user