GitHub Wiki Launchers

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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();
    }
})();