Skip to content

Instantly share code, notes, and snippets.

@skylord123
Last active July 24, 2025 16:28
Show Gist options
  • Save skylord123/10c09eb8a4cf8b1244de6b13e07d4c67 to your computer and use it in GitHub Desktop.
Save skylord123/10c09eb8a4cf8b1244de6b13e07d4c67 to your computer and use it in GitHub Desktop.
Copy teams chat into markdown format

Teams Chat to Markdown Bookmarklet

Since Microsoft conveniently removed any decent way to copy Teams chats (with timestamps, names, you know—useful stuff), this bookmarklet is your only shot at getting that functionality back.

This bookmarklet allows you to extract chat messages from the Microsoft Teams web application and converts them directly into formatted Markdown. It provides an easy-to-use interface to copy and export conversations seamlessly.

🚀 Features

  • Markdown Formatting: Converts chat messages, including inline code, code blocks, quotes, images, and emojis into Markdown format.
  • Timestamp and Author Information: Clearly formats author names and timestamps.
  • Dialog Interface: Presents the Markdown content in a user-friendly modal dialog with easy copy and close options.
  • Easy to Install: Simply add it to your browser bookmarks.
  • Domain Validation: Automatically checks if the bookmarklet is run on the correct Teams domain.

⚡ Installation

  1. Create a new bookmark in your browser.
  2. Paste the provided JavaScript snippet into the URL field of the bookmark.
  3. Name your bookmark something memorable, like "Teams Chat to Markdown."

📌 Usage

  1. Navigate to any Microsoft Teams chat conversation in your browser (https://teams.microsoft.com).
  2. Click the bookmark you created.
  3. A dialog box appears displaying your chat conversation formatted in Markdown.
  4. Click Copy to copy the Markdown text to your clipboard, or click Close to dismiss the dialog.

🛠 Customization

  • Dialog Styling: Adjust the CSS styles within the JavaScript snippet to modify the appearance of the dialog or textarea size.
  • Output Formatting: Edit the JavaScript directly if you wish to tweak the Markdown formatting rules.

📄 License

This bookmarklet is released under the Unlicense. Feel free to use, modify, and distribute it freely.

javascript:(function(){
if (window.location.host !== 'teams.microsoft.com') {
alert('Error: This bookmarklet only works on teams.microsoft.com');
return;
}
const dlgId = "teams-chat-md-export-dialog";
let old = document.getElementById(dlgId);
if (old) old.remove();
function extractTeamsChat() {
function formatTimestamp(s) {
const d = new Date(s);
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const year = d.getFullYear();
let hours = d.getHours();
const mins = String(d.getMinutes()).padStart(2, "0");
const ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12 || 12;
return `${month}/${day}/${year} ${hours}:${mins} ${ampm} `;
}
function getMinuteKey(s) {
return Math.floor(new Date(s).getTime() / 60000) * 60000;
}
function decodeEntities(t) {
return t
.replace(/ /g," ")
.replace(/>/g,">")
.replace(/&lt;/g,"<")
.replace(/&amp;/g,"&")
.replace(/&quot;/g,'"')
.replace(/&#39;/g,"'");
}
function extractPre(preEl) {
const cls = preEl.getAttribute("class")||"";
const lang = (cls.match(/language-([a-z0-9\-_]+)/i)||[])[1]||"";
let raw = preEl.querySelector("code")?.innerHTML || preEl.innerHTML;
raw = raw
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/div>\s*<div>/gi, "\n")
.replace(/<div>|<\/div>/gi, "\n")
.replace(/<[^>]+>/g, "");
raw = decodeEntities(raw).trim();
return `\n\`\`\`${lang}\n${raw}\n\`\`\`\n`;
}
function extractEmojis(container) {
let out = "";
container.querySelectorAll("img").forEach(img => {
const alt = (img.alt||"").trim();
const custom = (img.getAttribute("itemtype")||"").includes("CustomEmoji")
|| (img.getAttribute("data-tid")||"").startsWith("custom-emoji-");
if (!alt) return;
out += custom ? `[${alt}] ` : `${alt} `;
});
return out.trim();
}
function processContent(el) {
const clone = el.cloneNode(true);
clone.querySelectorAll('[data-testid="lazy-image-wrapper"]').forEach(span=>{
const img = span.querySelector("img");
if (!img) return;
const src = img.getAttribute("data-gallery-src")
|| img.getAttribute("data-orig-src")
|| img.src;
const alt = img.getAttribute("aria-label")||img.alt||"";
span.replaceWith(`![${alt}](${src})`);
});
const quoteCards = clone.querySelectorAll('[data-tid="quoted-reply-card"]');
const quoteBuf = [];
quoteCards.forEach((card,i)=>{
const author = card.querySelector("span:not([data-tid])")?.textContent.trim()||"";
const when = card.querySelector('[data-tid="quoted-reply-timestamp"]')?.textContent.trim()||"";
const prev = card.querySelector('[data-tid="quoted-reply-preview-content"]')?.textContent.trim()||"";
quoteBuf[i] = `> ${author}${when} \n> ${prev}\n\n`;
card.parentElement.replaceWith(`__QUOTE_${i}__`);
});
clone.querySelectorAll('[data-tid="code-block-editor-deserialized-language"]').forEach(x=>x.remove());
const preEls = clone.querySelectorAll("pre");
const preBuf = [];
preEls.forEach((pre,i)=>{
preBuf[i] = extractPre(pre);
pre.replaceWith(`__PRE_${i}__`);
});
const inlineCodes = clone.querySelectorAll("code");
const codeBuf = [];
inlineCodes.forEach((codeEl,i)=>{
if (codeEl.closest("pre")) return;
const txt = decodeEntities(codeEl.textContent.trim());
codeBuf[i] = `\`${txt}\``;
codeEl.replaceWith(`__CODE_${i}__`);
});
const bqs = clone.querySelectorAll("blockquote");
const bqBuf = [];
bqs.forEach((bq,i)=>{
let text = "";
bq.querySelectorAll("p").forEach((p,pi)=>{
let r = p.innerHTML.replace(/<br\s*\/?>/gi,"\n").replace(/<[^>]+>/g,"");
r = decodeEntities(r);
r.split("\n").forEach(line => text += `> ${line.trim()}\n`);
if (pi < bq.querySelectorAll("p").length - 1) text += ">\n";
});
bqBuf[i] = text.trimEnd() + "\n\n";
bq.replaceWith(`__BQ_${i}__`);
});
let html = clone.innerHTML
.replace(/__QUOTE_(\d+)__/g, (_,n) => quoteBuf[n])
.replace(/__BQ_(\d+)__/g, (_,n) => "\n" + bqBuf[n])
.replace(/<br\s*\/?>/gi,"\n")
.replace(/<\/p>\s*<p>/gi,"\n")
.replace(/<p>|<\/p>/gi,"")
.replace(/<[^>]+>/g,"");
html = decodeEntities(html).trim();
preBuf.forEach((r,i)=> { html = html.replace(`__PRE_${i}__`, r); });
codeBuf.forEach((r,i)=>{ html = html.replace(`__CODE_${i}__`,r); });
html = html.replace(/\n{3,}/g, "\n\n").trim();
const emojis = extractEmojis(clone);
if (!html && emojis) return emojis;
if (emojis) return (html + " " + emojis).trim();
return html;
}
const msgs = document.querySelectorAll('[data-tid="chat-pane-message"]');
let out = "", lastAuthor = "", lastKey = 0;
msgs.forEach(node => {
const mid = node.getAttribute("data-mid");
if (!mid) return;
const au = document.querySelector(`#author-${mid}`)?.textContent.trim() || "Unknown";
const tsEl = document.querySelector(`#timestamp-${mid}`);
let ts = "", key = 0;
if (tsEl) {
const dt = tsEl.getAttribute("datetime");
if (dt) { ts = formatTimestamp(dt); key = getMinuteKey(dt); }
}
const contentEl = node.querySelector('[id^="content-"]');
const txt = contentEl ? processContent(contentEl) : "";
if (!txt) return;
const sameGroup = (au === lastAuthor && key === lastKey && lastAuthor);
if (sameGroup) {
out += txt + "\n";
} else {
if (out) out += "\n";
out += ts
? `**${au}** - ${ts}\n${txt}\n`
: `**${au}**\n${txt}\n`;
}
lastAuthor = au;
lastKey = key;
});
return out.trim();
}
const chatMarkdown = extractTeamsChat();
const dlg = document.createElement("div");
dlg.id = dlgId;
dlg.style = [
"all:unset",
"position:fixed;top:0;left:0",
"width:100vw;height:100vh",
"background:rgba(0,0,0,0.25)",
"display:flex;align-items:center;justify-content:center",
"z-index:9999999"
].join(";");
const box = document.createElement("div");
box.style = [
"background:#fff",
"border-radius:10px",
"box-shadow:0 6px 32px rgba(0,0,0,0.25)",
"max-width:90vw;max-height:90vh",
"padding:20px",
"display:flex;flex-direction:column"
].join(";");
const ta = document.createElement("textarea");
ta.readOnly = true;
ta.value = chatMarkdown;
ta.style = [
"width:60vw;max-width:800px",
"height:80vh;max-height:90vh",
"font-family:monospace",
"font-size:14px",
"line-height:1.3",
"margin-bottom:12px",
"resize:vertical"
].join(";");
box.appendChild(ta);
const btnBar = document.createElement("div");
btnBar.style = "display:flex;justify-content:flex-end;gap:8px";
const copyBtn = document.createElement("button");
copyBtn.textContent = "Copy";
copyBtn.onclick = () => {
ta.select();
document.execCommand("copy") || navigator.clipboard.writeText(ta.value);
copyBtn.textContent = "Copied!";
setTimeout(()=> copyBtn.textContent = "Copy", 1200);
};
btnBar.appendChild(copyBtn);
const closeBtn = document.createElement("button");
closeBtn.textContent = "Close";
closeBtn.onclick = () => dlg.remove();
btnBar.appendChild(closeBtn);
box.appendChild(btnBar);
dlg.appendChild(box);
document.body.appendChild(dlg);
ta.focus();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment