Add table of contents(TOC) for README in Codeberg.
当前为
// ==UserScript== // @name Codeberg README TOC (DEBUG) // @namespace https://greasyfork.org/en/scripts/codeberg-readme-toc // @version 0.1.1 // @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);