您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将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 ? `\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 ? `\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(); } })();