Export all rendered text from any webpage to a Markdown file. Preserves headings, lists, links, bold/italic, code, tables, and blockquotes.
// ==UserScript==
// @name Export Website to Markdown
// @namespace https://github.com/theelderemo/export-website-as-markdown
// @version 1.0.0
// @description Export all rendered text from any webpage to a Markdown file. Preserves headings, lists, links, bold/italic, code, tables, and blockquotes.
// @author theelderemo
// @license MIT
// @match *://*/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const SKIP_TAGS = new Set([
"SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "SVG", "CANVAS",
"AUDIO", "VIDEO", "IFRAME", "OBJECT", "EMBED", "HEAD", "META",
"LINK", "INPUT", "TEXTAREA", "SELECT", "BUTTON",
]);
const BLOCK_TAGS = new Set([
"P", "DIV", "SECTION", "ARTICLE", "MAIN", "ASIDE", "FOOTER",
"HEADER", "NAV", "FIGURE", "FIGCAPTION", "DETAILS", "SUMMARY",
"DIALOG", "ADDRESS", "FIELDSET", "LEGEND", "FORM",
]);
function isVisible(el) {
if (el.nodeType !== Node.ELEMENT_NODE) return true;
const style = window.getComputedStyle(el);
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
style.visibility !== "collapse" &&
parseFloat(style.opacity) > 0 &&
el.getAttribute("aria-hidden") !== "true"
);
}
function nodeToMarkdown(node, ctx) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
const clean = text.replace(/[\r\n]+/g, " ").replace(/\t/g, " ");
if (!clean.trim()) return "";
return applyInlineStyle(clean, ctx);
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
if (SKIP_TAGS.has(node.tagName)) return "";
if (!isVisible(node)) return "";
const tag = node.tagName;
if (/^H[1-6]$/.test(tag)) {
const level = parseInt(tag[1]);
const prefix = "#".repeat(level);
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `\n\n${prefix} ${inner}\n\n` : "";
}
if (tag === "HR") return "\n\n---\n\n";
if (tag === "BLOCKQUOTE") {
const inner = childrenToMarkdown(node, ctx).trim();
if (!inner) return "";
return (
"\n\n" +
inner
.split("\n")
.map((l) => `> ${l}`)
.join("\n") +
"\n\n"
);
}
if (tag === "PRE") {
const codeEl = node.querySelector("code");
const lang = codeEl
? (codeEl.className.match(/language-(\S+)/) || [])[1] || ""
: "";
const text = (codeEl || node).innerText || node.textContent;
return `\n\n\`\`\`${lang}\n${text.trim()}\n\`\`\`\n\n`;
}
if (tag === "CODE" && node.closest("pre")) return "";
if (tag === "CODE") {
const text = node.textContent.trim();
return text ? `\`${text}\`` : "";
}
if (["STRONG", "B"].includes(tag)) {
const inner = childrenToMarkdown(node, { ...ctx, bold: true }).trim();
return inner ? `**${inner}**` : "";
}
if (["EM", "I"].includes(tag)) {
const inner = childrenToMarkdown(node, { ...ctx, italic: true }).trim();
return inner ? `_${inner}_` : "";
}
if (tag === "MARK") {
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `==${inner}==` : "";
}
if (["S", "STRIKE", "DEL"].includes(tag)) {
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `~~${inner}~~` : "";
}
if (tag === "SUP") {
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `^${inner}^` : "";
}
if (tag === "SUB") {
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `~${inner}~` : "";
}
if (tag === "A") {
const href = node.getAttribute("href") || "";
const inner = childrenToMarkdown(node, ctx).trim();
if (!inner) return "";
const absHref = href
? new URL(href, window.location.href).href
: "";
return absHref && absHref !== inner ? `[${inner}](${absHref})` : inner;
}
if (tag === "IMG") {
const alt = node.getAttribute("alt") || "";
const src = node.getAttribute("src") || "";
const absSrc = src ? new URL(src, window.location.href).href : "";
return alt ? `` : "";
}
if (tag === "BR") return " \n";
if (tag === "UL") {
const items = listItems(node, ctx, false);
return items ? `\n\n${items}\n\n` : "";
}
if (tag === "OL") {
const items = listItems(node, ctx, true);
return items ? `\n\n${items}\n\n` : "";
}
if (tag === "LI") {
return childrenToMarkdown(node, ctx).trim();
}
if (tag === "DL") {
return "\n\n" + childrenToMarkdown(node, ctx).trim() + "\n\n";
}
if (tag === "DT") {
return `\n**${childrenToMarkdown(node, ctx).trim()}**\n`;
}
if (tag === "DD") {
return `: ${childrenToMarkdown(node, ctx).trim()}\n`;
}
if (tag === "TABLE") {
return tableToMarkdown(node, ctx);
}
if (["THEAD", "TBODY", "TFOOT", "TR", "TH", "TD"].includes(tag)) {
return childrenToMarkdown(node, ctx);
}
if (tag === "P" || BLOCK_TAGS.has(tag)) {
const inner = childrenToMarkdown(node, ctx).trim();
return inner ? `\n\n${inner}\n\n` : "";
}
return childrenToMarkdown(node, ctx);
}
function childrenToMarkdown(node, ctx) {
let out = "";
for (const child of node.childNodes) {
out += nodeToMarkdown(child, ctx);
}
return out;
}
function applyInlineStyle(text, ctx) {
return text;
}
function listItems(ulEl, ctx, ordered) {
const items = [];
let idx = 1;
for (const child of ulEl.children) {
if (child.tagName !== "LI") continue;
if (!isVisible(child)) continue;
const bullet = ordered ? `${idx}.` : "-";
idx++;
const inner = childrenToMarkdown(child, ctx)
.trim()
.replace(/\n{2,}/g, "\n")
.replace(/\n/g, "\n ");
items.push(`${bullet} ${inner}`);
}
return items.join("\n");
}
function tableToMarkdown(tableEl, ctx) {
const rows = [];
for (const section of ["THEAD", "TBODY", "TFOOT"]) {
const sectionEl = tableEl.querySelector(section);
if (sectionEl) {
for (const row of sectionEl.querySelectorAll("tr")) {
if (isVisible(row)) rows.push(row);
}
}
}
if (rows.length === 0) {
for (const row of tableEl.querySelectorAll("tr")) {
if (isVisible(row)) rows.push(row);
}
}
if (rows.length === 0) return "";
const matrix = rows.map((row) =>
[...row.querySelectorAll("th, td")]
.filter(isVisible)
.map((cell) => childrenToMarkdown(cell, ctx).trim().replace(/\|/g, "\\|"))
);
const colCount = Math.max(...matrix.map((r) => r.length));
const pad = (arr) => {
while (arr.length < colCount) arr.push("");
return arr;
};
const lines = [];
matrix.forEach((cells, i) => {
lines.push(`| ${pad(cells).join(" | ")} |`);
if (i === 0) {
lines.push(`|${" --- |".repeat(colCount)}`);
}
});
return `\n\n${lines.join("\n")}\n\n`;
}
function postProcess(md) {
return md
.replace(/\n{4,}/g, "\n\n\n")
.replace(/[ \t]+\n/g, "\n")
.replace(/^\n+/, "")
.replace(/\n+$/, "\n");
}
function exportToMarkdown() {
const title = document.title || window.location.hostname;
const url = window.location.href;
const date = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
let md = `# ${title}\n\n`;
md += `> **Source:** [${url}](${url}) \n`;
md += `> **Exported:** ${date}\n\n`;
md += "---\n\n";
const body = document.body;
if (!body) {
alert("No document body found.");
return;
}
const rawMd = childrenToMarkdown(body, {});
md += postProcess(rawMd);
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 60);
const filename = `${slug || "export"}.md`;
const blob = new Blob([md], { type: "text/markdown;charset=utf-8" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(a.href);
a.remove();
}, 1000);
showToast(`✓ Exported as ${filename}`);
}
function showToast(message) {
const toast = document.createElement("div");
Object.assign(toast.style, {
position: "fixed",
bottom: "80px",
right: "20px",
background: "#1a1a2e",
color: "#e2f0ff",
padding: "10px 18px",
borderRadius: "8px",
fontFamily: "system-ui, sans-serif",
fontSize: "13px",
fontWeight: "500",
zIndex: "2147483647",
boxShadow: "0 4px 20px rgba(0,0,0,0.35)",
transition: "opacity 0.4s ease",
opacity: "1",
pointerEvents: "none",
});
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => (toast.style.opacity = "0"), 2400);
setTimeout(() => toast.remove(), 2800);
}
function createButton() {
const btn = document.createElement("button");
btn.textContent = "⬇ MD";
btn.title = "Export page to Markdown";
const baseStyle = {
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: "2147483647",
background: "#1a1a2e",
color: "#7dd3fc",
border: "1.5px solid #334155",
borderRadius: "10px",
padding: "8px 14px",
fontFamily: "system-ui, monospace",
fontSize: "12px",
fontWeight: "700",
letterSpacing: "0.05em",
cursor: "pointer",
boxShadow: "0 4px 18px rgba(0,0,0,0.4)",
transition: "transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease",
userSelect: "none",
};
Object.assign(btn.style, baseStyle);
btn.addEventListener("mouseenter", () => {
btn.style.background = "#0f172a";
btn.style.boxShadow = "0 6px 24px rgba(0,0,0,0.5)";
btn.style.transform = "translateY(-2px)";
});
btn.addEventListener("mouseleave", () => {
btn.style.background = "#1a1a2e";
btn.style.boxShadow = "0 4px 18px rgba(0,0,0,0.4)";
btn.style.transform = "translateY(0)";
});
btn.addEventListener("mousedown", () => {
btn.style.transform = "translateY(0) scale(0.96)";
});
btn.addEventListener("mouseup", () => {
btn.style.transform = "translateY(-2px)";
});
let isDragging = false, startX, startY, startRight, startBottom;
btn.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;
isDragging = false;
startX = e.clientX;
startY = e.clientY;
startRight = parseInt(btn.style.right) || 20;
startBottom = parseInt(btn.style.bottom) || 20;
const onMove = (ev) => {
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) isDragging = true;
if (isDragging) {
btn.style.right = `${startRight - dx}px`;
btn.style.bottom = `${startBottom - dy}px`;
}
};
const onUp = () => {
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onUp);
};
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onUp);
});
btn.addEventListener("click", (e) => {
if (isDragging) {
e.stopImmediatePropagation();
isDragging = false;
return;
}
exportToMarkdown();
});
document.body.appendChild(btn);
}
document.addEventListener("keydown", (e) => {
if (e.altKey && e.shiftKey && e.key === "M") {
e.preventDefault();
exportToMarkdown();
}
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", createButton);
} else {
createButton();
}
})();