Skip to content

Instantly share code, notes, and snippets.

@oztalha
Last active January 8, 2026 01:03
Show Gist options
  • Select an option

  • Save oztalha/8724b72bf1f37d721c8b55eb02228cc6 to your computer and use it in GitHub Desktop.

Select an option

Save oztalha/8724b72bf1f37d721c8b55eb02228cc6 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Microsoft Loop to Markdown
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Convert Microsoft Loop pages to Markdown
// @author Talha Oz (ozt@)
// @match https://loop.cloud.microsoft/*
// @match https://*.loop.cloud.microsoft.com/*
// @grant GM_setClipboard
// @updateURL https://gist.github.com/oztalha/8724b72bf1f37d721c8b55eb02228cc6/raw/loop-to-markdown.user.js
// @downloadURL https://gist.github.com/oztalha/8724b72bf1f37d721c8b55eb02228cc6/raw/loop-to-markdown.user.js
// ==/UserScript==
(function() {
'use strict';
const normalize = text => text?.trim().replace(/\s+/g, ' ') || '';
const getMention = el => {
const avatar = el.querySelector('.fui-Avatar[aria-label]');
return avatar ? `@${avatar.getAttribute('aria-label')}` : '';
};
const getTextContent = (container, skipTables = false) => {
let text = '';
container.querySelectorAll('.scriptor-textRun, [data-testid="resolvedAtMention"]').forEach(node => {
if (skipTables && node.closest('table')) return;
if (node.dataset.testid === 'resolvedAtMention') {
text += getMention(node);
} else if (!node.closest('[data-testid="resolvedAtMention"]')) {
if (node.classList.contains('scriptor-hyperlink')) {
const href = (node.getAttribute('title') || '').split('\n')[0];
if (href) text += `[${node.textContent}](${href})`;
} else if (node.classList.contains('scriptor-code-editor')) {
text += '`' + node.textContent + '`';
} else {
text += node.textContent;
}
}
});
return normalize(text);
};
const parseTable = table => {
const lines = [];
const headers = [];
table.querySelectorAll('[role="columnheader"]').forEach(th => {
const label = th.querySelector('[aria-label]');
if (label) headers.push(label.getAttribute('aria-label'));
});
if (headers.length) {
lines.push('| ' + headers.join(' | ') + ' |', '| ' + headers.map(() => '---').join(' | ') + ' |');
}
table.querySelectorAll('tbody tr[data-rowid]').forEach(row => {
if (row.dataset.rowid === 'HEADER_ROW_ID') return;
const cells = [...row.querySelectorAll('[role="cell"]')].map(cell => getTextContent(cell).replace(/\|/g, '\\|'));
if (cells.length) lines.push('| ' + cells.join(' | ') + ' |');
});
return lines;
};
function convertToMarkdown() {
const pages = [...document.querySelectorAll('.scriptor-pageFrame')].filter(p => !p.closest('table'));
if (!pages.length) return alert('No Loop content found');
const lines = [], processed = new Set(), codeTexts = new Set();
// Title from first paragraph
const firstPara = pages[0]?.querySelector('.scriptor-paragraph:not([role="heading"] *)');
if (firstPara && !firstPara.querySelector('[role="heading"]') && !firstPara.closest('.scriptor-listItem, table')) {
const title = normalize(firstPara.textContent);
if (title) { lines.push(`# ${title}`, ''); processed.add(firstPara); }
}
pages.forEach(page => {
page.querySelectorAll('.scriptor-paragraph, .scriptor-listItem, .scriptor-component-code-block, [role="table"]').forEach(el => {
if (processed.has(el)) return;
const inTable = el.closest('table');
if (inTable && inTable !== el) return;
// Table
if (el.getAttribute('role') === 'table') {
const tableLines = parseTable(el);
if (tableLines.length) lines.push('', ...tableLines, '');
processed.add(el);
return;
}
// Code block
if (el.classList.contains('scriptor-paragraph') && el.closest('.scriptor-component-code-block')) return;
const codeBlock = el.querySelector('.scriptor-code-wrap-on') ||
(el.classList.contains('scriptor-component-code-block') ? el.querySelector('.scriptor-code-editor') : null);
if (codeBlock) {
const code = [...codeBlock.querySelectorAll('.scriptor-paragraph')].map(p => p.textContent).join('\n').trim() || codeBlock.textContent.trim();
if (code) { lines.push('', '```', code, '```', ''); codeTexts.add(normalize(code)); processed.add(el); }
return;
}
// Heading
const heading = el.querySelector('[role="heading"]');
if (heading) {
const level = heading.getAttribute('aria-level') || '1';
const text = getTextContent(heading, true);
if (text) lines.push('', `${'#'.repeat(Math.min(+level + 1, 4))} ${text}`, '');
processed.add(el);
return;
}
// List item
if (el.classList.contains('scriptor-listItem')) {
const li = el.querySelector('li');
if (!li) return;
const text = getTextContent(li);
if (!text) return;
const margin = parseInt((el.getAttribute('style') || '').match(/margin-left:\s*(\d+)/)?.[1] || 0);
const indent = ' '.repeat(Math.max(0, Math.floor((margin - 27) / 27)));
const checkbox = li.querySelector('.scriptor-listItem-marker-checkbox');
const checked = checkbox?.getAttribute('aria-checked') === 'true';
const marker = checkbox ? (checked ? '- [x] ' : '- [ ] ') : '- ';
lines.push(indent + marker + text);
processed.add(el);
return;
}
// Paragraph
if (!el.closest('.scriptor-listItem') && !el.querySelector('.scriptor-code-wrap-on')) {
let text = getTextContent(el, true);
// Capture embedded quip links
el.querySelectorAll('a[href*="quip"]').forEach(link => {
const href = link.getAttribute('href');
if (href) text += ` [${link.textContent.trim() || href.split('/').pop()}](${href})`;
});
text = normalize(text);
if (text && ![...codeTexts].some(c => c.includes(text))) lines.push('', text, '');
processed.add(el);
}
});
});
let markdown = lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
markdown = markdown.replace(/(```\n[\s\S]*?\n```)\n\n\1/g, '$1');
GM_setClipboard(markdown, 'text');
const note = document.createElement('div');
note.textContent = '✓ Markdown copied!';
note.style.cssText = 'position:fixed;top:20px;right:20px;background:#4CAF50;color:white;padding:12px 16px;border-radius:5px;z-index:10000;font-family:sans-serif';
document.body.appendChild(note);
setTimeout(() => note.remove(), 2000);
}
const btn = document.createElement('button');
btn.textContent = '📋 Copy as Markdown';
btn.style.cssText = 'position:fixed;bottom:20px;left:20px;background:#0078D4;color:white;border:none;padding:8px 12px;border-radius:5px;cursor:pointer;z-index:10000;font-family:sans-serif;font-size:12px';
btn.onclick = convertToMarkdown;
document.body.appendChild(btn);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment