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.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
  }
})();