ChatGPT Markdown Export

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

);

})();