perf(CodeBlock): improve long codeblock loading experience (#4167)
* perf(CodeBlock): improve long codeblock loading experience * refactor: use requestIdleCallback rather than observer * refactor: simplify setting expanded and unwrapped * refactor: simplify logic * refactor: revert to observer * fix: turn mermaid listener to passive to avoid scrolling performance downgrade * feat: add lru cache for syntax highlighting * refactor: adjust cache options * feat: add highlighter cache * fix: highlighter should be loaded before highlighting * refactor: reduce cache time * refactor: adjust cache size and hash * refactor: decrease cache size * fix: restore the behaviour of ShowExpandButton * fix: check streaming status * fix: empty code * refactor: improve streaming check * fix: optimizeDeps excludes * refactor: adjust cache policy * feat: add a setting for code caching * feat: add more settings for code cache * fix: initialize service * refactor: prevent accident cache reset, update settings * refactor: update code cache service * fix: revert unecessary changes * refactor: adjust cache settings * fix: update migrate version * chore: update to shiki v3 * fix: import path * refactor: remove highlighter cache, improve fallbacks * fix: revert path changes * style: fix lint errors * style: improve readability * style: improve readability * chore: update migrate version * chore: update packages
This commit is contained in:
parent
641dfc60b0
commit
95639df35c
@ -118,6 +118,7 @@
|
|||||||
"@types/diff": "^7",
|
"@types/diff": "^7",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
|
"@types/lru-cache": "^7.10.10",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
@ -179,7 +180,7 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
"shiki": "^1.22.2",
|
"shiki": "^3.2.1",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
|
|||||||
@ -1,21 +1,33 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { CodeCacheService } from '@renderer/services/CodeCacheService'
|
||||||
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'
|
import { createContext, type PropsWithChildren, use, useCallback, useMemo } from 'react'
|
||||||
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
|
import { bundledLanguages, bundledThemes, createHighlighter, type Highlighter } from 'shiki'
|
||||||
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki'
|
|
||||||
|
let highlighterPromise: Promise<Highlighter> | null = null
|
||||||
|
|
||||||
|
async function getHighlighter() {
|
||||||
|
if (!highlighterPromise) {
|
||||||
|
highlighterPromise = createHighlighter({
|
||||||
|
langs: ['javascript', 'typescript', 'python', 'java', 'markdown'],
|
||||||
|
themes: ['one-light', 'material-theme-darker']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await highlighterPromise
|
||||||
|
}
|
||||||
|
|
||||||
interface SyntaxHighlighterContextType {
|
interface SyntaxHighlighterContextType {
|
||||||
codeToHtml: (code: string, language: string) => Promise<string>
|
codeToHtml: (code: string, language: string, enableCache: boolean) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const SyntaxHighlighterContext = createContext<SyntaxHighlighterContextType | undefined>(undefined)
|
const SyntaxHighlighterContext = createContext<SyntaxHighlighterContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [highlighter, setHighlighter] = useState<HighlighterGeneric<BundledLanguage, BundledTheme> | null>(null)
|
|
||||||
const { codeStyle } = useSettings()
|
const { codeStyle } = useSettings()
|
||||||
useMermaid()
|
useMermaid()
|
||||||
|
|
||||||
@ -27,29 +39,14 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
|||||||
return codeStyle
|
return codeStyle
|
||||||
}, [theme, codeStyle])
|
}, [theme, codeStyle])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initHighlighter = async () => {
|
|
||||||
const commonLanguages = ['javascript', 'typescript', 'python', 'java', 'markdown']
|
|
||||||
|
|
||||||
const hl = await createHighlighter({
|
|
||||||
themes: [highlighterTheme],
|
|
||||||
langs: commonLanguages
|
|
||||||
})
|
|
||||||
|
|
||||||
setHighlighter(hl)
|
|
||||||
|
|
||||||
// Load all themes and languages
|
|
||||||
// hl.loadTheme(...(Object.keys(bundledThemes) as BundledTheme[]))
|
|
||||||
// hl.loadLanguage(...(Object.keys(bundledLanguages) as BundledLanguage[]))
|
|
||||||
}
|
|
||||||
|
|
||||||
initHighlighter()
|
|
||||||
}, [highlighterTheme])
|
|
||||||
|
|
||||||
const codeToHtml = useCallback(
|
const codeToHtml = useCallback(
|
||||||
async (_code: string, language: string) => {
|
async (_code: string, language: string, enableCache: boolean) => {
|
||||||
{
|
{
|
||||||
if (!highlighter) return ''
|
if (!_code) return ''
|
||||||
|
|
||||||
|
const key = CodeCacheService.generateCacheKey(_code, language, highlighterTheme)
|
||||||
|
const cached = enableCache ? CodeCacheService.getCachedResult(key) : null
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
const languageMap: Record<string, string> = {
|
const languageMap: Record<string, string> = {
|
||||||
vab: 'vb'
|
vab: 'vb'
|
||||||
@ -61,25 +58,41 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
|||||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage as BundledLanguage)) {
|
const highlighter = await getHighlighter()
|
||||||
if (mappedLanguage in bundledLanguages || mappedLanguage === 'text') {
|
|
||||||
await highlighter.loadLanguage(mappedLanguage as BundledLanguage)
|
if (!highlighter.getLoadedThemes().includes(highlighterTheme)) {
|
||||||
} else {
|
const themeImportFn = bundledThemes[highlighterTheme]
|
||||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
if (themeImportFn) {
|
||||||
|
await highlighter.loadTheme(await themeImportFn())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return highlighter.codeToHtml(code, {
|
if (!highlighter.getLoadedLanguages().includes(mappedLanguage)) {
|
||||||
|
const languageImportFn = bundledLanguages[mappedLanguage]
|
||||||
|
if (languageImportFn) {
|
||||||
|
await highlighter.loadLanguage(await languageImportFn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成高亮HTML
|
||||||
|
const html = highlighter.codeToHtml(code, {
|
||||||
lang: mappedLanguage,
|
lang: mappedLanguage,
|
||||||
theme: highlighterTheme
|
theme: highlighterTheme
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置缓存
|
||||||
|
if (enableCache) {
|
||||||
|
CodeCacheService.setCachedResult(key, html, _code.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Error highlighting code for language '${mappedLanguage}':`, error)
|
console.debug(`Error highlighting code for language '${mappedLanguage}':`, error)
|
||||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[highlighter, highlighterTheme]
|
[highlighterTheme]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <SyntaxHighlighterContext value={{ codeToHtml }}>{children}</SyntaxHighlighterContext>
|
return <SyntaxHighlighterContext value={{ codeToHtml }}>{children}</SyntaxHighlighterContext>
|
||||||
|
|||||||
@ -40,7 +40,6 @@ export const useMermaid = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
e.preventDefault()
|
|
||||||
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
|
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
|
||||||
if (!mermaidElement) return
|
if (!mermaidElement) return
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ export const useMermaid = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('wheel', handleWheel, { passive: false })
|
document.addEventListener('wheel', handleWheel, { passive: true })
|
||||||
return () => document.removeEventListener('wheel', handleWheel)
|
return () => document.removeEventListener('wheel', handleWheel)
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,14 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"settings.code_collapsible": "Code block collapsible",
|
"settings.code_collapsible": "Code block collapsible",
|
||||||
"settings.code_wrappable": "Code block wrappable",
|
"settings.code_wrappable": "Code block wrappable",
|
||||||
|
"settings.code_cacheable": "Code block cache",
|
||||||
|
"settings.code_cacheable.tip": "Caching code blocks can reduce the rendering time of long code blocks, but it will increase memory usage",
|
||||||
|
"settings.code_cache_max_size": "Max cache size",
|
||||||
|
"settings.code_cache_max_size.tip": "The maximum number of characters allowed to be cached (thousand characters), calculated according to the highlighted code. The length of the highlighted code is much longer than the pure text.",
|
||||||
|
"settings.code_cache_ttl": "Cache TTL",
|
||||||
|
"settings.code_cache_ttl.tip": "Cache expiration time (minutes)",
|
||||||
|
"settings.code_cache_threshold": "Cache threshold",
|
||||||
|
"settings.code_cache_threshold.tip": "The minimum number of characters allowed to be cached (thousand characters), calculated according to the actual code. Only code blocks exceeding the threshold will be cached.",
|
||||||
"settings.context_count": "Context",
|
"settings.context_count": "Context",
|
||||||
"settings.context_count.tip": "The number of previous messages to keep in the context.",
|
"settings.context_count.tip": "The number of previous messages to keep in the context.",
|
||||||
"settings.max": "Max",
|
"settings.max": "Max",
|
||||||
|
|||||||
@ -159,6 +159,14 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "コードブロック折り畳み",
|
"settings.code_collapsible": "コードブロック折り畳み",
|
||||||
"settings.code_wrappable": "コードブロック折り返し",
|
"settings.code_wrappable": "コードブロック折り返し",
|
||||||
|
"settings.code_cacheable": "コードブロックキャッシュ",
|
||||||
|
"settings.code_cacheable.tip": "コードブロックのキャッシュは長いコードブロックのレンダリング時間を短縮できますが、メモリ使用量が増加します",
|
||||||
|
"settings.code_cache_max_size": "キャッシュ上限",
|
||||||
|
"settings.code_cache_max_size.tip": "キャッシュできる文字数の上限(千字符)。ハイライトされたコードの長さは純粋なテキストよりもはるかに長くなります。",
|
||||||
|
"settings.code_cache_ttl": "キャッシュ期限",
|
||||||
|
"settings.code_cache_ttl.tip": "キャッシュの有効期限(分単位)。",
|
||||||
|
"settings.code_cache_threshold": "キャッシュ閾値",
|
||||||
|
"settings.code_cache_threshold.tip": "キャッシュできる最小のコード長(千字符)。キャッシュできる最小のコード長を超えたコードブロックのみがキャッシュされます。",
|
||||||
"settings.context_count": "コンテキスト",
|
"settings.context_count": "コンテキスト",
|
||||||
"settings.context_count.tip": "コンテキストに保持する以前のメッセージの数",
|
"settings.context_count.tip": "コンテキストに保持する以前のメッセージの数",
|
||||||
"settings.max": "最大",
|
"settings.max": "最大",
|
||||||
|
|||||||
@ -159,6 +159,14 @@
|
|||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"settings.code_collapsible": "Блок кода свернут",
|
"settings.code_collapsible": "Блок кода свернут",
|
||||||
"settings.code_wrappable": "Блок кода можно переносить",
|
"settings.code_wrappable": "Блок кода можно переносить",
|
||||||
|
"settings.code_cacheable": "Кэш блока кода",
|
||||||
|
"settings.code_cacheable.tip": "Кэширование блока кода может уменьшить время рендеринга длинных блоков кода, но увеличит использование памяти",
|
||||||
|
"settings.code_cache_max_size": "Максимальный размер кэша",
|
||||||
|
"settings.code_cache_max_size.tip": "Максимальное количество символов, которое может быть кэшировано (тысяч символов), рассчитывается по кэшированному коду. Длина кэшированного кода значительно превышает длину чистого текста.",
|
||||||
|
"settings.code_cache_ttl": "Время жизни кэша",
|
||||||
|
"settings.code_cache_ttl.tip": "Время жизни кэша (минуты)",
|
||||||
|
"settings.code_cache_threshold": "Пороговое значение кэша",
|
||||||
|
"settings.code_cache_threshold.tip": "Минимальное количество символов для кэширования (тысяч символов), рассчитывается по фактическому коду. Будут кэшированы только те блоки кода, которые превышают пороговое значение",
|
||||||
"settings.context_count": "Контекст",
|
"settings.context_count": "Контекст",
|
||||||
"settings.context_count.tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте.",
|
"settings.context_count.tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте.",
|
||||||
"settings.max": "Максимум",
|
"settings.max": "Максимум",
|
||||||
|
|||||||
@ -161,6 +161,14 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "代码块可折叠",
|
"settings.code_collapsible": "代码块可折叠",
|
||||||
"settings.code_wrappable": "代码块可换行",
|
"settings.code_wrappable": "代码块可换行",
|
||||||
|
"settings.code_cacheable": "代码块缓存",
|
||||||
|
"settings.code_cacheable.tip": "缓存代码块可以减少长代码块的渲染时间,但会增加内存占用",
|
||||||
|
"settings.code_cache_max_size": "缓存上限",
|
||||||
|
"settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多。",
|
||||||
|
"settings.code_cache_ttl": "缓存期限",
|
||||||
|
"settings.code_cache_ttl.tip": "缓存过期时间(分钟)",
|
||||||
|
"settings.code_cache_threshold": "缓存阈值",
|
||||||
|
"settings.code_cache_threshold.tip": "允许缓存的最小代码长度(千字符),超过阈值的代码块才会被缓存",
|
||||||
"settings.context_count": "上下文数",
|
"settings.context_count": "上下文数",
|
||||||
"settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10",
|
"settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10",
|
||||||
"settings.max": "不限",
|
"settings.max": "不限",
|
||||||
|
|||||||
@ -159,6 +159,14 @@
|
|||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"settings.code_collapsible": "程式碼區塊可折疊",
|
"settings.code_collapsible": "程式碼區塊可折疊",
|
||||||
"settings.code_wrappable": "程式碼區塊可自動換行",
|
"settings.code_wrappable": "程式碼區塊可自動換行",
|
||||||
|
"settings.code_cacheable": "程式碼區塊快取",
|
||||||
|
"settings.code_cacheable.tip": "快取程式碼區塊可以減少長程式碼區塊的渲染時間,但會增加記憶體使用量",
|
||||||
|
"settings.code_cache_max_size": "快取上限",
|
||||||
|
"settings.code_cache_max_size.tip": "允許快取的字元數上限(千字符),按照高亮後的程式碼計算。高亮後的程式碼長度相比純文字會長很多。",
|
||||||
|
"settings.code_cache_ttl": "快取期限",
|
||||||
|
"settings.code_cache_ttl.tip": "快取的存活時間(分鐘)",
|
||||||
|
"settings.code_cache_threshold": "快取門檻",
|
||||||
|
"settings.code_cache_threshold.tip": "允許快取的最小程式碼長度(千字符),超過門檻的程式碼區塊才會被快取",
|
||||||
"settings.context_count": "上下文",
|
"settings.context_count": "上下文",
|
||||||
"settings.context_count.tip": "在上下文中保留的前幾則訊息。",
|
"settings.context_count.tip": "在上下文中保留的前幾則訊息。",
|
||||||
"settings.max": "最大",
|
"settings.max": "最大",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvide
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import React, { memo, useEffect, useRef, useState } from 'react'
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -32,6 +32,8 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
|
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
|
||||||
const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false)
|
const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false)
|
||||||
const codeContentRef = useRef<HTMLDivElement>(null)
|
const codeContentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const childrenLengthRef = useRef(0)
|
||||||
|
const isStreamingRef = useRef(false)
|
||||||
|
|
||||||
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
|
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
|
||||||
|
|
||||||
@ -39,39 +41,69 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
|
|
||||||
const shouldShowExpandButtonRef = useRef(false)
|
const shouldShowExpandButtonRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const shouldHighlight = useCallback((lang: string) => {
|
||||||
const loadHighlightedCode = async () => {
|
const NON_HIGHLIGHT_LANGS = ['mermaid', 'plantuml', 'svg']
|
||||||
const highlightedHtml = await codeToHtml(children, language)
|
return !NON_HIGHLIGHT_LANGS.includes(lang)
|
||||||
if (codeContentRef.current) {
|
}, [])
|
||||||
codeContentRef.current.innerHTML = highlightedHtml
|
|
||||||
const isShowExpandButton = codeContentRef.current.scrollHeight > 350
|
const highlightCode = useCallback(async () => {
|
||||||
if (shouldShowExpandButtonRef.current === isShowExpandButton) return
|
if (!codeContentRef.current) return
|
||||||
shouldShowExpandButtonRef.current = isShowExpandButton
|
const codeElement = codeContentRef.current
|
||||||
setShouldShowExpandButton(shouldShowExpandButtonRef.current)
|
|
||||||
}
|
// 只在非流式输出状态才尝试启用cache
|
||||||
}
|
const highlightedHtml = await codeToHtml(children, language, !isStreamingRef.current)
|
||||||
loadHighlightedCode()
|
|
||||||
}, [children, language, codeToHtml])
|
codeElement.innerHTML = highlightedHtml
|
||||||
|
codeElement.style.opacity = '1'
|
||||||
|
|
||||||
|
const isShowExpandButton = codeElement.scrollHeight > 350
|
||||||
|
if (shouldShowExpandButtonRef.current === isShowExpandButton) return
|
||||||
|
shouldShowExpandButtonRef.current = isShowExpandButton
|
||||||
|
setShouldShowExpandButton(shouldShowExpandButtonRef.current)
|
||||||
|
}, [language, codeToHtml, children])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!codeCollapsible) {
|
// 跳过非文本代码块
|
||||||
setIsExpanded(true)
|
if (!codeContentRef.current || !shouldHighlight(language)) return
|
||||||
setShouldShowExpandButton(false)
|
|
||||||
|
let isMounted = true
|
||||||
|
const codeElement = codeContentRef.current
|
||||||
|
|
||||||
|
if (childrenLengthRef.current > 0 && childrenLengthRef.current !== children?.length) {
|
||||||
|
isStreamingRef.current = true
|
||||||
} else {
|
} else {
|
||||||
setIsExpanded(!codeCollapsible)
|
isStreamingRef.current = false
|
||||||
if (codeContentRef.current) {
|
codeElement.style.opacity = '0.1'
|
||||||
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (childrenLengthRef.current === 0) {
|
||||||
|
// 挂载时显示原始代码
|
||||||
|
codeElement.textContent = children
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(async (entries) => {
|
||||||
|
if (entries[0].isIntersecting && isMounted) {
|
||||||
|
setTimeout(highlightCode, 0)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(codeElement)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
childrenLengthRef.current = children?.length
|
||||||
|
isMounted = false
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [children, highlightCode, language, shouldHighlight])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsExpanded(!codeCollapsible)
|
||||||
|
setShouldShowExpandButton(codeCollapsible && (codeContentRef.current?.scrollHeight ?? 0) > 350)
|
||||||
}, [codeCollapsible])
|
}, [codeCollapsible])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!codeWrappable) {
|
setIsUnwrapped(!codeWrappable)
|
||||||
// 如果未启动代码块换行功能
|
|
||||||
setIsUnwrapped(true)
|
|
||||||
} else {
|
|
||||||
setIsUnwrapped(!codeWrappable) // 被换行
|
|
||||||
}
|
|
||||||
}, [codeWrappable])
|
}, [codeWrappable])
|
||||||
|
|
||||||
if (language === 'mermaid') {
|
if (language === 'mermaid') {
|
||||||
@ -227,6 +259,7 @@ const CodeBlockWrapper = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const CodeContent = styled.div<{ isShowLineNumbers: boolean; isUnwrapped: boolean; isCodeWrappable: boolean }>`
|
const CodeContent = styled.div<{ isShowLineNumbers: boolean; isUnwrapped: boolean; isCodeWrappable: boolean }>`
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
.shiki {
|
.shiki {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,10 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import {
|
import {
|
||||||
SendMessageShortcut,
|
SendMessageShortcut,
|
||||||
setAutoTranslateWithSpace,
|
setAutoTranslateWithSpace,
|
||||||
|
setCodeCacheable,
|
||||||
|
setCodeCacheMaxSize,
|
||||||
|
setCodeCacheThreshold,
|
||||||
|
setCodeCacheTTL,
|
||||||
setCodeCollapsible,
|
setCodeCollapsible,
|
||||||
setCodeShowLineNumbers,
|
setCodeShowLineNumbers,
|
||||||
setCodeStyle,
|
setCodeStyle,
|
||||||
@ -75,6 +79,10 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
codeShowLineNumbers,
|
codeShowLineNumbers,
|
||||||
codeCollapsible,
|
codeCollapsible,
|
||||||
codeWrappable,
|
codeWrappable,
|
||||||
|
codeCacheable,
|
||||||
|
codeCacheMaxSize,
|
||||||
|
codeCacheTTL,
|
||||||
|
codeCacheThreshold,
|
||||||
mathEngine,
|
mathEngine,
|
||||||
autoTranslateWithSpace,
|
autoTranslateWithSpace,
|
||||||
pasteLongTextThreshold,
|
pasteLongTextThreshold,
|
||||||
@ -331,6 +339,74 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
|
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_cacheable')}{' '}
|
||||||
|
<Tooltip title={t('chat.settings.code_cacheable.tip')}>
|
||||||
|
<QuestionIcon style={{ marginLeft: 4 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<Switch size="small" checked={codeCacheable} onChange={(checked) => dispatch(setCodeCacheable(checked))} />
|
||||||
|
</SettingRow>
|
||||||
|
{codeCacheable && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_cache_max_size')}
|
||||||
|
<Tooltip title={t('chat.settings.code_cache_max_size.tip')}>
|
||||||
|
<QuestionIcon style={{ marginLeft: 4 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={1000}
|
||||||
|
max={10000}
|
||||||
|
step={1000}
|
||||||
|
value={codeCacheMaxSize}
|
||||||
|
onChange={(value) => dispatch(setCodeCacheMaxSize(value ?? 1000))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_cache_ttl')}
|
||||||
|
<Tooltip title={t('chat.settings.code_cache_ttl.tip')}>
|
||||||
|
<QuestionIcon style={{ marginLeft: 4 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={15}
|
||||||
|
max={720}
|
||||||
|
step={15}
|
||||||
|
value={codeCacheTTL}
|
||||||
|
onChange={(value) => dispatch(setCodeCacheTTL(value ?? 15))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_cache_threshold')}
|
||||||
|
<Tooltip title={t('chat.settings.code_cache_threshold.tip')}>
|
||||||
|
<QuestionIcon style={{ marginLeft: 4 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={0}
|
||||||
|
max={50}
|
||||||
|
step={1}
|
||||||
|
value={codeCacheThreshold}
|
||||||
|
onChange={(value) => dispatch(setCodeCacheThreshold(value ?? 2))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>
|
<SettingRowTitleSmall>
|
||||||
{t('chat.settings.thought_auto_collapse')}
|
{t('chat.settings.thought_auto_collapse')}
|
||||||
|
|||||||
218
src/renderer/src/services/CodeCacheService.ts
Normal file
218
src/renderer/src/services/CodeCacheService.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import store from '@renderer/store'
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNV-1a哈希函数,用于计算字符串哈希值
|
||||||
|
* @param input 输入字符串
|
||||||
|
* @param maxInputLength 最大计算长度,默认50000字符
|
||||||
|
* @returns 哈希值的36进制字符串表示
|
||||||
|
*/
|
||||||
|
const fastHash = (input: string, maxInputLength: number = 50000) => {
|
||||||
|
let hash = 2166136261 // FNV偏移基数
|
||||||
|
const count = Math.min(input.length, maxInputLength)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
hash ^= input.charCodeAt(i)
|
||||||
|
hash *= 16777619 // FNV素数
|
||||||
|
hash >>>= 0 // 保持为32位无符号整数
|
||||||
|
}
|
||||||
|
return hash.toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增强的哈希函数,对长内容使用三段采样计算哈希
|
||||||
|
* @param input 输入字符串
|
||||||
|
* @returns 哈希值或组合哈希值
|
||||||
|
*/
|
||||||
|
const enhancedHash = (input: string) => {
|
||||||
|
const THRESHOLD = 50000
|
||||||
|
|
||||||
|
if (input.length <= THRESHOLD) {
|
||||||
|
return fastHash(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mid = Math.floor(input.length / 2)
|
||||||
|
|
||||||
|
// 三段hash保证唯一性
|
||||||
|
const frontSection = input.slice(0, 10000)
|
||||||
|
const midSection = input.slice(mid - 15000, mid + 15000)
|
||||||
|
const endSection = input.slice(-10000)
|
||||||
|
|
||||||
|
return `${fastHash(frontSection)}-${fastHash(midSection)}-${fastHash(endSection)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高亮结果缓存实例
|
||||||
|
let highlightCache: LRUCache<string, string> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存设置是否发生变化
|
||||||
|
*/
|
||||||
|
const haveSettingsChanged = (prev: any, current: any) => {
|
||||||
|
if (!prev || !current) return true
|
||||||
|
|
||||||
|
return (
|
||||||
|
prev.codeCacheable !== current.codeCacheable ||
|
||||||
|
prev.codeCacheMaxSize !== current.codeCacheMaxSize ||
|
||||||
|
prev.codeCacheTTL !== current.codeCacheTTL ||
|
||||||
|
prev.codeCacheThreshold !== current.codeCacheThreshold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码缓存服务
|
||||||
|
* 提供代码高亮结果的缓存管理和哈希计算功能
|
||||||
|
*/
|
||||||
|
export const CodeCacheService = {
|
||||||
|
/**
|
||||||
|
* 缓存上次使用的配置
|
||||||
|
*/
|
||||||
|
_lastConfig: {
|
||||||
|
codeCacheable: false,
|
||||||
|
codeCacheMaxSize: 0,
|
||||||
|
codeCacheTTL: 0,
|
||||||
|
codeCacheThreshold: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前缓存配置
|
||||||
|
* @returns 当前配置对象
|
||||||
|
*/
|
||||||
|
getConfig() {
|
||||||
|
try {
|
||||||
|
if (!store || !store.getState) return this._lastConfig
|
||||||
|
|
||||||
|
const { codeCacheable, codeCacheMaxSize, codeCacheTTL, codeCacheThreshold } = store.getState().settings
|
||||||
|
|
||||||
|
return { codeCacheable, codeCacheMaxSize, codeCacheTTL, codeCacheThreshold }
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[CodeCacheService] Failed to get config', error)
|
||||||
|
return this._lastConfig
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并确保缓存配置是最新的
|
||||||
|
* 每次缓存操作前调用
|
||||||
|
* @returns 当前缓存实例或null
|
||||||
|
*/
|
||||||
|
ensureCache() {
|
||||||
|
const currentConfig = this.getConfig()
|
||||||
|
|
||||||
|
// 检查配置是否变化
|
||||||
|
if (haveSettingsChanged(this._lastConfig, currentConfig)) {
|
||||||
|
this._lastConfig = currentConfig
|
||||||
|
this._updateCacheInstance(currentConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlightCache
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新缓存实例
|
||||||
|
* @param config 缓存配置
|
||||||
|
*/
|
||||||
|
_updateCacheInstance(config: any) {
|
||||||
|
try {
|
||||||
|
const { codeCacheable, codeCacheMaxSize, codeCacheTTL } = config
|
||||||
|
const newMaxSize = codeCacheMaxSize * 1000
|
||||||
|
const newTTLMilliseconds = codeCacheTTL * 60 * 1000
|
||||||
|
|
||||||
|
// 根据配置决定是否创建或清除缓存
|
||||||
|
if (codeCacheable) {
|
||||||
|
if (!highlightCache) {
|
||||||
|
// 缓存不存在,创建新缓存
|
||||||
|
highlightCache = new LRUCache<string, string>({
|
||||||
|
max: 200, // 最大缓存条目数
|
||||||
|
maxSize: newMaxSize, // 最大缓存大小
|
||||||
|
sizeCalculation: (value) => value.length, // 缓存大小计算
|
||||||
|
ttl: newTTLMilliseconds // 缓存过期时间(毫秒)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从当前缓存获取配置信息
|
||||||
|
const maxSize = highlightCache.max || 0
|
||||||
|
const ttl = highlightCache.ttl || 0
|
||||||
|
|
||||||
|
// 检查实际配置是否变化
|
||||||
|
if (maxSize !== newMaxSize || ttl !== newTTLMilliseconds) {
|
||||||
|
console.log('[CodeCacheService] Cache config changed, recreating cache')
|
||||||
|
highlightCache.clear()
|
||||||
|
highlightCache = new LRUCache<string, string>({
|
||||||
|
max: 500,
|
||||||
|
maxSize: newMaxSize,
|
||||||
|
sizeCalculation: (value) => value.length,
|
||||||
|
ttl: newTTLMilliseconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (highlightCache) {
|
||||||
|
// 缓存被禁用,清理资源
|
||||||
|
highlightCache.clear()
|
||||||
|
highlightCache = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[CodeCacheService] Failed to update cache config', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成缓存键
|
||||||
|
* @param code 代码内容
|
||||||
|
* @param language 代码语言
|
||||||
|
* @param theme 高亮主题
|
||||||
|
* @returns 缓存键
|
||||||
|
*/
|
||||||
|
generateCacheKey: (code: string, language: string, theme: string) => {
|
||||||
|
return `${language}|${theme}|${code.length}|${enhancedHash(code)}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的高亮结果
|
||||||
|
* @param key 缓存键
|
||||||
|
* @returns 缓存的HTML或null
|
||||||
|
*/
|
||||||
|
getCachedResult: (key: string) => {
|
||||||
|
try {
|
||||||
|
// 确保缓存配置是最新的
|
||||||
|
CodeCacheService.ensureCache()
|
||||||
|
|
||||||
|
if (!store || !store.getState) return null
|
||||||
|
const { codeCacheable } = store.getState().settings
|
||||||
|
if (!codeCacheable) return null
|
||||||
|
|
||||||
|
return highlightCache?.get(key) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[CodeCacheService] Failed to get cached result', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存结果
|
||||||
|
* @param key 缓存键
|
||||||
|
* @param html 高亮HTML
|
||||||
|
* @param codeLength 代码长度
|
||||||
|
*/
|
||||||
|
setCachedResult: (key: string, html: string, codeLength: number) => {
|
||||||
|
try {
|
||||||
|
// 确保缓存配置是最新的
|
||||||
|
CodeCacheService.ensureCache()
|
||||||
|
|
||||||
|
if (!store || !store.getState) return
|
||||||
|
const { codeCacheable, codeCacheThreshold } = store.getState().settings
|
||||||
|
|
||||||
|
// 判断是否可以缓存
|
||||||
|
if (!codeCacheable || codeLength < codeCacheThreshold * 1000) return
|
||||||
|
|
||||||
|
highlightCache?.set(key, html)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[CodeCacheService] Failed to set cached result', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空缓存
|
||||||
|
*/
|
||||||
|
clear: () => {
|
||||||
|
highlightCache?.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 89,
|
version: 91,
|
||||||
blacklist: ['runtime', 'messages'],
|
blacklist: ['runtime', 'messages'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1186,6 +1186,17 @@ const migrateConfig = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'91': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
state.settings.codeCacheable = false
|
||||||
|
state.settings.codeCacheMaxSize = 1000
|
||||||
|
state.settings.codeCacheTTL = 15
|
||||||
|
state.settings.codeCacheThreshold = 2
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,11 @@ export interface SettingsState {
|
|||||||
codeShowLineNumbers: boolean
|
codeShowLineNumbers: boolean
|
||||||
codeCollapsible: boolean
|
codeCollapsible: boolean
|
||||||
codeWrappable: boolean
|
codeWrappable: boolean
|
||||||
|
// 代码块缓存
|
||||||
|
codeCacheable: boolean
|
||||||
|
codeCacheMaxSize: number
|
||||||
|
codeCacheTTL: number
|
||||||
|
codeCacheThreshold: number
|
||||||
mathEngine: 'MathJax' | 'KaTeX'
|
mathEngine: 'MathJax' | 'KaTeX'
|
||||||
messageStyle: 'plain' | 'bubble'
|
messageStyle: 'plain' | 'bubble'
|
||||||
codeStyle: CodeStyleVarious
|
codeStyle: CodeStyleVarious
|
||||||
@ -139,6 +144,10 @@ const initialState: SettingsState = {
|
|||||||
codeShowLineNumbers: false,
|
codeShowLineNumbers: false,
|
||||||
codeCollapsible: false,
|
codeCollapsible: false,
|
||||||
codeWrappable: false,
|
codeWrappable: false,
|
||||||
|
codeCacheable: false,
|
||||||
|
codeCacheMaxSize: 1000, // 缓存最大容量,千字符数
|
||||||
|
codeCacheTTL: 15, // 缓存过期时间,分钟
|
||||||
|
codeCacheThreshold: 2, // 允许缓存的最小代码长度,千字符数
|
||||||
mathEngine: 'KaTeX',
|
mathEngine: 'KaTeX',
|
||||||
messageStyle: 'plain',
|
messageStyle: 'plain',
|
||||||
codeStyle: 'auto',
|
codeStyle: 'auto',
|
||||||
@ -303,6 +312,18 @@ const settingsSlice = createSlice({
|
|||||||
setCodeWrappable: (state, action: PayloadAction<boolean>) => {
|
setCodeWrappable: (state, action: PayloadAction<boolean>) => {
|
||||||
state.codeWrappable = action.payload
|
state.codeWrappable = action.payload
|
||||||
},
|
},
|
||||||
|
setCodeCacheable: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.codeCacheable = action.payload
|
||||||
|
},
|
||||||
|
setCodeCacheMaxSize: (state, action: PayloadAction<number>) => {
|
||||||
|
state.codeCacheMaxSize = action.payload
|
||||||
|
},
|
||||||
|
setCodeCacheTTL: (state, action: PayloadAction<number>) => {
|
||||||
|
state.codeCacheTTL = action.payload
|
||||||
|
},
|
||||||
|
setCodeCacheThreshold: (state, action: PayloadAction<number>) => {
|
||||||
|
state.codeCacheThreshold = action.payload
|
||||||
|
},
|
||||||
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
||||||
state.mathEngine = action.payload
|
state.mathEngine = action.payload
|
||||||
},
|
},
|
||||||
@ -471,6 +492,10 @@ export const {
|
|||||||
setCodeShowLineNumbers,
|
setCodeShowLineNumbers,
|
||||||
setCodeCollapsible,
|
setCodeCollapsible,
|
||||||
setCodeWrappable,
|
setCodeWrappable,
|
||||||
|
setCodeCacheable,
|
||||||
|
setCodeCacheMaxSize,
|
||||||
|
setCodeCacheTTL,
|
||||||
|
setCodeCacheThreshold,
|
||||||
setMathEngine,
|
setMathEngine,
|
||||||
setFoldDisplayMode,
|
setFoldDisplayMode,
|
||||||
setGridColumns,
|
setGridColumns,
|
||||||
|
|||||||
154
yarn.lock
154
yarn.lock
@ -3079,70 +3079,68 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/core@npm:1.29.2":
|
"@shikijs/core@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/core@npm:1.29.2"
|
resolution: "@shikijs/core@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/engine-javascript": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
"@shikijs/engine-oniguruma": "npm:1.29.2"
|
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||||
"@shikijs/types": "npm:1.29.2"
|
|
||||||
"@shikijs/vscode-textmate": "npm:^10.0.1"
|
|
||||||
"@types/hast": "npm:^3.0.4"
|
"@types/hast": "npm:^3.0.4"
|
||||||
hast-util-to-html: "npm:^9.0.4"
|
hast-util-to-html: "npm:^9.0.5"
|
||||||
checksum: 10c0/b1bb0567babcee64608224d652ceb4076d387b409fb8ee767f7684c68f03cfaab0e17f42d0a3372fc7be1fe165af9a3a349efc188f6e7c720d4df1108c1ab78c
|
checksum: 10c0/d9d1d5587e40ab15f343dfac4c1f109f6a4b9b97640ec22d5c831917a3932958cbcfce36e90ff92fffe9f4d286f902c0d16d0ad2ebdcfbbaa808a1fb87b4f680
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/engine-javascript@npm:1.29.2":
|
"@shikijs/engine-javascript@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/engine-javascript@npm:1.29.2"
|
resolution: "@shikijs/engine-javascript@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/types": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
"@shikijs/vscode-textmate": "npm:^10.0.1"
|
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||||
oniguruma-to-es: "npm:^2.2.0"
|
oniguruma-to-es: "npm:^4.1.0"
|
||||||
checksum: 10c0/b61f9e9079493c19419ff64af6454c4360a32785d47f49b41e87752e66ddbf7466dd9cce67f4d5d4a8447e31d96b4f0a39330e9f26e8bd2bc2f076644e78dff7
|
checksum: 10c0/7b629bdbc54c51d198628a6c2dcf85db80259b8ebc70b622517837990d75d91a6240cc51cce36f7dc8a0776d0445b5b02a5b23726d9cf4e084712820528cb45c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/engine-oniguruma@npm:1.29.2":
|
"@shikijs/engine-oniguruma@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/engine-oniguruma@npm:1.29.2"
|
resolution: "@shikijs/engine-oniguruma@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/types": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
"@shikijs/vscode-textmate": "npm:^10.0.1"
|
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||||
checksum: 10c0/87d77e05af7fe862df40899a7034cbbd48d3635e27706873025e5035be578584d012f850208e97ca484d5e876bf802d4e23d0394d25026adb678eeb1d1f340ff
|
checksum: 10c0/8c4c51738740f9cfa610ccefaaea2378833820336e4329bb88b9a2208e3deb994b0b7bea2d6657eb915fb668ca2090a2168a84dfeac2b820c1fee00631ca9bed
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/langs@npm:1.29.2":
|
"@shikijs/langs@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/langs@npm:1.29.2"
|
resolution: "@shikijs/langs@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/types": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
checksum: 10c0/137af52ec19ab10bb167ec67e2dc6888d77dedddb3be37708569cb8e8d54c057d09df335261276012d11ac38366ba57b9eae121cc0b7045859638c25648b0563
|
checksum: 10c0/8a4e8c066795f1e96686bee271ad9783febcb1cece2ebb9815dfb3d59c856ac869cf9dddc7d90cbcd186a782ddc0628b37486fcc4a46516be6825907f0e74178
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/themes@npm:1.29.2":
|
"@shikijs/themes@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/themes@npm:1.29.2"
|
resolution: "@shikijs/themes@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/types": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
checksum: 10c0/1f7d3fc8615890d83b50c73c13e5182438dee579dd9a121d605bbdcc2dc877cafc9f7e23a3e1342345cd0b9161e3af6425b0fbfac949843f22b2a60527a8fb69
|
checksum: 10c0/674aae42244832142f584037504ab102dc141f9918f5b11b62aa0dc1abb6a763daf74f86124ae5f2362116dd095b5fc62c9a249aa8c14fdae847e5b8b955b11b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/types@npm:1.29.2":
|
"@shikijs/types@npm:3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "@shikijs/types@npm:1.29.2"
|
resolution: "@shikijs/types@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/vscode-textmate": "npm:^10.0.1"
|
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||||
"@types/hast": "npm:^3.0.4"
|
"@types/hast": "npm:^3.0.4"
|
||||||
checksum: 10c0/37b4ac315effc03e7185aca1da0c2631ac55bdf613897476bd1d879105c41f86ccce6ebd0b78779513d88cc2ee371039f7efd95d604f77f21f180791978822b3
|
checksum: 10c0/3380fde198d466a8771137b7ca3d4756a54d7d24c6e65f852737472a280c12c07f2123b9ad3d7eb2edec86d8d2c53bc207abe0fc0c7f78d337e52e742dc34edf
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@shikijs/vscode-textmate@npm:^10.0.1":
|
"@shikijs/vscode-textmate@npm:^10.0.2":
|
||||||
version: 10.0.2
|
version: 10.0.2
|
||||||
resolution: "@shikijs/vscode-textmate@npm:10.0.2"
|
resolution: "@shikijs/vscode-textmate@npm:10.0.2"
|
||||||
checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462
|
checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462
|
||||||
@ -3494,6 +3492,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/lru-cache@npm:^7.10.10":
|
||||||
|
version: 7.10.10
|
||||||
|
resolution: "@types/lru-cache@npm:7.10.10"
|
||||||
|
dependencies:
|
||||||
|
lru-cache: "npm:*"
|
||||||
|
checksum: 10c0/ab85558867cb059bebd42074c1cd517eb41efb1db22b9d26dfdc58df01c83ff9c212a562b4ec3d5936418ffb03e626a0f30463026aa5fb5ced41e3b4b4af057f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/markdown-it@npm:^14":
|
"@types/markdown-it@npm:^14":
|
||||||
version: 14.1.2
|
version: 14.1.2
|
||||||
resolution: "@types/markdown-it@npm:14.1.2"
|
resolution: "@types/markdown-it@npm:14.1.2"
|
||||||
@ -3942,6 +3949,7 @@ __metadata:
|
|||||||
"@types/diff": "npm:^7"
|
"@types/diff": "npm:^7"
|
||||||
"@types/fs-extra": "npm:^11"
|
"@types/fs-extra": "npm:^11"
|
||||||
"@types/lodash": "npm:^4.17.5"
|
"@types/lodash": "npm:^4.17.5"
|
||||||
|
"@types/lru-cache": "npm:^7.10.10"
|
||||||
"@types/markdown-it": "npm:^14"
|
"@types/markdown-it": "npm:^14"
|
||||||
"@types/md5": "npm:^2.3.5"
|
"@types/md5": "npm:^2.3.5"
|
||||||
"@types/node": "npm:^18.19.9"
|
"@types/node": "npm:^18.19.9"
|
||||||
@ -4020,7 +4028,7 @@ __metadata:
|
|||||||
remark-math: "npm:^6.0.0"
|
remark-math: "npm:^6.0.0"
|
||||||
rollup-plugin-visualizer: "npm:^5.12.0"
|
rollup-plugin-visualizer: "npm:^5.12.0"
|
||||||
sass: "npm:^1.77.2"
|
sass: "npm:^1.77.2"
|
||||||
shiki: "npm:^1.22.2"
|
shiki: "npm:^3.2.1"
|
||||||
string-width: "npm:^7.2.0"
|
string-width: "npm:^7.2.0"
|
||||||
styled-components: "npm:^6.1.11"
|
styled-components: "npm:^6.1.11"
|
||||||
tar: "npm:^7.4.3"
|
tar: "npm:^7.4.3"
|
||||||
@ -8599,7 +8607,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"hast-util-to-html@npm:^9.0.4":
|
"hast-util-to-html@npm:^9.0.5":
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
resolution: "hast-util-to-html@npm:9.0.5"
|
resolution: "hast-util-to-html@npm:9.0.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -10194,6 +10202,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lru-cache@npm:*":
|
||||||
|
version: 11.1.0
|
||||||
|
resolution: "lru-cache@npm:11.1.0"
|
||||||
|
checksum: 10c0/85c312f7113f65fae6a62de7985348649937eb34fb3d212811acbf6704dc322a421788aca253b62838f1f07049a84cc513d88f494e373d3756514ad263670a64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
|
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
|
||||||
version: 10.4.3
|
version: 10.4.3
|
||||||
resolution: "lru-cache@npm:10.4.3"
|
resolution: "lru-cache@npm:10.4.3"
|
||||||
@ -12126,14 +12141,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"oniguruma-to-es@npm:^2.2.0":
|
"oniguruma-parser@npm:^0.5.4":
|
||||||
version: 2.3.0
|
version: 0.5.4
|
||||||
resolution: "oniguruma-to-es@npm:2.3.0"
|
resolution: "oniguruma-parser@npm:0.5.4"
|
||||||
|
checksum: 10c0/934866661ab8ff379f7605121c96d0f22bede073c7fda5b7ae60da2eebdd1380dff5058118dd67df2cdacbdd275167f3e72cf87dc87882f8b7feda014785de00
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"oniguruma-to-es@npm:^4.1.0":
|
||||||
|
version: 4.1.0
|
||||||
|
resolution: "oniguruma-to-es@npm:4.1.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex-xs: "npm:^1.0.0"
|
emoji-regex-xs: "npm:^1.0.0"
|
||||||
regex: "npm:^5.1.1"
|
oniguruma-parser: "npm:^0.5.4"
|
||||||
regex-recursion: "npm:^5.1.1"
|
regex: "npm:^6.0.1"
|
||||||
checksum: 10c0/57ad95f3e9a50be75e7d54e582d8d4da4003f983fd04d99ccc9d17d2dc04e30ea64126782f2e758566bcef2c4c55db0d6a3d344f35ca179dd92ea5ca92fc0313
|
regex-recursion: "npm:^6.0.2"
|
||||||
|
checksum: 10c0/8f3fc7f524a7fa78cecc3a2af29d19a834563b25de4eb3ed138ffe062075dfedacd997d86951b38cc5c5d6b5c083df1f5a10a442b742df9399eaa6ea9aa68392
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -14071,13 +14094,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"regex-recursion@npm:^5.1.1":
|
"regex-recursion@npm:^6.0.2":
|
||||||
version: 5.1.1
|
version: 6.0.2
|
||||||
resolution: "regex-recursion@npm:5.1.1"
|
resolution: "regex-recursion@npm:6.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
regex: "npm:^5.1.1"
|
|
||||||
regex-utilities: "npm:^2.3.0"
|
regex-utilities: "npm:^2.3.0"
|
||||||
checksum: 10c0/c61c284bc41f2b271dfa0549d657a5a26397108b860d7cdb15b43080196681c0092bf8cf920a8836213e239d1195c4ccf6db9be9298bce4e68c9daab1febeab9
|
checksum: 10c0/68e8b6889680e904b75d7f26edaf70a1a4dc1087406bff53face4c2929d918fd77c72223843fe816ac8ed9964f96b4160650e8d5909e26a998c6e9de324dadb1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -14088,12 +14110,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"regex@npm:^5.1.1":
|
"regex@npm:^6.0.1":
|
||||||
version: 5.1.1
|
version: 6.0.1
|
||||||
resolution: "regex@npm:5.1.1"
|
resolution: "regex@npm:6.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
regex-utilities: "npm:^2.3.0"
|
regex-utilities: "npm:^2.3.0"
|
||||||
checksum: 10c0/314e032f0fe09497ce7a160b99675c4a16c7524f0a24833f567cbbf3a2bebc26bf59737dc5c23f32af7c74aa7a6bd3f809fc72c90c49a05faf8be45677db508a
|
checksum: 10c0/687b3e063d4ca19b0de7c55c24353f868a0fb9ba21512692470d2fb412e3a410894dd5924c91ea49d8cb8fa865e36ec956e52436ae0a256bdc095ff136c30aba
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -14814,19 +14836,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"shiki@npm:^1.22.2":
|
"shiki@npm:^3.2.1":
|
||||||
version: 1.29.2
|
version: 3.2.1
|
||||||
resolution: "shiki@npm:1.29.2"
|
resolution: "shiki@npm:3.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@shikijs/core": "npm:1.29.2"
|
"@shikijs/core": "npm:3.2.1"
|
||||||
"@shikijs/engine-javascript": "npm:1.29.2"
|
"@shikijs/engine-javascript": "npm:3.2.1"
|
||||||
"@shikijs/engine-oniguruma": "npm:1.29.2"
|
"@shikijs/engine-oniguruma": "npm:3.2.1"
|
||||||
"@shikijs/langs": "npm:1.29.2"
|
"@shikijs/langs": "npm:3.2.1"
|
||||||
"@shikijs/themes": "npm:1.29.2"
|
"@shikijs/themes": "npm:3.2.1"
|
||||||
"@shikijs/types": "npm:1.29.2"
|
"@shikijs/types": "npm:3.2.1"
|
||||||
"@shikijs/vscode-textmate": "npm:^10.0.1"
|
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||||
"@types/hast": "npm:^3.0.4"
|
"@types/hast": "npm:^3.0.4"
|
||||||
checksum: 10c0/9ef452021582c405501077082c4ae8d877027dca6488d2c7a1963ed661567f121b4cc5dea9dfab26689504b612b8a961f3767805cbeaaae3c1d6faa5e6f37eb0
|
checksum: 10c0/8153b5a354c508815c8a20c03bf182aa863d07e865bc603e136c633c60abb729655c5487109111ed8a3e5a4aff0275f3b714b05c5b129085c8ed69069537f0c0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user