import { useEffect, useRef, type MouseEvent } from "react" import DOMPurify from "dompurify" import { TriangleAlertIcon } from "lucide-react" import { marked } from "marked" import Prism from "prismjs" import "prismjs/components/prism-bash" import "prismjs/components/prism-diff" import "prismjs/components/prism-javascript" import "prismjs/components/prism-json" import "prismjs/components/prism-jsx" import "prismjs/components/prism-markdown" import "prismjs/components/prism-powershell" import "prismjs/components/prism-tsx" import "prismjs/components/prism-typescript" import "prismjs/components/prism-yaml" import { useTranslation } from "react-i18next" import { Skeleton } from "@/components/ui/skeleton" import { openURL } from "@/utils/dataSender" import { usePluginStore } from "@/store" import { pluginDeprecationStatus, pluginReadmeStatus, type PluginDetailSelectedItem, type PluginReadmeState, } from "./types" import { buildPluginDeprecationKey } from "./utils" interface PluginDetailReadmeTabContentProps { selectedItem: PluginDetailSelectedItem | null readmeState: PluginReadmeState | null } function renderSanitizedMarkdown(markdown: string) { const parsed = marked.parse(markdown, { gfm: true, breaks: true, }) const html = typeof parsed === "string" ? parsed : markdown return DOMPurify.sanitize(html) } function resolveLeadingIndentSize(line: string) { return line.match(/^[ \t]*/)?.[0].length ?? 0 } function normalizeCodeBlockFirstLineSpacing(codeText: string) { const normalizedText = codeText.replace(/\r\n/g, "\n").replace(/^\uFEFF/, "") const lines = normalizedText.split("\n") const firstLineIndex = lines.findIndex((line) => line.trim().length > 0) if (firstLineIndex < 0) { return normalizedText } const nextLine = lines .slice(firstLineIndex + 1) .find((line) => line.trim().length > 0) if (!nextLine) { return normalizedText } const firstLine = lines[firstLineIndex] ?? "" const firstIndentSize = resolveLeadingIndentSize(firstLine) const nextIndentSize = resolveLeadingIndentSize(nextLine) if (firstIndentSize === nextIndentSize + 1) { lines[firstLineIndex] = firstLine.slice(1) } return lines.join("\n") } export function PluginDetailReadmeTabContent({ selectedItem, readmeState, }: PluginDetailReadmeTabContentProps) { const { t } = useTranslation() const articleRef = useRef(null) const deprecationByPlugin = usePluginStore.use.deprecationByPlugin() const deprecationState = selectedItem ? deprecationByPlugin[buildPluginDeprecationKey(selectedItem.fullName, selectedItem.version)] : undefined const showDeprecationBanner = Boolean( deprecationState?.status === pluginDeprecationStatus.Ready && deprecationState.isDeprecated ) const deprecationMessage = deprecationState?.message.trim() ?? "" // Post-process rendered markdown (spacing, grouped links, syntax highlighting) when README is ready. useEffect(() => { if (readmeState?.status !== pluginReadmeStatus.Ready) { return } const articleElement = articleRef.current if (!articleElement) { return } const codeElements = articleElement.querySelectorAll("pre code") codeElements.forEach((codeElement) => { const source = codeElement.textContent ?? "" const normalized = normalizeCodeBlockFirstLineSpacing(source) if (normalized !== source) { codeElement.textContent = normalized } }) const paragraphElements = articleElement.querySelectorAll("p") paragraphElements.forEach((paragraphElement) => { if (paragraphElement.querySelectorAll("a").length > 1) { paragraphElement.classList.add("app-plugin-readme-link-group") } }) Prism.highlightAllUnder(articleElement) }, [readmeState?.content, readmeState?.status]) const handleReadmeLinkClick = (event: MouseEvent) => { const target = event.target if (!(target instanceof Element)) { return } const anchor = target.closest("a[href]") if (!(anchor instanceof HTMLAnchorElement)) { return } event.preventDefault() event.stopPropagation() openURL(anchor.href) } if (!selectedItem) { return null } return (
{showDeprecationBanner ? (
{t("PLUGIN_DEPRECATED_TITLE")}
{deprecationMessage ? (

{deprecationMessage}

) : null}
) : null} {readmeState?.status === pluginReadmeStatus.Loading ? (
) : null} {readmeState?.status === pluginReadmeStatus.Error ? (
{t("FAILED")}

{readmeState.errorMessage ?? t("TIPS_GET_PLUGIN_LIST_FAILED")}

) : null} {readmeState?.status === pluginReadmeStatus.Empty ? (
{t("PLUGIN_NO_README")}
) : null} {readmeState?.status === pluginReadmeStatus.Ready ? (
) : null}
) }