DeepWiki转Markdown

将DeepWiki页面转换为Markdown格式,支持图表转换

// ==UserScript==
// @name         DeepWiki to Markdown
// @name:zh-CN   DeepWiki转Markdown
// @name:zh-TW   DeepWiki轉Markdown
// @name:ja      DeepWikiをMarkdownに変換
// @name:ko      DeepWiki를 Markdown으로
// @name:ru      DeepWiki в Markdown
// @name:es      DeepWiki a Markdown
// @name:fr      DeepWiki vers Markdown
// @name:de      DeepWiki zu Markdown
// @name:it      DeepWiki a Markdown
// @name:pt      DeepWiki para Markdown
// @name:ar      DeepWiki إلى Markdown
// @name:hi      DeepWiki से Markdown
// @name:tr      DeepWiki'den Markdown'a
// @name:vi      DeepWiki sang Markdown
// @name:th      DeepWiki เป็น Markdown
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Convert DeepWiki pages to Markdown format with diagram support
// @description:zh-CN  将DeepWiki页面转换为Markdown格式,支持图表转换
// @description:zh-TW  將DeepWiki頁面轉換為Markdown格式,支援圖表轉換
// @description:ja     DeepWikiページをMarkdown形式に変換し、図表変換をサポート
// @description:ko     DeepWiki 페이지를 Markdown 형식으로 변환하고 다이어그램 지원
// @description:ru     Конвертируйте страницы DeepWiki в формат Markdown с поддержкой диаграмм
// @description:es     Convierte páginas de DeepWiki a formato Markdown con soporte para diagramas
// @description:fr     Convertit les pages DeepWiki au format Markdown avec prise en charge des diagrammes
// @description:de     Konvertiert DeepWiki-Seiten in das Markdown-Format mit Diagrammunterstützung
// @description:it     Converti le pagine DeepWiki in formato Markdown con supporto per diagrammi
// @description:pt     Converte páginas DeepWiki para formato Markdown com suporte a diagramas
// @description:ar     تحويل صفحات DeepWiki إلى تنسيق Markdown مع دعم الرسوم البيانية
// @description:hi     DeepWiki पृष्ठों को Markdown प्रारूप में चित्र समर्थन के साथ रूपांतरित करें
// @description:tr     DeepWiki sayfalarını diyagram desteğiyle Markdown formatına dönüştürün
// @description:vi     Chuyển đổi trang DeepWiki sang định dạng Markdown với hỗ trợ sơ đồ
// @description:th     แปลงหน้า DeepWiki เป็นรูปแบบ Markdown พร้อมรองรับไดอะแกรม
// @author       zxmfke,aspen138
// @match        https://deepwiki.com/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @icon         https://deepwiki.com/icon.png?66aaf51e0e68c818
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==================== UTILITY FUNCTIONS ====================

    function downloadFile(content, filename, mimeType = 'text/markdown') {
        const blob = new Blob([content], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        setTimeout(() => URL.revokeObjectURL(url), 100);
    }

    // ==================== CONVERSION FUNCTIONS ====================

    // Function to auto-detect programming language from code content
    function detectCodeLanguage(codeText) {
        if (!codeText || codeText.trim().length < 10) return '';

        const code = codeText.trim();
        const firstLine = code.split('\n')[0].trim();
        const lines = code.split('\n');

        // JavaScript/TypeScript patterns
        if (code.includes('function ') || code.includes('const ') || code.includes('let ') ||
            code.includes('var ') || code.includes('=>') || code.includes('console.log') ||
            code.includes('require(') || code.includes('import ') || code.includes('export ')) {
            if (code.includes(': ') && (code.includes('interface ') || code.includes('type ') ||
                code.includes('enum ') || code.includes('implements '))) {
                return 'typescript';
            }
            return 'javascript';
        }

        // Python patterns
        if (code.includes('def ') || code.includes('import ') || code.includes('from ') ||
            code.includes('print(') || code.includes('if __name__') || code.includes('class ') ||
            firstLine.startsWith('#!') && firstLine.includes('python')) {
            return 'python';
        }

        // Java patterns
        if (code.includes('public class ') || code.includes('private ') || code.includes('public static void main') ||
            code.includes('System.out.println') || code.includes('import java.')) {
            return 'java';
        }

        // C# patterns
        if (code.includes('using System') || code.includes('namespace ') || code.includes('public class ') ||
            code.includes('Console.WriteLine') || code.includes('[Attribute]')) {
            return 'csharp';
        }

        // C/C++ patterns
        if (code.includes('#include') || code.includes('int main') || code.includes('printf(') ||
            code.includes('cout <<') || code.includes('std::')) {
            return code.includes('std::') || code.includes('cout') ? 'cpp' : 'c';
        }

        // Go patterns
        if (code.includes('package ') || code.includes('func ') || code.includes('import (') ||
            code.includes('fmt.Printf') || code.includes('go ')) {
            return 'go';
        }

        // Rust patterns
        if (code.includes('fn ') || code.includes('let mut') || code.includes('println!') ||
            code.includes('use std::') || code.includes('impl ')) {
            return 'rust';
        }

        // PHP patterns
        if (code.includes('<?php') || code.includes('$') && (code.includes('echo ') || code.includes('print '))) {
            return 'php';
        }

        // Ruby patterns
        if (code.includes('def ') && (code.includes('end') || code.includes('puts ') || code.includes('require '))) {
            return 'ruby';
        }

        // Shell/Bash patterns
        if (firstLine.startsWith('#!') && (firstLine.includes('bash') || firstLine.includes('sh')) ||
            code.includes('#!/bin/') || code.includes('echo ') && code.includes('$')) {
            return 'bash';
        }

        // SQL patterns
        if (code.match(/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\b/i)) {
            return 'sql';
        }

        // CSS patterns
        if (code.includes('{') && code.includes('}') && code.includes(':') &&
            (code.includes('color:') || code.includes('margin:') || code.includes('padding:') || code.includes('#'))) {
            return 'css';
        }

        // HTML patterns
        if (code.includes('<') && code.includes('>') &&
            (code.includes('<!DOCTYPE') || code.includes('<html') || code.includes('<div') || code.includes('<p'))) {
            return 'html';
        }

        // XML patterns
        if (code.includes('<?xml') || (code.includes('<') && code.includes('>') && code.includes('</'))) {
            return 'xml';
        }

        // JSON patterns
        if (code.startsWith('{') && code.endsWith('}') || code.startsWith('[') && code.endsWith(']')) {
            try {
                JSON.parse(code);
                return 'json';
            } catch (e) {
                // Not valid JSON
            }
        }

        // YAML patterns
        if (lines.some(line => line.match(/^\s*\w+:\s*/) && !line.includes('{') && !line.includes(';'))) {
            return 'yaml';
        }

        // Markdown patterns
        if (code.includes('# ') || code.includes('## ') || code.includes('```') || code.includes('[') && code.includes('](')) {
            return 'markdown';
        }

        // Docker patterns
        if (firstLine.startsWith('FROM ') || code.includes('RUN ') || code.includes('COPY ') || code.includes('WORKDIR ')) {
            return 'dockerfile';
        }

        // Default fallback
        return '';
    }

    // Function for Flowchart
    function convertFlowchartSvgToMermaidText(svgElement) {
        if (!svgElement) return null;

        console.log("Starting flowchart conversion with hierarchical logic...");
        let mermaidCode = "flowchart TD\n\n";
        const nodes = {};
        const clusters = {};
        const parentMap = {}; // Maps a child SVG ID to its parent SVG ID
        const allElements = {}; // All nodes and clusters, for easy lookup

        // 1. Collect all nodes
        svgElement.querySelectorAll('g.node').forEach(nodeEl => {
            const svgId = nodeEl.id;
            if (!svgId) return;

            let textContent = "";
            const pElementForText = nodeEl.querySelector('.label foreignObject div > span > p, .label foreignObject div > p');
            if (pElementForText) {
                let rawParts = [];
                pElementForText.childNodes.forEach(child => {
                    if (child.nodeType === Node.TEXT_NODE) rawParts.push(child.textContent);
                    else if (child.nodeName.toUpperCase() === 'BR') rawParts.push('<br>');
                    else if (child.nodeType === Node.ELEMENT_NODE) rawParts.push(child.textContent || '');
                });
                textContent = rawParts.join('').trim().replace(/"/g, '#quot;');
            }
            if (!textContent.trim()) {
                const nodeLabel = nodeEl.querySelector('.nodeLabel, .label, foreignObject span, foreignObject div, text');
                if (nodeLabel && nodeLabel.textContent) {
                    textContent = nodeLabel.textContent.trim().replace(/"/g, '#quot;');
                }
            }

            let mermaidId = svgId.replace(/^flowchart-/, '').replace(/-\d+$/, '');

            const bbox = nodeEl.getBoundingClientRect();
            if (bbox.width > 0 || bbox.height > 0) {
                nodes[svgId] = {
                    type: 'node',
                    mermaidId: mermaidId,
                    text: textContent,
                    svgId: svgId,
                    bbox: bbox,
                };
                allElements[svgId] = nodes[svgId];
            }
        });

        // 2. Collect all clusters
        svgElement.querySelectorAll('g.cluster').forEach(clusterEl => {
            const svgId = clusterEl.id;
            if (!svgId) return;

            let title = "";
            const labelEl = clusterEl.querySelector('.cluster-label, .label');
            if (labelEl && labelEl.textContent) {
                title = labelEl.textContent.trim();
            }
            if (!title) {
                title = svgId;
            }

            const rect = clusterEl.querySelector('rect');
            const bbox = rect ? rect.getBoundingClientRect() : clusterEl.getBoundingClientRect();

            if (bbox.width > 0 || bbox.height > 0) {
                clusters[svgId] = {
                    type: 'cluster',
                    mermaidId: svgId, // Use stable SVG ID for mermaid ID
                    title: title,
                    svgId: svgId,
                    bbox: bbox,
                };
                allElements[svgId] = clusters[svgId];
            }
        });

        // 3. Build hierarchy (parentMap) by checking for geometric containment
        for (const childId in allElements) {
            const child = allElements[childId];
            let potentialParentId = null;
            let minArea = Infinity;

            for (const parentId in clusters) {
                if (childId === parentId) continue;
                const parent = clusters[parentId];

                if (child.bbox.left >= parent.bbox.left &&
                    child.bbox.right <= parent.bbox.right &&
                    child.bbox.top >= parent.bbox.top &&
                    child.bbox.bottom <= parent.bbox.bottom) {

                    const area = parent.bbox.width * parent.bbox.height;
                    if (area < minArea) {
                        minArea = area;
                        potentialParentId = parentId;
                    }
                }
            }
            if (potentialParentId) {
                parentMap[childId] = potentialParentId;
            }
        }

        // 4. Process edges and assign to their lowest common ancestor cluster
        const edges = [];
        const edgeLabels = {};
        svgElement.querySelectorAll('g.edgeLabel').forEach(labelEl => {
            const text = labelEl.textContent?.trim();
            const bbox = labelEl.getBoundingClientRect();
            if(text) {
                edgeLabels[labelEl.id] = {
                    text,
                    x: bbox.left + bbox.width / 2,
                    y: bbox.top + bbox.height / 2
                };
            }
        });

        svgElement.querySelectorAll('path.flowchart-link').forEach(path => {
            const pathId = path.id;
            if (!pathId) return;

            let sourceNode = null;
            let targetNode = null;
            let idParts = pathId.replace(/^(L_|FL_)/, '').split('_');
            if(idParts.length > 1 && idParts[idParts.length-1].match(/^\d+$/)){
                idParts.pop();
            }
            idParts = idParts.join('_');

            for (let i = 1; i < idParts.length; i++) {
                const potentialSourceName = idParts.substring(0,i);
                const potentialTargetName = idParts.substring(i);
                const foundSourceNode = Object.values(nodes).find(n => n.mermaidId === potentialSourceName);
                const foundTargetNode = Object.values(nodes).find(n => n.mermaidId === potentialTargetName);
                if(foundSourceNode && foundTargetNode){
                    sourceNode = foundSourceNode;
                    targetNode = foundTargetNode;
                    break;
                }
            }

            if (!sourceNode || !targetNode) { // Fallback for complex names
                const pathIdParts = pathId.replace(/^(L_|FL_)/, '').split('_');
                if(pathIdParts.length > 2){
                    for (let i = 1; i < pathIdParts.length; i++) {
                        const sName = pathIdParts.slice(0, i).join('_');
                        const tName = pathIdParts.slice(i, pathIdParts.length -1).join('_');
                        const foundSourceNode = Object.values(nodes).find(n => n.mermaidId === sName);
                        const foundTargetNode = Object.values(nodes).find(n => n.mermaidId === tName);
                        if(foundSourceNode && foundTargetNode){
                            sourceNode = foundSourceNode;
                            targetNode = foundTargetNode;
                            break;
                        }
                    }
                }
            }

            if (!sourceNode || !targetNode) {
                console.warn("Could not determine source/target for edge:", pathId);
                return;
            }

            let label = "";
            try {
                const totalLength = path.getTotalLength();
                if (totalLength > 0) {
                    const midPoint = path.getPointAtLength(totalLength / 2);
                    let closestLabel = null;
                    let closestDist = Infinity;
                    for (const labelId in edgeLabels) {
                        const currentLabel = edgeLabels[labelId];
                        const dist = Math.sqrt(Math.pow(currentLabel.x - midPoint.x, 2) + Math.pow(currentLabel.y - midPoint.y, 2));
                        if (dist < closestDist) {
                            closestDist = dist;
                            closestLabel = currentLabel;
                        }
                    }
                    if (closestLabel && closestDist < 75) {
                        label = closestLabel.text;
                    }
                }
            } catch (e) {
                console.error("Error matching label for edge " + pathId, e);
            }

            const labelPart = label ? `|"${label}"|` : "";
            const edgeText = `${sourceNode.mermaidId} -->${labelPart} ${targetNode.mermaidId}`;

            // Find Lowest Common Ancestor
            const sourceAncestors = [parentMap[sourceNode.svgId]];
            while (sourceAncestors[sourceAncestors.length - 1]) {
                sourceAncestors.push(parentMap[sourceAncestors[sourceAncestors.length - 1]]);
            }
            let lca = parentMap[targetNode.svgId];
            while (lca && !sourceAncestors.includes(lca)) {
                lca = parentMap[lca];
            }

            edges.push({ text: edgeText, parentId: lca || 'root' });
        });

        // 5. Generate Mermaid output
        const definedNodeMermaidIds = new Set();
        for (const svgId in nodes) {
            const node = nodes[svgId];
            if (!definedNodeMermaidIds.has(node.mermaidId)) {
                mermaidCode += `${node.mermaidId}["${node.text}"]\n`;
                definedNodeMermaidIds.add(node.mermaidId);
            }
        }
        mermaidCode += '\n';

        // Group children and edges by parent
        const childrenMap = {};
        const edgeMap = {};

        for (const childId in parentMap) {
            const parentId = parentMap[childId];
            if (!childrenMap[parentId]) childrenMap[parentId] = [];
            childrenMap[parentId].push(childId);
        }

        edges.forEach(edge => {
            const parentId = edge.parentId || 'root';
            if (!edgeMap[parentId]) edgeMap[parentId] = [];
            edgeMap[parentId].push(edge.text);
        });

        // Add top-level edges
        (edgeMap['root'] || []).forEach(edgeText => {
            mermaidCode += `${edgeText}\n`;
        });

        function buildSubgraphOutput(clusterId) {
            const cluster = clusters[clusterId];
            if (!cluster) return;

            mermaidCode += `\nsubgraph ${cluster.mermaidId} ["${cluster.title}"]\n`;

            const childItems = childrenMap[clusterId] || [];

            // Render nodes within this subgraph
            childItems.filter(id => nodes[id]).forEach(nodeId => {
                mermaidCode += `    ${nodes[nodeId].mermaidId}\n`;
            });

            // Render edges within this subgraph
            (edgeMap[clusterId] || []).forEach(edgeText => {
                mermaidCode += `    ${edgeText}\n`;
            });

            // Render nested subgraphs
            childItems.filter(id => clusters[id]).forEach(subClusterId => {
                buildSubgraphOutput(subClusterId);
            });

            mermaidCode += "end\n";
        }

        const topLevelClusters = Object.keys(clusters).filter(id => !parentMap[id]);
        topLevelClusters.forEach(buildSubgraphOutput);

        if (Object.keys(nodes).length === 0 && Object.keys(clusters).length === 0) return null;
        return '```mermaid\n' + mermaidCode.trim() + '\n```';
    }

    // Function for Class Diagram
    function convertClassDiagramSvgToMermaidText(svgElement) {
        if (!svgElement) return null;
        const mermaidLines = ['classDiagram'];
        const classData = {};

        // 1. Parse Classes and their geometric information
        svgElement.querySelectorAll('g.node.default[id^="classId-"]').forEach(node => {
            const classIdSvg = node.getAttribute('id');
            if (!classIdSvg) return;

            const classNameMatch = classIdSvg.match(/^classId-([^-]+(?:-[^-]+)*)-(\d+)$/);
            if (!classNameMatch) return;
            const className = classNameMatch[1];

            let cx = 0, cy = 0, halfWidth = 0, halfHeight = 0;
            const transform = node.getAttribute('transform');
            if (transform) {
                const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                if (match) {
                    cx = parseFloat(match[1]);
                    cy = parseFloat(match[2]);
                }
            }
            const pathForBounds = node.querySelector('g.basic.label-container > path[d^="M-"]');
            if (pathForBounds) {
                const d = pathForBounds.getAttribute('d');
                const dMatch = d.match(/M-([0-9.]+)\s+-([0-9.]+)/); // Extracts W and H from M-W -H
                if (dMatch && dMatch.length >= 3) {
                    halfWidth = parseFloat(dMatch[1]);
                    halfHeight = parseFloat(dMatch[2]);
                }
            }

            if (!classData[className]) {
                classData[className] = {
                    stereotype: "",
                    members: [],
                    methods: [],
                    svgId: classIdSvg,
                    x: cx,
                    y: cy,
                    width: halfWidth * 2,
                    height: halfHeight * 2
                };
            }
            const stereotypeElem = node.querySelector('g.annotation-group.text foreignObject span.nodeLabel p, g.annotation-group.text foreignObject div p');
            if (stereotypeElem && stereotypeElem.textContent.trim()) {
                classData[className].stereotype = stereotypeElem.textContent.trim();
            }
            node.querySelectorAll('g.members-group.text g.label foreignObject span.nodeLabel p, g.members-group.text g.label foreignObject div p').forEach(m => {
                const txt = m.textContent.trim();
                if (txt) classData[className].members.push(txt);
            });
            node.querySelectorAll('g.methods-group.text g.label foreignObject span.nodeLabel p, g.methods-group.text g.label foreignObject div p').forEach(m => {
                const txt = m.textContent.trim();
                if (txt) classData[className].methods.push(txt);
            });
        });

        // 2. Parse Notes
        const notes = [];

        // Method 1: Find traditional rect.note and text.noteText
        svgElement.querySelectorAll('g').forEach(g => {
            const noteRect = g.querySelector('rect.note');
            const noteText = g.querySelector('text.noteText');

            if (noteRect && noteText) {
                const text = noteText.textContent.trim();
                const x = parseFloat(noteRect.getAttribute('x'));
                const y = parseFloat(noteRect.getAttribute('y'));
                const width = parseFloat(noteRect.getAttribute('width'));
                const height = parseFloat(noteRect.getAttribute('height'));

                if (text && !isNaN(x) && !isNaN(y)) {
                    notes.push({
                        text: text,
                        x: x,
                        y: y,
                        width: width || 0,
                        height: height || 0,
                        id: g.id || `note_${notes.length}`
                    });
                }
            }
        });

        // Method 2: Find other note formats (like node undefined type)
        svgElement.querySelectorAll('g.node.undefined, g[id^="note"]').forEach(g => {
            // Check if it's a note (by background color, id or other features)
            const hasNoteBackground = g.querySelector('path[fill="#fff5ad"], path[style*="#fff5ad"], path[style*="fill:#fff5ad"]');
            const isNoteId = g.id && g.id.includes('note');

            if (hasNoteBackground || isNoteId) {
                // Try to get text from foreignObject
                let text = '';
                const foreignObject = g.querySelector('foreignObject');
                if (foreignObject) {
                    const textEl = foreignObject.querySelector('p, span.nodeLabel, .nodeLabel');
                    if (textEl) {
                        text = textEl.textContent.trim();
                    }
                }

                // If no text found, try other selectors
                if (!text) {
                    const textEl = g.querySelector('text, .label text, tspan');
                    if (textEl) {
                        text = textEl.textContent.trim();
                    }
                }

                if (text) {
                    // Get position information
                    const transform = g.getAttribute('transform');
                    let x = 0, y = 0;
                    if (transform) {
                        const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                        if (match) {
                            x = parseFloat(match[1]);
                            y = parseFloat(match[2]);
                        }
                    }

                    // Check if this note has already been added
                    const existingNote = notes.find(n => n.text === text && Math.abs(n.x - x) < 10 && Math.abs(n.y - y) < 10);
                    if (!existingNote) {
                        notes.push({
                            text: text,
                            x: x,
                            y: y,
                            width: 0,
                            height: 0,
                            id: g.id || `note_${notes.length}`
                        });
                    }
                }
            }
        });

        // 3. Parse Note-to-Class Connections
        const noteTargets = {}; // Maps note.id to target className
        const connectionThreshold = 50; // Increase connection threshold

        // Find note connection paths, support multiple path types
        const noteConnections = [
            ...svgElement.querySelectorAll('path.relation.edge-pattern-dotted'),
            ...svgElement.querySelectorAll('path[id^="edgeNote"]'),
            ...svgElement.querySelectorAll('path.edge-thickness-normal.edge-pattern-dotted')
        ];

        noteConnections.forEach(pathEl => {
            const dAttr = pathEl.getAttribute('d');
            if (!dAttr) return;

            // Improved path parsing, support Bezier curves
            const pathPoints = [];

            // Parse various path commands
            const commands = dAttr.match(/[A-Za-z][^A-Za-z]*/g) || [];
            let currentX = 0, currentY = 0;

            commands.forEach(cmd => {
                const parts = cmd.match(/[A-Za-z]|[-+]?\d*\.?\d+/g) || [];
                const type = parts[0];
                const coords = parts.slice(1).map(Number);

                switch(type.toUpperCase()) {
                    case 'M': // Move to
                        if (coords.length >= 2) {
                            currentX = coords[0];
                            currentY = coords[1];
                            pathPoints.push({x: currentX, y: currentY});
                        }
                        break;
                    case 'L': // Line to
                        for (let i = 0; i < coords.length; i += 2) {
                            if (coords[i+1] !== undefined) {
                                currentX = coords[i];
                                currentY = coords[i+1];
                                pathPoints.push({x: currentX, y: currentY});
                            }
                        }
                        break;
                    case 'C': // Cubic bezier
                        for (let i = 0; i < coords.length; i += 6) {
                            if (coords[i+5] !== undefined) {
                                // Get end point coordinates
                                currentX = coords[i+4];
                                currentY = coords[i+5];
                                pathPoints.push({x: currentX, y: currentY});
                            }
                        }
                        break;
                    case 'Q': // Quadratic bezier
                        for (let i = 0; i < coords.length; i += 4) {
                            if (coords[i+3] !== undefined) {
                                currentX = coords[i+2];
                                currentY = coords[i+3];
                                pathPoints.push({x: currentX, y: currentY});
                            }
                        }
                        break;
                }
            });

            if (pathPoints.length < 2) return;

            const pathStart = pathPoints[0];
            const pathEnd = pathPoints[pathPoints.length - 1];

            // Find the closest note to path start point
            let closestNote = null;
            let minDistToNote = Infinity;
            notes.forEach(note => {
                const dist = Math.sqrt(Math.pow(note.x - pathStart.x, 2) + Math.pow(note.y - pathStart.y, 2));
                if (dist < minDistToNote) {
                    minDistToNote = dist;
                    closestNote = note;
                }
            });

            // Find the closest class to path end point
            let targetClassName = null;
            let minDistToClass = Infinity;
            for (const currentClassName in classData) {
                const classInfo = classData[currentClassName];
                const classCenterX = classInfo.x;
                const classCenterY = classInfo.y;
                const classWidth = classInfo.width || 200; // Default width
                const classHeight = classInfo.height || 200; // Default height

                // Calculate distance from path end to class center
                const distToCenter = Math.sqrt(
                    Math.pow(pathEnd.x - classCenterX, 2) +
                    Math.pow(pathEnd.y - classCenterY, 2)
                );

                // Also calculate distance to class boundary
                const classLeft = classCenterX - classWidth/2;
                const classRight = classCenterX + classWidth/2;
                const classTop = classCenterY - classHeight/2;
                const classBottom = classCenterY + classHeight/2;

                const dx = Math.max(classLeft - pathEnd.x, 0, pathEnd.x - classRight);
                const dy = Math.max(classTop - pathEnd.y, 0, pathEnd.y - classBottom);
                const distToEdge = Math.sqrt(dx*dx + dy*dy);

                // Use the smaller distance as the judgment criterion
                const finalDist = Math.min(distToCenter, distToEdge + classWidth/4);

                if (finalDist < minDistToClass) {
                    minDistToClass = finalDist;
                    targetClassName = currentClassName;
                }
            }

            // Relax connection conditions
            if (closestNote && targetClassName &&
                minDistToNote < connectionThreshold &&
                minDistToClass < connectionThreshold * 2) {

                const existing = noteTargets[closestNote.id];
                const currentScore = minDistToNote + minDistToClass;

                if (!existing || currentScore < existing.score) {
                    noteTargets[closestNote.id] = {
                        name: targetClassName,
                        score: currentScore,
                        noteDistance: minDistToNote,
                        classDistance: minDistToClass
                    };
                }
            }
        });

        // 4. Add Note Definitions to Mermaid output
        const noteMermaidLines = [];
        notes.forEach(note => {
            const targetInfo = noteTargets[note.id];
            if (targetInfo && targetInfo.name) {
                noteMermaidLines.push(`    note for ${targetInfo.name} "${note.text}"`);
            } else {
                noteMermaidLines.push(`    note "${note.text}"`);
            }
        });
        // Insert notes after 'classDiagram' line
        if (noteMermaidLines.length > 0) {
            mermaidLines.splice(1, 0, ...noteMermaidLines);
        }

        // 5. Add Class Definitions
        for (const className in classData) {
            const data = classData[className];
            if (data.stereotype) {
                mermaidLines.push(`    class ${className} {`);
                mermaidLines.push(`        ${data.stereotype}`);
            } else {
                mermaidLines.push(`    class ${className} {`);
            }
            data.members.forEach(member => { mermaidLines.push(`        ${member}`); });
            data.methods.forEach(method => { mermaidLines.push(`        ${method}`); });
            mermaidLines.push('    }');
        }

        const pathElements = Array.from(svgElement.querySelectorAll('path.relation[id^="id_"]'));
        const labelElements = Array.from(svgElement.querySelectorAll('g.edgeLabels .edgeLabel foreignObject p'));

        pathElements.forEach((path, index) => {
            const id = path.getAttribute('id');
            if (!id || !id.startsWith('id_')) return;

            // Remove 'id_' prefix and trailing number (e.g., '_1')
            let namePart = id.substring(3).replace(/_\d+$/, '');

            const idParts = namePart.split('_');
            let fromClass = null;
            let toClass = null;

            // Iterate through possible split points to find valid class names
            for (let i = 1; i < idParts.length; i++) {
                const potentialFrom = idParts.slice(0, i).join('_');
                const potentialTo = idParts.slice(i).join('_');

                if (classData[potentialFrom] && classData[potentialTo]) {
                    fromClass = potentialFrom;
                    toClass = potentialTo;
                    break; // Found a valid pair
                }
            }

            if (!fromClass || !toClass) {
                console.error("Could not parse class relation from ID:", id);
                return; // Skip if we couldn't parse
            }

            // Get key attributes
            const markerEndAttr = path.getAttribute('marker-end') || "";
            const markerStartAttr = path.getAttribute('marker-start') || "";
            const pathClass = path.getAttribute('class') || "";

            // Determine line style: solid or dashed
            const isDashed = path.classList.contains('dashed-line') ||
                             path.classList.contains('dotted-line') ||
                             pathClass.includes('dashed') ||
                             pathClass.includes('dotted');
            const lineStyle = isDashed ? ".." : "--";

            let relationshipType = "";

            // Inheritance relation: <|-- or --|> (corrected inheritance relationship judgment)
            if (markerStartAttr.includes('extensionStart')) {
                // marker-start has extension, arrow at start point, means: toClass inherits fromClass
                if (isDashed) {
                    // Dashed inheritance (implementation relationship): fromClass <|.. toClass
                    relationshipType = `${fromClass} <|.. ${toClass}`;
                } else {
                    // Solid inheritance: fromClass <|-- toClass
                    relationshipType = `${fromClass} <|${lineStyle} ${toClass}`;
                }
            }
            else if (markerEndAttr.includes('extensionEnd')) {
                // marker-end has extension, arrow at end point, means: fromClass inherits toClass
                if (isDashed) {
                    // Dashed inheritance (implementation relationship): toClass <|.. fromClass
                    relationshipType = `${toClass} <|.. ${fromClass}`;
                } else {
                    // Solid inheritance: toClass <|-- fromClass
                    relationshipType = `${toClass} <|${lineStyle} ${fromClass}`;
                }
            }
            // Implementation relation: ..|> (corrected implementation relationship judgment)
            else if (markerStartAttr.includes('lollipopStart') || markerStartAttr.includes('implementStart')) {
                relationshipType = `${toClass} ..|> ${fromClass}`;
            }
            else if (markerEndAttr.includes('implementEnd') || markerEndAttr.includes('lollipopEnd') ||
                     (markerEndAttr.includes('interfaceEnd') && isDashed)) {
                relationshipType = `${fromClass} ..|> ${toClass}`;
            }
            // Composition relation: *-- (corrected composition relationship judgment)
            else if (markerStartAttr.includes('compositionStart')) {
                // marker-start has composition, diamond at start point, means: fromClass *-- toClass
                relationshipType = `${fromClass} *${lineStyle} ${toClass}`;
            }
            else if (markerEndAttr.includes('compositionEnd') ||
                     markerEndAttr.includes('diamondEnd') && markerEndAttr.includes('filled')) {
                relationshipType = `${toClass} *${lineStyle} ${fromClass}`;
            }
            // Aggregation relation: o-- (corrected aggregation relationship judgment)
            else if (markerStartAttr.includes('aggregationStart')) {
                // marker-start has aggregation, empty diamond at start point, means: toClass --o fromClass
                relationshipType = `${toClass} ${lineStyle}o ${fromClass}`;
            }
            else if (markerEndAttr.includes('aggregationEnd') ||
                     markerEndAttr.includes('diamondEnd') && !markerEndAttr.includes('filled')) {
                relationshipType = `${fromClass} o${lineStyle} ${toClass}`;
            }
            // Dependency relation: ..> or --> (corrected dependency relationship judgment)
            else if (markerStartAttr.includes('dependencyStart')) {
                if (isDashed) {
                    relationshipType = `${toClass} <.. ${fromClass}`;
                } else {
                    relationshipType = `${toClass} <-- ${fromClass}`;
                }
            }
            else if (markerEndAttr.includes('dependencyEnd')) {
                if (isDashed) {
                    relationshipType = `${fromClass} ..> ${toClass}`;
                } else {
                    relationshipType = `${fromClass} --> ${toClass}`;
                }
            }
            // Association relation: --> (corrected association relationship judgment)
            else if (markerStartAttr.includes('arrowStart') || markerStartAttr.includes('openStart')) {
                relationshipType = `${toClass} <${lineStyle} ${fromClass}`;
            }
            else if (markerEndAttr.includes('arrowEnd') || markerEndAttr.includes('openEnd')) {
                relationshipType = `${fromClass} ${lineStyle}> ${toClass}`;
            }
            // Arrowless solid line link: --
            else if (lineStyle === "--" && !markerEndAttr.includes('End') && !markerStartAttr.includes('Start')) {
                relationshipType = `${fromClass} -- ${toClass}`;
            }
            // Arrowless dashed line link: ..
            else if (lineStyle === ".." && !markerEndAttr.includes('End') && !markerStartAttr.includes('Start')) {
                relationshipType = `${fromClass} .. ${toClass}`;
            }
            // Default relation
            else {
                relationshipType = `${fromClass} ${lineStyle} ${toClass}`;
            }

            // Get relationship label text
            const labelText = (labelElements[index] && labelElements[index].textContent) ?
                               labelElements[index].textContent.trim() : "";

            if (relationshipType) {
                mermaidLines.push(`    ${relationshipType}${labelText ? ' : ' + labelText : ''}`);
            }
        });

        if (mermaidLines.length <= 1 && Object.keys(classData).length === 0 && notes.length === 0) return null;
        return '```mermaid\n' + mermaidLines.join('\n') + '\n```';
    }

    // Function for Sequence Diagram
    function convertSequenceDiagramSvgToMermaidText(svgElement) {
        if (!svgElement) return null;

        // 1. Parse participants
        const participants = [];
        console.log("Looking for sequence participants..."); // DEBUG

        // Find all participant text elements
        svgElement.querySelectorAll('text.actor-box').forEach((textEl) => {
            const name = textEl.textContent.trim().replace(/^"|"$/g, ''); // Remove quotes
            const x = parseFloat(textEl.getAttribute('x'));
            console.log("Found participant:", name, "at x:", x); // DEBUG
            if (name && !isNaN(x)) {
                participants.push({ name, x });
            }
        });

        console.log("Total participants found:", participants.length); // DEBUG
        participants.sort((a, b) => a.x - b.x);

        // Remove duplicate participants
        const uniqueParticipants = [];
        const seenNames = new Set();
        participants.forEach(p => {
            if (!seenNames.has(p.name)) {
                uniqueParticipants.push(p);
                seenNames.add(p.name);
            }
        });

        // 2. Parse Notes
        const notes = [];
        svgElement.querySelectorAll('g').forEach(g => {
            const noteRect = g.querySelector('rect.note');
            const noteText = g.querySelector('text.noteText');

            if (noteRect && noteText) {
                const text = noteText.textContent.trim();
                const x = parseFloat(noteRect.getAttribute('x'));
                const width = parseFloat(noteRect.getAttribute('width'));
                const leftX = x;
                const rightX = x + width;

                // Find all participants within note coverage range
                const coveredParticipants = [];
                uniqueParticipants.forEach(p => {
                    // Check if participant is within note's horizontal range
                    if (p.x >= leftX && p.x <= rightX) {
                        coveredParticipants.push(p);
                    }
                });

                // Sort by x coordinate
                coveredParticipants.sort((a, b) => a.x - b.x);

                if (coveredParticipants.length > 0) {
                    let noteTarget;
                    if (coveredParticipants.length === 1) {
                        // Single participant
                        noteTarget = coveredParticipants[0].name;
                    } else {
                        // Multiple participants, use first and last
                        const firstParticipant = coveredParticipants[0].name;
                        const lastParticipant = coveredParticipants[coveredParticipants.length - 1].name;
                        noteTarget = `${firstParticipant},${lastParticipant}`;
                    }

                    notes.push({
                        text: text,
                        target: noteTarget,
                        y: parseFloat(noteRect.getAttribute('y'))
                    });
                }
            }
        });

        // 3. Parse message lines and message text
        const messages = [];

        // Collect all message texts
        const messageTexts = [];
        svgElement.querySelectorAll('text.messageText').forEach(textEl => {
            const text = textEl.textContent.trim();
            const y = parseFloat(textEl.getAttribute('y'));
            const x = parseFloat(textEl.getAttribute('x'));
            if (text && !isNaN(y)) {
                messageTexts.push({ text, y, x });
            }
        });
        messageTexts.sort((a, b) => a.y - b.y);
        console.log("Found message texts:", messageTexts.length); // DEBUG

        // Collect all message lines
        const messageLines = [];
        svgElement.querySelectorAll('line.messageLine0, line.messageLine1').forEach(lineEl => {
            const x1 = parseFloat(lineEl.getAttribute('x1'));
            const y1 = parseFloat(lineEl.getAttribute('y1'));
            const x2 = parseFloat(lineEl.getAttribute('x2'));
            const y2 = parseFloat(lineEl.getAttribute('y2'));
            const isDashed = lineEl.classList.contains('messageLine1');

            if (!isNaN(x1) && !isNaN(y1) && !isNaN(x2) && !isNaN(y2)) {
                messageLines.push({ x1, y1, x2, y2, isDashed });
            }
        });

        // Collect all curved message paths (self messages)
        svgElement.querySelectorAll('path.messageLine0, path.messageLine1').forEach(pathEl => {
            const d = pathEl.getAttribute('d');
            const isDashed = pathEl.classList.contains('messageLine1');

            if (d) {
                // Parse path, check if it's a self message
                const moveMatch = d.match(/M\s*([^,\s]+)[,\s]+([^,\s]+)/);
                const endMatch = d.match(/([^,\s]+)[,\s]+([^,\s]+)$/);

                if (moveMatch && endMatch) {
                    const x1 = parseFloat(moveMatch[1]);
                    const y1 = parseFloat(moveMatch[2]);
                    const x2 = parseFloat(endMatch[1]);
                    const y2 = parseFloat(endMatch[2]);

                    // Check if it's a self message (start and end x coordinates are close)
                    if (Math.abs(x1 - x2) < 20) { // Allow some margin of error
                        messageLines.push({
                            x1, y1, x2, y2, isDashed,
                            isSelfMessage: true
                        });
                    }
                }
            }
        });

        messageLines.sort((a, b) => a.y1 - b.y1);
        console.log("Found message lines:", messageLines.length); // DEBUG

        // 4. Match message lines and message text
        for (let i = 0; i < Math.min(messageLines.length, messageTexts.length); i++) {
            const line = messageLines[i];
            const messageText = messageTexts[i];

            let fromParticipant = null;
            let toParticipant = null;

            if (line.isSelfMessage) {
                // Self message - find participant closest to x1
                let minDist = Infinity;
                for (const p of uniqueParticipants) {
                    const dist = Math.abs(p.x - line.x1);
                    if (dist < minDist) {
                        minDist = dist;
                        fromParticipant = toParticipant = p.name;
                    }
                }
            } else {
                // Find sender and receiver based on x coordinates
                let minDist1 = Infinity;
                for (const p of uniqueParticipants) {
                    const dist = Math.abs(p.x - line.x1);
                    if (dist < minDist1) {
                        minDist1 = dist;
                        fromParticipant = p.name;
                    }
                }

                let minDist2 = Infinity;
                for (const p of uniqueParticipants) {
                    const dist = Math.abs(p.x - line.x2);
                    if (dist < minDist2) {
                        minDist2 = dist;
                        toParticipant = p.name;
                    }
                }
            }

            if (fromParticipant && toParticipant) {
                // Determine arrow type
                let arrow;
                if (line.isDashed) {
                    arrow = '-->>'; // Dashed arrow
                } else {
                    arrow = '->>'; // Solid arrow
                }

                messages.push({
                    from: fromParticipant,
                    to: toParticipant,
                    text: messageText.text,
                    arrow: arrow,
                    y: line.y1,
                    isSelfMessage: line.isSelfMessage || false
                });

                console.log(`Message ${i + 1}: ${fromParticipant} ${arrow} ${toParticipant}: ${messageText.text}`); // DEBUG
            }
        }

        // 5. Parse loop areas
        const loops = [];
        const loopLines = svgElement.querySelectorAll('line.loopLine');
        if (loopLines.length >= 4) {
            const xs = Array.from(loopLines).map(line => [
                parseFloat(line.getAttribute('x1')),
                parseFloat(line.getAttribute('x2'))
            ]).flat();
            const ys = Array.from(loopLines).map(line => [
                parseFloat(line.getAttribute('y1')),
                parseFloat(line.getAttribute('y2'))
            ]).flat();

            const xMin = Math.min(...xs);
            const xMax = Math.max(...xs);
            const yMin = Math.min(...ys);
            const yMax = Math.max(...ys);

            let loopText = '';
            const loopTextEl = svgElement.querySelector('.loopText');
            if (loopTextEl) {
                loopText = loopTextEl.textContent.trim();
            }

            loops.push({ xMin, xMax, yMin, yMax, text: loopText });
            console.log("Found loop:", loopText, "from y", yMin, "to", yMax); // DEBUG
        }

        // 6. Generate Mermaid code
        let mermaidOutput = "sequenceDiagram\n";

        // Add participants
        uniqueParticipants.forEach(p => {
            mermaidOutput += `  participant ${p.name}\n`;
        });
        mermaidOutput += "\n";

        // Sort all events by y coordinate (messages, notes, loops)
        const events = [];

        messages.forEach(msg => {
            events.push({ type: 'message', y: msg.y, data: msg });
        });

        notes.forEach(note => {
            events.push({ type: 'note', y: note.y, data: note });
        });

        loops.forEach(loop => {
            events.push({ type: 'loop_start', y: loop.yMin - 1, data: loop });
            events.push({ type: 'loop_end', y: loop.yMax + 1, data: loop });
        });

        events.sort((a, b) => a.y - b.y);

        // Generate events
        let loopStack = [];
        events.forEach(event => {
            if (event.type === 'loop_start') {
                const text = event.data.text ? ` ${event.data.text}` : '';
                mermaidOutput += `  loop${text}\n`;
                loopStack.push(event.data);
            } else if (event.type === 'loop_end') {
                if (loopStack.length > 0) {
                    mermaidOutput += `  end\n`;
                    loopStack.pop();
                }
            } else if (event.type === 'note') {
                const indent = loopStack.length > 0 ? '  ' : '';
                mermaidOutput += `${indent}  note over ${event.data.target}: ${event.data.text}\n`;
            } else if (event.type === 'message') {
                const indent = loopStack.length > 0 ? '  ' : '';
                const msg = event.data;
                mermaidOutput += `${indent}  ${msg.from}${msg.arrow}${msg.to}: ${msg.text}\n`;
            }
        });

        // Close remaining loops
        while (loopStack.length > 0) {
            mermaidOutput += `  end\n`;
            loopStack.pop();
        }

        if (uniqueParticipants.length === 0 && messages.length === 0) return null;
        console.log("Sequence diagram conversion completed. Participants:", uniqueParticipants.length, "Messages:", messages.length, "Notes:", notes.length); // DEBUG
        console.log("Generated sequence mermaid code:", mermaidOutput.substring(0, 200) + "..."); // DEBUG
        return '```mermaid\n' + mermaidOutput.trim() + '\n```';
    }

    // Function for State Diagram
    function convertStateDiagramSvgToMermaidText(svgElement) {
        if (!svgElement) return null;

        console.log("Converting state diagram...");

        const nodes = [];

        // 1. Parse all states
        svgElement.querySelectorAll('g.node.statediagram-state').forEach(stateEl => {
            const stateName = stateEl.querySelector('foreignObject .nodeLabel p, foreignObject .nodeLabel span')?.textContent.trim();
            if (!stateName) return;

            const transform = stateEl.getAttribute('transform');
            const rect = stateEl.querySelector('rect.basic.label-container');
            if (!transform || !rect) return;

            const transformMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
            if (!transformMatch) return;

            const tx = parseFloat(transformMatch[1]);
            const ty = parseFloat(transformMatch[2]);
            const rx = parseFloat(rect.getAttribute('x'));
            const ry = parseFloat(rect.getAttribute('y'));
            const width = parseFloat(rect.getAttribute('width'));
            const height = parseFloat(rect.getAttribute('height'));

            nodes.push({
                name: stateName,
                x1: tx + rx,
                y1: ty + ry,
                x2: tx + rx + width,
                y2: ty + ry + height
            });
            console.log(`Found State: ${stateName}`, nodes[nodes.length-1]);
        });

        // 2. Find start state
        const startStateEl = svgElement.querySelector('g.node.default circle.state-start');
        if (startStateEl) {
            const startGroup = startStateEl.closest('g.node');
            const transform = startGroup.getAttribute('transform');
            const transformMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
            const r = parseFloat(startStateEl.getAttribute('r'));
            if (transformMatch && r) {
                const tx = parseFloat(transformMatch[1]);
                const ty = parseFloat(transformMatch[2]);
                nodes.push({
                    name: '[*]',
                    x1: tx - r,
                    y1: ty - r,
                    x2: tx + r,
                    y2: ty + r,
                    isSpecial: true
                });
                console.log("Found Start State", nodes[nodes.length-1]);
            }
        }

        // 3. Find end state
        svgElement.querySelectorAll('g.node.default').forEach(endGroup => {
            if (endGroup.querySelectorAll('path').length >= 2) {
                const transform = endGroup.getAttribute('transform');
                if(transform) {
                    const transformMatch = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                    if (transformMatch) {
                        const tx = parseFloat(transformMatch[1]);
                        const ty = parseFloat(transformMatch[2]);
                        const r = 7; // Mermaid end circle radius is 7
                        nodes.push({
                            name: '[*]',
                            x1: tx - r,
                            y1: ty - r,
                            x2: tx + r,
                            y2: ty + r,
                            isSpecial: true
                        });
                        console.log("Found End State", nodes[nodes.length-1]);
                    }
                }
            }
        });

        // 4. Get all labels
        const labels = [];
        svgElement.querySelectorAll('g.edgeLabel').forEach(labelEl => {
            const text = labelEl.querySelector('foreignObject .edgeLabel p, foreignObject .edgeLabel span')?.textContent.trim().replace(/^"|"$/g, '');
            const transform = labelEl.getAttribute('transform');
            if (text && transform) {
                const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                if (match) {
                    labels.push({
                        text: text,
                        x: parseFloat(match[1]),
                        y: parseFloat(match[2])
                    });
                }
            }
        });

        function getDistanceToBox(px, py, box) {
            const dx = Math.max(box.x1 - px, 0, px - box.x2);
            const dy = Math.max(box.y1 - py, 0, py - box.y2);
            return Math.sqrt(dx * dx + dy * dy);
        }

        function getDistance(x1, y1, x2, y2) {
            return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
        }

        const transitions = [];

        // 5. Process paths
        svgElement.querySelectorAll('path.transition').forEach(pathEl => {
            const dAttr = pathEl.getAttribute('d');
            if (!dAttr) return;

            const startMatch = dAttr.match(/M\s*([^,\s]+)[,\s]+([^,\s]+)/);
            // More robustly find the last coordinate pair in the d string
            const pathSegments = dAttr.split(/[A-Za-z]/);
            const lastSegment = pathSegments[pathSegments.length-1].trim();
            const endCoords = lastSegment.split(/[\s,]+/).map(parseFloat);

            if (!startMatch || endCoords.length < 2) return;

            const startX = parseFloat(startMatch[1]);
            const startY = parseFloat(startMatch[2]);
            const endX = endCoords[endCoords.length - 2];
            const endY = endCoords[endCoords.length - 1];

            let sourceNode = null, targetNode = null;
            let minSourceDist = Infinity, minTargetDist = Infinity;

            nodes.forEach(node => {
                const distToStart = getDistanceToBox(startX, startY, node);
                if (distToStart < minSourceDist) {
                    minSourceDist = distToStart;
                    sourceNode = node;
                }
                const distToEnd = getDistanceToBox(endX, endY, node);
                if (distToEnd < minTargetDist) {
                    minTargetDist = distToEnd;
                    targetNode = node;
                }
            });

            let transitionLabel = '';
            if (sourceNode && targetNode && (minSourceDist < 5) && (minTargetDist < 5)) {
                // Find label
                const midX = (startX + endX) / 2;
                const midY = (startY + endY) / 2;
                let closestLabel = null;
                let minLabelDist = Infinity;

                labels.forEach(label => {
                    const dist = getDistance(midX, midY, label.x, label.y);
                    if (dist < minLabelDist) {
                        minLabelDist = dist;
                        closestLabel = label;
                    }
                });

                if (closestLabel && minLabelDist < 150) { // Arbitrary threshold, seems to work
                    transitionLabel = closestLabel.text;
                }

                if(sourceNode === targetNode) return; // Ignore self-loops for now

                const newTransition = {
                    from: sourceNode.name,
                    to: targetNode.name,
                    label: transitionLabel
                };

                // Avoid adding duplicates
                if (!transitions.some(t => t.from === newTransition.from && t.to === newTransition.to && t.label === newTransition.label)) {
                    transitions.push(newTransition);
                }
            }
        });

        // 6. Generate Mermaid code
        let mermaidCode = "stateDiagram-v2\n";
        transitions.forEach(t => {
            let line = `    ${t.from} --> ${t.to}`;
            if (t.label) {
                line += ` : "${t.label}"`;
            }
            mermaidCode += line + '\n';
        });

        if (transitions.length === 0) return null;

        console.log("State diagram conversion completed. Transitions:", transitions.length);
        console.log("Generated state diagram mermaid code:", mermaidCode);

        return '```mermaid\n' + mermaidCode.trim() + '\n```';
    }

    // Main processNode function
    function processNode(node) {
        let resultMd = "";

        if (node.nodeType === Node.TEXT_NODE) {
            if (node.parentNode && node.parentNode.nodeName === 'PRE') { return node.textContent; }
            return node.textContent;
        }

        if (node.nodeType !== Node.ELEMENT_NODE) return "";

        const element = node;
        const style = window.getComputedStyle(element);

        if (
            (style.display === "none" || style.visibility === "hidden") &&
            !["DETAILS", "SUMMARY"].includes(element.nodeName)
        ) {
            return "";
        }

        if (element.matches('button, [role="button"], nav, footer, aside, script, style, noscript, iframe, embed, object, header')) {
            return "";
        }
        if (element.classList.contains("bg-input-dark") && element.querySelector("svg")){
            return "";
        }

        try {
            switch (element.nodeName) {
                case "P": {
                    let txt = "";
                    element.childNodes.forEach((c) => {
                        try { txt += processNode(c); } catch (e) { console.error("Error processing child of P:", c, e); txt += "[err]";}
                    });
                    txt = txt.trim();
                    if (txt.startsWith("```mermaid") && txt.endsWith("```")) {
                        resultMd = txt + "\n\n";
                    } else if (txt) {
                        resultMd = txt + "\n\n";
                    } else {
                        resultMd = "\n";
                    }
                    break;
                }
                case "H1": resultMd = (element.textContent.trim() ? `# ${element.textContent.trim()}\n\n` : ""); break;
                case "H2": resultMd = (element.textContent.trim() ? `## ${element.textContent.trim()}\n\n` : ""); break;
                case "H3": resultMd = (element.textContent.trim() ? `### ${element.textContent.trim()}\n\n` : ""); break;
                case "H4": resultMd = (element.textContent.trim() ? `#### ${element.textContent.trim()}\n\n` : ""); break;
                case "H5": resultMd = (element.textContent.trim() ? `##### ${element.textContent.trim()}\n\n` : ""); break;
                case "H6": resultMd = (element.textContent.trim() ? `###### ${element.textContent.trim()}\n\n` : ""); break;
                case "UL": {
                    let list = "";
                    const isSourceList = (
                        (element.previousElementSibling && /source/i.test(element.previousElementSibling.textContent)) ||
                        (element.parentElement && /source/i.test(element.parentElement.textContent)) ||
                        element.classList.contains('source-list')
                    );
                    element.querySelectorAll(":scope > li").forEach((li) => {
                        let liTxt = "";
                        li.childNodes.forEach((c) => { try { liTxt += processNode(c); } catch (e) { console.error("Error processing child of LI:", c, e); liTxt += "[err]";}});
                        if (isSourceList) {
                            liTxt = liTxt.trim().replace(/\n+/g, ' ');
                        } else {
                            liTxt = liTxt.trim().replace(/\n\n$/, "").replace(/^\n\n/, "");
                        }
                        if (liTxt) list += `* ${liTxt}\n`;
                    });
                    resultMd = list + (list ? "\n" : "");
                    break;
                }
                case "OL": {
                    let list = "";
                    let i = 1;
                    const isSourceList = (
                        (element.previousElementSibling && /source/i.test(element.previousElementSibling.textContent)) ||
                        (element.parentElement && /source/i.test(element.parentElement.textContent)) ||
                        element.classList.contains('source-list')
                    );
                    element.querySelectorAll(":scope > li").forEach((li) => {
                        let liTxt = "";
                        li.childNodes.forEach((c) => { try { liTxt += processNode(c); } catch (e) { console.error("Error processing child of LI:", c, e); liTxt += "[err]";}});
                        if (isSourceList) {
                            liTxt = liTxt.trim().replace(/\n+/g, ' ');
                        } else {
                            liTxt = liTxt.trim().replace(/\n\n$/, "").replace(/^\n\n/, "");
                        }
                        if (liTxt) {
                            list += `${i}. ${liTxt}\n`;
                            i++;
                        }
                    });
                    resultMd = list + (list ? "\n" : "");
                    break;
                }
                case "PRE": {
                    const svgElement = element.querySelector('svg[id^="mermaid-"]');
                    let mermaidOutput = null;

                    if (svgElement) {
                        const diagramTypeDesc = svgElement.getAttribute('aria-roledescription');
                        const diagramClass = svgElement.getAttribute('class');

                        console.log("Found SVG in PRE: desc=", diagramTypeDesc, "class=", diagramClass);
                        if (diagramTypeDesc && diagramTypeDesc.includes('flowchart')) {
                            console.log("Trying to convert flowchart...");
                            mermaidOutput = convertFlowchartSvgToMermaidText(svgElement);
                        } else if (diagramTypeDesc && diagramTypeDesc.includes('class')) {
                            console.log("Trying to convert class diagram...");
                            mermaidOutput = convertClassDiagramSvgToMermaidText(svgElement);
                        } else if (diagramTypeDesc && diagramTypeDesc.includes('sequence')) {
                            console.log("Trying to convert sequence diagram...");
                            mermaidOutput = convertSequenceDiagramSvgToMermaidText(svgElement);
                        } else if (diagramTypeDesc && diagramTypeDesc.includes('stateDiagram')) {
                            console.log("Trying to convert state diagram...");
                            mermaidOutput = convertStateDiagramSvgToMermaidText(svgElement);
                        } else if (diagramClass && diagramClass.includes('flowchart')) {
                            console.log("Trying to convert flowchart by class...");
                            mermaidOutput = convertFlowchartSvgToMermaidText(svgElement);
                        } else if (diagramClass && (diagramClass.includes('classDiagram') || diagramClass.includes('class'))) {
                            console.log("Trying to convert class diagram by class...");
                            mermaidOutput = convertClassDiagramSvgToMermaidText(svgElement);
                        } else if (diagramClass && (diagramClass.includes('sequenceDiagram') || diagramClass.includes('sequence'))) {
                            console.log("Trying to convert sequence diagram by class...");
                            mermaidOutput = convertSequenceDiagramSvgToMermaidText(svgElement);
                        } else if (diagramClass && (diagramClass.includes('statediagram') || diagramClass.includes('stateDiagram'))) {
                            console.log("Trying to convert state diagram by class...");
                            mermaidOutput = convertStateDiagramSvgToMermaidText(svgElement);
                        }

                        if (mermaidOutput) {
                            console.log("Successfully converted SVG to mermaid:", mermaidOutput.substring(0, 100) + "...");
                        } else {
                            console.log("Failed to convert SVG, using fallback");
                        }
                    }

                    if (mermaidOutput) {
                        resultMd = `\n${mermaidOutput}\n\n`;
                    } else {
                        const code = element.querySelector("code");
                        let lang = "";
                        let txt = "";
                        if (code) {
                            txt = code.textContent;
                            const cls = Array.from(code.classList).find((c) => c.startsWith("language-"));
                            if (cls) lang = cls.replace("language-", "");
                        } else {
                            txt = element.textContent;
                        }
                        if (!lang) {
                            const preCls = Array.from(element.classList).find((c) => c.startsWith("language-"));
                            if (preCls) lang = preCls.replace("language-", "");
                        }
                        if (!lang && txt.trim()) {
                            lang = detectCodeLanguage(txt);
                        }
                        resultMd = `\`\`\`${lang}\n${txt.trim()}\n\`\`\`\n\n`;
                    }
                    break;
                }
                case "A": {
                    const href = element.getAttribute("href");
                    let initialTextFromNodes = "";
                    element.childNodes.forEach(c => {
                        try {
                            initialTextFromNodes += processNode(c);
                        } catch (e) {
                            console.error("Error processing child of A:", c, e);
                            initialTextFromNodes += "[err]";
                        }
                    });
                    let text = initialTextFromNodes.trim();

                    if (!text && element.querySelector('img')) {
                        text = element.querySelector('img').alt || 'image';
                    }

                    if (href && (href.startsWith('http') || href.startsWith('https') || href.startsWith('/') || href.startsWith('#') || href.startsWith('mailto:'))) {

                        let finalLinkDisplayText = text;

                        const lineInfoMatch = href.match(/#L(\d+)(?:-L(\d+))?$/);

                        if (lineInfoMatch) {
                            const pathPart = href.substring(0, href.indexOf('#'));
                            let filenameFromPath = pathPart.substring(pathPart.lastIndexOf('/') + 1) || "link";

                            const startLine = lineInfoMatch[1];
                            const endLine = lineInfoMatch[2];

                            let displayFilename = filenameFromPath;

                            const trimmedInitialText = initialTextFromNodes.trim();
                            let textToParseForFilename = trimmedInitialText;

                            const isSourcesContext = trimmedInitialText.startsWith("Sources: [") && trimmedInitialText.endsWith("]");

                            if (isSourcesContext) {
                                const sourcesContentMatch = trimmedInitialText.match(/^Sources:\s+\[(.*)\]$/);
                                if (sourcesContentMatch && sourcesContentMatch[1]) {
                                    textToParseForFilename = sourcesContentMatch[1].trim();
                                }
                            }

                            const filenameHintMatch = textToParseForFilename.match(/^[\w\/\.-]+(?:\.\w+)?/);
                            if (filenameHintMatch && filenameHintMatch[0]) {
                                if (pathPart.includes(filenameHintMatch[0])) {
                                    displayFilename = filenameHintMatch[0];
                                }
                            }

                            let lineRefText;
                            if (endLine && endLine !== startLine) {
                                lineRefText = `L${startLine}-L${endLine}`;
                            } else {
                                lineRefText = `L${startLine}`;
                            }

                            let constructedText = `${displayFilename} ${lineRefText}`;

                            if (isSourcesContext) {
                                finalLinkDisplayText = `Sources: [${constructedText}]`;
                            } else {
                                finalLinkDisplayText = constructedText;
                            }
                        }

                        text = finalLinkDisplayText.trim() || (href ? href : "");

                        resultMd = `[${text}](${href})`;
                        if (window.getComputedStyle(element).display !== "inline") {
                            resultMd += "\n\n";
                        }
                    } else {
                        text = text.trim() || (href ? href : "");
                        resultMd = text;
                        if (window.getComputedStyle(element).display !== "inline" && text.trim()) {
                            resultMd += "\n\n";
                        }
                    }
                    break;
                }
                case "IMG":
                    if (element.closest && element.closest('a')) return "";
                    resultMd = (element.src ? `![${element.alt || ""}](${element.src})\n\n` : "");
                    break;
                case "BLOCKQUOTE": {
                    let qt = "";
                    element.childNodes.forEach((c) => { try { qt += processNode(c); } catch (e) { console.error("Error processing child of BLOCKQUOTE:", c, e); qt += "[err]";}});
                    const trimmedQt = qt.trim();
                    if (trimmedQt) {
                        resultMd = trimmedQt.split("\n").map((l) => `> ${l.trim() ? l : ''}`).filter(l => l.trim() !== '>').join("\n") + "\n\n";
                    } else {
                        resultMd = "";
                    }
                    break;
                }
                case "HR":
                    resultMd = "\n---\n\n";
                    break;
                case "STRONG":
                case "B": {
                    let st = "";
                    element.childNodes.forEach((c) => { try { st += processNode(c); } catch (e) { console.error("Error processing child of STRONG/B:", c, e); st += "[err]";}});
                    return `**${st.trim()}**`;
                }
                case "EM":
                case "I": {
                    let em = "";
                    element.childNodes.forEach((c) => { try { em += processNode(c); } catch (e) { console.error("Error processing child of EM/I:", c, e); em += "[err]";}});
                    return `*${em.trim()}*`;
                }
                case "CODE": {
                    if (element.parentNode && element.parentNode.nodeName === 'PRE') {
                        return element.textContent;
                    }
                    return `\`${element.textContent.trim()}\``;
                }
                case "BR":
                    if (element.parentNode && ['P', 'DIV', 'LI'].includes(element.parentNode.nodeName) ) {
                        const nextSibling = element.nextSibling;
                        if (!nextSibling || (nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim() !== '') || nextSibling.nodeType === Node.ELEMENT_NODE) {
                            return "  \n";
                        }
                    }
                    return "";
                case "TABLE": {
                    let tableMd = "";
                    const headerRows = Array.from(element.querySelectorAll(':scope > thead > tr, :scope > tr:first-child'));
                    const bodyRows = Array.from(element.querySelectorAll(':scope > tbody > tr'));
                    const allRows = Array.from(element.rows);

                    let rowsToProcessForHeader = headerRows;
                    if (headerRows.length === 0 && allRows.length > 0) {
                        rowsToProcessForHeader = [allRows[0]];
                    }

                    if (rowsToProcessForHeader.length > 0) {
                        const headerRowElement = rowsToProcessForHeader[0];
                        let headerContent = "|"; let separator = "|";
                        Array.from(headerRowElement.cells).forEach(cell => {
                            let cellText = ""; cell.childNodes.forEach(c => { try { cellText += processNode(c); } catch (e) { console.error("Error processing child of TH/TD (Header):", c, e); cellText += "[err]";}});
                            headerContent += ` ${cellText.trim().replace(/\|/g, "\\|")} |`; separator += ` --- |`;
                        });
                        tableMd += `${headerContent}\n${separator}\n`;
                    }

                    let rowsToProcessForBody = bodyRows;
                    if (bodyRows.length === 0 && allRows.length > (headerRows.length > 0 ? 1 : 0) ) {
                        rowsToProcessForBody = headerRows.length > 0 ? allRows.slice(1) : allRows;
                    }

                    rowsToProcessForBody.forEach(row => {
                        if (rowsToProcessForHeader.length > 0 && rowsToProcessForHeader.includes(row)) return;

                        let rowContent = "|";
                        Array.from(row.cells).forEach(cell => {
                            let cellText = ""; cell.childNodes.forEach(c => { try { cellText += processNode(c); } catch (e) { console.error("Error processing child of TH/TD (Body):", c, e); cellText += "[err]";}});
                            rowContent += ` ${cellText.trim().replace(/\|/g, "\\|").replace(/\n+/g, ' <br> ')} |`;
                        });
                        tableMd += `${rowContent}\n`;
                    });
                    resultMd = tableMd + (tableMd ? "\n" : "");
                    break;
                }
                case "THEAD": case "TBODY": case "TFOOT": case "TR": case "TH": case "TD":
                    return "";

                case "DETAILS": {
                    let summaryText = "Details"; const summaryElem = element.querySelector('summary');
                    if (summaryElem) { let tempSummary = ""; summaryElem.childNodes.forEach(c => { try { tempSummary += processNode(c); } catch (e) { console.error("Error processing child of SUMMARY:", c, e); tempSummary += "[err]";}}); summaryText = tempSummary.trim() || "Details"; }
                    let detailsContent = "";
                    Array.from(element.childNodes).forEach(child => { if (child.nodeName !== "SUMMARY") { try { detailsContent += processNode(child); } catch (e) { console.error("Error processing child of DETAILS:", c, e); detailsContent += "[err]";}}});
                    resultMd = `> **${summaryText}**\n${detailsContent.trim().split('\n').map(l => `> ${l}`).join('\n')}\n\n`;
                    break;
                }
                case "SUMMARY": return "";

                case "DIV":
                case "SPAN":
                case "SECTION":
                case "ARTICLE":
                case "MAIN":
                default: {
                    let txt = "";
                    element.childNodes.forEach((c) => { try { txt += processNode(c); } catch (e) { console.error("Error processing child of DEFAULT case:", c, element.nodeName, e); txt += "[err]";}});

                    const d = window.getComputedStyle(element);
                    const isBlock = ["block", "flex", "grid", "list-item", "table",
                                     "table-row-group", "table-header-group", "table-footer-group"].includes(d.display);

                    if (isBlock && txt.trim()) {
                        if (txt.endsWith('\n\n')) {
                            resultMd = txt;
                        } else if (txt.endsWith('\n')) {
                            resultMd = txt + '\n';
                        } else {
                            resultMd = txt.trimEnd() + "\n\n";
                        }
                    } else {
                        return txt;
                    }
                }
            }
        } catch (error) {
            console.error("Unhandled error in processNode for element:", element.nodeName, element, error);
            return `\n[ERROR_PROCESSING_ELEMENT: ${element.nodeName}]\n\n`;
        }
        return resultMd;
    }

    // ==================== MAIN CONVERSION FUNCTION ====================

    function convertPageToMarkdown() {
        try {
            const headTitle = document.title || "";
            const formattedHeadTitle = headTitle.replace(/[\/|]/g, '-').replace(/\s+/g, '-').replace('---','-');

            const title =
                document.querySelector('.container > div:nth-child(1) a[data-selected="true"]')?.textContent?.trim() ||
                document.querySelector(".container > div:nth-child(1) h1")?.textContent?.trim() ||
                document.querySelector("h1")?.textContent?.trim() ||
                "Untitled";

            const contentContainer =
                document.querySelector(".container > div:nth-child(2) .prose") ||
                document.querySelector(".container > div:nth-child(2) .prose-custom") ||
                document.querySelector(".container > div:nth-child(2)") ||
                document.body;

            let markdown = ``;
            let markdownTitle = title.replace(/\s+/g, '-');

            contentContainer.childNodes.forEach((child) => {
                markdown += processNode(child);
            });

            markdown = markdown.trim().replace(/\n{3,}/g, "\n\n");

            return {
                success: true,
                markdown,
                markdownTitle,
                headTitle: formattedHeadTitle
            };
        } catch (error) {
            console.error("Error converting to Markdown:", error);
            return { success: false, error: error.message };
        }
    }

    function extractAllPages() {
        try {
            const headTitle = document.title || "";
            const formattedHeadTitle = headTitle.replace(/[\/|]/g, '-').replace(/\s+/g, '-').replace('---','-');

            const baseUrl = window.location.origin;

            const sidebarLinks = Array.from(document.querySelectorAll('.border-r-border ul li a'));

            const pages = sidebarLinks.map(link => {
                return {
                    url: new URL(link.getAttribute('href'), baseUrl).href,
                    title: link.textContent.trim(),
                    selected: link.getAttribute('data-selected') === 'true'
                };
            });

            const currentPageTitle =
                document.querySelector('.container > div:nth-child(1) a[data-selected="true"]')?.textContent?.trim() ||
                document.querySelector(".container > div:nth-child(1) h1")?.textContent?.trim() ||
                document.querySelector("h1")?.textContent?.trim() ||
                "Untitled";

            return {
                success: true,
                pages: pages,
                currentTitle: currentPageTitle,
                baseUrl: baseUrl,
                headTitle: formattedHeadTitle
            };
        } catch (error) {
            console.error("Error extracting page links:", error);
            return { success: false, error: error.message };
        }
    }

    // ==================== UI INJECTION ====================

    function createUI() {
        // Create floating panel
        const panel = document.createElement('div');
        panel.id = 'deepwiki-md-panel';
        panel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            border: 2px solid #4CAF50;
            border-radius: 8px;
            padding: 12px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            z-index: 10000;
            font-family: Arial, sans-serif;
            width: 200px;
        `;

        panel.innerHTML = `
            <div style="margin-bottom: 10px; font-weight: bold; color: #333;">DeepWiki to Markdown</div>
            <button id="dw-convert-btn" style="width: 100%; padding: 6px 10px; margin-bottom: 6px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Convert Current Page</button>
            <button id="dw-batch-btn" style="width: 100%; padding: 6px 10px; margin-bottom: 6px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Batch Download All</button>
            <button id="dw-cancel-btn" style="width: 100%; padding: 6px 10px; margin-bottom: 6px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; display: none;">Cancel</button>
            <div id="dw-status" style="margin-top: 8px; font-size: 11px; color: #666; line-height: 1.3;"></div>
        `;

        document.body.appendChild(panel);

        // Event listeners
        document.getElementById('dw-convert-btn').addEventListener('click', handleConvertCurrent);
        document.getElementById('dw-batch-btn').addEventListener('click', handleBatchConvert);
        document.getElementById('dw-cancel-btn').addEventListener('click', handleCancel);
    }

    // ==================== EVENT HANDLERS ====================

    let isCancelled = false;
    let convertedPages = [];

    function updateStatus(message, type = 'info') {
        const statusEl = document.getElementById('dw-status');
        statusEl.textContent = message;
        statusEl.style.color = type === 'error' ? '#f44336' : (type === 'success' ? '#4CAF50' : '#666');
    }

    function handleConvertCurrent() {
        updateStatus('Converting page...', 'info');
        const result = convertPageToMarkdown();

        if (result.success) {
            const fileName = result.headTitle
                ? `${result.headTitle}-${result.markdownTitle}.md`
                : `${result.markdownTitle}.md`;

            downloadFile(result.markdown, fileName);
            updateStatus('Conversion successful! Downloading...', 'success');
        } else {
            updateStatus('Conversion failed: ' + result.error, 'error');
        }
    }

    async function handleBatchConvert() {
        isCancelled = false;
        document.getElementById('dw-cancel-btn').style.display = 'block';
        document.getElementById('dw-batch-btn').disabled = true;

        updateStatus('Extracting all page links...', 'info');

        const extractResult = extractAllPages();

        if (!extractResult.success) {
            updateStatus('Failed to extract page links: ' + extractResult.error, 'error');
            document.getElementById('dw-cancel-btn').style.display = 'none';
            document.getElementById('dw-batch-btn').disabled = false;
            return;
        }

        const allPages = extractResult.pages;
        const folderName = extractResult.headTitle || extractResult.currentTitle.replace(/\s+/g, '-');

        convertedPages = [];
        updateStatus(`Found ${allPages.length} pages, starting batch conversion`, 'info');

        let processedCount = 0;
        let errorCount = 0;

        for (const page of allPages) {
            if (isCancelled) {
                updateStatus(`Operation cancelled. Processed: ${processedCount}, Failed: ${errorCount}`, 'info');
                document.getElementById('dw-cancel-btn').style.display = 'none';
                document.getElementById('dw-batch-btn').disabled = false;
                return;
            }

            try {
                updateStatus(`Processing ${processedCount + 1}/${allPages.length}: ${page.title}`, 'info');

                // Fetch page content via AJAX
                const htmlContent = await fetchPageContent(page.url);

                if (!htmlContent) {
                    errorCount++;
                    console.error(`Failed to fetch page: ${page.title}`);
                    continue;
                }

                if (isCancelled) {
                    updateStatus(`Operation cancelled. Processed: ${processedCount}, Failed: ${errorCount}`, 'info');
                    document.getElementById('dw-cancel-btn').style.display = 'none';
                    document.getElementById('dw-batch-btn').disabled = false;
                    return;
                }

                // Parse and convert the fetched HTML
                const convertResult = convertHTMLToMarkdown(htmlContent);

                if (convertResult.success) {
                    convertedPages.push({
                        title: convertResult.markdownTitle || page.title.replace(/\s+/g, '-'),
                        content: convertResult.markdown
                    });
                    processedCount++;
                } else {
                    errorCount++;
                    console.error(`Page processing failed: ${page.title}`, convertResult.error);
                }
            } catch (err) {
                errorCount++;
                console.error(`Error processing page: ${page.title}`, err);
            }
        }

        if (!isCancelled && convertedPages.length > 0) {
            updateStatus(`Batch conversion complete! Success: ${processedCount}, Failed: ${errorCount}, Creating ZIP...`, 'success');
            await downloadAllPagesAsZip(folderName);
        }

        document.getElementById('dw-cancel-btn').style.display = 'none';
        document.getElementById('dw-batch-btn').disabled = false;
    }

    // Fetch page content using GM_xmlhttpRequest
    function fetchPageContent(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(response.responseText);
                    } else {
                        console.error(`Failed to fetch ${url}: ${response.status}`);
                        resolve(null);
                    }
                },
                onerror: function(error) {
                    console.error(`Error fetching ${url}:`, error);
                    resolve(null);
                }
            });
        });
    }

    // Convert fetched HTML to Markdown
    function convertHTMLToMarkdown(htmlString) {
        try {
            // Create a temporary DOM parser
            const parser = new DOMParser();
            const doc = parser.parseFromString(htmlString, 'text/html');

            // Extract title
            const headTitle = doc.title || "";
            const formattedHeadTitle = headTitle.replace(/[\/|]/g, '-').replace(/\s+/g, '-').replace('---','-');

            const title =
                doc.querySelector('.container > div:nth-child(1) a[data-selected="true"]')?.textContent?.trim() ||
                doc.querySelector(".container > div:nth-child(1) h1")?.textContent?.trim() ||
                doc.querySelector("h1")?.textContent?.trim() ||
                "Untitled";

            const contentContainer =
                doc.querySelector(".container > div:nth-child(2) .prose") ||
                doc.querySelector(".container > div:nth-child(2) .prose-custom") ||
                doc.querySelector(".container > div:nth-child(2)") ||
                doc.body;

            let markdown = ``;
            let markdownTitle = title.replace(/\s+/g, '-');

            contentContainer.childNodes.forEach((child) => {
                markdown += processNodeFromDoc(child, doc);
            });

            markdown = markdown.trim().replace(/\n{3,}/g, "\n\n");

            return {
                success: true,
                markdown,
                markdownTitle,
                headTitle: formattedHeadTitle
            };
        } catch (error) {
            console.error("Error converting HTML to Markdown:", error);
            return { success: false, error: error.message };
        }
    }

    // Process node from parsed document (doesn't use getComputedStyle)
    function processNodeFromDoc(node, doc) {
        let resultMd = "";

        if (node.nodeType === Node.TEXT_NODE) {
            if (node.parentNode && node.parentNode.nodeName === 'PRE') { return node.textContent; }
            return node.textContent;
        }

        if (node.nodeType !== Node.ELEMENT_NODE) return "";

        const element = node;

        // Check style attribute for hidden elements
        const styleAttr = element.getAttribute('style') || '';
        if (styleAttr.includes('display: none') || styleAttr.includes('visibility: hidden')) {
            if (!["DETAILS", "SUMMARY"].includes(element.nodeName)) {
                return "";
            }
        }

        if (element.matches('button, [role="button"], nav, footer, aside, script, style, noscript, iframe, embed, object, header')) {
            return "";
        }
        if (element.classList.contains("bg-input-dark") && element.querySelector("svg")){
            return "";
        }

        try {
            switch (element.nodeName) {
                case "P": {
                    let txt = "";
                    element.childNodes.forEach((c) => {
                        try { txt += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of P:", c, e); txt += "[err]";}
                    });
                    txt = txt.trim();
                    if (txt.startsWith("```mermaid") && txt.endsWith("```")) {
                        resultMd = txt + "\n\n";
                    } else if (txt) {
                        resultMd = txt + "\n\n";
                    } else {
                        resultMd = "\n";
                    }
                    break;
                }
                case "H1": resultMd = (element.textContent.trim() ? `# ${element.textContent.trim()}\n\n` : ""); break;
                case "H2": resultMd = (element.textContent.trim() ? `## ${element.textContent.trim()}\n\n` : ""); break;
                case "H3": resultMd = (element.textContent.trim() ? `### ${element.textContent.trim()}\n\n` : ""); break;
                case "H4": resultMd = (element.textContent.trim() ? `#### ${element.textContent.trim()}\n\n` : ""); break;
                case "H5": resultMd = (element.textContent.trim() ? `##### ${element.textContent.trim()}\n\n` : ""); break;
                case "H6": resultMd = (element.textContent.trim() ? `###### ${element.textContent.trim()}\n\n` : ""); break;
                case "UL": {
                    let list = "";
                    const isSourceList = (
                        (element.previousElementSibling && /source/i.test(element.previousElementSibling.textContent)) ||
                        (element.parentElement && /source/i.test(element.parentElement.textContent)) ||
                        element.classList.contains('source-list')
                    );
                    element.querySelectorAll(":scope > li").forEach((li) => {
                        let liTxt = "";
                        li.childNodes.forEach((c) => { try { liTxt += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of LI:", c, e); liTxt += "[err]";}});
                        if (isSourceList) {
                            liTxt = liTxt.trim().replace(/\n+/g, ' ');
                        } else {
                            liTxt = liTxt.trim().replace(/\n\n$/, "").replace(/^\n\n/, "");
                        }
                        if (liTxt) list += `* ${liTxt}\n`;
                    });
                    resultMd = list + (list ? "\n" : "");
                    break;
                }
                case "OL": {
                    let list = "";
                    let i = 1;
                    const isSourceList = (
                        (element.previousElementSibling && /source/i.test(element.previousElementSibling.textContent)) ||
                        (element.parentElement && /source/i.test(element.parentElement.textContent)) ||
                        element.classList.contains('source-list')
                    );
                    element.querySelectorAll(":scope > li").forEach((li) => {
                        let liTxt = "";
                        li.childNodes.forEach((c) => { try { liTxt += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of LI:", c, e); liTxt += "[err]";}});
                        if (isSourceList) {
                            liTxt = liTxt.trim().replace(/\n+/g, ' ');
                        } else {
                            liTxt = liTxt.trim().replace(/\n\n$/, "").replace(/^\n\n/, "");
                        }
                        if (liTxt) {
                            list += `${i}. ${liTxt}\n`;
                            i++;
                        }
                    });
                    resultMd = list + (list ? "\n" : "");
                    break;
                }
                case "PRE": {
                    // For parsed HTML, skip complex SVG conversion (getBoundingClientRect won't work)
                    // Just extract code content instead
                    const code = element.querySelector("code");
                    let lang = "";
                    let txt = "";
                    if (code) {
                        txt = code.textContent;
                        const cls = Array.from(code.classList).find((c) => c.startsWith("language-"));
                        if (cls) lang = cls.replace("language-", "");
                    } else {
                        txt = element.textContent;
                    }
                    if (!lang) {
                        const preCls = Array.from(element.classList).find((c) => c.startsWith("language-"));
                        if (preCls) lang = preCls.replace("language-", "");
                    }
                    if (!lang && txt.trim()) {
                        lang = detectCodeLanguage(txt);
                    }

                    // Check if it's a mermaid diagram
                    const svgElement = element.querySelector('svg[id^="mermaid-"]');
                    if (svgElement && !lang) {
                        lang = 'mermaid';
                        // Try to extract mermaid code from data attribute or just mark as mermaid
                        txt = txt.trim() || '// Mermaid diagram - please view original page';
                    }

                    resultMd = `\`\`\`${lang}\n${txt.trim()}\n\`\`\`\n\n`;
                    break;
                }
                case "A": {
                    const href = element.getAttribute("href");
                    let initialTextFromNodes = "";
                    element.childNodes.forEach(c => {
                        try {
                            initialTextFromNodes += processNodeFromDoc(c, doc);
                        } catch (e) {
                            console.error("Error processing child of A:", c, e);
                            initialTextFromNodes += "[err]";
                        }
                    });
                    let text = initialTextFromNodes.trim();

                    if (!text && element.querySelector('img')) {
                        text = element.querySelector('img').alt || 'image';
                    }

                    if (href && (href.startsWith('http') || href.startsWith('https') || href.startsWith('/') || href.startsWith('#') || href.startsWith('mailto:'))) {
                        let finalLinkDisplayText = text;
                        const lineInfoMatch = href.match(/#L(\d+)(?:-L(\d+))?$/);

                        if (lineInfoMatch) {
                            const pathPart = href.substring(0, href.indexOf('#'));
                            let filenameFromPath = pathPart.substring(pathPart.lastIndexOf('/') + 1) || "link";
                            const startLine = lineInfoMatch[1];
                            const endLine = lineInfoMatch[2];
                            let displayFilename = filenameFromPath;
                            const trimmedInitialText = initialTextFromNodes.trim();
                            let textToParseForFilename = trimmedInitialText;
                            const isSourcesContext = trimmedInitialText.startsWith("Sources: [") && trimmedInitialText.endsWith("]");

                            if (isSourcesContext) {
                                const sourcesContentMatch = trimmedInitialText.match(/^Sources:\s+\[(.*)\]$/);
                                if (sourcesContentMatch && sourcesContentMatch[1]) {
                                    textToParseForFilename = sourcesContentMatch[1].trim();
                                }
                            }

                            const filenameHintMatch = textToParseForFilename.match(/^[\w\/\.-]+(?:\.\w+)?/);
                            if (filenameHintMatch && filenameHintMatch[0]) {
                                if (pathPart.includes(filenameHintMatch[0])) {
                                    displayFilename = filenameHintMatch[0];
                                }
                            }

                            let lineRefText;
                            if (endLine && endLine !== startLine) {
                                lineRefText = `L${startLine}-L${endLine}`;
                            } else {
                                lineRefText = `L${startLine}`;
                            }

                            let constructedText = `${displayFilename} ${lineRefText}`;

                            if (isSourcesContext) {
                                finalLinkDisplayText = `Sources: [${constructedText}]`;
                            } else {
                                finalLinkDisplayText = constructedText;
                            }
                        }

                        text = finalLinkDisplayText.trim() || (href ? href : "");
                        resultMd = `[${text}](${href})`;

                        // Check display style from style attribute
                        const displayStyle = element.getAttribute('style')?.includes('display') ?
                                           element.getAttribute('style').match(/display:\s*([^;]+)/)?.[1] : 'inline';
                        if (displayStyle !== "inline") {
                            resultMd += "\n\n";
                        }
                    } else {
                        text = text.trim() || (href ? href : "");
                        resultMd = text;
                        const displayStyle = element.getAttribute('style')?.includes('display') ?
                                           element.getAttribute('style').match(/display:\s*([^;]+)/)?.[1] : 'inline';
                        if (displayStyle !== "inline" && text.trim()) {
                            resultMd += "\n\n";
                        }
                    }
                    break;
                }
                case "IMG":
                    if (element.closest && element.closest('a')) return "";
                    resultMd = (element.src ? `![${element.alt || ""}](${element.src})\n\n` : "");
                    break;
                case "BLOCKQUOTE": {
                    let qt = "";
                    element.childNodes.forEach((c) => { try { qt += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of BLOCKQUOTE:", c, e); qt += "[err]";}});
                    const trimmedQt = qt.trim();
                    if (trimmedQt) {
                        resultMd = trimmedQt.split("\n").map((l) => `> ${l.trim() ? l : ''}`).filter(l => l.trim() !== '>').join("\n") + "\n\n";
                    } else {
                        resultMd = "";
                    }
                    break;
                }
                case "HR":
                    resultMd = "\n---\n\n";
                    break;
                case "STRONG":
                case "B": {
                    let st = "";
                    element.childNodes.forEach((c) => { try { st += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of STRONG/B:", c, e); st += "[err]";}});
                    return `**${st.trim()}**`;
                }
                case "EM":
                case "I": {
                    let em = "";
                    element.childNodes.forEach((c) => { try { em += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of EM/I:", c, e); em += "[err]";}});
                    return `*${em.trim()}*`;
                }
                case "CODE": {
                    if (element.parentNode && element.parentNode.nodeName === 'PRE') {
                        return element.textContent;
                    }
                    return `\`${element.textContent.trim()}\``;
                }
                case "BR":
                    if (element.parentNode && ['P', 'DIV', 'LI'].includes(element.parentNode.nodeName) ) {
                        const nextSibling = element.nextSibling;
                        if (!nextSibling || (nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim() !== '') || nextSibling.nodeType === Node.ELEMENT_NODE) {
                            return "  \n";
                        }
                    }
                    return "";
                case "TABLE": {
                    let tableMd = "";
                    const headerRows = Array.from(element.querySelectorAll(':scope > thead > tr, :scope > tr:first-child'));
                    const bodyRows = Array.from(element.querySelectorAll(':scope > tbody > tr'));
                    const allRows = Array.from(element.rows);

                    let rowsToProcessForHeader = headerRows;
                    if (headerRows.length === 0 && allRows.length > 0) {
                        rowsToProcessForHeader = [allRows[0]];
                    }

                    if (rowsToProcessForHeader.length > 0) {
                        const headerRowElement = rowsToProcessForHeader[0];
                        let headerContent = "|"; let separator = "|";
                        Array.from(headerRowElement.cells).forEach(cell => {
                            let cellText = ""; cell.childNodes.forEach(c => { try { cellText += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of TH/TD (Header):", c, e); cellText += "[err]";}});
                            headerContent += ` ${cellText.trim().replace(/\|/g, "\\|")} |`; separator += ` --- |`;
                        });
                        tableMd += `${headerContent}\n${separator}\n`;
                    }

                    let rowsToProcessForBody = bodyRows;
                    if (bodyRows.length === 0 && allRows.length > (headerRows.length > 0 ? 1 : 0) ) {
                        rowsToProcessForBody = headerRows.length > 0 ? allRows.slice(1) : allRows;
                    }

                    rowsToProcessForBody.forEach(row => {
                        if (rowsToProcessForHeader.length > 0 && rowsToProcessForHeader.includes(row)) return;

                        let rowContent = "|";
                        Array.from(row.cells).forEach(cell => {
                            let cellText = ""; cell.childNodes.forEach(c => { try { cellText += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of TH/TD (Body):", c, e); cellText += "[err]";}});
                            rowContent += ` ${cellText.trim().replace(/\|/g, "\\|").replace(/\n+/g, ' <br> ')} |`;
                        });
                        tableMd += `${rowContent}\n`;
                    });
                    resultMd = tableMd + (tableMd ? "\n" : "");
                    break;
                }
                case "THEAD": case "TBODY": case "TFOOT": case "TR": case "TH": case "TD":
                    return "";

                case "DETAILS": {
                    let summaryText = "Details"; const summaryElem = element.querySelector('summary');
                    if (summaryElem) { let tempSummary = ""; summaryElem.childNodes.forEach(c => { try { tempSummary += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of SUMMARY:", c, e); tempSummary += "[err]";}}); summaryText = tempSummary.trim() || "Details"; }
                    let detailsContent = "";
                    Array.from(element.childNodes).forEach(child => { if (child.nodeName !== "SUMMARY") { try { detailsContent += processNodeFromDoc(child, doc); } catch (e) { console.error("Error processing child of DETAILS:", c, e); detailsContent += "[err]";}}});
                    resultMd = `> **${summaryText}**\n${detailsContent.trim().split('\n').map(l => `> ${l}`).join('\n')}\n\n`;
                    break;
                }
                case "SUMMARY": return "";

                case "DIV":
                case "SPAN":
                case "SECTION":
                case "ARTICLE":
                case "MAIN":
                default: {
                    let txt = "";
                    element.childNodes.forEach((c) => { try { txt += processNodeFromDoc(c, doc); } catch (e) { console.error("Error processing child of DEFAULT case:", c, element.nodeName, e); txt += "[err]";}});

                    // Simple heuristic: block-level elements based on tag name
                    const blockElements = ["DIV", "SECTION", "ARTICLE", "MAIN", "HEADER", "FOOTER", "NAV", "ASIDE"];
                    const isBlock = blockElements.includes(element.nodeName);

                    if (isBlock && txt.trim()) {
                        if (txt.endsWith('\n\n')) {
                            resultMd = txt;
                        } else if (txt.endsWith('\n')) {
                            resultMd = txt + '\n';
                        } else {
                            resultMd = txt.trimEnd() + "\n\n";
                        }
                    } else {
                        return txt;
                    }
                }
            }
        } catch (error) {
            console.error("Unhandled error in processNodeFromDoc for element:", element.nodeName, element, error);
            return `\n[ERROR_PROCESSING_ELEMENT: ${element.nodeName}]\n\n`;
        }
        return resultMd;
    }

    function handleCancel() {
        isCancelled = true;
        updateStatus('Cancelling batch operation...', 'info');
        document.getElementById('dw-cancel-btn').style.display = 'none';
        document.getElementById('dw-batch-btn').disabled = false;
    }

    async function downloadAllPagesAsZip(folderName) {
        try {
            console.log('Preparing to download', convertedPages.length, 'files');
            updateStatus('Preparing downloads...', 'info');

            // Create index file
            let indexContent = `# ${folderName}\n\n## Content Index\n\n`;
            convertedPages.forEach(page => {
                indexContent += `- [${page.title}](${page.title}.md)\n`;
            });

            // Option 1: Download files individually with delay
            updateStatus('Downloading files individually (check your downloads folder)...', 'info');

            // Download README first
            console.log('Downloading README.md');
            downloadFile(indexContent, `${folderName}/README.md`);
            await new Promise(resolve => setTimeout(resolve, 500));

            // Download each markdown file with a delay to avoid browser blocking
            for (let i = 0; i < convertedPages.length; i++) {
                const page = convertedPages[i];
                console.log(`Downloading ${i + 1}/${convertedPages.length}: ${page.title}.md`);
                updateStatus(`Downloading ${i + 1}/${convertedPages.length}: ${page.title}.md`, 'info');

                downloadFile(page.content, `${folderName}/${page.title}.md`);

                // Small delay between downloads
                await new Promise(resolve => setTimeout(resolve, 300));
            }

            updateStatus(`Successfully downloaded ${convertedPages.length + 1} files! Check your downloads folder.`, 'success');
            console.log('All downloads complete!');

        } catch (error) {
            console.error('Error downloading files:', error);
            updateStatus('Error downloading files: ' + error.message, 'error');
        }
    }

    // ==================== INITIALIZATION ====================

    // Wait for page to fully load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createUI);
    } else {
        createUI();
    }

})();