ChatGPT Markdown Export

导出 ChatGPT 对话为 Markdown,支持代码块、列表、表格等格式化

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Markdown Export
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  导出 ChatGPT 对话为 Markdown,支持代码块、列表、表格等格式化
// @author       Kiracle
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function(){

"use strict";

////////////////////////////////////////////////
//// 标题
////////////////////////////////////////////////

function getTitle(){

let t=document.title
.replace(" - ChatGPT","")
.trim();

if(!t) t="chatgpt-chat";

return t;

}

////////////////////////////////////////////////
//// DOM → Markdown
////////////////////////////////////////////////

function parse(node){

let md="";

node.childNodes.forEach(child=>{

if(child.nodeType===3){

md+=child.textContent;

return;

}

let tag=child.nodeName;

if(tag==="P"){

md+=parse(child)+"\n";

}

else if(tag==="BR"){

md+="\n";

}

else if(tag==="STRONG"||tag==="B"){

md+="**"+parse(child)+"**";

}

else if(tag==="EM"||tag==="I"){

md+="*"+parse(child)+"*";

}

else if(tag==="CODE" && child.parentNode.nodeName!=="PRE"){

md+="`"+child.textContent+"`";

}

else if(tag==="PRE"){

let code=child.innerText.trim();

let lang="";

let c=child.querySelector("code");

if(c){

let m=c.className.match(/language-(\w+)/);

if(m) lang=m[1];

}

md+="\n```"+lang+"\n"+
code+
"\n```\n";

}

else if(tag==="UL"){

child.querySelectorAll(":scope > li")
.forEach(li=>{

md+="- "+parse(li).trim()+"\n";

});

}

else if(tag==="OL"){

let i=1;

child.querySelectorAll(":scope > li")
.forEach(li=>{

md+=i+". "+
parse(li).trim()+"\n";

i++;

});

}

else if(tag==="BLOCKQUOTE"){

md+="> "+
parse(child).trim()+
"\n";

}

else if(tag==="A"){

md+="["+
parse(child)+
"]("+
child.href+
")";

}

else if(tag==="IMG"){

md+="![]("+
child.src+
")";

}

else if(tag==="TABLE"){

md+=table(child);

}

else{

md+=parse(child);

}

});

return md;

}

////////////////////////////////////////////////
//// 表格
////////////////////////////////////////////////

function table(t){

let md="\n";

let rows=t.querySelectorAll("tr");

rows.forEach((row,i)=>{

let cols=row.querySelectorAll("th,td");

let line="|";

cols.forEach(col=>{

line+=parse(col).trim()+"|";

});

md+=line+"\n";

if(i===0){

let sep="|";

cols.forEach(()=>{

sep+="---|";

});

md+=sep+"\n";

}

});

return md+"\n";

}

////////////////////////////////////////////////
//// 获取消息
////////////////////////////////////////////////

function getMessages(){

let nodes=document.querySelectorAll(
"[data-message-author-role]"
);

let arr=[];

nodes.forEach(n=>{

let role=n.getAttribute(
"data-message-author-role"
);

let content=n.querySelector(
".markdown,.prose"
)||n;

let text=parse(content).trim();

if(text){

arr.push({

role:role,
text:text

});

}

});

return arr;

}

////////////////////////////////////////////////
//// Markdown优化(核心)
////////////////////////////////////////////////

function clean(md){

md=md.replace(/\r\n/g,"\n");

// 超过2空行 → 1空行
md=md.replace(/\n{3,}/g,"\n\n");

// 列表紧凑
md=md.replace(/\n\n- /g,"\n- ");

md=md.replace(/\n\n\d+\. /g,
m=>"\n"+m.trim()
);

// 标题规范
md=md.replace(
/(#+ .*?)\n{2,}/g,
"$1\n"
);

// 代码块规范
md=md.replace(
/\n{2,}```/g,
"\n```"
);

md=md.replace(
/```\n{2,}/g,
"```\n"
);

// 引用规范
md=md.replace(
/\n\n> /g,
"\n> "
);

// 去头尾空白
md=md.trim();

return md+"\n";

}

////////////////////////////////////////////////
//// 生成markdown
////////////////////////////////////////////////

function build(messages){

let md="# "+getTitle()+"\n\n";

md+="Export time: "+
new Date().toLocaleString()+
"\n\n---\n\n";

messages.forEach(m=>{

if(m.role==="user"){

md+="## 🧑 User\n";

}else{

md+="## 🤖 ChatGPT\n";

}

md+=m.text+"\n\n";

});

return clean(md);

}

////////////////////////////////////////////////
//// 下载
////////////////////////////////////////////////

function download(md){

let blob=new Blob(
[md],
{type:"text/markdown"}
);

let url=URL.createObjectURL(blob);

let a=document.createElement("a");

let name=getTitle()
.replace(/[\\/:*?"<>|]/g,"");

let time=new Date()
.toISOString()
.slice(0,19)
.replace(/:/g,"-");

a.download=
name+"-"+time+".md";

a.href=url;

document.body.appendChild(a);

a.click();

a.remove();

URL.revokeObjectURL(url);

}

////////////////////////////////////////////////
//// 导出
////////////////////////////////////////////////

function exportMD(){

let msgs=getMessages();

if(!msgs.length){

alert("No messages");

return;

}

download(build(msgs));

}

////////////////////////////////////////////////
//// UI
////////////////////////////////////////////////

function addBtn(){

if(
document.getElementById(
"md-export"
)
)return;

let btn=document.createElement(
"button"
);

btn.id="md-export";

btn.innerText="MD";

btn.style.margin="6px";

btn.style.padding=
"6px 12px";

btn.style.borderRadius=
"8px";

btn.style.border="none";

btn.style.background=
"#10a37f";

btn.style.color="white";

btn.style.cursor="pointer";

btn.onclick=exportMD;

let nav=
document.querySelector("nav");

if(nav){

nav.appendChild(btn);

}

}

new MutationObserver(addBtn)
.observe(

document.body,

{childList:true,subtree:true}

);

})();