Codeberg README TOC

Add table of contents(TOC) for README in Codeberg.

2025/12/15のページです。最新版はこちら

スクリプトをインストールするには、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         Codeberg README TOC
// @namespace    https://greasyfork.org/en/scripts/codeberg-readme-toc
// @version      0.1.2
// @author       aspen138, Claude Code
// @description  Add table of contents(TOC) for README in Codeberg.
// @license      MIT
// @icon         https://codeberg.org/assets/img/favicon.png
// @homepage     https://github.com/your-repo/codeberg-readme-toc#readme
// @homepageURL  https://github.com/your-repo/codeberg-readme-toc#readme
// @source       https://github.com/your-repo/codeberg-readme-toc
// @supportURL   https://github.com/your-repo/codeberg-readme-toc/issues
// @match        https://codeberg.org/**
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js
// ==/UserScript==

(o=>{const t=document.createElement("style");t.dataset.source="vite-plugin-monkey",t.textContent=o,document.head.append(t)})(" /* Main container for two-column layout */ .codeberg-toc-layout-container { display: flex !important; gap: 20px !important; width: 100% !important; } /* README content - left side with reduced width */ .codeberg-toc-main-content { flex: 0 0 65% !important; max-width: 65% !important; min-width: 0 !important; } /* TOC container - right side */ #codeberg-readme-toc { flex: 0 0 30% !important; max-width: 30% !important; position: sticky !important; top: 20px !important; max-height: calc(100vh - 40px) !important; padding: 16px !important; background: var(--color-canvas-subtle, #f6f8fa) !important; border: 1px solid var(--color-border-muted, #d1d9e0) !important; border-radius: 6px !important; display: flex !important; flex-direction: column !important; } /* TOC title */ #codeberg-readme-toc h2 { margin: 0 0 12px 0 !important; font-size: 16px !important; font-weight: 600 !important; color: var(--color-fg-default, #1f2328) !important; border-bottom: 1px solid var(--color-border-muted, #d1d9e0) !important; padding-bottom: 8px !important; } /* TOC list */ #codeberg-readme-toc ul { list-style: none !important; margin: 0 !important; padding: 0 !important; overflow-y: auto !important; flex: 1 !important; } #codeberg-readme-toc ul li { margin-bottom: 4px !important; line-height: 1.4 !important; } /* TOC links */ #codeberg-readme-toc a { color: var(--color-fg-default, #1f2328) !important; text-decoration: none !important; display: block !important; padding: 4px 8px !important; border-radius: 4px !important; font-size: 13px !important; transition: background-color 0.2s ease !important; } #codeberg-readme-toc a:hover { color: var(--color-accent-fg, #0969da) !important; background-color: var(--color-canvas-default, #ffffff) !important; } /* Responsive design */ @media (max-width: 1200px) { .codeberg-toc-layout-container { flex-direction: column !important; } .codeberg-toc-main-content { flex: 1 !important; max-width: 100% !important; } #codeberg-readme-toc { flex: none !important; max-width: 100% !important; position: static !important; margin-top: 20px !important; } } ");

(function (require$$0, require$$0$1) {
  'use strict';

  var jsxRuntimeExports = {};
  var jsxRuntime = {
    get exports() {
      return jsxRuntimeExports;
    },
    set exports(v) {
      jsxRuntimeExports = v;
    }
  };
  var reactJsxRuntime_production_min = {};
  /**
   * @license React
   * react-jsx-runtime.production.min.js
   *
   * Copyright (c) Facebook, Inc. and its affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   */
  var f = require$$0, k = Symbol.for("react.element"), l = Symbol.for("react.fragment"), m$1 = Object.prototype.hasOwnProperty, n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, p = { key: true, ref: true, __self: true, __source: true };
  function q(c, a, g) {
    var b, d = {}, e = null, h = null;
    void 0 !== g && (e = "" + g);
    void 0 !== a.key && (e = "" + a.key);
    void 0 !== a.ref && (h = a.ref);
    for (b in a)
      m$1.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
    if (c && c.defaultProps)
      for (b in a = c.defaultProps, a)
        void 0 === d[b] && (d[b] = a[b]);
    return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current };
  }
  reactJsxRuntime_production_min.Fragment = l;
  reactJsxRuntime_production_min.jsx = q;
  reactJsxRuntime_production_min.jsxs = q;
  (function(module) {
    {
      module.exports = reactJsxRuntime_production_min;
    }
  })(jsxRuntime);
  var client = {};
  var m = require$$0$1;
  {
    client.createRoot = m.createRoot;
    client.hydrateRoot = m.hydrateRoot;
  }
  const name = "codeberg-readme-toc";

  // Debug configuration
  const DEBUG = false;
  const LOG_PREFIX = "[Codeberg TOC]";

  function debugLog(...args) {
    if (DEBUG) {
      console.log(LOG_PREFIX, ...args);
    }
  }

  function debugElement(label, element) {
    if (DEBUG) {
      console.log(LOG_PREFIX, label, element);
      if (element) {
        console.log(LOG_PREFIX, `${label} - tagName:`, element.tagName);
        console.log(LOG_PREFIX, `${label} - className:`, element.className);
        console.log(LOG_PREFIX, `${label} - id:`, element.id);
        console.log(LOG_PREFIX, `${label} - innerHTML length:`, element.innerHTML.length);
      }
    }
  }

  function addDebugStyle() {
    if (DEBUG) {
      const style = document.createElement("style");
      style.textContent = `
        .debug-codeberg-toc {
          border: 2px solid red !important;
          background: rgba(255, 0, 0, 0.1) !important;
          position: relative !important;
        }
        .debug-codeberg-toc::before {
          content: "TOC Container";
          position: absolute;
          top: -20px;
          left: 0;
          background: red;
          color: white;
          padding: 2px 5px;
          font-size: 12px;
          z-index: 9999;
        }
      `;
      document.head.appendChild(style);
    }
  }
  function assert$1(el) {
    if (!el) {
      throw new Error("Element not exists");
    }
  }
  function ensureElements() {
    debugLog("=== ensureElements() called ===");
    debugLog("Current URL:", window.location.href);
    debugLog("Current pathname:", window.location.pathname);

    var _a;
    // Find the README container - look for the file-view div inside the readme section
    const readmeSection = document.querySelector("#readme");
    debugElement("README Section", readmeSection);

    if (!readmeSection) {
      debugLog("❌ No README section found with #readme selector");
      // Try alternative selectors
      debugLog("Trying alternative selectors...");
      const alternatives = [
        'div[id="readme"]',
        '.readme',
        '.file-content',
        '.markdown-body'
      ];
      for (const selector of alternatives) {
        const alt = document.querySelector(selector);
        debugLog(`Trying ${selector}:`, alt);
      }
      return null;
    }

    const container = readmeSection.querySelector(".file-view.markup.markdown");
    debugElement("Container (.file-view.markup.markdown)", container);

    if (!container) {
      debugLog("❌ No container found with .file-view.markup.markdown selector");
      // Try alternative selectors within readme
      debugLog("Looking for alternative containers within README section...");
      const altContainers = [
        '.file-view',
        '.markup',
        '.markdown',
        '.ui.segment'
      ];
      for (const selector of altContainers) {
        const alt = readmeSection.querySelector(selector);
        debugLog(`Trying ${selector}:`, alt);
        debugElement(`Alternative container ${selector}`, alt);
      }
      return null;
    }

    // Find all heading elements (h1-h6) within the markdown content
    const headings = container.querySelectorAll("h1, h2, h3, h4, h5, h6");
    debugLog(`Found ${headings.length} headings:`, headings);

    // Log each heading
    headings.forEach((heading, index) => {
      debugLog(`Heading ${index + 1}:`, {
        tagName: heading.tagName,
        id: heading.id,
        text: heading.textContent?.trim(),
        anchor: heading.querySelector('a.anchor'),
        innerHTML: heading.innerHTML.substring(0, 100) + '...'
      });
    });

    if (!container || !headings.length) {
      debugLog("❌ Missing container or no headings found");
      debugLog("Container exists:", !!container);
      debugLog("Headings count:", headings.length);
      return null;
    }

    debugLog("✅ All elements found successfully");
    return { container, headings, readmeSection };
  }
  function assert(x) {
    if (!x) {
      throw new Error("Assertion failed");
    }
  }
  function getToc() {
    debugLog("=== getToc() called ===");
    const elements = ensureElements();
    if (!elements) {
      debugLog("❌ getToc: No elements found");
      return [];
    }

    const tocItems = [...elements.headings].map((heading, index) => {
      var _a;
      const depth = Number(heading.tagName.slice(1));
      const anchor = heading.querySelector("a.anchor");
      const text = (_a = heading.textContent) == null ? void 0 : _a.trim();
      const url = anchor == null ? void 0 : anchor.href;

      debugLog(`Processing heading ${index + 1}:`, {
        tagName: heading.tagName,
        depth,
        text,
        anchor,
        url
      });

      return {
        depth,
        text,
        url
      };
    }).filter(item => {
      const isValid = item.text && item.url;
      if (!isValid) {
        debugLog("❌ Filtering out invalid item:", item);
      }
      return isValid;
    });

    debugLog(`✅ Generated TOC with ${tocItems.length} items:`, tocItems);
    return tocItems;
  }
  const Toc = ({ toc }) => {
    return /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { children: toc.map((h, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("li", { style: { paddingLeft: (h.depth - 1) * 16 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: h.url, children: h.text }) }, i)) });
  };
  function App() {
    debugLog("=== App() called ===");
    const toc = getToc();
    debugLog("App received TOC:", toc);

    if (!toc.length) {
      debugLog("❌ App: No TOC items, returning null");
      return null;
    }

    debugLog("✅ App: Rendering TOC component");
    return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
      /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "h4 mb-3", children: "Table of Contents" }),
      /* @__PURE__ */ jsxRuntimeExports.jsx(Toc, { toc })
    ] });
  }
  async function render() {
    debugLog("=== render() called ===");

    let root = document.querySelector(`#${name}`);
    if (root) {
      debugLog("❌ render: TOC already exists, skipping");
      return;
    }

    const elements = ensureElements();
    if (!elements) {
      debugLog("❌ render: No elements found, cannot render");
      return;
    }

    debugLog("✅ render: Elements found, proceeding with render");
    const { readmeSection } = elements;

    // Insert TOC after the README header but before the content
    const fileHeader = readmeSection.querySelector(".file-header");
    const fileView = readmeSection.querySelector(".file-view");

    debugElement("File header", fileHeader);
    debugElement("File view", fileView);

    if (!fileHeader || !fileView) {
      debugLog("❌ render: Missing file header or file view");
      debugLog("File header exists:", !!fileHeader);
      debugLog("File view exists:", !!fileView);

      // Try alternative insertion points
      debugLog("Trying alternative insertion points...");

      // Try inserting at the beginning of readme section
      if (readmeSection.firstElementChild) {
        debugLog("Attempting to insert at beginning of README section");
        root = document.createElement("div");
        root.id = name;
        if (DEBUG) root.classList.add("debug-codeberg-toc");

        readmeSection.insertBefore(root, readmeSection.firstElementChild);

        const reactRoot = client.createRoot(root);
        const app = /* @__PURE__ */ jsxRuntimeExports.jsx(require$$0.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, {}) });
        reactRoot.render(app);

        debugLog("✅ render: TOC inserted at beginning of README section");
        return;
      }

      debugLog("❌ render: Cannot find suitable insertion point");
      return;
    }

    // Create the two-column layout structure
    debugLog("Creating two-column layout structure...");

    // Create layout container
    const layoutContainer = document.createElement("div");
    layoutContainer.className = "codeberg-toc-layout-container";

    // Create content wrapper for README
    const contentWrapper = document.createElement("div");
    contentWrapper.className = "codeberg-toc-main-content";

    // Create TOC container
    root = document.createElement("div");
    root.id = name;
    if (DEBUG) root.classList.add("debug-codeberg-toc");

    // Insert the layout container after the file header
    if (fileHeader.nextElementSibling) {
      debugLog("Inserting layout container before fileHeader.nextElementSibling:", fileHeader.nextElementSibling);
      fileHeader.parentNode.insertBefore(layoutContainer, fileHeader.nextElementSibling);
    } else {
      debugLog("Inserting layout container as last child of fileHeader parent");
      fileHeader.parentNode.appendChild(layoutContainer);
    }

    // Move the file view into the content wrapper
    debugLog("Moving file view into content wrapper...");
    contentWrapper.appendChild(fileView);

    // Add content wrapper and TOC to layout container
    layoutContainer.appendChild(contentWrapper);
    layoutContainer.appendChild(root);

    debugLog("✅ render: Two-column layout structure created");

    const reactRoot = client.createRoot(root);
    const app = /* @__PURE__ */ jsxRuntimeExports.jsx(require$$0.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, {}) });

    debugLog("✅ render: About to render React app");
    try {
      reactRoot.render(app);
      debugLog("✅ render: React app rendered successfully");
    } catch (error) {
      debugLog("❌ render: React render failed:", error);
      console.error(LOG_PREFIX, "React render error:", error);
    }
  }
  function run() {
    debugLog("=== run() called ===");
    debugLog("Current URL:", window.location.href);
    debugLog("Current pathname:", window.location.pathname);

    // Check if we're on the right page
    const isRepoPage = window.location.pathname.includes('/src/') ||
                       window.location.href.includes('codeberg.org') && !window.location.pathname.includes('/api/');

    debugLog("Is repo page?", isRepoPage);
    debugLog("Path includes '/src/'?", window.location.pathname.includes('/src/'));

    if (!isRepoPage) {
      debugLog("❌ run: Not on a repository page, skipping");
      return;
    }

    debugLog("✅ run: On repository page, proceeding with render");

    render().then(() => {
      debugLog("✅ run: render() completed successfully");
    }).catch((error) => {
      debugLog("❌ run: render() failed with error:", error);
      console.error(LOG_PREFIX, "Render error:", error);
    });
  }

  // Listen for navigation events in Codeberg (similar to GitHub's pjax/turbo)
  // Codeberg might use different navigation events, so we'll use a more generic approach
  function initTOC() {
    debugLog("=== initTOC() called ===");

    // Add debug styles
    addDebugStyle();

    // Initial load
    debugLog("Running initial TOC generation");
    run();

    // Watch for DOM changes that might indicate navigation
    const observer = new MutationObserver((mutations) => {
      let shouldRun = false;

      mutations.forEach((mutation) => {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          // Check if README content was added
          const hasReadme = Array.from(mutation.addedNodes).some(node =>
            node.nodeType === 1 &&
            (node.querySelector && node.querySelector('#readme'))
          );
          if (hasReadme) {
            debugLog("🔄 MutationObserver: README content detected, will run TOC");
            shouldRun = true;
          }
        }
      });

      if (shouldRun) {
        debugLog("🔄 MutationObserver: Triggering TOC generation after DOM change");
        setTimeout(run, 100); // Small delay to ensure DOM is ready
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });

    debugLog("✅ initTOC: MutationObserver set up");
  }

  // Start when DOM is ready
  debugLog("=== Script initialization ===");
  debugLog("Document ready state:", document.readyState);
  debugLog("React available:", typeof React !== 'undefined');
  debugLog("ReactDOM available:", typeof ReactDOM !== 'undefined');

  if (document.readyState === 'loading') {
    debugLog("Document still loading, waiting for DOMContentLoaded");
    document.addEventListener('DOMContentLoaded', () => {
      debugLog("DOMContentLoaded event fired, calling initTOC");
      initTOC();
    });
  } else {
    debugLog("Document already loaded, calling initTOC immediately");
    initTOC();
  }

})(React, ReactDOM);