Export Website to Markdown

Export all rendered text from any webpage to a Markdown file. Preserves headings, lists, links, bold/italic, code, tables, and blockquotes.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 ? `![${alt}](${absSrc})` : "";
    }

    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();
  }
})();