Last active
January 8, 2026 01:03
-
-
Save oztalha/8724b72bf1f37d721c8b55eb02228cc6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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