ChatGPT Markdown Export

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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}

);

})();