GitHub Wiki Launchers

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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