GitHub Wiki Launchers

Buttons for opening the current GitHub repository on DeepWiki or codewiki.google.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GitHub Wiki Launchers
// @version      1.1.0
// @description  Buttons for opening the current GitHub repository on DeepWiki or codewiki.google.
// @match        *://github.com/*
// @license      MIT
// @namespace https://greasyfork.org/users/1412785
// ==/UserScript==

(() => {
    'use strict';

    const SCRIPT_ID = 'github-wiki-actions';
    const ACTION_LIST_SELECTORS = Object.freeze([
        'ul.UnderlineNav-actions',
        'ul.pagehead-actions',
    ]);
    const HISTORY_METHODS = ['pushState', 'replaceState'];
    const URL_EVENTS = ['pjax:end', 'turbo:render', 'turbo:load', 'popstate'];
    const TREE_BRANCHES = new Set(['main', 'master']);
    const BUTTONS = Object.freeze([
        {
            id: 'deepwiki',
            label: 'DeepWiki',
            title: 'Open this repository on DeepWiki',
            href: (repoPath) => `https://deepwiki.com${repoPath}`,
        },
        {
            id: 'codewiki',
            label: 'CodeWiki',
            title: 'Open this repository on codewiki.google',
            href: (repoPath) => `https://codewiki.google/github.com${repoPath}`,
        },
    ]);

    const cleanups = new Set();
    let fatalError = null;
    let lastRenderedRepo = null;

    const rerender = (reason = 'manual') => {
        if (fatalError) {
            return;
        }
        try {
            updateButtons();
        } catch (error) {
            fatalError = error instanceof Error ? error : new Error(String(error));
            teardown();
            console.error(`[${SCRIPT_ID}] ${reason}`, fatalError);
            throw fatalError;
        }
    };

    function registerCleanup(task) {
        cleanups.add(task);
    }

    function teardown() {
        cleanups.forEach((task) => {
            try {
                task();
            } catch (error) {
                console.error(`[${SCRIPT_ID}] cleanup failure`, error);
            }
        });
        cleanups.clear();
    }

    function normalizeRepoPath(pathname) {
        const trimmed = pathname.replace(/\/+$/, '');
        const segments = trimmed.split('/').filter(Boolean);
        if (segments.length < 2) {
            return null;
        }

        const [owner, repo, third, fourth] = segments;
        if (!owner || !repo) {
            return null;
        }

        if (segments.length === 2) {
            return `/${owner}/${repo}`;
        }

        if (third === 'tree' && TREE_BRANCHES.has(fourth ?? '')) {
            return `/${owner}/${repo}`;
        }

        return null;
    }

    function findActionList() {
        for (const selector of ACTION_LIST_SELECTORS) {
            const list = document.querySelector(selector);
            if (list) {
                return list;
            }
        }
        return null;
    }

    function purgeButtons(list) {
        list.querySelectorAll(`[data-${SCRIPT_ID}]`).forEach((node) => node.remove());
    }

    function createButton(definition, repoPath) {
        const item = document.createElement('li');
        item.setAttribute(`data-${SCRIPT_ID}`, definition.id);

        const anchor = document.createElement('a');
        anchor.classList.add('btn', 'btn-sm');
        anchor.target = '_blank';
        anchor.rel = 'noreferrer noopener';
        anchor.href = definition.href(repoPath);
        anchor.textContent = definition.label;
        anchor.title = definition.title;

        item.appendChild(anchor);
        return item;
    }

    function updateButtons() {
        const repoPath = normalizeRepoPath(location.pathname);
        const list = repoPath ? findActionList() : null;

        if (!repoPath) {
            lastRenderedRepo = null;
            if (list) {
                purgeButtons(list);
            }
            return;
        }

        if (!list) {
            if (document.readyState === 'complete') {
                throw new Error('GitHub repository action list missing; cannot inject wiki buttons.');
            }
            return;
        }

        if (lastRenderedRepo === repoPath && list.querySelector(`[data-${SCRIPT_ID}]`)) {
            return;
        }

        purgeButtons(list);
        const fragment = document.createDocumentFragment();
        BUTTONS.forEach((definition) => fragment.appendChild(createButton(definition, repoPath)));
        list.insertBefore(fragment, list.firstChild);
        lastRenderedRepo = repoPath;
    }

    function observeDomMutations() {
        const body = document.body;
        if (!body) {
            throw new Error('document.body missing; cannot observe mutations.');
        }
        const observer = new MutationObserver(() => rerender('mutation'));
        observer.observe(body, { childList: true, subtree: true });
        registerCleanup(() => observer.disconnect());
    }

    function observeUrlChanges() {
        URL_EVENTS.forEach((eventName) => {
            const handler = () => rerender(`event:${eventName}`);
            window.addEventListener(eventName, handler, { passive: true });
            registerCleanup(() => window.removeEventListener(eventName, handler));
        });

        HISTORY_METHODS.forEach((method) => {
            const original = history[method];
            if (typeof original !== 'function') {
                return;
            }
            history[method] = function patchedHistory(...args) {
                const result = original.apply(this, args);
                rerender(`history:${method}`);
                return result;
            };
            registerCleanup(() => {
                history[method] = original;
            });
        });
    }

    function bootstrap() {
        observeDomMutations();
        observeUrlChanges();
        rerender('bootstrap');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
    } else {
        bootstrap();
    }
})();