GitHub Wiki Launchers

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
    }
})();