|
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(/</g,"<") |
|
.replace(/&/g,"&") |
|
.replace(/"/g,'"') |
|
.replace(/'/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(``); |
|
}); |
|
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(); |
|
})(); |