ChatGPT Markdown Export

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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}

);

})();