mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Add Mermaid diagram export actions (#9857)
* feat: add Mermaid diagram export actions * fix: stabilize Mermaid export pipeline * fix: format Mermaid image save handler * fix: limit Mermaid actions to copy and download * fix: polish Mermaid copy download controls * fix: simplify Mermaid action menus * fix: center Mermaid dropdown chevrons * refactor: use shared dropdowns for Mermaid actions
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support copying, previewing, and exporting rendered Mermaid diagrams.
|
||||
@@ -6,6 +6,7 @@ Chat Markdown renders fenced `mermaid` code blocks as diagrams after a response
|
||||
|
||||
- Valid `mermaid` fences render inline as SVG diagrams.
|
||||
- The original Mermaid source remains available through the existing code-block copy button.
|
||||
- Rendered diagrams include Copy and Download menus for Mermaid source, SVG, and PNG formats.
|
||||
- Invalid Mermaid syntax shows a contained error state and keeps the source visible.
|
||||
- Diagrams are not rendered while a message is streaming, which avoids repeated parse/render work on every token.
|
||||
- Diagram colors are derived from the active VS Code/Kilo CSS variables so light, dark, and high-contrast themes can render with matching backgrounds, text, borders, and link colors.
|
||||
@@ -13,4 +14,4 @@ Chat Markdown renders fenced `mermaid` code blocks as diagrams after a response
|
||||
## Limitations
|
||||
|
||||
- Mermaid is bundled by the current webview build, so bundle splitting remains a future optimization.
|
||||
- Advanced legacy actions are not restored yet: AI syntax fixing, PNG open/save, export, and zoom modal.
|
||||
- Advanced legacy actions are not restored yet: AI syntax fixing and zoom modal.
|
||||
|
||||
@@ -16,7 +16,13 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types
|
||||
import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController"
|
||||
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
|
||||
import { buildWebviewHtml, getWebviewFontSize } from "./utils"
|
||||
import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState, watchTelemetryState } from "./services/telemetry" // prettier-ignore
|
||||
import { saveImage } from "./kilo-provider/save-image"
|
||||
import {
|
||||
TelemetryProxy,
|
||||
type TelemetryPropertiesProvider,
|
||||
pushTelemetryState,
|
||||
watchTelemetryState,
|
||||
} from "./services/telemetry"
|
||||
import {
|
||||
sessionToWebview,
|
||||
indexProvidersById,
|
||||
@@ -747,7 +753,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
console.error("[Kilo New] handleForkSession failed:", e),
|
||||
)
|
||||
break
|
||||
|
||||
case "retryConnection":
|
||||
console.log("[Kilo New] KiloProvider: 🔄 Retrying connection...")
|
||||
this.initializeConnection().catch((e) =>
|
||||
@@ -760,6 +765,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "previewImage":
|
||||
this.handlePreviewImage(message.dataUrl, message.filename)
|
||||
break
|
||||
case "saveImage":
|
||||
return saveImage(this.getWorkspaceDirectory(this.currentSession?.id), message)
|
||||
case "openFile":
|
||||
if (message.filePath) {
|
||||
this.handleOpenFile(message.filePath, message.line, message.column)
|
||||
|
||||
@@ -481,6 +481,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
return null
|
||||
}
|
||||
if (m.type === "previewImage") return msg
|
||||
if (m.type === "saveImage") return msg
|
||||
if (m.type === "agentManager.showExistingLocalTerminal") {
|
||||
this.terminalManager.syncLocalOnSessionSwitch()
|
||||
return null
|
||||
|
||||
@@ -564,6 +564,12 @@ interface PreviewImageIn {
|
||||
filename: string
|
||||
}
|
||||
|
||||
interface SaveImageIn {
|
||||
type: "saveImage"
|
||||
dataUrl: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
interface LoadMessagesIn {
|
||||
type: "loadMessages"
|
||||
sessionID: string
|
||||
@@ -750,6 +756,7 @@ export type AgentManagerInMessage =
|
||||
| OpenFileIn
|
||||
| GenericOpenFileIn
|
||||
| PreviewImageIn
|
||||
| SaveImageIn
|
||||
| LoadMessagesIn
|
||||
| SendMessageIn
|
||||
| SendCommandIn
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { parseImage } from "../image-preview"
|
||||
|
||||
type ImageMessage = {
|
||||
dataUrl: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
export function saveImage(dir: string, msg: ImageMessage) {
|
||||
void save(dir, msg).catch((err) => console.error("[Kilo New] KiloProvider: Failed to save image:", err))
|
||||
}
|
||||
|
||||
async function save(dir: string, msg: ImageMessage) {
|
||||
const img = parseImage(msg.dataUrl, msg.filename)
|
||||
if (!img) return undefined
|
||||
|
||||
const uri = await vscode.window.showSaveDialog({
|
||||
defaultUri: vscode.Uri.file(path.join(dir, img.name)),
|
||||
filters: { Images: [img.ext] },
|
||||
saveLabel: "Save",
|
||||
})
|
||||
if (!uri) return undefined
|
||||
return vscode.workspace.fs.writeFile(uri, img.data)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ import { WorktreeModeProvider } from "../src/context/worktree-mode"
|
||||
import { ChatView } from "../src/components/chat"
|
||||
import HistoryView from "../src/components/history/HistoryView"
|
||||
import { NewWorktreeDialog } from "./NewWorktreeDialog"
|
||||
import { LanguageBridge, DataBridge } from "../src/App"
|
||||
import { LanguageBridge, DataBridge, MermaidDownloadBridge } from "../src/App"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
import { nextSelectionAfterDelete, adjacentHint, restoreLocalSessions, reconcileLocalSessions, LOCAL } from "./navigate"
|
||||
@@ -122,7 +122,6 @@ import { createMarkdownRender } from "./review-preferences"
|
||||
import { setTabWidths } from "./tab-widths"
|
||||
import "./agent-manager.css"
|
||||
import "./agent-manager-review.css"
|
||||
|
||||
const REVIEW_TAB_ID = "review"
|
||||
|
||||
interface SetupState {
|
||||
@@ -3141,6 +3140,7 @@ export const AgentManagerApp: Component = () => {
|
||||
<ThemeProvider defaultTheme="kilo-vscode">
|
||||
<DialogProvider>
|
||||
<VSCodeProvider>
|
||||
<MermaidDownloadBridge />
|
||||
<ServerProvider>
|
||||
<LanguageBridge>
|
||||
<MarkedProvider>
|
||||
|
||||
@@ -179,6 +179,27 @@ export const LanguageBridge: Component<{ children: any }> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
type MermaidImageEvent = CustomEvent<{ dataUrl: string; filename: string }>
|
||||
|
||||
export const MermaidDownloadBridge: Component = () => {
|
||||
const vscode = useVSCode()
|
||||
|
||||
onMount(() => {
|
||||
const save = (event: Event) => {
|
||||
const detail = (event as MermaidImageEvent).detail
|
||||
if (!detail?.dataUrl || !detail.filename) return
|
||||
event.preventDefault()
|
||||
vscode.postMessage({ type: "saveImage", dataUrl: detail.dataUrl, filename: detail.filename })
|
||||
}
|
||||
window.addEventListener("kilo:save-image", save)
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("kilo:save-image", save)
|
||||
})
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Inner app component that uses the contexts
|
||||
const AppContent: Component = () => {
|
||||
const [currentView, setCurrentView] = createSignal<ViewType>("newTask")
|
||||
@@ -343,6 +364,7 @@ const App: Component = () => {
|
||||
<ThemeProvider defaultTheme="kilo-vscode">
|
||||
<DialogProvider>
|
||||
<VSCodeProvider>
|
||||
<MermaidDownloadBridge />
|
||||
<ServerProvider>
|
||||
<LanguageBridge>
|
||||
<MarkedProvider>
|
||||
|
||||
@@ -802,6 +802,12 @@ export interface PreviewImageRequest {
|
||||
filename: string
|
||||
}
|
||||
|
||||
export interface SaveImageRequest {
|
||||
type: "saveImage"
|
||||
dataUrl: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
// Set default base branch (webview → extension)
|
||||
export interface SetDefaultBaseBranchRequest {
|
||||
type: "agentManager.setDefaultBaseBranch"
|
||||
@@ -1124,6 +1130,7 @@ export type WebviewMessage =
|
||||
| RetryConnectionRequest
|
||||
| OpenSubAgentViewerRequest
|
||||
| PreviewImageRequest
|
||||
| SaveImageRequest
|
||||
| SetDefaultBaseBranchRequest
|
||||
| AgentManagerOpenSessionsMessage
|
||||
| RequestAutoApproveStateMessage
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ComponentProps, createEffect, createResource, createSignal, onCleanup,
|
||||
import { isServer } from "solid-js/web"
|
||||
import { stream } from "./markdown-stream"
|
||||
import { tryFastRender } from "../kilocode/markdown-fast-path" // kilocode_change
|
||||
import { hasMermaid, preserveMermaid, renderMermaid } from "../kilocode/markdown-mermaid" // kilocode_change
|
||||
import { hasMermaid, preserveMermaid, renderMermaid, type MermaidLabels } from "../kilocode/markdown-mermaid" // kilocode_change
|
||||
|
||||
type Entry = {
|
||||
hash: string
|
||||
@@ -340,6 +340,23 @@ export function Markdown(
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
}
|
||||
|
||||
// kilocode_change start: Mermaid diagram rendering
|
||||
const mermaid = {
|
||||
rendering: i18n.t("ui.mermaid.rendering"),
|
||||
renderError: (message: string) => i18n.t("ui.mermaid.renderError", { message }),
|
||||
errorDefault: i18n.t("ui.mermaid.errorDefault"),
|
||||
errorEmpty: i18n.t("ui.mermaid.errorEmpty"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
download: i18n.t("ui.mermaid.download"),
|
||||
copySource: i18n.t("ui.mermaid.copySource"),
|
||||
copySvg: i18n.t("ui.mermaid.copySvg"),
|
||||
copyPng: i18n.t("ui.mermaid.copyPng"),
|
||||
downloadSvg: i18n.t("ui.mermaid.downloadSvg"),
|
||||
downloadPng: i18n.t("ui.mermaid.downloadPng"),
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start
|
||||
const fast = tryFastRender(container, content, local.streaming, decorate, setupCodeCopy, () => labels, copyCleanup)
|
||||
if (fast.handled) {
|
||||
@@ -352,7 +369,7 @@ export function Markdown(
|
||||
pendingLabels = undefined
|
||||
}
|
||||
copyCleanup = fast.copyCleanup
|
||||
kickMermaid(container, local.streaming ?? false)
|
||||
kickMermaid(container, local.streaming ?? false, mermaid)
|
||||
kickHighlight(container, labels)
|
||||
return
|
||||
}
|
||||
@@ -417,7 +434,7 @@ export function Markdown(
|
||||
const fromHash = fromEl.getAttribute("data-source-hash")
|
||||
const toCode = toEl.querySelector("code")?.textContent ?? ""
|
||||
if (fromHash === fnv1a(toCode)) return false
|
||||
// Source changed during streaming — fall through so morphdom replaces
|
||||
// Source changed during streaming — fall through so morphdom replaces // kilocode_change
|
||||
// the stale highlighted block with the updated plain block, which will
|
||||
// be re-highlighted on the next deferredHighlight pass.
|
||||
}
|
||||
@@ -426,7 +443,7 @@ export function Markdown(
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
kickMermaid(container, local.streaming ?? false) // kilocode_change
|
||||
kickMermaid(container, local.streaming ?? false, mermaid) // kilocode_change
|
||||
kickHighlight(container, nextLabels)
|
||||
})
|
||||
// kilocode_change end
|
||||
@@ -456,7 +473,7 @@ export function Markdown(
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start: Mermaid diagram rendering
|
||||
function kickMermaid(container: HTMLDivElement, streaming: boolean) {
|
||||
function kickMermaid(container: HTMLDivElement, streaming: boolean, labels: MermaidLabels) {
|
||||
mermaidState.signal.aborted = true
|
||||
mermaidState.gen++
|
||||
if (!hasMermaid(container)) return
|
||||
@@ -465,7 +482,7 @@ export function Markdown(
|
||||
const gen = mermaidState.gen
|
||||
const signal = { aborted: false }
|
||||
mermaidState.signal = signal
|
||||
void renderMermaid(container, signal).catch((err) => {
|
||||
void renderMermaid(container, signal, labels).catch((err) => {
|
||||
if (gen !== mermaidState.gen || signal.aborted) return
|
||||
console.warn("Mermaid render failed", err)
|
||||
})
|
||||
|
||||
Generated
+12
@@ -96,6 +96,18 @@ export const dict = {
|
||||
"ui.textField.copied": "تم النسخ",
|
||||
|
||||
"ui.imagePreview.alt": "معاينة الصورة",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "جارٍ عرض مخطط Mermaid...",
|
||||
"ui.mermaid.renderError": "فشل عرض Mermaid: {{message}}",
|
||||
"ui.mermaid.errorDefault": "تعذر عرض مخطط Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "عرض Mermaid مخططًا فارغًا.",
|
||||
"ui.mermaid.download": "تنزيل",
|
||||
"ui.mermaid.copySource": "نسخ مصدر Mermaid",
|
||||
"ui.mermaid.copySvg": "نسخ SVG",
|
||||
"ui.mermaid.copyPng": "نسخ PNG",
|
||||
"ui.mermaid.downloadSvg": "تنزيل SVG",
|
||||
"ui.mermaid.downloadPng": "تنزيل PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "محتوى قابل للتمرير",
|
||||
|
||||
"ui.tool.read": "قراءة",
|
||||
|
||||
Generated
+12
@@ -96,6 +96,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Copiado",
|
||||
|
||||
"ui.imagePreview.alt": "Visualização de imagem",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Renderizando diagrama Mermaid...",
|
||||
"ui.mermaid.renderError": "Falha ao renderizar Mermaid: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Não foi possível renderizar o diagrama Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid renderizou um diagrama vazio.",
|
||||
"ui.mermaid.download": "Baixar",
|
||||
"ui.mermaid.copySource": "Copiar código Mermaid",
|
||||
"ui.mermaid.copySvg": "Copiar SVG",
|
||||
"ui.mermaid.copyPng": "Copiar PNG",
|
||||
"ui.mermaid.downloadSvg": "Baixar SVG",
|
||||
"ui.mermaid.downloadPng": "Baixar PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "conteúdo rolável",
|
||||
|
||||
"ui.tool.read": "Ler",
|
||||
|
||||
Generated
+12
@@ -100,6 +100,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Kopirano",
|
||||
|
||||
"ui.imagePreview.alt": "Pregled slike",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Renderovanje Mermaid dijagrama...",
|
||||
"ui.mermaid.renderError": "Renderovanje Mermaid dijagrama nije uspjelo: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Nije moguće renderovati Mermaid dijagram.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid je renderovao prazan dijagram.",
|
||||
"ui.mermaid.download": "Preuzmi",
|
||||
"ui.mermaid.copySource": "Kopiraj Mermaid izvor",
|
||||
"ui.mermaid.copySvg": "Kopiraj SVG",
|
||||
"ui.mermaid.copyPng": "Kopiraj PNG",
|
||||
"ui.mermaid.downloadSvg": "Preuzmi SVG",
|
||||
"ui.mermaid.downloadPng": "Preuzmi PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "sadržaj za pomjeranje",
|
||||
|
||||
"ui.tool.read": "Čitanje",
|
||||
|
||||
Generated
+12
@@ -95,6 +95,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Kopieret",
|
||||
|
||||
"ui.imagePreview.alt": "Billedforhåndsvisning",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Renderer Mermaid-diagram...",
|
||||
"ui.mermaid.renderError": "Mermaid-rendering mislykkedes: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Kan ikke rendere Mermaid-diagram.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid renderede et tomt diagram.",
|
||||
"ui.mermaid.download": "Download",
|
||||
"ui.mermaid.copySource": "Kopiér Mermaid-kilde",
|
||||
"ui.mermaid.copySvg": "Kopiér SVG",
|
||||
"ui.mermaid.copyPng": "Kopiér PNG",
|
||||
"ui.mermaid.downloadSvg": "Download SVG",
|
||||
"ui.mermaid.downloadPng": "Download PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "rulbart indhold",
|
||||
|
||||
"ui.tool.read": "Læs",
|
||||
|
||||
Generated
+12
@@ -101,6 +101,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Kopiert",
|
||||
|
||||
"ui.imagePreview.alt": "Bildvorschau",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Mermaid-Diagramm wird gerendert...",
|
||||
"ui.mermaid.renderError": "Mermaid-Rendering fehlgeschlagen: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Mermaid-Diagramm kann nicht gerendert werden.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid hat ein leeres Diagramm gerendert.",
|
||||
"ui.mermaid.download": "Herunterladen",
|
||||
"ui.mermaid.copySource": "Mermaid-Quelltext kopieren",
|
||||
"ui.mermaid.copySvg": "SVG kopieren",
|
||||
"ui.mermaid.copyPng": "PNG kopieren",
|
||||
"ui.mermaid.downloadSvg": "SVG herunterladen",
|
||||
"ui.mermaid.downloadPng": "PNG herunterladen",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "scrollbarer Inhalt",
|
||||
|
||||
"ui.tool.read": "Lesen",
|
||||
|
||||
@@ -102,6 +102,18 @@ export const dict: Record<string, string> = {
|
||||
"ui.textField.copied": "Copied",
|
||||
|
||||
"ui.imagePreview.alt": "Image preview",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Rendering Mermaid diagram...",
|
||||
"ui.mermaid.renderError": "Mermaid render failed: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Unable to render Mermaid diagram.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid rendered an empty diagram.",
|
||||
"ui.mermaid.download": "Download",
|
||||
"ui.mermaid.copySource": "Copy Mermaid source",
|
||||
"ui.mermaid.copySvg": "Copy SVG",
|
||||
"ui.mermaid.copyPng": "Copy PNG",
|
||||
"ui.mermaid.downloadSvg": "Download SVG",
|
||||
"ui.mermaid.downloadPng": "Download PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "scrollable content",
|
||||
|
||||
"ui.tool.read": "Read",
|
||||
|
||||
Generated
+12
@@ -96,6 +96,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Copiado",
|
||||
|
||||
"ui.imagePreview.alt": "Vista previa de imagen",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Renderizando diagrama Mermaid...",
|
||||
"ui.mermaid.renderError": "Error al renderizar Mermaid: {{message}}",
|
||||
"ui.mermaid.errorDefault": "No se puede renderizar el diagrama Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid renderizó un diagrama vacío.",
|
||||
"ui.mermaid.download": "Descargar",
|
||||
"ui.mermaid.copySource": "Copiar código fuente Mermaid",
|
||||
"ui.mermaid.copySvg": "Copiar SVG",
|
||||
"ui.mermaid.copyPng": "Copiar PNG",
|
||||
"ui.mermaid.downloadSvg": "Descargar SVG",
|
||||
"ui.mermaid.downloadPng": "Descargar PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "contenido desplazable",
|
||||
|
||||
"ui.tool.read": "Leer",
|
||||
|
||||
Generated
+12
@@ -96,6 +96,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Copié",
|
||||
|
||||
"ui.imagePreview.alt": "Aperçu de l'image",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Rendu du diagramme Mermaid...",
|
||||
"ui.mermaid.renderError": "Échec du rendu Mermaid : {{message}}",
|
||||
"ui.mermaid.errorDefault": "Impossible de rendre le diagramme Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid a rendu un diagramme vide.",
|
||||
"ui.mermaid.download": "Télécharger",
|
||||
"ui.mermaid.copySource": "Copier la source Mermaid",
|
||||
"ui.mermaid.copySvg": "Copier le SVG",
|
||||
"ui.mermaid.copyPng": "Copier le PNG",
|
||||
"ui.mermaid.downloadSvg": "Télécharger le SVG",
|
||||
"ui.mermaid.downloadPng": "Télécharger le PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "contenu défilable",
|
||||
|
||||
"ui.tool.read": "Lire",
|
||||
|
||||
Generated
+12
@@ -95,6 +95,18 @@ export const dict = {
|
||||
"ui.textField.copied": "コピーしました",
|
||||
|
||||
"ui.imagePreview.alt": "画像プレビュー",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Mermaid 図をレンダリング中...",
|
||||
"ui.mermaid.renderError": "Mermaid のレンダリングに失敗しました: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Mermaid 図をレンダリングできません。",
|
||||
"ui.mermaid.errorEmpty": "Mermaid が空の図をレンダリングしました。",
|
||||
"ui.mermaid.download": "ダウンロード",
|
||||
"ui.mermaid.copySource": "Mermaid ソースをコピー",
|
||||
"ui.mermaid.copySvg": "SVG をコピー",
|
||||
"ui.mermaid.copyPng": "PNG をコピー",
|
||||
"ui.mermaid.downloadSvg": "SVG をダウンロード",
|
||||
"ui.mermaid.downloadPng": "PNG をダウンロード",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "スクロール可能なコンテンツ",
|
||||
|
||||
"ui.tool.read": "読み込み",
|
||||
|
||||
Generated
+12
@@ -96,6 +96,18 @@ export const dict = {
|
||||
"ui.textField.copied": "복사됨",
|
||||
|
||||
"ui.imagePreview.alt": "이미지 미리보기",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Mermaid 다이어그램 렌더링 중...",
|
||||
"ui.mermaid.renderError": "Mermaid 렌더링 실패: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Mermaid 다이어그램을 렌더링할 수 없습니다.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid가 빈 다이어그램을 렌더링했습니다.",
|
||||
"ui.mermaid.download": "다운로드",
|
||||
"ui.mermaid.copySource": "Mermaid 소스 복사",
|
||||
"ui.mermaid.copySvg": "SVG 복사",
|
||||
"ui.mermaid.copyPng": "PNG 복사",
|
||||
"ui.mermaid.downloadSvg": "SVG 다운로드",
|
||||
"ui.mermaid.downloadPng": "PNG 다운로드",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "스크롤 가능한 콘텐츠",
|
||||
|
||||
"ui.tool.read": "읽기",
|
||||
|
||||
Generated
+12
@@ -97,6 +97,18 @@ export const dict: Record<string, string> = {
|
||||
"ui.textField.copied": "Gekopieerd",
|
||||
|
||||
"ui.imagePreview.alt": "Afbeeldingsvoorbeeld",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Mermaid-diagram renderen...",
|
||||
"ui.mermaid.renderError": "Mermaid-rendering mislukt: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Kan Mermaid-diagram niet renderen.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid heeft een leeg diagram gerenderd.",
|
||||
"ui.mermaid.download": "Downloaden",
|
||||
"ui.mermaid.copySource": "Mermaid-bron kopiëren",
|
||||
"ui.mermaid.copySvg": "SVG kopiëren",
|
||||
"ui.mermaid.copyPng": "PNG kopiëren",
|
||||
"ui.mermaid.downloadSvg": "SVG downloaden",
|
||||
"ui.mermaid.downloadPng": "PNG downloaden",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "scrollbare inhoud",
|
||||
|
||||
"ui.fileSearch.placeholder": "Zoeken",
|
||||
|
||||
Generated
+12
@@ -99,6 +99,18 @@ export const dict: Record<Keys, string> = {
|
||||
"ui.textField.copied": "Kopiert",
|
||||
|
||||
"ui.imagePreview.alt": "Bildeforhåndsvisning",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Gjengir Mermaid-diagram...",
|
||||
"ui.mermaid.renderError": "Mermaid-gjengivelse mislyktes: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Kan ikke gjengi Mermaid-diagram.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid gjenga et tomt diagram.",
|
||||
"ui.mermaid.download": "Last ned",
|
||||
"ui.mermaid.copySource": "Kopier Mermaid-kilde",
|
||||
"ui.mermaid.copySvg": "Kopier SVG",
|
||||
"ui.mermaid.copyPng": "Kopier PNG",
|
||||
"ui.mermaid.downloadSvg": "Last ned SVG",
|
||||
"ui.mermaid.downloadPng": "Last ned PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "rullbart innhold",
|
||||
|
||||
"ui.tool.read": "Les",
|
||||
|
||||
Generated
+12
@@ -95,6 +95,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Skopiowano",
|
||||
|
||||
"ui.imagePreview.alt": "Podgląd obrazu",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Renderowanie diagramu Mermaid...",
|
||||
"ui.mermaid.renderError": "Renderowanie Mermaid nie powiodło się: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Nie można wyrenderować diagramu Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid wyrenderował pusty diagram.",
|
||||
"ui.mermaid.download": "Pobierz",
|
||||
"ui.mermaid.copySource": "Kopiuj źródło Mermaid",
|
||||
"ui.mermaid.copySvg": "Kopiuj SVG",
|
||||
"ui.mermaid.copyPng": "Kopiuj PNG",
|
||||
"ui.mermaid.downloadSvg": "Pobierz SVG",
|
||||
"ui.mermaid.downloadPng": "Pobierz PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "treść przewijana",
|
||||
|
||||
"ui.tool.read": "Odczyt",
|
||||
|
||||
Generated
+12
@@ -95,6 +95,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Скопировано",
|
||||
|
||||
"ui.imagePreview.alt": "Предпросмотр изображения",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Отрисовка диаграммы Mermaid...",
|
||||
"ui.mermaid.renderError": "Не удалось отрисовать Mermaid: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Не удалось отрисовать диаграмму Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid отрисовал пустую диаграмму.",
|
||||
"ui.mermaid.download": "Скачать",
|
||||
"ui.mermaid.copySource": "Копировать исходный код Mermaid",
|
||||
"ui.mermaid.copySvg": "Копировать SVG",
|
||||
"ui.mermaid.copyPng": "Копировать PNG",
|
||||
"ui.mermaid.downloadSvg": "Скачать SVG",
|
||||
"ui.mermaid.downloadPng": "Скачать PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "прокручиваемый контент",
|
||||
|
||||
"ui.tool.read": "Чтение",
|
||||
|
||||
Generated
+12
@@ -97,6 +97,18 @@ export const dict = {
|
||||
"ui.textField.copied": "คัดลอกแล้ว",
|
||||
|
||||
"ui.imagePreview.alt": "ตัวอย่างรูปภาพ",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "กำลังเรนเดอร์ไดอะแกรม Mermaid...",
|
||||
"ui.mermaid.renderError": "เรนเดอร์ Mermaid ไม่สำเร็จ: {{message}}",
|
||||
"ui.mermaid.errorDefault": "ไม่สามารถเรนเดอร์ไดอะแกรม Mermaid ได้",
|
||||
"ui.mermaid.errorEmpty": "Mermaid เรนเดอร์ไดอะแกรมว่าง",
|
||||
"ui.mermaid.download": "ดาวน์โหลด",
|
||||
"ui.mermaid.copySource": "คัดลอกซอร์ส Mermaid",
|
||||
"ui.mermaid.copySvg": "คัดลอก SVG",
|
||||
"ui.mermaid.copyPng": "คัดลอก PNG",
|
||||
"ui.mermaid.downloadSvg": "ดาวน์โหลด SVG",
|
||||
"ui.mermaid.downloadPng": "ดาวน์โหลด PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "เนื้อหาที่เลื่อนได้",
|
||||
|
||||
"ui.tool.read": "อ่าน",
|
||||
|
||||
Generated
+12
@@ -102,6 +102,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Kopyalandı",
|
||||
|
||||
"ui.imagePreview.alt": "Görsel önizleme",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Mermaid diyagramı işleniyor...",
|
||||
"ui.mermaid.renderError": "Mermaid işleme başarısız: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Mermaid diyagramı işlenemiyor.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid boş bir diyagram işledi.",
|
||||
"ui.mermaid.download": "İndir",
|
||||
"ui.mermaid.copySource": "Mermaid kaynağını kopyala",
|
||||
"ui.mermaid.copySvg": "SVG kopyala",
|
||||
"ui.mermaid.copyPng": "PNG kopyala",
|
||||
"ui.mermaid.downloadSvg": "SVG indir",
|
||||
"ui.mermaid.downloadPng": "PNG indir",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "kaydırılabilir içerik",
|
||||
|
||||
"ui.tool.read": "Oku",
|
||||
|
||||
Generated
+12
@@ -102,6 +102,18 @@ export const dict = {
|
||||
"ui.textField.copied": "Скопійовано",
|
||||
|
||||
"ui.imagePreview.alt": "Попередній перегляд зображення",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "Відтворення діаграми Mermaid...",
|
||||
"ui.mermaid.renderError": "Не вдалося відтворити Mermaid: {{message}}",
|
||||
"ui.mermaid.errorDefault": "Не вдалося відтворити діаграму Mermaid.",
|
||||
"ui.mermaid.errorEmpty": "Mermaid відтворив порожню діаграму.",
|
||||
"ui.mermaid.download": "Завантажити",
|
||||
"ui.mermaid.copySource": "Копіювати вихідний код Mermaid",
|
||||
"ui.mermaid.copySvg": "Копіювати SVG",
|
||||
"ui.mermaid.copyPng": "Копіювати PNG",
|
||||
"ui.mermaid.downloadSvg": "Завантажити SVG",
|
||||
"ui.mermaid.downloadPng": "Завантажити PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "вміст з прокруткою",
|
||||
|
||||
"ui.fileSearch.placeholder": "Знайти",
|
||||
|
||||
Generated
+12
@@ -100,6 +100,18 @@ export const dict = {
|
||||
"ui.textField.copied": "已复制",
|
||||
|
||||
"ui.imagePreview.alt": "图片预览",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "正在渲染 Mermaid 图表...",
|
||||
"ui.mermaid.renderError": "Mermaid 渲染失败:{{message}}",
|
||||
"ui.mermaid.errorDefault": "无法渲染 Mermaid 图表。",
|
||||
"ui.mermaid.errorEmpty": "Mermaid 渲染了一个空图表。",
|
||||
"ui.mermaid.download": "下载",
|
||||
"ui.mermaid.copySource": "复制 Mermaid 源码",
|
||||
"ui.mermaid.copySvg": "复制 SVG",
|
||||
"ui.mermaid.copyPng": "复制 PNG",
|
||||
"ui.mermaid.downloadSvg": "下载 SVG",
|
||||
"ui.mermaid.downloadPng": "下载 PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "可滚动内容",
|
||||
|
||||
"ui.tool.read": "读取",
|
||||
|
||||
Generated
+12
@@ -100,6 +100,18 @@ export const dict = {
|
||||
"ui.textField.copied": "已複製",
|
||||
|
||||
"ui.imagePreview.alt": "圖片預覽",
|
||||
// kilocode_change start
|
||||
"ui.mermaid.rendering": "正在渲染 Mermaid 圖表...",
|
||||
"ui.mermaid.renderError": "Mermaid 渲染失敗:{{message}}",
|
||||
"ui.mermaid.errorDefault": "無法渲染 Mermaid 圖表。",
|
||||
"ui.mermaid.errorEmpty": "Mermaid 渲染了一個空圖表。",
|
||||
"ui.mermaid.download": "下載",
|
||||
"ui.mermaid.copySource": "複製 Mermaid 原始碼",
|
||||
"ui.mermaid.copySvg": "複製 SVG",
|
||||
"ui.mermaid.copyPng": "複製 PNG",
|
||||
"ui.mermaid.downloadSvg": "下載 SVG",
|
||||
"ui.mermaid.downloadPng": "下載 PNG",
|
||||
// kilocode_change end
|
||||
"ui.scrollView.ariaLabel": "可捲動內容",
|
||||
|
||||
"ui.tool.read": "讀取",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import { render } from "solid-js/web"
|
||||
import { Button } from "../components/button"
|
||||
import { DropdownMenu } from "../components/dropdown-menu"
|
||||
import type { MermaidLabels } from "./markdown-mermaid"
|
||||
|
||||
type Props = {
|
||||
labels: MermaidLabels
|
||||
onCopySource: () => Promise<void>
|
||||
onCopySvg: () => Promise<void>
|
||||
onCopyPng: () => Promise<void>
|
||||
onDownloadSvg: () => void
|
||||
onDownloadPng: () => Promise<void>
|
||||
}
|
||||
|
||||
function Chevron() {
|
||||
return (
|
||||
<span data-slot="markdown-mermaid-chevron" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 16" fill="none">
|
||||
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Trigger(props: { label: string; copied?: boolean; copiedLabel?: string }) {
|
||||
return (
|
||||
<DropdownMenu.Trigger as={Button} variant="secondary" size="small" class="markdown-mermaid-trigger">
|
||||
<span>{props.copied ? props.copiedLabel : props.label}</span>
|
||||
<Chevron />
|
||||
</DropdownMenu.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function Item(props: { label: string; onSelect: () => void }) {
|
||||
return (
|
||||
<DropdownMenu.Item onSelect={props.onSelect}>
|
||||
<DropdownMenu.ItemLabel>{props.label}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export function MermaidActions(props: Props) {
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const copy = (run: () => Promise<void>) => {
|
||||
void run().then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-slot="markdown-mermaid-actions">
|
||||
<DropdownMenu gutter={4} placement="bottom-start">
|
||||
<Trigger label={props.labels.copy} copied={copied()} copiedLabel={props.labels.copied} />
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<Item label={props.labels.copySource} onSelect={() => copy(props.onCopySource)} />
|
||||
<Item label={props.labels.copySvg} onSelect={() => copy(props.onCopySvg)} />
|
||||
<Item label={props.labels.copyPng} onSelect={() => copy(props.onCopyPng)} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu gutter={4} placement="bottom-start">
|
||||
<Trigger label={props.labels.download} />
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<Item label={props.labels.downloadSvg} onSelect={props.onDownloadSvg} />
|
||||
<Item label={props.labels.downloadPng} onSelect={() => void props.onDownloadPng()} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function mountMermaidActions(el: HTMLElement, props: Props) {
|
||||
const host = document.createElement("div")
|
||||
host.setAttribute("data-slot", "markdown-mermaid-actions-root")
|
||||
el.insertBefore(host, el.firstChild)
|
||||
return render(() => <MermaidActions {...props} />, host)
|
||||
}
|
||||
@@ -4,6 +4,10 @@
|
||||
border-radius: 6px;
|
||||
background: var(--surface-base);
|
||||
overflow: hidden;
|
||||
|
||||
> [data-slot="markdown-copy-button"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="markdown-mermaid"] {
|
||||
@@ -15,11 +19,43 @@
|
||||
}
|
||||
|
||||
[data-component="markdown-mermaid"][data-state="rendered"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-slot="markdown-mermaid-actions"] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
[data-slot="markdown-mermaid-menu-chevron"] {
|
||||
align-items: center;
|
||||
color: var(--text-weak);
|
||||
display: inline-flex;
|
||||
flex: 0 0 12px;
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.15s ease;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
[data-slot="markdown-mermaid-menu-chevron"] > svg {
|
||||
display: block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
[data-expanded] [data-slot="markdown-mermaid-menu-chevron"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
[data-component="markdown-mermaid"][data-state="rendered"] > svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import DOMPurify from "dompurify"
|
||||
import { fnv1a } from "../context/marked"
|
||||
import { mountMermaidActions } from "./markdown-mermaid-actions"
|
||||
|
||||
const svgConfig = {
|
||||
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
||||
@@ -10,11 +11,43 @@ const svgConfig = {
|
||||
|
||||
type Mermaid = typeof import("mermaid").default
|
||||
|
||||
export type MermaidLabels = {
|
||||
rendering: string
|
||||
renderError: (message: string) => string
|
||||
errorDefault: string
|
||||
errorEmpty: string
|
||||
copied: string
|
||||
copy: string
|
||||
download: string
|
||||
copySource: string
|
||||
copySvg: string
|
||||
copyPng: string
|
||||
downloadSvg: string
|
||||
downloadPng: string
|
||||
}
|
||||
|
||||
const labels: MermaidLabels = {
|
||||
rendering: "Rendering Mermaid diagram...",
|
||||
renderError: (message) => `Mermaid render failed: ${message}`,
|
||||
errorDefault: "Unable to render Mermaid diagram.",
|
||||
errorEmpty: "Mermaid rendered an empty diagram.",
|
||||
copied: "Copied",
|
||||
copy: "Copy",
|
||||
download: "Download",
|
||||
copySource: "Copy Mermaid source",
|
||||
copySvg: "Copy SVG",
|
||||
copyPng: "Copy PNG",
|
||||
downloadSvg: "Download SVG",
|
||||
downloadPng: "Download PNG",
|
||||
}
|
||||
|
||||
const cache: { promise?: Promise<Mermaid>; id: number; queue: Promise<void> } = {
|
||||
id: 0,
|
||||
queue: Promise.resolve(),
|
||||
}
|
||||
|
||||
const actions = new WeakMap<HTMLElement, () => void>()
|
||||
|
||||
async function load() {
|
||||
if (!cache.promise) {
|
||||
cache.promise = import("mermaid").then((mod) => mod.default)
|
||||
@@ -164,10 +197,14 @@ function sanitize(svg: string) {
|
||||
return DOMPurify.sanitize(svg, svgConfig)
|
||||
}
|
||||
|
||||
function message(err: unknown) {
|
||||
function mergeLabels(input?: Partial<MermaidLabels>) {
|
||||
return { ...labels, ...input }
|
||||
}
|
||||
|
||||
function message(err: unknown, labels: MermaidLabels) {
|
||||
if (err instanceof Error) return err.message
|
||||
if (typeof err === "string") return err
|
||||
return "Unable to render Mermaid diagram."
|
||||
return labels.errorDefault
|
||||
}
|
||||
|
||||
function panel(wrapper: HTMLElement) {
|
||||
@@ -183,14 +220,117 @@ function panel(wrapper: HTMLElement) {
|
||||
return el
|
||||
}
|
||||
|
||||
function fail(wrapper: HTMLElement, pre: HTMLPreElement, err: unknown) {
|
||||
function fail(wrapper: HTMLElement, pre: HTMLPreElement, err: unknown, labels: MermaidLabels) {
|
||||
const el = panel(wrapper)
|
||||
el.setAttribute("data-state", "error")
|
||||
el.textContent = `Mermaid render failed: ${message(err)}`
|
||||
el.textContent = labels.renderError(message(err, labels))
|
||||
wrapper.setAttribute("data-mermaid-state", "error")
|
||||
pre.hidden = false
|
||||
}
|
||||
|
||||
function cleanupActions(el: HTMLElement) {
|
||||
const dispose = actions.get(el)
|
||||
if (!dispose) return
|
||||
dispose()
|
||||
actions.delete(el)
|
||||
}
|
||||
|
||||
function serialize(svg: SVGSVGElement) {
|
||||
const clone = svg.cloneNode(true) as SVGSVGElement
|
||||
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg")
|
||||
return new XMLSerializer().serializeToString(clone)
|
||||
}
|
||||
|
||||
function dataUrl(type: string, content: string) {
|
||||
return `data:${type};base64,${btoa(unescape(encodeURIComponent(content)))}`
|
||||
}
|
||||
|
||||
function size(svg: SVGSVGElement) {
|
||||
const box = svg.viewBox.baseVal
|
||||
const rect = svg.getBoundingClientRect()
|
||||
const width = Math.max(Math.ceil(box?.width || rect.width || 1), 1)
|
||||
const height = Math.max(Math.ceil(box?.height || rect.height || 1), 1)
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
async function png(svg: SVGSVGElement) {
|
||||
const source = serialize(svg)
|
||||
const url = dataUrl("image/svg+xml", source)
|
||||
const img = new Image()
|
||||
const dims = size(svg)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve()
|
||||
img.onerror = () => reject(new Error("Unable to export Mermaid diagram."))
|
||||
img.src = url
|
||||
})
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = dims.width
|
||||
canvas.height = dims.height
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) throw new Error("Unable to export Mermaid diagram.")
|
||||
ctx.drawImage(img, 0, 0, dims.width, dims.height)
|
||||
return canvas.toDataURL("image/png")
|
||||
}
|
||||
|
||||
function download(url: string, filename: string) {
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
function save(url: string, filename: string) {
|
||||
const event = new CustomEvent("kilo:save-image", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { dataUrl: url, filename },
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
if (event.defaultPrevented) return
|
||||
download(url, filename)
|
||||
}
|
||||
|
||||
async function copyText(text: string) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
async function copyPng(svg: SVGSVGElement) {
|
||||
const url = await png(svg)
|
||||
const blob = await (await fetch(url)).blob()
|
||||
if (typeof ClipboardItem === "undefined") {
|
||||
await navigator.clipboard.writeText(serialize(svg))
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
|
||||
}
|
||||
|
||||
function renderActions(el: HTMLDivElement, pre: HTMLPreElement, source: string, labels: MermaidLabels) {
|
||||
const svg = el.querySelector("svg")
|
||||
if (!(svg instanceof SVGSVGElement)) return
|
||||
|
||||
cleanupActions(el)
|
||||
const old = el.querySelector('[data-slot="markdown-mermaid-actions-root"]')
|
||||
old?.remove()
|
||||
const sourceText = pre.querySelector("code")?.textContent ?? source
|
||||
const sourceSvg = () => serialize(svg)
|
||||
const sourceSvgUrl = () => dataUrl("image/svg+xml", sourceSvg())
|
||||
|
||||
actions.set(
|
||||
el,
|
||||
mountMermaidActions(el, {
|
||||
labels,
|
||||
onCopySource: () => copyText(sourceText),
|
||||
onCopySvg: () => copyText(sourceSvg()),
|
||||
onCopyPng: () => copyPng(svg),
|
||||
onDownloadSvg: () => save(sourceSvgUrl(), "mermaid-diagram.svg"),
|
||||
onDownloadPng: async () => save(await png(svg), "mermaid-diagram.png"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function preserveMermaid(fromEl: Element, toEl: Element) {
|
||||
if (!(fromEl instanceof HTMLElement)) return false
|
||||
if (!(toEl instanceof HTMLElement)) return false
|
||||
@@ -217,7 +357,8 @@ async function svg(renderer: Mermaid, source: string, cfg: ReturnType<typeof con
|
||||
})
|
||||
}
|
||||
|
||||
export async function renderMermaid(root: HTMLDivElement, signal: { aborted: boolean }) {
|
||||
export async function renderMermaid(root: HTMLDivElement, signal: { aborted: boolean }, input?: Partial<MermaidLabels>) {
|
||||
const label = mergeLabels(input)
|
||||
const blocks = Array.from(root.querySelectorAll('pre > code[data-lang="mermaid"]'))
|
||||
if (blocks.length === 0) return
|
||||
|
||||
@@ -228,7 +369,7 @@ export async function renderMermaid(root: HTMLDivElement, signal: { aborted: boo
|
||||
if (!(pre instanceof HTMLPreElement)) continue
|
||||
if (!(wrapper instanceof HTMLElement)) continue
|
||||
if (wrapper.getAttribute("data-component") !== "markdown-code") continue
|
||||
fail(wrapper, pre, err)
|
||||
fail(wrapper, pre, err, label)
|
||||
}
|
||||
})
|
||||
if (!renderer) return
|
||||
@@ -270,7 +411,7 @@ export async function renderMermaid(root: HTMLDivElement, signal: { aborted: boo
|
||||
const el = panel(wrapper)
|
||||
if (!keep) {
|
||||
el.setAttribute("data-state", "rendering")
|
||||
el.textContent = "Rendering Mermaid diagram..."
|
||||
el.textContent = label.rendering
|
||||
pre.hidden = false
|
||||
} else {
|
||||
pre.hidden = true
|
||||
@@ -281,15 +422,17 @@ export async function renderMermaid(root: HTMLDivElement, signal: { aborted: boo
|
||||
if (signal.aborted || !root.isConnected || !wrapper.isConnected) return
|
||||
|
||||
const safe = sanitize(result.svg)
|
||||
if (!safe) throw new Error("Mermaid rendered an empty diagram.")
|
||||
if (!safe) throw new Error(label.errorEmpty)
|
||||
|
||||
cleanupActions(el)
|
||||
el.setAttribute("data-state", "rendered")
|
||||
el.innerHTML = safe
|
||||
renderActions(el, pre, source, label)
|
||||
wrapper.setAttribute("data-mermaid-state", "rendered")
|
||||
pre.hidden = true
|
||||
} catch (err) {
|
||||
if (signal.aborted || !root.isConnected || !wrapper.isConnected) return
|
||||
fail(wrapper, pre, err)
|
||||
fail(wrapper, pre, err, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user