mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-28 09:08:13 +00:00
feat(ui): add line wrap toggle to markdown code blocks (#599)
## Summary - add a wrap toggle beside the copy action in Markdown code block headers - default Markdown code blocks to wrapped lines for both plain and Shiki-highlighted output - preserve per-block wrap choices across async Markdown and syntax-highlight re-renders - add localized labels for the new Markdown code block wrap action ## Validation - npm run typecheck --workspace @codenomad/ui ## Notes - Left unrelated untracked .opencode/package-lock.json out of the commit.
This commit is contained in:
parent
709557d12d
commit
40cee071a3
13 changed files with 199 additions and 32 deletions
|
|
@ -15,6 +15,7 @@ interface ResolvedMarkdownSnapshot {
|
|||
themeKey: string
|
||||
highlightEnabled: boolean
|
||||
escapeRawHtml: boolean
|
||||
defaultCodeBlockWrap: boolean
|
||||
partId: string | undefined
|
||||
cacheId: string
|
||||
version: string
|
||||
|
|
@ -96,6 +97,7 @@ interface MarkdownProps {
|
|||
size?: "base" | "sm" | "tight"
|
||||
disableHighlight?: boolean
|
||||
escapeRawHtml?: boolean
|
||||
defaultCodeBlockWrap?: boolean
|
||||
onRendered?: () => void
|
||||
}
|
||||
|
||||
|
|
@ -105,11 +107,54 @@ export function Markdown(props: MarkdownProps) {
|
|||
let containerRef: HTMLDivElement | undefined
|
||||
let latestRequestKey = ""
|
||||
let cleanupLanguageListener: (() => void) | undefined
|
||||
const codeBlockWrapOverrides = new Map<string, boolean>()
|
||||
|
||||
const notifyRendered = () => {
|
||||
Promise.resolve().then(() => props.onRendered?.())
|
||||
}
|
||||
|
||||
const codeBlockWrapKey = (codeBlock: HTMLElement): string | null => {
|
||||
const key = codeBlock.getAttribute("data-code-block-key")
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
return `${resolved().cacheId}:${key}`
|
||||
}
|
||||
|
||||
const applyCodeBlockWrapState = (codeBlock: HTMLElement, enabled: boolean) => {
|
||||
codeBlock.setAttribute("data-wrap-lines", enabled ? "true" : "false")
|
||||
|
||||
const button = codeBlock.querySelector<HTMLButtonElement>(".code-block-wrap")
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
|
||||
const label = enabled ? t("markdown.codeBlock.wrap.disable") : t("markdown.codeBlock.wrap.enable")
|
||||
button.classList.toggle("active", enabled)
|
||||
button.setAttribute("aria-pressed", enabled ? "true" : "false")
|
||||
button.setAttribute("aria-label", label)
|
||||
button.setAttribute("title", label)
|
||||
|
||||
const text = button.querySelector(".wrap-text")
|
||||
if (text) {
|
||||
text.textContent = label
|
||||
}
|
||||
}
|
||||
|
||||
const syncCodeBlockWrapStates = () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const codeBlocks = containerRef.querySelectorAll<HTMLElement>(".markdown-code-block")
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const key = codeBlockWrapKey(codeBlock)
|
||||
const defaultEnabled = codeBlock.getAttribute("data-wrap-lines") !== "false"
|
||||
const enabled = key ? (codeBlockWrapOverrides.get(key) ?? defaultEnabled) : defaultEnabled
|
||||
applyCodeBlockWrapState(codeBlock, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = createMemo(() => {
|
||||
const part = props.part
|
||||
const rawText = typeof part.text === "string" ? part.text : ""
|
||||
|
|
@ -117,11 +162,23 @@ export function Markdown(props: MarkdownProps) {
|
|||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||
const highlightEnabled = !props.disableHighlight
|
||||
const escapeRawHtml = Boolean(props.escapeRawHtml)
|
||||
const defaultCodeBlockWrap = props.defaultCodeBlockWrap ?? true
|
||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
||||
const cacheId = resolvePartCacheId(part, text)
|
||||
const version = resolvePartVersion(part, text)
|
||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${version}`
|
||||
return { part, text, themeKey, highlightEnabled, escapeRawHtml, partId, cacheId, version, requestKey }
|
||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${defaultCodeBlockWrap ? 1 : 0}:${version}`
|
||||
return {
|
||||
part,
|
||||
text,
|
||||
themeKey,
|
||||
highlightEnabled,
|
||||
escapeRawHtml,
|
||||
defaultCodeBlockWrap,
|
||||
partId,
|
||||
cacheId,
|
||||
version,
|
||||
requestKey,
|
||||
}
|
||||
})
|
||||
|
||||
const cacheHandle = useGlobalCache({
|
||||
|
|
@ -129,8 +186,8 @@ export function Markdown(props: MarkdownProps) {
|
|||
sessionId: () => props.sessionId,
|
||||
scope: "markdown",
|
||||
cacheId: () => {
|
||||
const { cacheId, themeKey, highlightEnabled } = resolved()
|
||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${resolved().escapeRawHtml ? 1 : 0}`
|
||||
const { cacheId, themeKey, highlightEnabled, escapeRawHtml, defaultCodeBlockWrap } = resolved()
|
||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${defaultCodeBlockWrap ? 1 : 0}`
|
||||
},
|
||||
version: () => resolved().version,
|
||||
})
|
||||
|
|
@ -144,7 +201,7 @@ export function Markdown(props: MarkdownProps) {
|
|||
text: snapshot.text,
|
||||
html: renderedHtml,
|
||||
theme: snapshot.themeKey,
|
||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}:${snapshot.defaultCodeBlockWrap ? "wrap" : "nowrap"}`,
|
||||
}
|
||||
setHtml(renderedHtml)
|
||||
if (options?.cache ?? true) {
|
||||
|
|
@ -159,6 +216,7 @@ export function Markdown(props: MarkdownProps) {
|
|||
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
||||
suppressHighlight: !snapshot.highlightEnabled,
|
||||
escapeRawHtml: snapshot.escapeRawHtml,
|
||||
defaultCodeBlockWrap: snapshot.defaultCodeBlockWrap,
|
||||
})
|
||||
const shouldCache = !snapshot.highlightEnabled || !markdown.hasPendingCodeHighlight(snapshot.text)
|
||||
|
||||
|
|
@ -170,7 +228,7 @@ export function Markdown(props: MarkdownProps) {
|
|||
createEffect(() => {
|
||||
const snapshot = resolved()
|
||||
latestRequestKey = snapshot.requestKey
|
||||
const cacheMode = `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`
|
||||
const cacheMode = `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}:${snapshot.defaultCodeBlockWrap ? "wrap" : "nowrap"}`
|
||||
|
||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||
if (!cache) return false
|
||||
|
|
@ -202,9 +260,33 @@ export function Markdown(props: MarkdownProps) {
|
|||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
html()
|
||||
Promise.resolve().then(syncCodeBlockWrapStates)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const handleClick = async (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const wrapButton = target.closest(".code-block-wrap") as HTMLButtonElement
|
||||
if (wrapButton) {
|
||||
event.preventDefault()
|
||||
const codeBlock = wrapButton.closest(".markdown-code-block") as HTMLElement | null
|
||||
if (!codeBlock) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = codeBlockWrapKey(codeBlock)
|
||||
const current = codeBlock.getAttribute("data-wrap-lines") !== "false"
|
||||
const next = !current
|
||||
if (key) {
|
||||
codeBlockWrapOverrides.set(key, next)
|
||||
}
|
||||
applyCodeBlockWrapState(codeBlock, next)
|
||||
props.onRendered?.()
|
||||
return
|
||||
}
|
||||
|
||||
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
|
||||
|
||||
if (!copyButton) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ export function createMarkdownContentRenderer(params: {
|
|||
|
||||
const size = options.size || "default"
|
||||
const disableHighlight = options.disableHighlight || false
|
||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}${options.wrap ? " tool-call-markdown-wrap" : ""}`
|
||||
const wrapEnabled = options.wrap ?? true
|
||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}${wrapEnabled ? " tool-call-markdown-wrap" : ""}`
|
||||
const state = params.toolState()
|
||||
const disableScrollTracking = options.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending")
|
||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||
|
|
@ -73,6 +74,7 @@ export function createMarkdownContentRenderer(params: {
|
|||
sessionId={params.sessionId}
|
||||
isDark={params.isDark()}
|
||||
disableHighlight={disableHighlight}
|
||||
defaultCodeBlockWrap={wrapEnabled}
|
||||
onRendered={handleMarkdownRendered}
|
||||
/>
|
||||
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "Kopieren",
|
||||
"markdown.codeBlock.copy.copied": "Kopiert!",
|
||||
"markdown.codeBlock.copy.failed": "Fehlgeschlagen",
|
||||
"markdown.codeBlock.wrap.enable": "Zeilenumbruch aktivieren",
|
||||
"markdown.codeBlock.wrap.disable": "Zeilenumbruch deaktivieren",
|
||||
|
||||
"markdown.copy": "Kopieren",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "Copy",
|
||||
"markdown.codeBlock.copy.copied": "Copied!",
|
||||
"markdown.codeBlock.copy.failed": "Failed",
|
||||
"markdown.codeBlock.wrap.enable": "Enable word wrap",
|
||||
"markdown.codeBlock.wrap.disable": "Disable word wrap",
|
||||
|
||||
"markdown.copy": "Copy",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "Copiar",
|
||||
"markdown.codeBlock.copy.copied": "¡Copiado!",
|
||||
"markdown.codeBlock.copy.failed": "Error",
|
||||
"markdown.codeBlock.wrap.enable": "Activar ajuste de línea",
|
||||
"markdown.codeBlock.wrap.disable": "Desactivar ajuste de línea",
|
||||
|
||||
"markdown.copy": "Copiar",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "Copier",
|
||||
"markdown.codeBlock.copy.copied": "Copié !",
|
||||
"markdown.codeBlock.copy.failed": "Échec",
|
||||
"markdown.codeBlock.wrap.enable": "Activer le retour à la ligne",
|
||||
"markdown.codeBlock.wrap.disable": "Désactiver le retour à la ligne",
|
||||
|
||||
"markdown.copy": "Copier",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "העתק",
|
||||
"markdown.codeBlock.copy.copied": "הועתק!",
|
||||
"markdown.codeBlock.copy.failed": "נכשל",
|
||||
"markdown.codeBlock.wrap.enable": "הפעל גלישת שורות",
|
||||
"markdown.codeBlock.wrap.disable": "כבה גלישת שורות",
|
||||
|
||||
"markdown.copy": "העתק",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "コピー",
|
||||
"markdown.codeBlock.copy.copied": "コピーしました!",
|
||||
"markdown.codeBlock.copy.failed": "失敗",
|
||||
"markdown.codeBlock.wrap.enable": "折り返しを有効にする",
|
||||
"markdown.codeBlock.wrap.disable": "折り返しを無効にする",
|
||||
|
||||
"markdown.copy": "コピー",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "प्रतिलिपि गर्नुहोस्",
|
||||
"markdown.codeBlock.copy.copied": "प्रतिलिपि गरियो!",
|
||||
"markdown.codeBlock.copy.failed": "असफल",
|
||||
"markdown.codeBlock.wrap.enable": "लाइन र्याप सक्षम गर्नुहोस्",
|
||||
"markdown.codeBlock.wrap.disable": "लाइन र्याप असक्षम गर्नुहोस्",
|
||||
|
||||
"markdown.copy": "प्रतिलिपि गर्नुहोस्",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "Копировать",
|
||||
"markdown.codeBlock.copy.copied": "Скопировано!",
|
||||
"markdown.codeBlock.copy.failed": "Не удалось",
|
||||
"markdown.codeBlock.wrap.enable": "Включить перенос строк",
|
||||
"markdown.codeBlock.wrap.disable": "Отключить перенос строк",
|
||||
|
||||
"markdown.copy": "Копировать",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export const markdownMessages = {
|
|||
"markdown.codeBlock.copy.label": "复制",
|
||||
"markdown.codeBlock.copy.copied": "已复制!",
|
||||
"markdown.codeBlock.copy.failed": "失败",
|
||||
"markdown.codeBlock.wrap.enable": "启用自动换行",
|
||||
"markdown.codeBlock.wrap.disable": "禁用自动换行",
|
||||
|
||||
"markdown.copy": "复制",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ let currentTheme: "light" | "dark" = "light"
|
|||
let isInitialized = false
|
||||
let highlightSuppressed = false
|
||||
let escapeRawHtmlEnabled = false
|
||||
let defaultCodeBlockWrapEnabled = true
|
||||
let rendererSetup = false
|
||||
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
||||
const codeBlockRenderOccurrences = new Map<string, number>()
|
||||
|
||||
// Dollar-delimited math is handled by marked-katex-extension; bracket delimiters
|
||||
// use the small parser-native rules registered in setupRenderer.
|
||||
|
|
@ -178,6 +180,19 @@ function sanitizeRawHtmlFragment(html: string): string {
|
|||
return template.innerHTML
|
||||
}
|
||||
|
||||
function hashString(value: string): string {
|
||||
let hash = 2166136261
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
hash ^= value.charCodeAt(index)
|
||||
hash = Math.imul(hash, 16777619)
|
||||
}
|
||||
return (hash >>> 0).toString(16)
|
||||
}
|
||||
|
||||
function resetCodeBlockRenderState() {
|
||||
codeBlockRenderOccurrences.clear()
|
||||
}
|
||||
|
||||
// Track loaded languages and queue for on-demand loading
|
||||
const loadedLanguages = new Set<string>()
|
||||
const queuedLanguages = new Set<string>()
|
||||
|
|
@ -507,29 +522,50 @@ function setupRenderer(isDark: boolean) {
|
|||
|
||||
// Use "text" as default when no language is specified
|
||||
const resolvedLang = lang && lang.trim() ? lang.trim() : "text"
|
||||
const occurrenceBaseKey = `${resolvedLang}\u0000${decodedCode}`
|
||||
const occurrence = codeBlockRenderOccurrences.get(occurrenceBaseKey) ?? 0
|
||||
codeBlockRenderOccurrences.set(occurrenceBaseKey, occurrence + 1)
|
||||
const codeBlockKey = hashString(`${occurrenceBaseKey}\u0000${occurrence}`)
|
||||
const escapedLang = escapeHtml(resolvedLang)
|
||||
const copyLabel = escapeHtml(tGlobal("markdown.copy"))
|
||||
const defaultWrapEnabled = defaultCodeBlockWrapEnabled
|
||||
const wrapLabel = escapeHtml(tGlobal(defaultWrapEnabled ? "markdown.codeBlock.wrap.disable" : "markdown.codeBlock.wrap.enable"))
|
||||
const wrapActiveClass = defaultWrapEnabled ? " active" : ""
|
||||
const wrapPressed = defaultWrapEnabled ? "true" : "false"
|
||||
|
||||
const header = `
|
||||
<div class="code-block-header">
|
||||
<span class="code-block-language">${escapedLang}</span>
|
||||
<button class="code-block-copy" data-code="${encodedCode}">
|
||||
<svg class="copy-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span class="copy-text">${copyLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
`.trim()
|
||||
<span class="code-block-actions">
|
||||
<button type="button" class="code-block-wrap${wrapActiveClass}" data-code-block-key="${codeBlockKey}" aria-pressed="${wrapPressed}" aria-label="${wrapLabel}" title="${wrapLabel}">
|
||||
<svg class="wrap-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M3 12h15a3 3 0 1 1 0 6h-4"></path>
|
||||
<path d="m16 16-2 2 2 2"></path>
|
||||
<path d="M3 18h7"></path>
|
||||
</svg>
|
||||
<span class="wrap-text">${wrapLabel}</span>
|
||||
</button>
|
||||
<button type="button" class="code-block-copy" data-code="${encodedCode}">
|
||||
<svg class="copy-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span class="copy-text">${copyLabel}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`.trim()
|
||||
|
||||
const renderCodeBlock = (body: string) => `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}" data-code-block-key="${codeBlockKey}" data-wrap-lines="${defaultWrapEnabled ? "true" : "false"}">${header}${body}</div>`
|
||||
|
||||
if (highlightSuppressed) {
|
||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre></div>`
|
||||
return renderCodeBlock(`<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre>`)
|
||||
}
|
||||
|
||||
// Skip highlighting for "text" language or when highlighter is not available
|
||||
if (resolvedLang === "text" || !highlighter) {
|
||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}<pre><code>${escapeHtml(decodedCode)}</code></pre></div>`
|
||||
return renderCodeBlock(`<pre><code>${escapeHtml(decodedCode)}</code></pre>`)
|
||||
}
|
||||
|
||||
// Resolve language and check if it's loaded
|
||||
|
|
@ -538,23 +574,23 @@ function setupRenderer(isDark: boolean) {
|
|||
|
||||
// Skip highlighting for "text" aliases
|
||||
if (langKey === "text" || raw === "text") {
|
||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre></div>`
|
||||
return renderCodeBlock(`<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre>`)
|
||||
}
|
||||
|
||||
// Use highlighting if language is loaded, otherwise fall back to plain code
|
||||
if (loadedLanguages.has(langKey)) {
|
||||
try {
|
||||
const html = highlighter!.codeToHtml(decodedCode, {
|
||||
lang: langKey,
|
||||
theme: currentTheme === "dark" ? "github-dark" : "github-light-high-contrast",
|
||||
})
|
||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}${html}</div>`
|
||||
const html = highlighter!.codeToHtml(decodedCode, {
|
||||
lang: langKey,
|
||||
theme: currentTheme === "dark" ? "github-dark" : "github-light-high-contrast",
|
||||
})
|
||||
return renderCodeBlock(html)
|
||||
} catch {
|
||||
// Fall through to plain code if highlighting fails
|
||||
}
|
||||
}
|
||||
|
||||
return `<div class="markdown-code-block" data-language="${escapedLang}" data-code="${encodedCode}">${header}<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre></div>`
|
||||
return renderCodeBlock(`<pre><code class="language-${escapedLang}">${escapeHtml(decodedCode)}</code></pre>`)
|
||||
}
|
||||
|
||||
renderer.link = (href: string, title: string | null | undefined, text: string) => {
|
||||
|
|
@ -599,6 +635,7 @@ export async function renderMarkdown(
|
|||
options?: {
|
||||
suppressHighlight?: boolean
|
||||
escapeRawHtml?: boolean
|
||||
defaultCodeBlockWrap?: boolean
|
||||
},
|
||||
): Promise<string> {
|
||||
if (!isInitialized) {
|
||||
|
|
@ -608,6 +645,7 @@ export async function renderMarkdown(
|
|||
|
||||
const suppressHighlight = options?.suppressHighlight ?? false
|
||||
const escapeRawHtml = options?.escapeRawHtml ?? false
|
||||
const defaultCodeBlockWrap = options?.defaultCodeBlockWrap ?? true
|
||||
const decoded = decodeHtmlEntities(content)
|
||||
|
||||
if (!suppressHighlight) {
|
||||
|
|
@ -617,15 +655,20 @@ export async function renderMarkdown(
|
|||
|
||||
const previousSuppressed = highlightSuppressed
|
||||
const previousEscapeRawHtml = escapeRawHtmlEnabled
|
||||
const previousDefaultCodeBlockWrap = defaultCodeBlockWrapEnabled
|
||||
highlightSuppressed = suppressHighlight
|
||||
escapeRawHtmlEnabled = escapeRawHtml
|
||||
defaultCodeBlockWrapEnabled = defaultCodeBlockWrap
|
||||
|
||||
try {
|
||||
// Proceed to parse immediately - highlighting will be available on next render
|
||||
resetCodeBlockRenderState()
|
||||
return marked.parse(decoded) as Promise<string>
|
||||
} finally {
|
||||
resetCodeBlockRenderState()
|
||||
highlightSuppressed = previousSuppressed
|
||||
escapeRawHtmlEnabled = previousEscapeRawHtml
|
||||
defaultCodeBlockWrapEnabled = previousDefaultCodeBlockWrap
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -242,6 +242,15 @@
|
|||
border-bottom: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.code-block-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
flex: 0 0 auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: var(--space-sm);
|
||||
}
|
||||
|
||||
.code-block-language {
|
||||
font-family: var(--message-part-header-font-family);
|
||||
color: var(--text-primary);
|
||||
|
|
@ -252,7 +261,8 @@
|
|||
line-height: var(--message-part-header-title-line-height);
|
||||
}
|
||||
|
||||
.code-block-copy {
|
||||
.code-block-copy,
|
||||
.code-block-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -262,28 +272,30 @@
|
|||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
flex: 0 0 auto;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
color 150ms ease;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: var(--space-sm);
|
||||
}
|
||||
|
||||
.code-block-copy:hover {
|
||||
.code-block-copy:hover,
|
||||
.code-block-wrap:hover,
|
||||
.code-block-wrap.active {
|
||||
background-color: transparent;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.code-block-copy .copy-icon {
|
||||
.code-block-copy .copy-icon,
|
||||
.code-block-wrap .wrap-icon {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.code-block-copy .copy-text {
|
||||
.code-block-copy .copy-text,
|
||||
.code-block-wrap .wrap-text {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
|
@ -311,4 +323,14 @@
|
|||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-code-block[data-wrap-lines="true"] pre,
|
||||
.markdown-code-block[data-wrap-lines="true"] code,
|
||||
.markdown-code-block[data-wrap-lines="true"] pre.shiki,
|
||||
.markdown-code-block[data-wrap-lines="true"] pre.shiki code,
|
||||
.markdown-code-block[data-wrap-lines="true"] .shiki {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue