您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export Perplexity.ai conversations as markdown with configurable citation styles
// ==UserScript== // @name Perplexity.ai Chat Exporter // @namespace https://github.com/ckep1/pplxport // @version 1.0.5 // @description Export Perplexity.ai conversations as markdown with configurable citation styles // @author Chris Kephart // @match https://www.perplexity.ai/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { "use strict"; // Style options const CITATION_STYLES = { ENDNOTES: "endnotes", INLINE: "inline", PARENTHESIZED: "parenthesized", }; const FORMAT_STYLES = { FULL: "full", // Include User/Assistant tags and all dividers CONCISE: "concise", // Just content, minimal dividers }; // Get user preferences function getPreferences() { return { citationStyle: GM_getValue("citationStyle", CITATION_STYLES.PARENTHESIZED), formatStyle: GM_getValue("formatStyle", FORMAT_STYLES.FULL), }; } // Register menu commands GM_registerMenuCommand("Use Endnotes Citation Style", () => { GM_setValue("citationStyle", CITATION_STYLES.ENDNOTES); alert("Citation style set to endnotes. Format: [1] with sources listed at end."); }); GM_registerMenuCommand("Use Inline Citation Style", () => { GM_setValue("citationStyle", CITATION_STYLES.INLINE); alert("Citation style set to inline. Format: [1](url)"); }); GM_registerMenuCommand("Use Parenthesized Citation Style", () => { GM_setValue("citationStyle", CITATION_STYLES.PARENTHESIZED); alert("Citation style set to parenthesized. Format: ([1](url))"); }); GM_registerMenuCommand("Full Format (with User/Assistant)", () => { GM_setValue("formatStyle", FORMAT_STYLES.FULL); alert("Format set to full with User/Assistant tags."); }); GM_registerMenuCommand("Concise Format (content only)", () => { GM_setValue("formatStyle", FORMAT_STYLES.CONCISE); alert("Format set to concise content only."); }); // Convert HTML content to markdown function htmlToMarkdown(html, citationStyle = CITATION_STYLES.PARENTHESIZED) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = html; tempDiv.querySelectorAll("code").forEach((codeElem) => { if (codeElem.style.whiteSpace && codeElem.style.whiteSpace.includes("pre-wrap")) { if (codeElem.parentElement.tagName.toLowerCase() !== "pre") { const pre = document.createElement("pre"); let language = ""; const prevDiv = codeElem.closest("div.pr-lg")?.previousElementSibling; if (prevDiv) { const langDiv = prevDiv.querySelector(".text-text-200"); if (langDiv) { language = langDiv.textContent.trim().toLowerCase(); langDiv.remove(); } } pre.dataset.language = language; pre.innerHTML = "<code>" + codeElem.innerHTML + "</code>"; codeElem.parentNode.replaceChild(pre, codeElem); } } }); // Process citations const citations = [...tempDiv.querySelectorAll("a.citation")]; const citationRefs = new Map(); citations.forEach((citation) => { // Find the inner <span> holding the citation number const numberSpan = citation.querySelector("span span"); const number = numberSpan ? numberSpan.textContent.trim() : null; const href = citation.getAttribute("href"); if (number && href) { citationRefs.set(number, { href }); } }); // Clean up citations based on style tempDiv.querySelectorAll(".citation").forEach((el) => { const numberSpan = el.querySelector("span span"); const number = numberSpan ? numberSpan.textContent.trim() : null; const href = el.getAttribute("href"); if (citationStyle === CITATION_STYLES.INLINE) { el.replaceWith(` [${number}](${href}) `); } else if (citationStyle === CITATION_STYLES.PARENTHESIZED) { el.replaceWith(` ([${number}](${href})) `); } else { el.replaceWith(` [${number}] `); } }); // Convert strong sections to headers and clean up content let text = tempDiv.innerHTML; // Basic HTML conversion text = text .replace(/<h1[^>]*>([\s\S]*?)<\/h1>/g, "# $1") .replace(/<h2[^>]*>([\s\S]*?)<\/h2>/g, "## $1") .replace(/<h3[^>]*>([\s\S]*?)<\/h3>/g, "### $1") .replace(/<h4[^>]*>([\s\S]*?)<\/h4>/g, "#### $1") .replace(/<h5[^>]*>([\s\S]*?)<\/h5>/g, "##### $1") .replace(/<h6[^>]*>([\s\S]*?)<\/h6>/g, "###### $1") .replace(/<p[^>]*>([\s\S]*?)<\/p>/g, "$1\n") .replace(/<br\s*\/?>/g, "\n") .replace(/<strong>([\s\S]*?)<\/strong>/g, "**$1**") .replace(/<em>([\s\S]*?)<\/em>/g, "*$1*") .replace(/<ul[^>]*>([\s\S]*?)<\/ul>/g, "$1\n") .replace(/<li[^>]*>([\s\S]*?)<\/li>/g, " - $1\n"); // Handle tables before removing remaining HTML text = text.replace(/<table[^>]*>([\s\S]*?)<\/table>/g, (match) => { const tableDiv = document.createElement("div"); tableDiv.innerHTML = match; const rows = []; // Process header rows const headerRows = tableDiv.querySelectorAll("thead tr"); if (headerRows.length > 0) { headerRows.forEach((row) => { const cells = [...row.querySelectorAll("th, td")].map((cell) => cell.textContent.trim() || " "); if (cells.length > 0) { rows.push(`| ${cells.join(" | ")} |`); // Add separator row after headers rows.push(`| ${cells.map(() => "---").join(" | ")} |`); } }); } // Process body rows const bodyRows = tableDiv.querySelectorAll("tbody tr"); bodyRows.forEach((row) => { const cells = [...row.querySelectorAll("td")].map((cell) => cell.textContent.trim() || " "); if (cells.length > 0) { rows.push(`| ${cells.join(" | ")} |`); } }); // Return markdown table with proper spacing return rows.length > 0 ? `\n\n${rows.join("\n")}\n\n` : ""; }); // Continue with remaining HTML conversion text = text .replace(/<pre[^>]*data-language="([^"]*)"[^>]*><code>([\s\S]*?)<\/code><\/pre>/g, "```$1\n$2\n```") .replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, "```\n$1\n```") .replace(/<code>(.*?)<\/code>/g, "`$1`") .replace(/<a\s+(?:[^>]*?\s+)?href="([^"]*)"[^>]*>(.*?)<\/a>/g, "[$2]($1)") .replace(/<[^>]+>/g, ""); // Remove any remaining HTML tags // Clean up whitespace // Convert bold text at start of line to h3 headers, but not if inside a list item text = text.replace(/^(\s*)\*\*([^*\n]+)\*\*(?!.*\n\s*-)/gm, "$1### $2"); // This fixes list items where the entire text was incorrectly converted to headers // We need to preserve both partial bold items and fully bold items text = text.replace(/^(\s*-\s+)###\s+([^\n]+)/gm, function(match, listPrefix, content) { // Check if the content contains bold markers if (content.includes("**")) { // If it already has bold markers, just remove the ### and keep the rest intact return `${listPrefix}${content}`; } else { // If it doesn't have bold markers (because it was fully bold before), // add them back (this was incorrectly converted to a header) return `${listPrefix}**${content}**`; } }); // Fix list spacing (no extra newlines between items) text = text.replace(/\n\s*-\s+/g, "\n- "); // Ensure headers have proper spacing text = text.replace(/([^\n])(\n#{1,3} )/g, "$1\n\n$2"); // Fix unbalanced or misplaced bold markers in list items text = text.replace(/^(\s*-\s+.*?)(\s\*\*\s*)$/gm, "$1"); // Remove trailing ** with space before // Fix citation and bold issues - make sure citations aren't wrapped in bold text = text.replace(/\*\*([^*]+)(\[[0-9]+\]\([^)]+\))\s*\*\*/g, "**$1**$2"); text = text.replace(/\*\*([^*]+)(\(\[[0-9]+\]\([^)]+\)\))\s*\*\*/g, "**$1**$2"); // Fix cases where a line ends with an extra bold marker after a citation text = text.replace(/(\[[0-9]+\]\([^)]+\))\s*\*\*/g, "$1"); text = text.replace(/(\(\[[0-9]+\]\([^)]+\)\))\s*\*\*/g, "$1"); // Clean up whitespace text = text .replace(/^[\s\n]+|[\s\n]+$/g, "") // Trim start and end .replace(/\n{3,}/g, "\n\n") // Max two consecutive newlines .replace(/^\s+/gm, "") // Remove leading spaces on each line .replace(/[ \t]+$/gm, "") // Remove trailing spaces .trim(); if (citationStyle === CITATION_STYLES.INLINE || citationStyle === CITATION_STYLES.PARENTHESIZED) { // Remove extraneous space before a period: e.g. " [1](url) ." -> " [1](url)." text = text.replace(/\s+\./g, "."); } // Add citations at the bottom for endnotes style if (citationStyle === CITATION_STYLES.ENDNOTES && citationRefs.size > 0) { text += "\n\n### Sources\n"; for (const [number, { href }] of citationRefs) { text += `[${number}] ${href}\n`; } } return text; } // Format the complete markdown document function formatMarkdown(conversations) { const title = document.title.replace(" | Perplexity", "").trim(); const timestamp = new Date().toISOString().split("T")[0]; const prefs = getPreferences(); let markdown = "---\n"; markdown += `title: ${title}\n`; markdown += `date: ${timestamp}\n`; markdown += `source: ${window.location.href}\n`; markdown += "---\n\n"; // Add newline after properties conversations.forEach((conv, index) => { if (conv.role === "Assistant") { if (prefs.formatStyle === FORMAT_STYLES.FULL) { markdown += `**${conv.role}:** ${conv.content}\n\n`; // Add newline after content } else { markdown += `${conv.content}\n\n`; // Add newline after content } // Add divider only between assistant responses, not after the last one const nextAssistant = conversations.slice(index + 1).find((c) => c.role === "Assistant"); if (nextAssistant) { markdown += "---\n\n"; // Add newline after divider } } else if (conv.role === "User" && prefs.formatStyle === FORMAT_STYLES.FULL) { markdown += `**${conv.role}:** ${conv.content}\n\n`; // Add newline after content markdown += "---\n\n"; // Add newline after divider } }); return markdown.trim(); // Trim any trailing whitespace at the very end } // Extract conversation content function extractConversation(citationStyle) { const conversation = []; console.log("Using updated selectors for Perplexity"); // Check for user query const userQueries = document.querySelectorAll(".whitespace-pre-line.text-pretty.break-words"); userQueries.forEach(query => { conversation.push({ role: "User", content: query.textContent.trim(), }); }); // Check for assistant responses const assistantResponses = document.querySelectorAll(".prose.text-pretty.dark\\:prose-invert"); assistantResponses.forEach(response => { const answerContent = response.cloneNode(true); conversation.push({ role: "Assistant", content: htmlToMarkdown(answerContent.innerHTML, citationStyle), }); }); // Fallback to more generic selectors if needed if (conversation.length === 0) { console.log("Attempting to use fallback selectors"); // Try more generic selectors that might match Perplexity's structure const queryElements = document.querySelectorAll("[class*='whitespace-pre-line'][class*='break-words']"); queryElements.forEach(query => { conversation.push({ role: "User", content: query.textContent.trim(), }); }); const responseElements = document.querySelectorAll("[class*='prose'][class*='prose-invert']"); responseElements.forEach(response => { const answerContent = response.cloneNode(true); conversation.push({ role: "Assistant", content: htmlToMarkdown(answerContent.innerHTML, citationStyle), }); }); } return conversation; } // Download markdown file function downloadMarkdown(content, filename) { const blob = new Blob([content], { type: "text/markdown" }); 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); URL.revokeObjectURL(url); } // Create and add export button function addExportButton() { const existingButton = document.getElementById("perplexity-export-btn"); if (existingButton) { existingButton.remove(); } const button = document.createElement("button"); button.id = "perplexity-export-btn"; button.textContent = "Export as Markdown"; button.style.cssText = ` position: fixed; bottom: 20px; right: 80px; padding: 8px 16px; background-color: #6366f1; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; z-index: 99999; font-family: system-ui, -apple-system, sans-serif; transition: background-color 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.1); `; button.addEventListener("mouseenter", () => { button.style.backgroundColor = "#4f46e5"; }); button.addEventListener("mouseleave", () => { button.style.backgroundColor = "#6366f1"; }); button.addEventListener("click", () => { const prefs = getPreferences(); const conversation = extractConversation(prefs.citationStyle); if (conversation.length === 0) { alert("No conversation content found to export."); return; } const title = document.title.replace(" | Perplexity", "").trim(); const safeTitle = title .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .replace(/^-+|-+$/g, ""); const filename = `${safeTitle}.md`; const markdown = formatMarkdown(conversation); downloadMarkdown(markdown, filename); }); document.body.appendChild(button); } // Initialize the script function init() { const observer = new MutationObserver(() => { if ((document.querySelector(".prose.text-pretty.dark\\:prose-invert") || document.querySelector("[class*='prose'][class*='prose-invert']")) && !document.getElementById("perplexity-export-btn")) { addExportButton(); } }); observer.observe(document.body, { childList: true, subtree: true, }); if (document.querySelector(".prose.text-pretty.dark\\:prose-invert") || document.querySelector("[class*='prose'][class*='prose-invert']")) { addExportButton(); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();